mirror of
https://github.com/Dvorinka/ClubLogos.git
synced 2026-06-04 12:02:56 +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/gin-gonic/gin v1.9.1
|
||||||
github.com/google/uuid v1.5.0
|
github.com/google/uuid v1.5.0
|
||||||
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/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -31,6 +33,7 @@ require (
|
|||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
golang.org/x/arch v0.7.0 // indirect
|
golang.org/x/arch v0.7.0 // indirect
|
||||||
golang.org/x/crypto v0.21.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/net v0.22.0 // indirect
|
||||||
golang.org/x/sys v0.18.0 // indirect
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
golang.org/x/text v0.14.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/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 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
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/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 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
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 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
|
|||||||
+65
-12
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -369,13 +370,43 @@ func getLogoWithMetadata(c *gin.Context) {
|
|||||||
|
|
||||||
// List all logos
|
// List all logos
|
||||||
func listLogos(c *gin.Context) {
|
func listLogos(c *gin.Context) {
|
||||||
rows, err := db.Query(`
|
q := strings.TrimSpace(c.Query("q"))
|
||||||
SELECT id, club_name, club_city, club_type, club_website,
|
sortParam := c.DefaultQuery("sort", "name")
|
||||||
has_svg, has_png, primary_format,
|
limitStr := c.Query("limit")
|
||||||
created_at, updated_at
|
pageStr := c.Query("page")
|
||||||
FROM logos
|
|
||||||
ORDER BY club_name
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
|
||||||
return
|
return
|
||||||
@@ -392,8 +423,7 @@ func listLogos(c *gin.Context) {
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var logo LogoMetadata
|
var logo LogoMetadata
|
||||||
var hasSVG, hasPNG int
|
var hasSVG, hasPNG int
|
||||||
|
if err := rows.Scan(
|
||||||
err := rows.Scan(
|
|
||||||
&logo.ID,
|
&logo.ID,
|
||||||
&logo.ClubName,
|
&logo.ClubName,
|
||||||
&logo.ClubCity,
|
&logo.ClubCity,
|
||||||
@@ -404,14 +434,12 @@ func listLogos(c *gin.Context) {
|
|||||||
&logo.PrimaryFormat,
|
&logo.PrimaryFormat,
|
||||||
&logo.CreatedAt,
|
&logo.CreatedAt,
|
||||||
&logo.UpdatedAt,
|
&logo.UpdatedAt,
|
||||||
)
|
); err != nil {
|
||||||
if err != nil {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
logo.HasSVG = hasSVG == 1
|
logo.HasSVG = hasSVG == 1
|
||||||
logo.HasPNG = hasPNG == 1
|
logo.HasPNG = hasPNG == 1
|
||||||
|
|
||||||
if logo.HasPNG {
|
if logo.HasPNG {
|
||||||
logo.LogoURL = fmt.Sprintf("%s/logos/%s?format=png", baseURL, logo.ID)
|
logo.LogoURL = fmt.Sprintf("%s/logos/%s?format=png", baseURL, logo.ID)
|
||||||
} else if logo.HasSVG {
|
} else if logo.HasSVG {
|
||||||
@@ -424,6 +452,31 @@ func listLogos(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, logos)
|
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) {
|
func uploadLogo(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/srwiley/oksvg"
|
||||||
|
"github.com/srwiley/rasterx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConvertSVGToPNG converts an SVG file to PNG format
|
// ConvertSVGToPNG converts an SVG file to PNG format
|
||||||
@@ -24,8 +27,12 @@ func ConvertSVGToPNG(svgPath, pngPath string, width int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no converter available, copy SVG as fallback and log warning
|
// Try pure-Go conversion
|
||||||
return fmt.Errorf("no SVG converter available (install ImageMagick or Inkscape)")
|
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
|
// ConvertPDFToPNG converts a PDF file to PNG format
|
||||||
@@ -87,6 +94,56 @@ func convertWithInkscape(svgPath, pngPath string, width int) error {
|
|||||||
return nil
|
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)
|
// OptimizePNG optimizes a PNG file (basic implementation)
|
||||||
func OptimizePNG(pngPath string) error {
|
func OptimizePNG(pngPath string) error {
|
||||||
// Open the file
|
// Open the file
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ func setupRoutes(r *gin.Engine) {
|
|||||||
logos.GET("/:id", getLogo)
|
logos.GET("/:id", getLogo)
|
||||||
logos.GET("/:id/json", getLogoWithMetadata)
|
logos.GET("/:id/json", getLogoWithMetadata)
|
||||||
logos.POST("/:id", uploadLogo)
|
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>
|
<a href="/" class="text-2xl font-bold gradient-text">🇨🇿 České Kluby Loga</a>
|
||||||
<div class="flex gap-4">
|
<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="/" 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="/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>
|
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Admin</a>
|
||||||
</div>
|
</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 = []
|
let allLogos = []
|
||||||
|
|
||||||
// Load logos
|
async function loadRecentLogos() {
|
||||||
async function loadLogos() {
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/logos`)
|
const response = await fetch(`${API_BASE_URL}/logos?sort=recent&limit=8`)
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch recent logos')
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch logos')
|
|
||||||
}
|
|
||||||
|
|
||||||
allLogos = await response.json()
|
allLogos = await response.json()
|
||||||
|
|
||||||
loadingState.classList.add('hidden')
|
loadingState.classList.add('hidden')
|
||||||
|
|
||||||
if (allLogos.length === 0) {
|
if (allLogos.length === 0) {
|
||||||
emptyState.classList.remove('hidden')
|
emptyState.classList.remove('hidden')
|
||||||
} else {
|
} else {
|
||||||
displayLogos(allLogos)
|
displayLogos(allLogos)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading logos:', error)
|
console.error('Error loading recent logos:', error)
|
||||||
loadingState.classList.add('hidden')
|
loadingState.classList.add('hidden')
|
||||||
emptyState.classList.remove('hidden')
|
emptyState.classList.remove('hidden')
|
||||||
}
|
}
|
||||||
@@ -116,20 +108,26 @@ function displayLogos(logos) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter logos
|
// Filter logos
|
||||||
function filterLogos(query) {
|
async function filterLogos(query) {
|
||||||
const filtered = allLogos.filter(logo =>
|
const q = query.trim()
|
||||||
logo.club_name.toLowerCase().includes(query.toLowerCase()) ||
|
if (!q) {
|
||||||
(logo.club_city && logo.club_city.toLowerCase().includes(query.toLowerCase()))
|
displayLogos(allLogos)
|
||||||
)
|
return
|
||||||
|
}
|
||||||
displayLogos(filtered)
|
try {
|
||||||
|
const resp = await fetch(`${API_BASE_URL}/logos?q=${encodeURIComponent(q)}&limit=50`)
|
||||||
if (filtered.length === 0 && query) {
|
if (!resp.ok) throw new Error('Search failed')
|
||||||
logoGrid.innerHTML = `
|
const results = await resp.json()
|
||||||
<div class="col-span-full text-center py-16">
|
displayLogos(results)
|
||||||
<p class="text-xl text-gray-400">No logos found matching "${query}"</p>
|
if (results.length === 0) {
|
||||||
</div>
|
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
|
// Browse button - scroll to gallery
|
||||||
if (browseBtn) {
|
if (browseBtn) {
|
||||||
browseBtn.addEventListener('click', () => {
|
browseBtn.addEventListener('click', () => {
|
||||||
document.getElementById('logoGallery').scrollIntoView({
|
window.location.href = '/logos.html'
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'start'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +197,7 @@ console.log('🇨🇿 Czech Clubs Logos API - Home')
|
|||||||
console.log('Backend API:', API_BASE_URL)
|
console.log('Backend API:', API_BASE_URL)
|
||||||
|
|
||||||
// Load logos on page load
|
// Load logos on page load
|
||||||
loadLogos()
|
loadRecentLogos()
|
||||||
|
|
||||||
// Show welcome notification
|
// Show welcome notification
|
||||||
setTimeout(() => {
|
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'),
|
main: resolve(__dirname, 'index.html'),
|
||||||
admin: resolve(__dirname, 'admin.html'),
|
admin: resolve(__dirname, 'admin.html'),
|
||||||
apiDocs: resolve(__dirname, 'api-docs.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
|
module club
|
||||||
|
|
||||||
go 1.25.1
|
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)
|
// Directories to scan (relative to project root)
|
||||||
scanDirs: [
|
scanDirs: [
|
||||||
|
'backend/logos',
|
||||||
'data/logos',
|
'data/logos',
|
||||||
'frontend/dist',
|
'frontend/dist',
|
||||||
],
|
],
|
||||||
@@ -74,7 +75,7 @@ async function getFileSize(filePath) {
|
|||||||
*/
|
*/
|
||||||
async function optimizeSvgFile(filePath) {
|
async function optimizeSvgFile(filePath) {
|
||||||
const svg = await fs.readFile(filePath, 'utf8');
|
const svg = await fs.readFile(filePath, 'utf8');
|
||||||
const result = optimize(svg, {
|
const result = optimizeSvg(svg, {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
multipass: true,
|
multipass: true,
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -100,6 +101,45 @@ async function optimizeSvgFile(filePath) {
|
|||||||
return true;
|
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
|
* Optimize PNG/JPG file
|
||||||
*/
|
*/
|
||||||
@@ -195,7 +235,11 @@ async function processFile(filePath) {
|
|||||||
// Optimize based on file type
|
// Optimize based on file type
|
||||||
switch (ext) {
|
switch (ext) {
|
||||||
case 'svg':
|
case 'svg':
|
||||||
optimized = await optimizeSvgFile(filePath);
|
{
|
||||||
|
const didOptimize = await optimizeSvgFile(filePath);
|
||||||
|
const didConvert = await convertSvgToPng(filePath);
|
||||||
|
optimized = didOptimize || didConvert;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'png':
|
case 'png':
|
||||||
case 'jpg':
|
case 'jpg':
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"main": "optimize-assets.js",
|
"main": "optimize-assets.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"optimize": "node optimize-assets.js",
|
"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": {
|
"dependencies": {
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user