mirror of
https://github.com/Dvorinka/ClubLogos.git
synced 2026-06-03 19:42:58 +00:00
upload
This commit is contained in:
+50
@@ -0,0 +1,50 @@
|
|||||||
|
# Go build artifacts
|
||||||
|
bin/
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Go coverage
|
||||||
|
coverage.out
|
||||||
|
|
||||||
|
# Node / frontend
|
||||||
|
node_modules/
|
||||||
|
frontend/node_modules/
|
||||||
|
scripts/node_modules/
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
/dist/
|
||||||
|
frontend/dist/
|
||||||
|
frontend/public/dist/
|
||||||
|
backend/logs/
|
||||||
|
backend/logs/*
|
||||||
|
|
||||||
|
# Database & data
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
/data/db/
|
||||||
|
/data/logs/
|
||||||
|
|
||||||
|
# Env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# OS / editor noise
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
*.pid
|
||||||
@@ -9,6 +9,7 @@ require (
|
|||||||
github.com/mattn/go-sqlite3 v1.14.19
|
github.com/mattn/go-sqlite3 v1.14.19
|
||||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
|
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
|
||||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
|
||||||
|
github.com/PuerkitoBio/goquery v1.9.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
+207
-28
@@ -1,10 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
neturl "net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -13,23 +15,32 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
var facrClient = NewFACRClient()
|
|
||||||
|
|
||||||
// ==================== Club Handlers ====================
|
// ==================== Club Handlers ====================
|
||||||
|
|
||||||
func searchClubs(c *gin.Context) {
|
func searchClubs(c *gin.Context) {
|
||||||
query := c.Query("q")
|
q := strings.TrimSpace(c.Query("q"))
|
||||||
if query == "" {
|
if q == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'q' is required"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'q' is required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clubs, err := facrClient.SearchClubs(query)
|
clubs, err := scrapeFotbalSearch(q)
|
||||||
if err != nil {
|
if err != nil || len(clubs) == 0 {
|
||||||
// Return demo data if FAČR API is unavailable
|
nq := removeDiacritics(strings.ToLower(q))
|
||||||
c.JSON(http.StatusOK, getDemoClubs(query))
|
if nq != strings.ToLower(q) {
|
||||||
|
if c2, err2 := scrapeFotbalSearch(nq); err2 == nil && len(c2) > 0 {
|
||||||
|
c.JSON(http.StatusOK, c2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, getDemoClubs(q))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,8 +54,8 @@ func getClub(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
club, err := facrClient.GetClub(id)
|
club, err := fetchClubByID(id)
|
||||||
if err != nil {
|
if err != nil || club == nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "club not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "club not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -52,6 +63,120 @@ func getClub(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, club)
|
c.JSON(http.StatusOK, club)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scrapeFotbalSearch(q string) ([]Club, error) {
|
||||||
|
vals := neturl.Values{}
|
||||||
|
vals.Set("q", q)
|
||||||
|
searchURL := "https://www.fotbal.cz/club/hledej?" + vals.Encode()
|
||||||
|
req, _ := http.NewRequest("GET", searchURL, nil)
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36")
|
||||||
|
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
|
||||||
|
req.Header.Set("Accept-Language", "cs-CZ,cs;q=0.9,en;q=0.8")
|
||||||
|
client := &http.Client{Timeout: 12 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
vals2 := neturl.Values{}
|
||||||
|
vals2.Set("q", "\""+q+"\"")
|
||||||
|
searchURL = "https://www.fotbal.cz/club/hledej?" + vals2.Encode()
|
||||||
|
req2, _ := http.NewRequest("GET", searchURL, nil)
|
||||||
|
req2.Header = req.Header.Clone()
|
||||||
|
resp2, err2 := client.Do(req2)
|
||||||
|
if err2 != nil {
|
||||||
|
return nil, err2
|
||||||
|
}
|
||||||
|
defer resp2.Body.Close()
|
||||||
|
if resp2.StatusCode != http.StatusOK {
|
||||||
|
return []Club{}, nil
|
||||||
|
}
|
||||||
|
resp = resp2
|
||||||
|
}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
_, _ = buf.ReadFrom(resp.Body)
|
||||||
|
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(buf.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clubs := []Club{}
|
||||||
|
doc.Find("li.ListItemSplit").Each(func(_ int, li *goquery.Selection) {
|
||||||
|
a := li.Find("a.Link--inverted").First()
|
||||||
|
href := strings.TrimSpace(a.AttrOr("href", ""))
|
||||||
|
if href == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(a.Find("span.H7").First().Text())
|
||||||
|
if name == "" {
|
||||||
|
name = strings.TrimSpace(a.Text())
|
||||||
|
}
|
||||||
|
logoURL := strings.TrimSpace(a.Find("img").First().AttrOr("src", ""))
|
||||||
|
address := strings.TrimSpace(li.Find(".ClubAddress p").First().Text())
|
||||||
|
clubType := "football"
|
||||||
|
if strings.Contains(strings.ToLower(href), "/futsal/") {
|
||||||
|
clubType = "futsal"
|
||||||
|
}
|
||||||
|
parts := strings.Split(strings.TrimRight(href, "/"), "/")
|
||||||
|
clubID := ""
|
||||||
|
if len(parts) > 0 {
|
||||||
|
clubID = parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(href, "http://") && !strings.HasPrefix(href, "https://") {
|
||||||
|
href = "https://www.fotbal.cz" + href
|
||||||
|
}
|
||||||
|
city := extractCityFromAddress(address)
|
||||||
|
clubs = append(clubs, Club{ID: clubID, Name: name, City: city, Type: clubType, Website: "", LogoURL: logoURL})
|
||||||
|
})
|
||||||
|
return clubs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchClubByID(id string) (*Club, error) {
|
||||||
|
tryFetch := func(base string, typ string) (*Club, error) {
|
||||||
|
url := fmt.Sprintf("%s/%s", base, id)
|
||||||
|
req, _ := http.NewRequest("GET", url, nil)
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36")
|
||||||
|
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||||
|
req.Header.Set("Accept-Language", "cs-CZ,cs;q=0.9,en;q=0.8")
|
||||||
|
client := &http.Client{Timeout: 12 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(doc.Find("h1.H4 span").First().Text())
|
||||||
|
address := strings.TrimSpace(doc.Find(".ClubAddress p").First().Text())
|
||||||
|
city := extractCityFromAddress(address)
|
||||||
|
logo := fmt.Sprintf("https://is1.fotbal.cz/media/kluby/%s/%s_crop.jpg", id, id)
|
||||||
|
return &Club{ID: id, Name: name, City: city, Type: typ, Website: "", LogoURL: logo}, nil
|
||||||
|
}
|
||||||
|
if club, err := tryFetch("https://www.fotbal.cz/souteze/club/club", "football"); err == nil && club != nil && club.Name != "" {
|
||||||
|
return club, nil
|
||||||
|
}
|
||||||
|
if club, err := tryFetch("https://www.fotbal.cz/futsal/club/club", "futsal"); err == nil && club != nil && club.Name != "" {
|
||||||
|
return club, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDiacritics(s string) string {
|
||||||
|
d := norm.NFD.String(s)
|
||||||
|
b := make([]rune, 0, len(d))
|
||||||
|
for _, r := range d {
|
||||||
|
if unicode.Is(unicode.Mn, r) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b = append(b, r)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
// Demo data fallback
|
// Demo data fallback
|
||||||
func getDemoClubs(query string) []Club {
|
func getDemoClubs(query string) []Club {
|
||||||
demoClubs := []Club{
|
demoClubs := []Club{
|
||||||
@@ -374,15 +499,24 @@ func listLogos(c *gin.Context) {
|
|||||||
sortParam := c.DefaultQuery("sort", "name")
|
sortParam := c.DefaultQuery("sort", "name")
|
||||||
limitStr := c.Query("limit")
|
limitStr := c.Query("limit")
|
||||||
pageStr := c.Query("page")
|
pageStr := c.Query("page")
|
||||||
|
typeParam := strings.TrimSpace(strings.ToLower(c.Query("type")))
|
||||||
|
|
||||||
base := "SELECT id, club_name, club_city, club_type, club_website, has_svg, has_png, primary_format, created_at, updated_at FROM logos"
|
base := "SELECT id, club_name, club_city, club_type, club_website, has_svg, has_png, primary_format, created_at, updated_at FROM logos"
|
||||||
where := ""
|
whereParts := []string{}
|
||||||
args := []interface{}{}
|
args := []interface{}{}
|
||||||
if q != "" {
|
if q != "" {
|
||||||
where = " WHERE LOWER(club_name) LIKE ? OR LOWER(club_city) LIKE ? OR id LIKE ?"
|
|
||||||
like := "%" + strings.ToLower(q) + "%"
|
like := "%" + strings.ToLower(q) + "%"
|
||||||
|
whereParts = append(whereParts, "(LOWER(club_name) LIKE ? OR LOWER(club_city) LIKE ? OR id LIKE ?)")
|
||||||
args = append(args, like, like, "%"+q+"%")
|
args = append(args, like, like, "%"+q+"%")
|
||||||
}
|
}
|
||||||
|
if typeParam == "football" || typeParam == "futsal" {
|
||||||
|
whereParts = append(whereParts, "LOWER(club_type) = ?")
|
||||||
|
args = append(args, typeParam)
|
||||||
|
}
|
||||||
|
where := ""
|
||||||
|
if len(whereParts) > 0 {
|
||||||
|
where = " WHERE " + strings.Join(whereParts, " AND ")
|
||||||
|
}
|
||||||
order := " ORDER BY club_name"
|
order := " ORDER BY club_name"
|
||||||
if sortParam == "recent" {
|
if sortParam == "recent" {
|
||||||
order = " ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC"
|
order = " ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC"
|
||||||
@@ -449,6 +583,62 @@ func listLogos(c *gin.Context) {
|
|||||||
logos = append(logos, logo)
|
logos = append(logos, logo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if q != "" && len(logos) == 0 {
|
||||||
|
limitClause2 := ""
|
||||||
|
args2 := []interface{}{}
|
||||||
|
if limitStr != "" {
|
||||||
|
if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 {
|
||||||
|
limitClause2 = " LIMIT ?"
|
||||||
|
args2 = append(args2, limit)
|
||||||
|
if pageStr != "" {
|
||||||
|
if page, err := strconv.Atoi(pageStr); err == nil {
|
||||||
|
if page < 1 { page = 1 }
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
limitClause2 += " OFFSET ?"
|
||||||
|
args2 = append(args2, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
where2 := ""
|
||||||
|
if typeParam == "football" || typeParam == "futsal" {
|
||||||
|
where2 = " WHERE LOWER(club_type) = ?"
|
||||||
|
args2 = append(args2, typeParam)
|
||||||
|
}
|
||||||
|
q2 := base + where2 + order + limitClause2
|
||||||
|
rows2, err2 := db.Query(q2, args2...)
|
||||||
|
if err2 == nil {
|
||||||
|
defer rows2.Close()
|
||||||
|
normQ := removeDiacritics(strings.ToLower(q))
|
||||||
|
tmp := []LogoMetadata{}
|
||||||
|
for rows2.Next() {
|
||||||
|
var logo LogoMetadata
|
||||||
|
var hasSVG2, hasPNG2 int
|
||||||
|
if err := rows2.Scan(
|
||||||
|
&logo.ID,
|
||||||
|
&logo.ClubName,
|
||||||
|
&logo.ClubCity,
|
||||||
|
&logo.ClubType,
|
||||||
|
&logo.ClubWebsite,
|
||||||
|
&hasSVG2,
|
||||||
|
&hasPNG2,
|
||||||
|
&logo.PrimaryFormat,
|
||||||
|
&logo.CreatedAt,
|
||||||
|
&logo.UpdatedAt,
|
||||||
|
); err != nil { continue }
|
||||||
|
logo.HasSVG = hasSVG2 == 1
|
||||||
|
logo.HasPNG = hasPNG2 == 1
|
||||||
|
if logo.HasPNG { logo.LogoURL = fmt.Sprintf("%s/logos/%s?format=png", baseURL, logo.ID) } else if logo.HasSVG { logo.LogoURL = fmt.Sprintf("%s/logos/%s?format=svg", baseURL, logo.ID) }
|
||||||
|
nameN := removeDiacritics(strings.ToLower(logo.ClubName))
|
||||||
|
cityN := removeDiacritics(strings.ToLower(logo.ClubCity))
|
||||||
|
if strings.Contains(nameN, normQ) || strings.Contains(cityN, normQ) || strings.Contains(strings.ToLower(logo.ID), strings.ToLower(q)) {
|
||||||
|
tmp = append(tmp, logo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logos = tmp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, logos)
|
c.JSON(http.StatusOK, logos)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,25 +686,14 @@ func uploadLogo(c *gin.Context) {
|
|||||||
clubType := c.PostForm("club_type")
|
clubType := c.PostForm("club_type")
|
||||||
clubWebsite := c.PostForm("club_website")
|
clubWebsite := c.PostForm("club_website")
|
||||||
|
|
||||||
// Derive metadata if missing
|
|
||||||
if clubName == "" {
|
if clubName == "" {
|
||||||
if club, err := facrClient.GetClub(id); err == nil && club != nil {
|
if club, err := fetchClubByID(id); err == nil && club != nil {
|
||||||
if club.Name != "" {
|
if club.Name != "" { clubName = club.Name }
|
||||||
clubName = club.Name
|
if clubType == "" && club.Type != "" { clubType = club.Type }
|
||||||
}
|
if clubCity == "" && club.City != "" { clubCity = club.City }
|
||||||
if clubType == "" && club.Type != "" {
|
if clubWebsite == "" && club.Website != "" { clubWebsite = club.Website }
|
||||||
clubType = club.Type
|
|
||||||
}
|
|
||||||
if clubCity == "" && club.City != "" {
|
|
||||||
clubCity = club.City
|
|
||||||
}
|
|
||||||
if clubWebsite == "" && club.Website != "" {
|
|
||||||
clubWebsite = club.Website
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if clubName == "" {
|
|
||||||
clubName = "Club " + id
|
|
||||||
}
|
}
|
||||||
|
if clubName == "" { clubName = "Club " + id }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get uploaded file
|
// Get uploaded file
|
||||||
|
|||||||
+4
-193
@@ -1,202 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="cs" class="dark">
|
<html lang="cs">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Admin - České Kluby Loga API</title>
|
<title>Admin - České Kluby Loga API</title>
|
||||||
<link rel="stylesheet" href="/src/style.css">
|
<link rel="stylesheet" href="/src/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-dark-bg text-white min-h-screen">
|
<body class="bg-dark-bg min-h-screen">
|
||||||
|
<div id="root"></div>
|
||||||
<!-- Navigation -->
|
<script type="module" src="/src/admin-main.tsx"></script>
|
||||||
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+4
-414
@@ -1,423 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="cs" class="dark">
|
<html lang="cs">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>API Dokumentace - České Kluby Loga API</title>
|
<title>API Dokumentace - České Kluby Loga API</title>
|
||||||
<link rel="stylesheet" href="/src/style.css">
|
<link rel="stylesheet" href="/src/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-dark-bg text-white min-h-screen">
|
<body class="bg-dark-bg min-h-screen">
|
||||||
|
<div id="root"></div>
|
||||||
<!-- Navigation -->
|
<script type="module" src="/src/docs-main.tsx"></script>
|
||||||
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+5
-174
@@ -1,182 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="cs" class="dark">
|
<html lang="cs">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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">
|
<link rel="stylesheet" href="/src/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-dark-bg text-white min-h-screen">
|
<body class="bg-dark-bg min-h-screen">
|
||||||
|
<div id="root"></div>
|
||||||
<!-- Navigation -->
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+4
-153
@@ -1,162 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="cs" class="dark">
|
<html lang="cs">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Detail Loga - České Kluby Loga API</title>
|
<title>Detail Loga - České Kluby Loga API</title>
|
||||||
<link rel="stylesheet" href="/src/style.css">
|
<link rel="stylesheet" href="/src/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-dark-bg text-white min-h-screen">
|
<body class="bg-dark-bg min-h-screen">
|
||||||
|
<div id="root"></div>
|
||||||
<!-- Navigation -->
|
<script type="module" src="/src/logo-main.tsx"></script>
|
||||||
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+4
-58
@@ -1,67 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="cs" class="dark">
|
<html lang="cs">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Všechna Loga - České Kluby Loga API</title>
|
<title>Všechna Loga - České Kluby Loga API</title>
|
||||||
<link rel="stylesheet" href="/src/style.css">
|
<link rel="stylesheet" href="/src/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-dark-bg text-white min-h-screen">
|
<body class="bg-dark-bg min-h-screen">
|
||||||
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
|
<div id="root"></div>
|
||||||
<div class="container mx-auto px-6 py-4">
|
<script type="module" src="/src/logos-main.tsx"></script>
|
||||||
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+240
@@ -17,6 +17,23 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||||
@@ -140,6 +157,40 @@
|
|||||||
"node": ">=14"
|
"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": {
|
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||||
"version": "4.52.3",
|
"version": "4.52.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz",
|
||||||
@@ -168,6 +219,91 @@
|
|||||||
"win32"
|
"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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -175,6 +311,44 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
@@ -481,6 +655,12 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
@@ -817,6 +997,11 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"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": {
|
"node_modules/lilconfig": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
@@ -837,6 +1022,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/lru-cache": {
|
||||||
"version": "10.4.3",
|
"version": "10.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
@@ -1230,6 +1426,29 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -1351,6 +1570,14 @@
|
|||||||
"queue-microtask": "^1.2.2"
|
"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": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@@ -1618,6 +1845,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"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": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||||
|
|||||||
Generated
+330
-1
@@ -8,12 +8,18 @@
|
|||||||
"name": "czech-clubs-logos-frontend",
|
"name": "czech-clubs-logos-frontend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gsap": "^3.12.5"
|
"gsap": "^3.12.5",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
|
"typescript": "^5.4.0",
|
||||||
"vite": "^5.2.11"
|
"vite": "^5.2.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -527,6 +533,12 @@
|
|||||||
"node": ">=14"
|
"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": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.52.3",
|
"version": "4.52.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz",
|
||||||
@@ -835,6 +847,219 @@
|
|||||||
"win32"
|
"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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -842,6 +1067,44 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
@@ -1148,6 +1411,12 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
@@ -1499,6 +1768,11 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"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": {
|
"node_modules/lilconfig": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
@@ -1519,6 +1793,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/lru-cache": {
|
||||||
"version": "10.4.3",
|
"version": "10.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
@@ -1912,6 +2197,29 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -2033,6 +2341,14 @@
|
|||||||
"queue-microtask": "^1.2.2"
|
"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": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@@ -2300,6 +2616,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"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": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||||
|
|||||||
@@ -9,12 +9,18 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gsap": "^3.12.5"
|
"gsap": "^3.12.5",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
|
"typescript": "^5.4.0",
|
||||||
"vite": "^5.2.11"
|
"vite": "^5.2.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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} | ||||||