mirror of
https://github.com/Dvorinka/ClubLogos.git
synced 2026-06-03 19:42:58 +00:00
enhance
This commit is contained in:
Binary file not shown.
@@ -7,6 +7,8 @@ require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/mattn/go-sqlite3 v1.14.19
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -31,6 +33,7 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
|
||||
@@ -63,6 +63,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -82,14 +86,18 @@ golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
|
||||
+65
-12
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -369,13 +370,43 @@ func getLogoWithMetadata(c *gin.Context) {
|
||||
|
||||
// List all logos
|
||||
func listLogos(c *gin.Context) {
|
||||
rows, err := db.Query(`
|
||||
SELECT id, club_name, club_city, club_type, club_website,
|
||||
has_svg, has_png, primary_format,
|
||||
created_at, updated_at
|
||||
FROM logos
|
||||
ORDER BY club_name
|
||||
`)
|
||||
q := strings.TrimSpace(c.Query("q"))
|
||||
sortParam := c.DefaultQuery("sort", "name")
|
||||
limitStr := c.Query("limit")
|
||||
pageStr := c.Query("page")
|
||||
|
||||
base := "SELECT id, club_name, club_city, club_type, club_website, has_svg, has_png, primary_format, created_at, updated_at FROM logos"
|
||||
where := ""
|
||||
args := []interface{}{}
|
||||
if q != "" {
|
||||
where = " WHERE LOWER(club_name) LIKE ? OR LOWER(club_city) LIKE ? OR id LIKE ?"
|
||||
like := "%" + strings.ToLower(q) + "%"
|
||||
args = append(args, like, like, "%"+q+"%")
|
||||
}
|
||||
order := " ORDER BY club_name"
|
||||
if sortParam == "recent" {
|
||||
order = " ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC"
|
||||
}
|
||||
limitClause := ""
|
||||
if limitStr != "" {
|
||||
if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 {
|
||||
limitClause = " LIMIT ?"
|
||||
args = append(args, limit)
|
||||
if pageStr != "" {
|
||||
if page, err := strconv.Atoi(pageStr); err == nil {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
offset := (page - 1) * limit
|
||||
limitClause += " OFFSET ?"
|
||||
args = append(args, offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query := base + where + order + limitClause
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
|
||||
return
|
||||
@@ -392,8 +423,7 @@ func listLogos(c *gin.Context) {
|
||||
for rows.Next() {
|
||||
var logo LogoMetadata
|
||||
var hasSVG, hasPNG int
|
||||
|
||||
err := rows.Scan(
|
||||
if err := rows.Scan(
|
||||
&logo.ID,
|
||||
&logo.ClubName,
|
||||
&logo.ClubCity,
|
||||
@@ -404,14 +434,12 @@ func listLogos(c *gin.Context) {
|
||||
&logo.PrimaryFormat,
|
||||
&logo.CreatedAt,
|
||||
&logo.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
logo.HasSVG = hasSVG == 1
|
||||
logo.HasPNG = hasPNG == 1
|
||||
|
||||
if logo.HasPNG {
|
||||
logo.LogoURL = fmt.Sprintf("%s/logos/%s?format=png", baseURL, logo.ID)
|
||||
} else if logo.HasSVG {
|
||||
@@ -424,6 +452,31 @@ func listLogos(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, logos)
|
||||
}
|
||||
|
||||
func deleteLogo(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "logo ID is required"})
|
||||
return
|
||||
}
|
||||
if _, err := uuid.Parse(id); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid UUID format"})
|
||||
return
|
||||
}
|
||||
|
||||
_, err := db.Exec("DELETE FROM logos WHERE id = ?", id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
|
||||
return
|
||||
}
|
||||
|
||||
pngPath := filepath.Join("./logos/png", id+".png")
|
||||
svgPath := filepath.Join("./logos/svg", id+".svg")
|
||||
os.Remove(pngPath)
|
||||
os.Remove(svgPath)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "id": id})
|
||||
}
|
||||
|
||||
func uploadLogo(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
|
||||
@@ -9,6 +9,9 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/srwiley/oksvg"
|
||||
"github.com/srwiley/rasterx"
|
||||
)
|
||||
|
||||
// ConvertSVGToPNG converts an SVG file to PNG format
|
||||
@@ -24,8 +27,12 @@ func ConvertSVGToPNG(svgPath, pngPath string, width int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If no converter available, copy SVG as fallback and log warning
|
||||
return fmt.Errorf("no SVG converter available (install ImageMagick or Inkscape)")
|
||||
// Try pure-Go conversion
|
||||
if err := convertWithGoRenderer(svgPath, pngPath, width); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("no SVG converter available (install ImageMagick or Inkscape, or ensure Go renderer deps)")
|
||||
}
|
||||
|
||||
// ConvertPDFToPNG converts a PDF file to PNG format
|
||||
@@ -87,6 +94,56 @@ func convertWithInkscape(svgPath, pngPath string, width int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertWithGoRenderer(svgPath, pngPath string, width int) error {
|
||||
f, err := os.Open(svgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open svg: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
icon, err := oksvg.ReadIconStream(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse svg: %w", err)
|
||||
}
|
||||
|
||||
vb := icon.ViewBox
|
||||
targetW := width
|
||||
if targetW <= 0 {
|
||||
targetW = int(vb.W)
|
||||
if targetW <= 0 {
|
||||
targetW = 512
|
||||
}
|
||||
}
|
||||
var targetH int
|
||||
if vb.W != 0 {
|
||||
targetH = int(float64(targetW) * (vb.H / vb.W))
|
||||
} else {
|
||||
targetH = targetW
|
||||
}
|
||||
if targetH <= 0 {
|
||||
targetH = targetW
|
||||
}
|
||||
|
||||
icon.SetTarget(0, 0, float64(targetW), float64(targetH))
|
||||
|
||||
rgba := image.NewRGBA(image.Rect(0, 0, targetW, targetH))
|
||||
scanner := rasterx.NewScannerGV(targetW, targetH, rgba, rgba.Bounds())
|
||||
raster := rasterx.NewDasher(targetW, targetH, scanner)
|
||||
icon.Draw(raster, 1.0)
|
||||
|
||||
out, err := os.Create(pngPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create png: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if err := png.Encode(out, rgba); err != nil {
|
||||
os.Remove(pngPath)
|
||||
return fmt.Errorf("encode png: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OptimizePNG optimizes a PNG file (basic implementation)
|
||||
func OptimizePNG(pngPath string) error {
|
||||
// Open the file
|
||||
|
||||
@@ -88,6 +88,7 @@ func setupRoutes(r *gin.Engine) {
|
||||
logos.GET("/:id", getLogo)
|
||||
logos.GET("/:id/json", getLogoWithMetadata)
|
||||
logos.POST("/:id", uploadLogo)
|
||||
logos.DELETE("/:id", deleteLogo)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Všechna Loga - České Kluby Loga API</title>
|
||||
<link rel="stylesheet" href="/src/style.css">
|
||||
</head>
|
||||
<body class="bg-dark-bg text-white min-h-screen">
|
||||
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
|
||||
<div class="container mx-auto px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="/" class="text-2xl font-bold gradient-text">České Kluby Loga</a>
|
||||
<div class="flex gap-4">
|
||||
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
|
||||
<a href="/logos.html" class="nav-link px-4 py-2 rounded-lg bg-accent-blue/20 transition-smooth">Všechna Loga</a>
|
||||
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">API Docs</a>
|
||||
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Admin</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<header class="border-b border-dark-border bg-dark-card">
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
<h1 class="text-3xl font-bold gradient-text mb-2">Všechna Loga</h1>
|
||||
<p class="text-gray-400">Procházejte všechna dostupná loga, vyhledávejte a spravujte</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container mx-auto px-6 py-12">
|
||||
<div class="mb-6 flex flex-col md:flex-row gap-3 md:items-center">
|
||||
<input
|
||||
type="text"
|
||||
id="allLogoSearch"
|
||||
placeholder="Hledat mezi všemi logy..."
|
||||
class="w-full md:max-w-lg bg-dark-card border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
|
||||
>
|
||||
<div class="text-xs text-gray-500">20 log na stránku • řazeno: nejnovější</div>
|
||||
</div>
|
||||
|
||||
<div id="allLoading" class="text-center py-12">
|
||||
<div class="spinner mx-auto"></div>
|
||||
<p class="mt-4 text-gray-400">Načítání log...</p>
|
||||
</div>
|
||||
|
||||
<div id="allEmpty" class="text-center py-16 hidden">
|
||||
<div class="text-6xl mb-4">⚽</div>
|
||||
<p class="text-xl text-gray-400 mb-4">Žádná loga nenalezena</p>
|
||||
</div>
|
||||
|
||||
<div id="allLogoGrid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-6"></div>
|
||||
|
||||
<div class="text-center mt-10">
|
||||
<button id="loadMoreBtn" class="px-6 py-3 bg-dark-card border border-dark-border rounded-lg hover:bg-dark-border transition-smooth hidden">Načíst další</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="border-t border-dark-border mt-20">
|
||||
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
|
||||
<p>🇨🇿 České Kluby Loga API</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="/src/logos.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+26
-31
@@ -44,27 +44,19 @@ const browseBtn = document.getElementById('browseBtn')
|
||||
|
||||
let allLogos = []
|
||||
|
||||
// Load logos
|
||||
async function loadLogos() {
|
||||
async function loadRecentLogos() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/logos`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch logos')
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/logos?sort=recent&limit=8`)
|
||||
if (!response.ok) throw new Error('Failed to fetch recent logos')
|
||||
allLogos = await response.json()
|
||||
|
||||
loadingState.classList.add('hidden')
|
||||
|
||||
if (allLogos.length === 0) {
|
||||
emptyState.classList.remove('hidden')
|
||||
} else {
|
||||
displayLogos(allLogos)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading logos:', error)
|
||||
console.error('Error loading recent logos:', error)
|
||||
loadingState.classList.add('hidden')
|
||||
emptyState.classList.remove('hidden')
|
||||
}
|
||||
@@ -116,20 +108,26 @@ function displayLogos(logos) {
|
||||
}
|
||||
|
||||
// Filter logos
|
||||
function filterLogos(query) {
|
||||
const filtered = allLogos.filter(logo =>
|
||||
logo.club_name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
(logo.club_city && logo.club_city.toLowerCase().includes(query.toLowerCase()))
|
||||
)
|
||||
|
||||
displayLogos(filtered)
|
||||
|
||||
if (filtered.length === 0 && query) {
|
||||
logoGrid.innerHTML = `
|
||||
<div class="col-span-full text-center py-16">
|
||||
<p class="text-xl text-gray-400">No logos found matching "${query}"</p>
|
||||
</div>
|
||||
`
|
||||
async function filterLogos(query) {
|
||||
const q = query.trim()
|
||||
if (!q) {
|
||||
displayLogos(allLogos)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE_URL}/logos?q=${encodeURIComponent(q)}&limit=50`)
|
||||
if (!resp.ok) throw new Error('Search failed')
|
||||
const results = await resp.json()
|
||||
displayLogos(results)
|
||||
if (results.length === 0) {
|
||||
logoGrid.innerHTML = `
|
||||
<div class="col-span-full text-center py-16">
|
||||
<p class="text-xl text-gray-400">No logos found matching "${q}"</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Search error:', e.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,10 +156,7 @@ if (gallerySearch) {
|
||||
// Browse button - scroll to gallery
|
||||
if (browseBtn) {
|
||||
browseBtn.addEventListener('click', () => {
|
||||
document.getElementById('logoGallery').scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
})
|
||||
window.location.href = '/logos.html'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -202,7 +197,7 @@ console.log('🇨🇿 Czech Clubs Logos API - Home')
|
||||
console.log('Backend API:', API_BASE_URL)
|
||||
|
||||
// Load logos on page load
|
||||
loadLogos()
|
||||
loadRecentLogos()
|
||||
|
||||
// Show welcome notification
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import './style.css'
|
||||
|
||||
const API_BASE_URL = '/api'
|
||||
|
||||
const grid = document.getElementById('allLogoGrid')
|
||||
const loading = document.getElementById('allLoading')
|
||||
const empty = document.getElementById('allEmpty')
|
||||
const loadMoreBtn = document.getElementById('loadMoreBtn')
|
||||
const searchInput = document.getElementById('allLogoSearch')
|
||||
|
||||
let page = 1
|
||||
const limit = 20
|
||||
let query = ''
|
||||
let isLoading = false
|
||||
let hasMore = true
|
||||
|
||||
async function loadPage(reset = false) {
|
||||
if (isLoading) return
|
||||
if (reset) {
|
||||
page = 1
|
||||
hasMore = true
|
||||
grid.innerHTML = ''
|
||||
empty.classList.add('hidden')
|
||||
loading.classList.remove('hidden')
|
||||
} else {
|
||||
loadMoreBtn.disabled = true
|
||||
loadMoreBtn.textContent = 'Načítání...'
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
try {
|
||||
const url = new URL(`${API_BASE_URL}/logos`, window.location.origin)
|
||||
url.searchParams.set('sort', 'recent')
|
||||
url.searchParams.set('limit', String(limit))
|
||||
url.searchParams.set('page', String(page))
|
||||
if (query) url.searchParams.set('q', query)
|
||||
|
||||
const resp = await fetch(url.toString().replace(window.location.origin, ''))
|
||||
if (!resp.ok) throw new Error('Failed to fetch logos')
|
||||
const data = await resp.json()
|
||||
|
||||
if (reset) loading.classList.add('hidden')
|
||||
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
appendCards(data)
|
||||
page += 1
|
||||
if (data.length < limit) {
|
||||
hasMore = false
|
||||
loadMoreBtn.classList.add('hidden')
|
||||
} else {
|
||||
loadMoreBtn.classList.remove('hidden')
|
||||
}
|
||||
} else {
|
||||
if (reset) empty.classList.remove('hidden')
|
||||
hasMore = false
|
||||
loadMoreBtn.classList.add('hidden')
|
||||
}
|
||||
} catch (_) {
|
||||
if (reset) {
|
||||
loading.classList.add('hidden')
|
||||
empty.classList.remove('hidden')
|
||||
}
|
||||
} finally {
|
||||
isLoading = false
|
||||
loadMoreBtn.disabled = false
|
||||
loadMoreBtn.textContent = 'Načíst další'
|
||||
}
|
||||
}
|
||||
|
||||
function appendCards(items) {
|
||||
const html = items.map(logo => {
|
||||
const logoUrl = `${API_BASE_URL}/logos/${logo.id}`
|
||||
return `
|
||||
<div class="logo-card bg-dark-card rounded-xl p-4 border border-dark-border hover:border-accent-blue transition-smooth group" data-id="${logo.id}">
|
||||
<div class="aspect-square bg-dark-bg rounded-lg flex items-center justify-center mb-3 overflow-hidden cursor-pointer">
|
||||
<img src="${logoUrl}" alt="${logo.club_name}" class="max-w-full max-h-full object-contain p-2 group-hover:scale-110 transition-transform duration-300" loading="lazy" onerror="this.parentElement.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>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1 min-w-0 cursor-pointer">
|
||||
<h3 class="font-semibold text-sm truncate mb-0.5">${logo.club_name}</h3>
|
||||
<p class="text-xs text-gray-400 truncate">${logo.club_type || 'fotbal'}</p>
|
||||
</div>
|
||||
<button class="delete-logo px-3 py-1.5 text-xs bg-red-600 rounded hover:bg-red-500 transition-smooth">Smazat</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}).join('')
|
||||
grid.insertAdjacentHTML('beforeend', html)
|
||||
}
|
||||
|
||||
grid.addEventListener('click', async (e) => {
|
||||
const delBtn = e.target.closest('.delete-logo')
|
||||
if (delBtn) {
|
||||
const card = delBtn.closest('.logo-card')
|
||||
const id = card.dataset.id
|
||||
const ok = confirm('Smazat toto logo?')
|
||||
if (!ok) return
|
||||
delBtn.disabled = true
|
||||
delBtn.textContent = 'Mazání...'
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE_URL}/logos/${id}`, { method: 'DELETE' })
|
||||
if (!resp.ok) throw new Error('Delete failed')
|
||||
card.remove()
|
||||
if (!grid.children.length) empty.classList.remove('hidden')
|
||||
} catch (_) {
|
||||
delBtn.textContent = 'Smazat'
|
||||
delBtn.disabled = false
|
||||
alert('Mazání selhalo')
|
||||
}
|
||||
return
|
||||
}
|
||||
const card = e.target.closest('.logo-card')
|
||||
if (card && !e.target.closest('.delete-logo')) {
|
||||
const id = card.dataset.id
|
||||
window.location.href = `/logo.html?id=${id}`
|
||||
}
|
||||
})
|
||||
|
||||
let searchTimer
|
||||
searchInput.addEventListener('input', () => {
|
||||
clearTimeout(searchTimer)
|
||||
searchTimer = setTimeout(() => {
|
||||
query = searchInput.value.trim()
|
||||
loadPage(true)
|
||||
}, 300)
|
||||
})
|
||||
|
||||
loadMoreBtn.addEventListener('click', () => {
|
||||
if (hasMore) loadPage(false)
|
||||
})
|
||||
|
||||
loadPage(true)
|
||||
@@ -22,7 +22,8 @@ export default defineConfig({
|
||||
main: resolve(__dirname, 'index.html'),
|
||||
admin: resolve(__dirname, 'admin.html'),
|
||||
apiDocs: resolve(__dirname, 'api-docs.html'),
|
||||
logo: resolve(__dirname, 'logo.html')
|
||||
logo: resolve(__dirname, 'logo.html'),
|
||||
logos: resolve(__dirname, 'logos.html')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
module club
|
||||
|
||||
go 1.25.1
|
||||
|
||||
require (
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
||||
)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -34,6 +34,7 @@ const CONFIG = {
|
||||
},
|
||||
// Directories to scan (relative to project root)
|
||||
scanDirs: [
|
||||
'backend/logos',
|
||||
'data/logos',
|
||||
'frontend/dist',
|
||||
],
|
||||
@@ -74,7 +75,7 @@ async function getFileSize(filePath) {
|
||||
*/
|
||||
async function optimizeSvgFile(filePath) {
|
||||
const svg = await fs.readFile(filePath, 'utf8');
|
||||
const result = optimize(svg, {
|
||||
const result = optimizeSvg(svg, {
|
||||
path: filePath,
|
||||
multipass: true,
|
||||
plugins: [
|
||||
@@ -100,6 +101,45 @@ async function optimizeSvgFile(filePath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function convertSvgToPng(svgPath) {
|
||||
try {
|
||||
const normalized = path.normalize(svgPath);
|
||||
let pngPath = normalized.replace(/\.svg$/i, '.png');
|
||||
|
||||
const mappings = [
|
||||
{
|
||||
from: path.join(path.sep, 'data', 'logos', 'svg') + path.sep,
|
||||
to: path.join(path.sep, 'data', 'logos', 'png') + path.sep,
|
||||
},
|
||||
{
|
||||
from: path.join(path.sep, 'backend', 'logos', 'svg') + path.sep,
|
||||
to: path.join(path.sep, 'backend', 'logos', 'png') + path.sep,
|
||||
},
|
||||
];
|
||||
|
||||
for (const { from, to } of mappings) {
|
||||
if (normalized.includes(from)) {
|
||||
pngPath = normalized.replace(from, to).replace(/\.svg$/i, '.png');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await fs.mkdir(path.dirname(pngPath), { recursive: true });
|
||||
|
||||
const image = sharp(svgPath, { density: 300 });
|
||||
await image
|
||||
.resize({ width: 512, fit: 'inside', withoutEnlargement: true })
|
||||
.png({ quality: CONFIG.compression.png.quality, effort: CONFIG.compression.png.effort })
|
||||
.toFile(pngPath);
|
||||
|
||||
console.log(chalk.green(` ✓ Converted to PNG: ${pngPath}`));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(chalk.yellow(` ⚠️ SVG to PNG conversion skipped: ${error.message}`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize PNG/JPG file
|
||||
*/
|
||||
@@ -195,7 +235,11 @@ async function processFile(filePath) {
|
||||
// Optimize based on file type
|
||||
switch (ext) {
|
||||
case 'svg':
|
||||
optimized = await optimizeSvgFile(filePath);
|
||||
{
|
||||
const didOptimize = await optimizeSvgFile(filePath);
|
||||
const didConvert = await convertSvgToPng(filePath);
|
||||
optimized = didOptimize || didConvert;
|
||||
}
|
||||
break;
|
||||
case 'png':
|
||||
case 'jpg':
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"main": "optimize-assets.js",
|
||||
"scripts": {
|
||||
"optimize": "node optimize-assets.js",
|
||||
"optimize:watch": "nodemon --watch ../data --watch ../frontend/dist -e svg,png,pdf --exec npm run optimize"
|
||||
"optimize:watch": "nodemon --watch ../backend/logos --watch ../data --watch ../frontend/dist -e svg,pdf --exec npm run optimize"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
|
||||
Reference in New Issue
Block a user