18 Commits
Author SHA1 Message Date
Tomas Dvorak 1621b3ba53 working 2025-12-02 22:54:00 +01:00
Tomas Dvorak 3588c38223 hghg 2025-12-02 13:55:31 +01:00
Tomas Dvorak b6ae5f0a5f Merge branch 'main' of https://github.com/Dvorinka/ClubLogos 2025-12-02 13:55:30 +01:00
Tomas Dvorak b27fe6fe0e upload 2025-12-02 13:52:21 +01:00
Tomas Dvorak 1824326b2f push 2 2025-12-01 12:45:19 +01:00
Tomas Dvorak 672ffbcdcd styele 2025-12-01 10:46:33 +01:00
Tomas Dvorak 6a9f25ffe9 update ui, search, new api endpoint 2025-12-01 10:05:27 +01:00
Tomas Dvorak 025f5beef1 push 2025-10-23 14:39:42 +02:00
Tomas Dvorak e6bc2eedb3 enhance 2025-10-22 20:35:22 +02:00
Tomáš Dvořák e47059385c cors 2025-10-05 16:41:41 +02:00
Tomáš Dvořák 30e180886d cors 2025-10-05 10:11:25 +02:00
Tomáš Dvořák eeff13d5bb bgbg 2025-10-03 22:05:32 +02:00
Tomáš DvořákandGitHub f15b2b5630 Update logo.js 2025-10-03 18:34:46 +02:00
Tomáš Dvořák df930e3d9c fefef 2025-10-03 18:28:55 +02:00
Tomáš Dvořák f964d43c50 push 2025-10-03 18:22:08 +02:00
Tomáš Dvořák 72f5cce5e3 fef 2025-10-03 17:53:02 +02:00
Tomáš Dvořák de415df1a9 ff 2025-10-03 17:44:17 +02:00
Tomáš Dvořák 07ed0c7a0c ddd 2025-10-03 17:43:39 +02:00
86 changed files with 7288 additions and 2464 deletions
+50
View File
@@ -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
-30
View File
@@ -1,30 +0,0 @@
# Binaries
*.exe
*.exe~
*.dll
*.so
*.dylib
main
# Test binary
*.test
# Output of the go coverage tool
*.out
# Database files
*.sqlite
*.db
# Logos directory
logos/
# Go workspace file
go.work
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
Binary file not shown.
Binary file not shown.
+9 -4
View File
@@ -3,13 +3,18 @@ module czech-clubs-logos-api
go 1.21
require (
github.com/PuerkitoBio/goquery v1.9.2
github.com/gin-contrib/cors v1.7.0
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
golang.org/x/text v0.14.0
)
require (
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/bytedance/sonic v1.11.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
@@ -30,10 +35,10 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
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/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+50 -6
View File
@@ -1,3 +1,7 @@
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.2 h1:ywfwo0a/3j9HR8wsYGWsIWl2mvRsI950HyoxiBERw5A=
@@ -63,6 +67,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=
@@ -77,19 +85,55 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
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/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
+335 -69
View File
@@ -1,34 +1,45 @@
package main
import (
"bytes"
"database/sql"
"fmt"
"log"
"net/http"
neturl "net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"unicode"
"github.com/PuerkitoBio/goquery"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"golang.org/x/text/unicode/norm"
)
var facrClient = NewFACRClient()
// ==================== Club Handlers ====================
func searchClubs(c *gin.Context) {
query := c.Query("q")
if query == "" {
q := strings.TrimSpace(c.Query("q"))
if q == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'q' is required"})
return
}
clubs, err := facrClient.SearchClubs(query)
if err != nil {
// Return demo data if FAČR API is unavailable
c.JSON(http.StatusOK, getDemoClubs(query))
clubs, err := scrapeFotbalSearch(q)
if err != nil || len(clubs) == 0 {
nq := removeDiacritics(strings.ToLower(q))
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
}
@@ -42,8 +53,8 @@ func getClub(c *gin.Context) {
return
}
club, err := facrClient.GetClub(id)
if err != nil {
club, err := fetchClubByID(id)
if err != nil || club == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "club not found"})
return
}
@@ -51,6 +62,120 @@ func getClub(c *gin.Context) {
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
func getDemoClubs(query string) []Club {
demoClubs := []Club{
@@ -191,18 +316,18 @@ func getDemoClubs(query string) []Club {
var results []Club
lowerQuery := strings.ToLower(query)
// Fuzzy matching: check contains in name, city, and partial matches
for _, club := range demoClubs {
lowerName := strings.ToLower(club.Name)
lowerCity := strings.ToLower(club.City)
// Exact contains match in name or city
if strings.Contains(lowerName, lowerQuery) || strings.Contains(lowerCity, lowerQuery) {
results = append(results, club)
continue
}
// Fuzzy match: check if query matches start of any word in name
words := strings.Fields(lowerName)
for _, word := range words {
@@ -219,21 +344,21 @@ func getDemoClubs(query string) []Club {
// ==================== Logo Handlers ====================
type LogoMetadata struct {
ID string `json:"id"`
ClubName string `json:"club_name"`
ClubCity string `json:"club_city,omitempty"`
ClubType string `json:"club_type,omitempty"`
ClubWebsite string `json:"club_website,omitempty"`
HasSVG bool `json:"has_svg"`
HasPNG bool `json:"has_png"`
PrimaryFormat string `json:"primary_format"`
LogoURL string `json:"logo_url"`
LogoURLSVG string `json:"logo_url_svg,omitempty"`
LogoURLPNG string `json:"logo_url_png,omitempty"`
FileSizeSVG int64 `json:"file_size_svg,omitempty"`
FileSizePNG int64 `json:"file_size_png,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID string `json:"id"`
ClubName string `json:"club_name"`
ClubCity string `json:"club_city,omitempty"`
ClubType string `json:"club_type,omitempty"`
ClubWebsite string `json:"club_website,omitempty"`
HasSVG bool `json:"has_svg"`
HasPNG bool `json:"has_png"`
PrimaryFormat string `json:"primary_format"`
LogoURL string `json:"logo_url"`
LogoURLSVG string `json:"logo_url_svg,omitempty"`
LogoURLPNG string `json:"logo_url_png,omitempty"`
FileSizeSVG int64 `json:"file_size_svg,omitempty"`
FileSizePNG int64 `json:"file_size_png,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// getLogo returns the logo file (PNG preferred, SVG fallback)
@@ -282,6 +407,10 @@ func getLogo(c *gin.Context) {
return
}
// Set CORS headers explicitly for file serving
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, OPTIONS")
c.Header("Access-Control-Allow-Headers", "*")
c.Header("Content-Type", contentType)
c.Header("Cache-Control", "public, max-age=31536000")
c.File(logoPath)
@@ -344,7 +473,7 @@ func getLogoWithMetadata(c *gin.Context) {
scheme = "https"
}
baseURL := fmt.Sprintf("%s://%s", scheme, c.Request.Host)
// Primary URL (PNG preferred)
if metadata.HasPNG {
metadata.LogoURL = fmt.Sprintf("%s/logos/%s?format=png", baseURL, id)
@@ -365,13 +494,52 @@ 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")
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"
whereParts := []string{}
args := []interface{}{}
if 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+"%")
}
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"
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
@@ -388,8 +556,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,
@@ -400,14 +567,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 {
@@ -417,9 +582,98 @@ func listLogos(c *gin.Context) {
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)
}
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 == "" {
@@ -433,26 +687,38 @@ func uploadLogo(c *gin.Context) {
return
}
// Get club name from form (required)
// Read metadata from form
clubName := c.PostForm("club_name")
if clubName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "club_name is required"})
return
}
// Optional fields
clubCity := c.PostForm("club_city")
clubType := c.PostForm("club_type")
clubWebsite := c.PostForm("club_website")
if clubName == "" {
if club, err := fetchClubByID(id); err == nil && club != nil {
if club.Name != "" {
clubName = club.Name
}
if clubType == "" && club.Type != "" {
clubType = club.Type
}
if clubCity == "" && club.City != "" {
clubCity = club.City
}
if clubWebsite == "" && club.Website != "" {
clubWebsite = club.Website
}
}
if clubName == "" {
clubName = "Club " + id
}
}
// Get uploaded file
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "no file provided"})
return
}
// Validate file type
ext := strings.ToLower(filepath.Ext(file.Filename))
if ext != ".svg" && ext != ".png" && ext != ".pdf" {
c.JSON(http.StatusBadRequest, gin.H{"error": "only .svg, .png and .pdf files are allowed"})
@@ -466,16 +732,16 @@ func uploadLogo(c *gin.Context) {
if ext == ".svg" || ext == ".pdf" {
pngPath = filepath.Join("./logos/png", id+".png")
if ext == ".svg" {
svgPath = filepath.Join("./logos/svg", id+".svg")
// Save SVG
if err := c.SaveUploadedFile(file, svgPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save SVG file"})
return
}
// Get SVG file size
if stat, err := os.Stat(svgPath); err == nil {
sizeSVG = stat.Size()
@@ -502,12 +768,12 @@ func uploadLogo(c *gin.Context) {
// PDF file - convert directly to PNG
pdfTempPath := filepath.Join("./logos/temp", id+".pdf")
os.MkdirAll("./logos/temp", 0755)
if err := c.SaveUploadedFile(file, pdfTempPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save PDF file"})
return
}
log.Printf("Converting PDF to PNG for club: %s", clubName)
if err := ConvertPDFToPNG(pdfTempPath, pngPath, 512); err != nil {
log.Printf("Error: Failed to convert PDF to PNG: %v", err)
@@ -515,15 +781,15 @@ func uploadLogo(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to convert PDF to PNG"})
return
}
// Clean up temp PDF
os.Remove(pdfTempPath)
// Optimize PNG
if err := OptimizePNG(pngPath); err != nil {
log.Printf("Warning: Failed to optimize PNG: %v", err)
}
// Get PNG file size
if stat, err := os.Stat(pngPath); err == nil {
sizePNG = stat.Size()
@@ -534,17 +800,17 @@ func uploadLogo(c *gin.Context) {
} else {
// PNG upload
pngPath = filepath.Join("./logos/png", id+".png")
if err := c.SaveUploadedFile(file, pngPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save PNG file"})
return
}
// Optimize PNG
if err := OptimizePNG(pngPath); err != nil {
log.Printf("Warning: Failed to optimize PNG: %v", err)
}
// Get PNG file size
if stat, err := os.Stat(pngPath); err == nil {
sizePNG = stat.Size()
@@ -568,14 +834,14 @@ func uploadLogo(c *gin.Context) {
}
response := gin.H{
"success": true,
"id": id,
"club_name": clubName,
"has_svg": hasSVG == 1,
"has_png": hasPNG == 1,
"size_svg": sizeSVG,
"size_png": sizePNG,
"message": "logo uploaded successfully",
"success": true,
"id": id,
"club_name": clubName,
"has_svg": hasSVG == 1,
"has_png": hasPNG == 1,
"size_svg": sizeSVG,
"size_png": sizePNG,
"message": "logo uploaded successfully",
}
c.JSON(http.StatusOK, response)
+67 -10
View File
@@ -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
@@ -39,14 +46,14 @@ func ConvertPDFToPNG(pdfPath, pngPath string, width int) error {
fmt.Sprintf("%s[0]", pdfPath), // Only first page
pngPath,
)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("PDF conversion failed (install ImageMagick and Ghostscript): %v - %s", err, stderr.String())
}
return nil
}
@@ -58,14 +65,14 @@ func convertWithImageMagick(svgPath, pngPath string, width int) error {
svgPath,
pngPath,
)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("imagemagick conversion failed: %v - %s", err, stderr.String())
}
return nil
}
@@ -76,10 +83,10 @@ func convertWithInkscape(svgPath, pngPath string, width int) error {
fmt.Sprintf("--export-width=%d", width),
svgPath,
)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("inkscape conversion failed: %v - %s", err, stderr.String())
}
@@ -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
+20 -8
View File
@@ -3,6 +3,7 @@ package main
import (
"database/sql"
"log"
"net/http"
"os"
"github.com/gin-contrib/cors"
@@ -38,21 +39,31 @@ func main() {
r := gin.Default()
r.MaxMultipartMemory = 32 << 20 // 32 MB
// CORS middleware
// CORS middleware - Allow all origins, methods, and headers
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
AllowOriginFunc: func(origin string) bool {
return true // Allow all origins
},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With", "Range", "Accept-Language", "Accept-Encoding", "Cache-Control", "Pragma", "If-Modified-Since"},
ExposeHeaders: []string{"*"},
AllowCredentials: false,
AllowOriginFunc: func(origin string) bool { return true },
}))
// Routes
setupRoutes(r)
// Global preflight handler for any path
r.OPTIONS("/*path", func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD")
reqHeaders := c.GetHeader("Access-Control-Request-Headers")
if reqHeaders == "" {
reqHeaders = "Origin, Content-Type, Accept, Authorization, X-Requested-With, Range, Accept-Language, Accept-Encoding, Cache-Control, Pragma, If-Modified-Since"
}
c.Header("Access-Control-Allow-Headers", reqHeaders)
c.Status(http.StatusNoContent)
})
// Start server
port := os.Getenv("PORT")
if port == "" {
@@ -88,6 +99,7 @@ func setupRoutes(r *gin.Engine) {
logos.GET("/:id", getLogo)
logos.GET("/:id/json", getLogoWithMetadata)
logos.POST("/:id", uploadLogo)
logos.DELETE("/:id", deleteLogo)
}
}
BIN
View File
Binary file not shown.
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 37 KiB

+4 -194
View File
@@ -1,203 +1,13 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin - České Kluby Loga API</title>
<link rel="stylesheet" href="/src/style.css">
</head>
<body class="bg-dark-bg text-white min-h-screen">
<!-- Navigation -->
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<a href="/" class="text-2xl font-bold gradient-text">České Kluby Loga</a>
<div class="flex gap-4">
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">API Docs</a>
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg bg-accent-blue/20 transition-smooth">Admin</a>
</div>
</div>
</div>
</nav>
<!-- Admin Header -->
<header class="border-b border-dark-border bg-dark-card">
<div class="container mx-auto px-6 py-8">
<h1 class="text-3xl font-bold gradient-text mb-2">Administrace</h1>
<p class="text-gray-400">Vyhledejte kluby a nahrajte jejich loga</p>
</div>
</header>
<main class="container mx-auto px-6 py-12">
<!-- Club Search Section -->
<section class="mb-12">
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 class="text-2xl font-bold mb-6">🔍 Vyhledat Klub</h2>
<div class="relative mb-6">
<input
type="text"
id="clubSearch"
placeholder="Hledat české kluby (např. Sparta, Slavia)..."
class="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
>
</div>
<!-- Search Results -->
<div id="searchResults" class="space-y-3">
<!-- Výsledky naplněné JavaScriptem -->
</div>
</div>
</section>
<!-- Upload Section -->
<section id="uploadSection" class="hidden">
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 class="text-2xl font-bold mb-6"><span style="font-size: 30px; display: inline-block; vertical-align: middle; line-height: 1;">⬆️</span> Nahrát Logo</h2>
<form id="uploadForm" class="space-y-6">
<!-- Club UUID (Read-only) -->
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">
UUID Klubu <span class="text-red-500">*</span>
</label>
<input
type="text"
id="clubUuid"
readonly
class="w-full bg-dark-bg/50 border border-dark-border rounded-lg px-4 py-3 text-gray-400 cursor-not-allowed"
>
</div>
<!-- Club Name (Required) -->
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">
Název Klubu <span class="text-red-500">*</span>
</label>
<input
type="text"
id="clubName"
required
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">Povinné: Nahrání bude zamítnuto bez názvu klubu</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 povinný (automatické zamítnutí bez něj)</li>
<li>UUID klubu musí být platné</li>
<li>Akceptovány pouze SVG, PNG a PDF soubory</li>
<li>Doporučeno průhledné pozadí</li>
</ul>
</div>
</form>
</div>
</section>
</main>
<!-- Footer -->
<footer class="border-t border-dark-border mt-20">
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
<p>🇨🇿 České Kluby Loga API | Administrace</p>
</div>
</footer>
<script type="module" src="/src/admin.js"></script>
<body class="bg-dark-bg min-h-screen">
<div id="root"></div>
<script type="module" src="/src/admin-main.tsx"></script>
</body>
</html>
+4 -414
View File
@@ -1,423 +1,13 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Dokumentace - České Kluby Loga API</title>
<link rel="stylesheet" href="/src/style.css">
</head>
<body class="bg-dark-bg text-white min-h-screen">
<!-- Navigation -->
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<a href="/" class="text-2xl font-bold gradient-text">🇨🇿 České Kluby Loga</a>
<div class="flex gap-4">
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg bg-accent-blue/20">API Docs</a>
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Admin</a>
</div>
</div>
</div>
</nav>
<!-- Header -->
<header class="border-b border-dark-border bg-dark-card">
<div class="container mx-auto px-6 py-12">
<h1 class="text-4xl font-bold gradient-text mb-3">📚 API Dokumentace</h1>
<p class="text-xl text-gray-400">Kompletní referenční příručka pro České Kluby Loga API</p>
<div class="mt-6 flex gap-4 items-center flex-wrap">
<div>
<span class="text-sm text-gray-400 mr-2">Frontend:</span>
<code class="bg-dark-bg px-4 py-2 rounded text-accent-blue">http://localhost:3000</code>
</div>
<div>
<span class="text-sm text-gray-400 mr-2">Backend API:</span>
<code class="bg-dark-bg px-4 py-2 rounded text-accent-green">http://localhost:8080</code>
</div>
</div>
<p class="text-sm text-gray-400 mt-3">💡 Ve vývojovém prostředí používejte relativní cesty (např. <code class="text-accent-blue">/logos</code>), Vite proxy je přesměruje na backend</p>
</div>
</header>
<!-- Main Content -->
<main class="container mx-auto px-6 py-12">
<!-- Quick Start -->
<section class="mb-16">
<h2 class="text-3xl font-bold mb-6">🚀 Rychlý Start</h2>
<div class="bg-gradient-to-br from-accent-green/10 to-accent-blue/10 rounded-xl p-6 border-2 border-accent-green/30">
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
<span class="text-2xl">⬆️</span>
Nahrání loga klubu - Základní příkaz
</h3>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">curl -X POST http://localhost:8080/logos/{club-uuid} \
-F "[email protected]" \
-F "club_name=Název Klubu"</code></pre>
<div class="mt-4 space-y-2">
<p class="text-sm text-gray-300"><strong class="text-accent-green">Povinné:</strong> Club UUID v URL, soubor loga (SVG/PNG/PDF), název klubu</p>
<p class="text-sm text-gray-300"><strong class="text-accent-blue">Volitelné:</strong> club_type, club_website, club_city</p>
</div>
</div>
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mt-6">
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
<span class="text-2xl">📥</span>
Stažení loga klubu
</h3>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm"># Přímo z backendu
curl http://localhost:8080/logos/{uuid}
# Přes frontend proxy
curl http://localhost:3000/api/logos/{uuid}</code></pre>
<p class="text-gray-400 mt-3 text-sm">Vrátí PNG obrázek loga (SVG jako fallback)</p>
</div>
</section>
<!-- Endpoints -->
<section class="mb-16">
<h2 class="text-3xl font-bold mb-6">📡 Endpointy</h2>
<!-- List Logos -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<div class="flex items-center gap-3 mb-4">
<span class="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">GET</span>
<code class="text-lg">/logos</code>
</div>
<p class="text-gray-400 mb-4">Seznam všech nahraných log</p>
<div class="bg-dark-bg rounded-lg p-4">
<h4 class="text-sm font-semibold text-gray-400 mb-2">Response 200:</h4>
<pre class="text-sm overflow-x-auto"><code>[
{
"id": "uuid-here",
"club_name": "AC Sparta Praha",
"club_type": "football",
"has_svg": true,
"has_png": true,
"logo_url": "http://localhost:8080/logos/uuid-here",
"created_at": "2024-01-01T12:00:00Z"
}
]</code></pre>
</div>
</div>
<!-- Get Logo File -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<div class="flex items-center gap-3 mb-4">
<span class="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">GET</span>
<code class="text-lg">/logos/:id</code>
</div>
<p class="text-gray-400 mb-4">Získání souboru loga (PNG preferováno, SVG jako fallback)</p>
<h4 class="text-sm font-semibold mb-2">Query Parameters (volitelné):</h4>
<div class="bg-dark-bg rounded-lg p-4 mb-4">
<code class="text-sm">format</code> <span class="text-gray-500">string</span> - "png" nebo "svg"
</div>
<div class="bg-dark-bg rounded-lg p-4">
<h4 class="text-sm font-semibold text-gray-400 mb-2">Response 200:</h4>
<p class="text-sm text-gray-400">Binární data obrázku (image/png nebo image/svg+xml)</p>
</div>
</div>
<!-- Get Logo Metadata -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<div class="flex items-center gap-3 mb-4">
<span class="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">GET</span>
<code class="text-lg">/logos/:id/json</code>
</div>
<p class="text-gray-400 mb-4">Získání metadat loga ve formátu JSON</p>
<div class="bg-dark-bg rounded-lg p-4">
<h4 class="text-sm font-semibold text-gray-400 mb-2">Response 200:</h4>
<pre class="text-sm overflow-x-auto"><code>{
"id": "uuid-here",
"club_name": "AC Sparta Praha",
"club_type": "football",
"club_website": "https://sparta.cz",
"has_svg": true,
"has_png": true,
"primary_format": "png",
"logo_url": "http://localhost:8080/logos/uuid-here",
"logo_url_svg": "http://localhost:8080/logos/uuid-here?format=svg",
"logo_url_png": "http://localhost:8080/logos/uuid-here?format=png",
"file_size_svg": 12345,
"file_size_png": 54321,
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}</code></pre>
</div>
</div>
<!-- Upload Logo -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6 border-2 border-accent-green/40">
<div class="flex items-center gap-3 mb-4">
<span class="px-3 py-1 bg-accent-green/20 text-accent-green rounded font-mono text-sm">POST</span>
<code class="text-lg">/logos/:id</code>
</div>
<p class="text-gray-400 mb-4">Nahrání nového loga klubu s kompletními daty (ID klubu, název, logo soubory)</p>
<h4 class="text-sm font-semibold mb-2">URL Parameters:</h4>
<div class="bg-dark-bg rounded-lg p-4 mb-4">
<code class="text-sm">:id</code> <span class="text-red-400">*</span> <span class="text-gray-500">UUID</span> - Jedinečné ID klubu (např. <code class="text-xs">550e8400-e29b-41d4-a716-446655440000</code>)
</div>
<h4 class="text-sm font-semibold mb-2">Content-Type:</h4>
<div class="bg-dark-bg rounded-lg p-4 mb-4">
<code class="text-sm">multipart/form-data</code>
</div>
<h4 class="text-sm font-semibold mb-2">Form Data (Povinné pole):</h4>
<div class="bg-dark-bg rounded-lg p-4 mb-4 space-y-3">
<div class="border-l-2 border-red-400 pl-3">
<code class="text-sm font-semibold text-red-400">file</code> <span class="text-red-400">*</span> <span class="text-gray-500">file (SVG nebo PNG)</span>
<p class="text-xs text-gray-500 mt-1">Soubor loga. Podporované formáty: SVG (doporučeno), PNG, PDF</p>
</div>
<div class="border-l-2 border-red-400 pl-3">
<code class="text-sm font-semibold text-red-400">club_name</code> <span class="text-red-400">*</span> <span class="text-gray-500">string</span>
<p class="text-xs text-gray-500 mt-1">Název klubu (např. "AC Sparta Praha")</p>
</div>
</div>
<h4 class="text-sm font-semibold mb-2">Form Data (Volitelné):</h4>
<div class="bg-dark-bg rounded-lg p-4 mb-4 space-y-3">
<div class="border-l-2 border-blue-400 pl-3">
<code class="text-sm">club_type</code> <span class="text-gray-500">string</span>
<p class="text-xs text-gray-500 mt-1">Typ klubu: <code>"football"</code> (výchozí) nebo <code>"futsal"</code></p>
</div>
<div class="border-l-2 border-blue-400 pl-3">
<code class="text-sm">club_website</code> <span class="text-gray-500">string</span>
<p class="text-xs text-gray-500 mt-1">URL webové stránky klubu (např. "https://sparta.cz")</p>
</div>
<div class="border-l-2 border-blue-400 pl-3">
<code class="text-sm">club_city</code> <span class="text-gray-500">string</span>
<p class="text-xs text-gray-500 mt-1">Město klubu (např. "Praha")</p>
</div>
</div>
<div class="bg-dark-bg rounded-lg p-4 mb-4">
<h4 class="text-sm font-semibold text-gray-400 mb-2">Response 200 (Úspěch):</h4>
<pre class="text-sm overflow-x-auto"><code>{
"success": true,
"id": "550e8400-e29b-41d4-a716-446655440000",
"club_name": "AC Sparta Praha",
"has_svg": true,
"has_png": true,
"size_svg": 12543,
"size_png": 45210,
"message": "logo uploaded successfully"
}</code></pre>
</div>
<div class="bg-red-900/20 rounded-lg p-4 border border-red-600/30">
<h4 class="text-sm font-semibold text-red-400 mb-2">Response 400 (Chyba):</h4>
<pre class="text-sm overflow-x-auto"><code>{
"error": "club_name is required"
}</code></pre>
<p class="text-xs text-gray-400 mt-2">Možné chyby: <code>"no file provided"</code>, <code>"invalid UUID format"</code>, <code>"only .svg, .png and .pdf files are allowed"</code></p>
</div>
</div>
</section>
<!-- Examples -->
<section class="mb-16">
<h2 class="text-3xl font-bold mb-6">💡 Příklady Použití - Nahrání Loga</h2>
<!-- cURL Example -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
<span>🔧</span> cURL (Terminal)
</h3>
<div class="space-y-4">
<div>
<h4 class="text-sm font-semibold mb-2 text-accent-green">Minimální nahrání (pouze povinná pole):</h4>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">curl -X POST http://localhost:8080/logos/550e8400-e29b-41d4-a716-446655440000 \
-F "file=@sparta_logo.svg" \
-F "club_name=AC Sparta Praha"</code></pre>
</div>
<div>
<h4 class="text-sm font-semibold mb-2 text-accent-blue">Kompletní nahrání (všechna data):</h4>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">curl -X POST http://localhost:8080/logos/550e8400-e29b-41d4-a716-446655440000 \
-F "file=@sparta_logo.svg" \
-F "club_name=AC Sparta Praha" \
-F "club_type=football" \
-F "club_website=https://sparta.cz" \
-F "club_city=Praha"</code></pre>
</div>
<div>
<h4 class="text-sm font-semibold mb-2 text-gray-400">Nahrání PNG místo SVG:</h4>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">curl -X POST http://localhost:8080/logos/550e8400-e29b-41d4-a716-446655440000 \
-F "file=@sparta_logo.png" \
-F "club_name=AC Sparta Praha"</code></pre>
</div>
</div>
</div>
<!-- JavaScript Example -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
<span>📜</span> JavaScript (Fetch API)
</h3>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">// Funkce pro nahrání loga s kompletními daty
async function uploadClubLogo(clubId, file, clubData) {
const formData = new FormData();
// Povinná pole
formData.append('file', file);
formData.append('club_name', clubData.name);
// Volitelná pole
if (clubData.type) formData.append('club_type', clubData.type);
if (clubData.website) formData.append('club_website', clubData.website);
if (clubData.city) formData.append('club_city', clubData.city);
const response = await fetch(`http://localhost:8080/logos/${clubId}`, {
method: 'POST',
body: formData
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
return await response.json();
}
// Použití s file input
const fileInput = document.getElementById('logoFile');
const clubId = '550e8400-e29b-41d4-a716-446655440000';
const result = await uploadClubLogo(clubId, fileInput.files[0], {
name: 'AC Sparta Praha',
type: 'football',
website: 'https://sparta.cz',
city: 'Praha'
});
console.log('Upload successful:', result);</code></pre>
</div>
<!-- Python Example -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
<span>🐍</span> Python (requests)
</h3>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">import requests
def upload_club_logo(club_id, file_path, club_name, **optional_data):
"""
Nahraje logo klubu s kompletními daty
Args:
club_id: UUID klubu
file_path: Cesta k souboru loga
club_name: Název klubu (povinný)
**optional_data: club_type, club_website, club_city
"""
with open(file_path, 'rb') as f:
files = {'file': f}
data = {'club_name': club_name}
data.update(optional_data)
response = requests.post(
f"http://localhost:8080/logos/{club_id}",
files=files,
data=data
)
response.raise_for_status()
return response.json()
# Použití
result = upload_club_logo(
club_id='550e8400-e29b-41d4-a716-446655440000',
file_path='sparta_logo.svg',
club_name='AC Sparta Praha',
club_type='football',
club_website='https://sparta.cz',
club_city='Praha'
)
print(f"Upload úspěšný: {result['message']}")
print(f"Has SVG: {result['has_svg']}, Has PNG: {result['has_png']}")</code></pre>
</div>
<!-- PowerShell Example -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
<span>💻</span> PowerShell
</h3>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm"># Nahrání loga s kompletními daty
$clubId = "550e8400-e29b-41d4-a716-446655440000"
$logoFile = "C:\logos\sparta_logo.svg"
$form = @{
file = Get-Item -Path $logoFile
club_name = "AC Sparta Praha"
club_type = "football"
club_website = "https://sparta.cz"
club_city = "Praha"
}
$result = Invoke-RestMethod `
-Uri "http://localhost:8080/logos/$clubId" `
-Method Post `
-Form $form
Write-Host "Upload úspěšný: $($result.message)" -ForegroundColor Green
Write-Host "Club: $($result.club_name)" -ForegroundColor Cyan</code></pre>
</div>
</section>
<!-- Error Codes -->
<section>
<h2 class="text-3xl font-bold mb-6">⚠️ Chybové Kódy</h2>
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<div class="space-y-4">
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-green-600/20 text-green-400 rounded text-sm font-mono">200</span>
<div>
<h4 class="font-semibold">OK</h4>
<p class="text-gray-400 text-sm">Požadavek úspěšně dokončen</p>
</div>
</div>
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">400</span>
<div>
<h4 class="font-semibold">Bad Request</h4>
<p class="text-gray-400 text-sm">Neplatné parametry nebo chybějící povinná pole</p>
</div>
</div>
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">404</span>
<div>
<h4 class="font-semibold">Not Found</h4>
<p class="text-gray-400 text-sm">Logo nebo klub nenalezen</p>
</div>
</div>
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">500</span>
<div>
<h4 class="font-semibold">Internal Server Error</h4>
<p class="text-gray-400 text-sm">Interní chyba serveru</p>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Footer -->
<footer class="border-t border-dark-border mt-20">
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
<p>🇨🇿 České Kluby Loga API</p>
</div>
</footer>
<body class="bg-dark-bg min-h-screen">
<div id="root"></div>
<script type="module" src="/src/docs-main.tsx"></script>
</body>
</html>
+5 -173
View File
@@ -1,181 +1,13 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🇨🇿 České Kluby Loga API</title>
<title>České Kluby Loga API</title>
<link rel="stylesheet" href="/src/style.css">
</head>
<body class="bg-dark-bg text-white min-h-screen">
<!-- Navigation -->
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<a href="/" class="text-2xl font-bold gradient-text">🇨🇿 České Kluby Loga</a>
<div class="flex gap-4">
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">API Docs</a>
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Admin</a>
</div>
</div>
</div>
</nav>
<!-- Hero Section -->
<header class="relative overflow-hidden border-b border-dark-border">
<div class="absolute inset-0 bg-gradient-to-br from-blue-600/10 to-green-600/10"></div>
<div class="container mx-auto px-6 py-20 relative z-10">
<div class="text-center hero-content max-w-4xl mx-auto">
<h1 class="text-5xl md:text-7xl font-bold mb-6">
<span class="gradient-text">České Kluby Loga CDN</span>
</h1>
<p class="text-xl text-gray-400 mb-8">
Vysoce kvalitní loga českých fotbalových a futsalových klubů s průhledným pozadím.
Založeno na UUID, API-first, připraveno pro produkci.
</p>
<div class="flex flex-wrap gap-4 justify-center">
<button id="browseBtn" class="px-8 py-4 bg-accent-blue rounded-lg font-semibold hover:bg-blue-600 transition-smooth text-lg">
🔍 Procházet Loga
</button>
<a href="/admin.html" class="px-8 py-4 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth text-lg">
⬆️ Nahrát Logo
</a>
</div>
</div>
</div>
</header>
<!-- Logo Gallery -->
<section class="container mx-auto px-6 py-16" id="logoGallery">
<div class="mb-8">
<h2 class="text-3xl font-bold mb-4">Dostupná Loga Klubů</h2>
<input
type="text"
id="gallerySearch"
placeholder="Filtrovat podle názvu klubu..."
class="w-full max-w-md bg-dark-card border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
>
</div>
<div id="logoGrid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-6">
<!-- Logos will be loaded here -->
</div>
<div id="loadingState" class="text-center py-16">
<div class="spinner mx-auto"></div>
<p class="mt-4 text-gray-400">Načítání log klubů...</p>
</div>
<div id="emptyState" class="text-center py-16 hidden">
<div class="text-6xl mb-4"></div>
<p class="text-xl text-gray-400 mb-4">Zatím nebyla nahrána žádná loga</p>
<a href="/admin.html" class="px-6 py-3 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth inline-block">
Nahrát První Logo
</a>
</div>
</section>
<!-- API Documentation Preview -->
<section class="bg-dark-card border-y border-dark-border py-16">
<div class="container mx-auto px-6">
<h2 class="text-3xl font-bold mb-8 text-center">Rychlá Referenční API</h2>
<div class="grid md:grid-cols-2 gap-6 max-w-4xl mx-auto">
<div class="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">GET</span>
<div class="flex-1">
<p class="font-mono text-sm mb-2">/logos</p>
<p class="text-gray-400 text-sm">Zobrazit všechna dostupná loga</p>
</div>
</div>
</div>
<div class="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">GET</span>
<div class="flex-1">
<p class="font-mono text-sm mb-2">/logos/:id</p>
<p class="text-gray-400 text-sm">Získat logo podle UUID (PNG/SVG)</p>
</div>
</div>
</div>
<div class="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">GET</span>
<div class="flex-1">
<p class="font-mono text-sm mb-2">/logos/:id/json</p>
<p class="text-gray-400 text-sm">Získat metadata loga</p>
</div>
</div>
</div>
<div class="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-accent-green/20 text-accent-green rounded-md text-sm font-mono">POST</span>
<div class="flex-1">
<p class="font-mono text-sm mb-2">/logos/:id</p>
<p class="text-gray-400 text-sm">Nahrát nové logo</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="container mx-auto px-6 py-16">
<h2 class="text-3xl font-bold mb-12 text-center">✨ Funkce</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<div class="text-3xl mb-4"></div>
<h3 class="text-xl font-semibold mb-2">Integrace s FAČR</h3>
<p class="text-gray-400">Přímá integrace s oficiálním českým fotbalovým registrem</p>
</div>
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<div class="text-3xl mb-4">🖼️</div>
<h3 class="text-xl font-semibold mb-2">SVG & PNG</h3>
<p class="text-gray-400">Nahrajte SVG, PNG se vygeneruje automaticky</p>
</div>
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<div class="text-3xl mb-4">🔄</div>
<h3 class="text-xl font-semibold mb-2">Založeno na UUID</h3>
<p class="text-gray-400">Konzistentní identifikace napříč všemi platformami</p>
</div>
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<div class="text-3xl mb-4">🌐</div>
<h3 class="text-xl font-semibold mb-2">Připraveno pro CDN</h3>
<p class="text-gray-400">Rychlé, cachovatelné, produkční API</p>
</div>
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<div class="text-3xl mb-4">📝</div>
<h3 class="text-xl font-semibold mb-2">Bohatá Metadata</h3>
<p class="text-gray-400">Název klubu, město, typ, web v ceně</p>
</div>
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<div class="text-3xl mb-4">🐳</div>
<h3 class="text-xl font-semibold mb-2">Připraveno pro Docker</h3>
<p class="text-gray-400">Nasazení jedním příkazem s Docker Compose</p>
</div>
</div>
</section>
<!-- Footer -->
<footer class="border-t border-dark-border mt-20">
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
<p>🇨🇿 České Kluby Loga API | Vytvořeno s ❤️ pro český fotbal</p>
<p class="text-sm mt-2">Poháněno FAČR Scraper API | Open Source MIT Licence</p>
</div>
</footer>
<script type="module" src="/src/home.js"></script>
<body class="bg-dark-bg min-h-screen">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+4 -153
View File
@@ -1,162 +1,13 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Detail Loga - České Kluby Loga API</title>
<link rel="stylesheet" href="/src/style.css">
</head>
<body class="bg-dark-bg text-white min-h-screen">
<!-- Navigation -->
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<a href="/" class="text-2xl font-bold gradient-text">🇨🇿 České Kluby Loga</a>
<div class="flex gap-4">
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">API Docs</a>
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Admin</a>
</div>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="container mx-auto px-6 py-12">
<!-- Loading State -->
<div id="loadingState" class="text-center py-12">
<div class="spinner mx-auto mb-4"></div>
<p class="text-gray-400">Načítání...</p>
</div>
<!-- Error State -->
<div id="errorState" class="hidden text-center py-12">
<svg class="mx-auto h-16 w-16 text-red-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h2 class="text-2xl font-bold mb-2">Logo nenalezeno</h2>
<p class="text-gray-400 mb-4">Logo s tímto UUID neexistuje</p>
<a href="/" class="px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth inline-block">
Zpět na hlavní stránku
</a>
</div>
<!-- Logo Detail -->
<div id="logoDetail" class="hidden">
<!-- Header -->
<div class="flex items-start justify-between mb-8">
<div>
<h1 id="clubName" class="text-4xl font-bold gradient-text mb-2"></h1>
<p id="clubMeta" class="text-gray-400"></p>
</div>
<a href="/admin.html" class="px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth">
✏️ Upravit
</a>
</div>
<!-- Logo Preview -->
<section class="mb-8">
<div class="bg-dark-card rounded-xl p-8 border border-dark-border">
<h2 class="text-2xl font-bold mb-6">📷 Náhled Loga</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Light Background -->
<div class="bg-white rounded-lg p-8 flex items-center justify-center min-h-[300px]">
<img id="logoPreviewLight" src="" alt="Logo na světlém pozadí" class="max-w-full max-h-64 object-contain">
</div>
<!-- Dark Background -->
<div class="bg-gray-900 rounded-lg p-8 flex items-center justify-center min-h-[300px]">
<img id="logoPreviewDark" src="" alt="Logo na tmavém pozadí" class="max-w-full max-h-64 object-contain">
</div>
</div>
</div>
</section>
<!-- Available Formats -->
<section class="mb-8">
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 class="text-2xl font-bold mb-6">💾 Dostupné Formáty</h2>
<div id="formatsGrid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Formats will be populated here -->
</div>
</div>
</section>
<!-- Variants -->
<section class="mb-8" id="variantsSection">
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 class="text-2xl font-bold mb-6">🎨 Varianty Loga</h2>
<div id="variantsGrid" class="space-y-4">
<!-- Variants will be populated here -->
</div>
</div>
</section>
<!-- Metadata -->
<section class="mb-8">
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 class="text-2xl font-bold mb-6">️ Informace</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 class="text-sm font-medium text-gray-400 mb-2">UUID</h3>
<p id="logoUuid" class="font-mono text-sm bg-dark-bg rounded px-3 py-2"></p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-400 mb-2">Typ Klubu</h3>
<p id="clubType" class="text-sm bg-dark-bg rounded px-3 py-2"></p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-400 mb-2">Webová Stránka</h3>
<p id="clubWebsite" class="text-sm bg-dark-bg rounded px-3 py-2"></p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-400 mb-2">Datum Nahrání</h3>
<p id="uploadDate" class="text-sm bg-dark-bg rounded px-3 py-2"></p>
</div>
</div>
</div>
</section>
<!-- API Usage -->
<section>
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 class="text-2xl font-bold mb-6">🔗 Použití API</h2>
<div class="space-y-4">
<div>
<h3 class="text-sm font-medium text-gray-400 mb-2">GET Logo (PNG preferováno)</h3>
<div class="bg-dark-bg rounded px-4 py-3 font-mono text-sm flex items-center justify-between">
<code id="apiUrlDefault"></code>
<button onclick="copyToClipboard('apiUrlDefault')" class="px-3 py-1 bg-accent-blue rounded text-xs hover:bg-blue-600 transition-smooth">
Kopírovat
</button>
</div>
</div>
<div>
<h3 class="text-sm font-medium text-gray-400 mb-2">GET Logo s Metadaty (JSON)</h3>
<div class="bg-dark-bg rounded px-4 py-3 font-mono text-sm flex items-center justify-between">
<code id="apiUrlJson"></code>
<button onclick="copyToClipboard('apiUrlJson')" class="px-3 py-1 bg-accent-blue rounded text-xs hover:bg-blue-600 transition-smooth">
Kopírovat
</button>
</div>
</div>
</div>
</div>
</section>
</div>
</main>
<!-- Footer -->
<footer class="border-t border-dark-border mt-20">
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
<p>🇨🇿 České Kluby Loga API</p>
</div>
</footer>
<script type="module" src="/src/logo.js"></script>
<body class="bg-dark-bg min-h-screen">
<div id="root"></div>
<script type="module" src="/src/logo-main.tsx"></script>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Všechna Loga - České Kluby Loga API</title>
<link rel="stylesheet" href="/src/style.css">
</head>
<body class="bg-dark-bg min-h-screen">
<div id="root"></div>
<script type="module" src="/src/logos-main.tsx"></script>
</body>
</html>
+33
View File
@@ -16,6 +16,15 @@ server {
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' '*' always;
add_header 'Access-Control-Expose-Headers' '*' always;
add_header 'Access-Control-Max-Age' '3600' always;
if ($request_method = 'OPTIONS') {
return 204;
}
}
# API proxy to backend
@@ -30,10 +39,33 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# CORS headers - Allow all origins
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD' always;
add_header 'Access-Control-Allow-Headers' '*' always;
add_header 'Access-Control-Expose-Headers' '*' always;
add_header 'Access-Control-Max-Age' '3600' always;
# Handle preflight requests
if ($request_method = 'OPTIONS') {
return 204;
}
}
# SPA fallback
location / {
# CORS headers - Allow all origins for static content
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD' always;
add_header 'Access-Control-Allow-Headers' '*' always;
add_header 'Access-Control-Expose-Headers' '*' always;
add_header 'Access-Control-Max-Age' '3600' always;
if ($request_method = 'OPTIONS') {
return 204;
}
try_files $uri $uri/ /index.html;
}
@@ -42,3 +74,4 @@ server {
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\autoprefixer\bin\autoprefixer" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\autoprefixer\bin\autoprefixer" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\baseline-browser-mapping\dist\cli.js" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\baseline-browser-mapping\dist\cli.js" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\browserslist\cli.js" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\browserslist\cli.js" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\cssesc\bin\cssesc" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\cssesc\bin\cssesc" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\esbuild\bin\esbuild" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\esbuild\bin\esbuild" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\glob\dist\esm\bin.mjs" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\glob\dist\esm\bin.mjs" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\jiti\bin\jiti.js" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\jiti\bin\jiti.js" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nanoid\bin\nanoid.cjs" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nanoid\bin\nanoid.cjs" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\which\bin\node-which" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\which\bin\node-which" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\resolve\bin\resolve" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\resolve\bin\resolve" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rollup\dist\bin\rollup" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rollup\dist\bin\rollup" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\sucrase\bin\sucrase-node" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\sucrase\bin\sucrase-node" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\sucrase\bin\sucrase" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\sucrase\bin\sucrase" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\tailwindcss\lib\cli.js" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\tailwindcss\lib\cli.js" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\tailwindcss\lib\cli.js" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\tailwindcss\lib\cli.js" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\update-browserslist-db\cli.js" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\update-browserslist-db\cli.js" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\vite\bin\vite.js" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\vite\bin\vite.js" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\yaml\bin.mjs" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\yaml\bin.mjs" %*
+240
View File
@@ -17,6 +17,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
@@ -140,6 +157,40 @@
"node": ">=14"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
"dev": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz",
"integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz",
"integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz",
@@ -168,6 +219,91 @@
"win32"
]
},
"node_modules/@swc/core": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.3.tgz",
"integrity": "sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.25"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.15.3",
"@swc/core-darwin-x64": "1.15.3",
"@swc/core-linux-arm-gnueabihf": "1.15.3",
"@swc/core-linux-arm64-gnu": "1.15.3",
"@swc/core-linux-arm64-musl": "1.15.3",
"@swc/core-linux-x64-gnu": "1.15.3",
"@swc/core-linux-x64-musl": "1.15.3",
"@swc/core-win32-arm64-msvc": "1.15.3",
"@swc/core-win32-ia32-msvc": "1.15.3",
"@swc/core-win32-x64-msvc": "1.15.3"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.3.tgz",
"integrity": "sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.3.tgz",
"integrity": "sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"dev": true
},
"node_modules/@swc/types": {
"version": "0.1.25",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
"dev": true,
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -175,6 +311,44 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true
},
"node_modules/@types/react": {
"version": "18.3.27",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/@vitejs/plugin-react-swc": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz",
"integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==",
"dev": true,
"dependencies": {
"@rolldown/pluginutils": "1.0.0-beta.27",
"@swc/core": "^1.12.11"
},
"peerDependencies": {
"vite": "^4 || ^5 || ^6 || ^7"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -481,6 +655,12 @@
"node": ">=4"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -817,6 +997,11 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -837,6 +1022,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -1230,6 +1426,29 @@
],
"license": "MIT"
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -1351,6 +1570,14 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -1618,6 +1845,19 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+15 -15
View File
@@ -1,15 +1,15 @@
# Installation
> `npm install --save @types/estree`
# Summary
This package contains type definitions for estree (https://github.com/estree/estree).
# Details
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/estree.
### Additional Details
* Last updated: Fri, 06 Jun 2025 00:04:33 GMT
* Dependencies: none
# Credits
These definitions were written by [RReverser](https://github.com/RReverser).
# Installation
> `npm install --save @types/estree`
# Summary
This package contains type definitions for estree (https://github.com/estree/estree).
# Details
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/estree.
### Additional Details
* Last updated: Fri, 06 Jun 2025 00:04:33 GMT
* Dependencies: none
# Credits
These definitions were written by [RReverser](https://github.com/RReverser).
+7 -7
View File
@@ -1,8 +1,8 @@
The MIT License (MIT)
Copyright (c) 2015 Dmitry Ivanov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The MIT License (MIT)
Copyright (c) 2015 Dmitry Ivanov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+11 -11
View File
@@ -1,11 +1,11 @@
A JSON with color names and its values. Based on http://dev.w3.org/csswg/css-color/#named-colors.
[![NPM](https://nodei.co/npm/color-name.png?mini=true)](https://nodei.co/npm/color-name/)
```js
var colors = require('color-name');
colors.red //[255,0,0]
```
<a href="LICENSE"><img src="https://upload.wikimedia.org/wikipedia/commons/0/0c/MIT_logo.svg" width="120"/></a>
A JSON with color names and its values. Based on http://dev.w3.org/csswg/css-color/#named-colors.
[![NPM](https://nodei.co/npm/color-name.png?mini=true)](https://nodei.co/npm/color-name/)
```js
var colors = require('color-name');
colors.red //[255,0,0]
```
<a href="LICENSE"><img src="https://upload.wikimedia.org/wikipedia/commons/0/0c/MIT_logo.svg" width="120"/></a>
+152 -152
View File
@@ -1,152 +1,152 @@
'use strict'
module.exports = {
"aliceblue": [240, 248, 255],
"antiquewhite": [250, 235, 215],
"aqua": [0, 255, 255],
"aquamarine": [127, 255, 212],
"azure": [240, 255, 255],
"beige": [245, 245, 220],
"bisque": [255, 228, 196],
"black": [0, 0, 0],
"blanchedalmond": [255, 235, 205],
"blue": [0, 0, 255],
"blueviolet": [138, 43, 226],
"brown": [165, 42, 42],
"burlywood": [222, 184, 135],
"cadetblue": [95, 158, 160],
"chartreuse": [127, 255, 0],
"chocolate": [210, 105, 30],
"coral": [255, 127, 80],
"cornflowerblue": [100, 149, 237],
"cornsilk": [255, 248, 220],
"crimson": [220, 20, 60],
"cyan": [0, 255, 255],
"darkblue": [0, 0, 139],
"darkcyan": [0, 139, 139],
"darkgoldenrod": [184, 134, 11],
"darkgray": [169, 169, 169],
"darkgreen": [0, 100, 0],
"darkgrey": [169, 169, 169],
"darkkhaki": [189, 183, 107],
"darkmagenta": [139, 0, 139],
"darkolivegreen": [85, 107, 47],
"darkorange": [255, 140, 0],
"darkorchid": [153, 50, 204],
"darkred": [139, 0, 0],
"darksalmon": [233, 150, 122],
"darkseagreen": [143, 188, 143],
"darkslateblue": [72, 61, 139],
"darkslategray": [47, 79, 79],
"darkslategrey": [47, 79, 79],
"darkturquoise": [0, 206, 209],
"darkviolet": [148, 0, 211],
"deeppink": [255, 20, 147],
"deepskyblue": [0, 191, 255],
"dimgray": [105, 105, 105],
"dimgrey": [105, 105, 105],
"dodgerblue": [30, 144, 255],
"firebrick": [178, 34, 34],
"floralwhite": [255, 250, 240],
"forestgreen": [34, 139, 34],
"fuchsia": [255, 0, 255],
"gainsboro": [220, 220, 220],
"ghostwhite": [248, 248, 255],
"gold": [255, 215, 0],
"goldenrod": [218, 165, 32],
"gray": [128, 128, 128],
"green": [0, 128, 0],
"greenyellow": [173, 255, 47],
"grey": [128, 128, 128],
"honeydew": [240, 255, 240],
"hotpink": [255, 105, 180],
"indianred": [205, 92, 92],
"indigo": [75, 0, 130],
"ivory": [255, 255, 240],
"khaki": [240, 230, 140],
"lavender": [230, 230, 250],
"lavenderblush": [255, 240, 245],
"lawngreen": [124, 252, 0],
"lemonchiffon": [255, 250, 205],
"lightblue": [173, 216, 230],
"lightcoral": [240, 128, 128],
"lightcyan": [224, 255, 255],
"lightgoldenrodyellow": [250, 250, 210],
"lightgray": [211, 211, 211],
"lightgreen": [144, 238, 144],
"lightgrey": [211, 211, 211],
"lightpink": [255, 182, 193],
"lightsalmon": [255, 160, 122],
"lightseagreen": [32, 178, 170],
"lightskyblue": [135, 206, 250],
"lightslategray": [119, 136, 153],
"lightslategrey": [119, 136, 153],
"lightsteelblue": [176, 196, 222],
"lightyellow": [255, 255, 224],
"lime": [0, 255, 0],
"limegreen": [50, 205, 50],
"linen": [250, 240, 230],
"magenta": [255, 0, 255],
"maroon": [128, 0, 0],
"mediumaquamarine": [102, 205, 170],
"mediumblue": [0, 0, 205],
"mediumorchid": [186, 85, 211],
"mediumpurple": [147, 112, 219],
"mediumseagreen": [60, 179, 113],
"mediumslateblue": [123, 104, 238],
"mediumspringgreen": [0, 250, 154],
"mediumturquoise": [72, 209, 204],
"mediumvioletred": [199, 21, 133],
"midnightblue": [25, 25, 112],
"mintcream": [245, 255, 250],
"mistyrose": [255, 228, 225],
"moccasin": [255, 228, 181],
"navajowhite": [255, 222, 173],
"navy": [0, 0, 128],
"oldlace": [253, 245, 230],
"olive": [128, 128, 0],
"olivedrab": [107, 142, 35],
"orange": [255, 165, 0],
"orangered": [255, 69, 0],
"orchid": [218, 112, 214],
"palegoldenrod": [238, 232, 170],
"palegreen": [152, 251, 152],
"paleturquoise": [175, 238, 238],
"palevioletred": [219, 112, 147],
"papayawhip": [255, 239, 213],
"peachpuff": [255, 218, 185],
"peru": [205, 133, 63],
"pink": [255, 192, 203],
"plum": [221, 160, 221],
"powderblue": [176, 224, 230],
"purple": [128, 0, 128],
"rebeccapurple": [102, 51, 153],
"red": [255, 0, 0],
"rosybrown": [188, 143, 143],
"royalblue": [65, 105, 225],
"saddlebrown": [139, 69, 19],
"salmon": [250, 128, 114],
"sandybrown": [244, 164, 96],
"seagreen": [46, 139, 87],
"seashell": [255, 245, 238],
"sienna": [160, 82, 45],
"silver": [192, 192, 192],
"skyblue": [135, 206, 235],
"slateblue": [106, 90, 205],
"slategray": [112, 128, 144],
"slategrey": [112, 128, 144],
"snow": [255, 250, 250],
"springgreen": [0, 255, 127],
"steelblue": [70, 130, 180],
"tan": [210, 180, 140],
"teal": [0, 128, 128],
"thistle": [216, 191, 216],
"tomato": [255, 99, 71],
"turquoise": [64, 224, 208],
"violet": [238, 130, 238],
"wheat": [245, 222, 179],
"white": [255, 255, 255],
"whitesmoke": [245, 245, 245],
"yellow": [255, 255, 0],
"yellowgreen": [154, 205, 50]
};
'use strict'
module.exports = {
"aliceblue": [240, 248, 255],
"antiquewhite": [250, 235, 215],
"aqua": [0, 255, 255],
"aquamarine": [127, 255, 212],
"azure": [240, 255, 255],
"beige": [245, 245, 220],
"bisque": [255, 228, 196],
"black": [0, 0, 0],
"blanchedalmond": [255, 235, 205],
"blue": [0, 0, 255],
"blueviolet": [138, 43, 226],
"brown": [165, 42, 42],
"burlywood": [222, 184, 135],
"cadetblue": [95, 158, 160],
"chartreuse": [127, 255, 0],
"chocolate": [210, 105, 30],
"coral": [255, 127, 80],
"cornflowerblue": [100, 149, 237],
"cornsilk": [255, 248, 220],
"crimson": [220, 20, 60],
"cyan": [0, 255, 255],
"darkblue": [0, 0, 139],
"darkcyan": [0, 139, 139],
"darkgoldenrod": [184, 134, 11],
"darkgray": [169, 169, 169],
"darkgreen": [0, 100, 0],
"darkgrey": [169, 169, 169],
"darkkhaki": [189, 183, 107],
"darkmagenta": [139, 0, 139],
"darkolivegreen": [85, 107, 47],
"darkorange": [255, 140, 0],
"darkorchid": [153, 50, 204],
"darkred": [139, 0, 0],
"darksalmon": [233, 150, 122],
"darkseagreen": [143, 188, 143],
"darkslateblue": [72, 61, 139],
"darkslategray": [47, 79, 79],
"darkslategrey": [47, 79, 79],
"darkturquoise": [0, 206, 209],
"darkviolet": [148, 0, 211],
"deeppink": [255, 20, 147],
"deepskyblue": [0, 191, 255],
"dimgray": [105, 105, 105],
"dimgrey": [105, 105, 105],
"dodgerblue": [30, 144, 255],
"firebrick": [178, 34, 34],
"floralwhite": [255, 250, 240],
"forestgreen": [34, 139, 34],
"fuchsia": [255, 0, 255],
"gainsboro": [220, 220, 220],
"ghostwhite": [248, 248, 255],
"gold": [255, 215, 0],
"goldenrod": [218, 165, 32],
"gray": [128, 128, 128],
"green": [0, 128, 0],
"greenyellow": [173, 255, 47],
"grey": [128, 128, 128],
"honeydew": [240, 255, 240],
"hotpink": [255, 105, 180],
"indianred": [205, 92, 92],
"indigo": [75, 0, 130],
"ivory": [255, 255, 240],
"khaki": [240, 230, 140],
"lavender": [230, 230, 250],
"lavenderblush": [255, 240, 245],
"lawngreen": [124, 252, 0],
"lemonchiffon": [255, 250, 205],
"lightblue": [173, 216, 230],
"lightcoral": [240, 128, 128],
"lightcyan": [224, 255, 255],
"lightgoldenrodyellow": [250, 250, 210],
"lightgray": [211, 211, 211],
"lightgreen": [144, 238, 144],
"lightgrey": [211, 211, 211],
"lightpink": [255, 182, 193],
"lightsalmon": [255, 160, 122],
"lightseagreen": [32, 178, 170],
"lightskyblue": [135, 206, 250],
"lightslategray": [119, 136, 153],
"lightslategrey": [119, 136, 153],
"lightsteelblue": [176, 196, 222],
"lightyellow": [255, 255, 224],
"lime": [0, 255, 0],
"limegreen": [50, 205, 50],
"linen": [250, 240, 230],
"magenta": [255, 0, 255],
"maroon": [128, 0, 0],
"mediumaquamarine": [102, 205, 170],
"mediumblue": [0, 0, 205],
"mediumorchid": [186, 85, 211],
"mediumpurple": [147, 112, 219],
"mediumseagreen": [60, 179, 113],
"mediumslateblue": [123, 104, 238],
"mediumspringgreen": [0, 250, 154],
"mediumturquoise": [72, 209, 204],
"mediumvioletred": [199, 21, 133],
"midnightblue": [25, 25, 112],
"mintcream": [245, 255, 250],
"mistyrose": [255, 228, 225],
"moccasin": [255, 228, 181],
"navajowhite": [255, 222, 173],
"navy": [0, 0, 128],
"oldlace": [253, 245, 230],
"olive": [128, 128, 0],
"olivedrab": [107, 142, 35],
"orange": [255, 165, 0],
"orangered": [255, 69, 0],
"orchid": [218, 112, 214],
"palegoldenrod": [238, 232, 170],
"palegreen": [152, 251, 152],
"paleturquoise": [175, 238, 238],
"palevioletred": [219, 112, 147],
"papayawhip": [255, 239, 213],
"peachpuff": [255, 218, 185],
"peru": [205, 133, 63],
"pink": [255, 192, 203],
"plum": [221, 160, 221],
"powderblue": [176, 224, 230],
"purple": [128, 0, 128],
"rebeccapurple": [102, 51, 153],
"red": [255, 0, 0],
"rosybrown": [188, 143, 143],
"royalblue": [65, 105, 225],
"saddlebrown": [139, 69, 19],
"salmon": [250, 128, 114],
"sandybrown": [244, 164, 96],
"seagreen": [46, 139, 87],
"seashell": [255, 245, 238],
"sienna": [160, 82, 45],
"silver": [192, 192, 192],
"skyblue": [135, 206, 235],
"slateblue": [106, 90, 205],
"slategray": [112, 128, 144],
"slategrey": [112, 128, 144],
"snow": [255, 250, 250],
"springgreen": [0, 255, 127],
"steelblue": [70, 130, 180],
"tan": [210, 180, 140],
"teal": [0, 128, 128],
"thistle": [216, 191, 216],
"tomato": [255, 99, 71],
"turquoise": [64, 224, 208],
"violet": [238, 130, 238],
"wheat": [245, 222, 179],
"white": [255, 255, 255],
"whitesmoke": [245, 245, 245],
"yellow": [255, 255, 0],
"yellowgreen": [154, 205, 50]
};
+28 -28
View File
@@ -1,28 +1,28 @@
{
"name": "color-name",
"version": "1.1.4",
"description": "A list of color names and its values",
"main": "index.js",
"files": [
"index.js"
],
"scripts": {
"test": "node test.js"
},
"repository": {
"type": "git",
"url": "[email protected]:colorjs/color-name.git"
},
"keywords": [
"color-name",
"color",
"color-keyword",
"keyword"
],
"author": "DY <[email protected]>",
"license": "MIT",
"bugs": {
"url": "https://github.com/colorjs/color-name/issues"
},
"homepage": "https://github.com/colorjs/color-name"
}
{
"name": "color-name",
"version": "1.1.4",
"description": "A list of color names and its values",
"main": "index.js",
"files": [
"index.js"
],
"scripts": {
"test": "node test.js"
},
"repository": {
"type": "git",
"url": "[email protected]:colorjs/color-name.git"
},
"keywords": [
"color-name",
"color",
"color-keyword",
"keyword"
],
"author": "DY <[email protected]>",
"license": "MIT",
"bugs": {
"url": "https://github.com/colorjs/color-name/issues"
},
"homepage": "https://github.com/colorjs/color-name"
}
+60 -60
View File
@@ -1,60 +1,60 @@
declare module 'Fraction';
export interface NumeratorDenominator {
n: number;
d: number;
}
type FractionConstructor = {
(fraction: Fraction): Fraction;
(num: number | string): Fraction;
(numerator: number, denominator: number): Fraction;
(numbers: [number | string, number | string]): Fraction;
(fraction: NumeratorDenominator): Fraction;
(firstValue: Fraction | number | string | [number | string, number | string] | NumeratorDenominator, secondValue?: number): Fraction;
};
export default class Fraction {
constructor (fraction: Fraction);
constructor (num: number | string);
constructor (numerator: number, denominator: number);
constructor (numbers: [number | string, number | string]);
constructor (fraction: NumeratorDenominator);
constructor (firstValue: Fraction | number | string | [number | string, number | string] | NumeratorDenominator, secondValue?: number);
s: number;
n: number;
d: number;
abs(): Fraction;
neg(): Fraction;
add: FractionConstructor;
sub: FractionConstructor;
mul: FractionConstructor;
div: FractionConstructor;
pow: FractionConstructor;
gcd: FractionConstructor;
lcm: FractionConstructor;
mod(n?: number | string | Fraction): Fraction;
ceil(places?: number): Fraction;
floor(places?: number): Fraction;
round(places?: number): Fraction;
inverse(): Fraction;
simplify(eps?: number): Fraction;
equals(n: number | string | Fraction): boolean;
compare(n: number | string | Fraction): number;
divisible(n: number | string | Fraction): boolean;
valueOf(): number;
toString(decimalPlaces?: number): string;
toLatex(excludeWhole?: boolean): string;
toFraction(excludeWhole?: boolean): string;
toContinued(): number[];
clone(): Fraction;
}
declare module 'Fraction';
export interface NumeratorDenominator {
n: number;
d: number;
}
type FractionConstructor = {
(fraction: Fraction): Fraction;
(num: number | string): Fraction;
(numerator: number, denominator: number): Fraction;
(numbers: [number | string, number | string]): Fraction;
(fraction: NumeratorDenominator): Fraction;
(firstValue: Fraction | number | string | [number | string, number | string] | NumeratorDenominator, secondValue?: number): Fraction;
};
export default class Fraction {
constructor (fraction: Fraction);
constructor (num: number | string);
constructor (numerator: number, denominator: number);
constructor (numbers: [number | string, number | string]);
constructor (fraction: NumeratorDenominator);
constructor (firstValue: Fraction | number | string | [number | string, number | string] | NumeratorDenominator, secondValue?: number);
s: number;
n: number;
d: number;
abs(): Fraction;
neg(): Fraction;
add: FractionConstructor;
sub: FractionConstructor;
mul: FractionConstructor;
div: FractionConstructor;
pow: FractionConstructor;
gcd: FractionConstructor;
lcm: FractionConstructor;
mod(n?: number | string | Fraction): Fraction;
ceil(places?: number): Fraction;
floor(places?: number): Fraction;
round(places?: number): Fraction;
inverse(): Fraction;
simplify(eps?: number): Fraction;
equals(n: number | string | Fraction): boolean;
compare(n: number | string | Fraction): number;
divisible(n: number | string | Fraction): boolean;
valueOf(): number;
toString(decimalPlaces?: number): string;
toLatex(excludeWhole?: boolean): string;
toFraction(excludeWhole?: boolean): string;
toContinued(): number[];
clone(): Fraction;
}
+46 -46
View File
@@ -1,46 +1,46 @@
# read-cache [![Build Status](https://travis-ci.org/TrySound/read-cache.svg?branch=master)](https://travis-ci.org/TrySound/read-cache)
Reads and caches the entire contents of a file until it is modified.
## Install
```
$ npm i read-cache
```
## Usage
```js
// foo.js
var readCache = require('read-cache');
readCache('foo.js').then(function (contents) {
console.log(contents);
});
```
## API
### readCache(path[, encoding])
Returns a promise that resolves with the file's contents.
### readCache.sync(path[, encoding])
Returns the content of the file.
### readCache.get(path[, encoding])
Returns the content of cached file or null.
### readCache.clear()
Clears the contents of the cache.
## License
MIT © [Bogdan Chadkin](mailto:[email protected])
# read-cache [![Build Status](https://travis-ci.org/TrySound/read-cache.svg?branch=master)](https://travis-ci.org/TrySound/read-cache)
Reads and caches the entire contents of a file until it is modified.
## Install
```
$ npm i read-cache
```
## Usage
```js
// foo.js
var readCache = require('read-cache');
readCache('foo.js').then(function (contents) {
console.log(contents);
});
```
## API
### readCache(path[, encoding])
Returns a promise that resolves with the file's contents.
### readCache.sync(path[, encoding])
Returns the content of the file.
### readCache.get(path[, encoding])
Returns the content of cached file or null.
### readCache.clear()
Clears the contents of the cache.
## License
MIT © [Bogdan Chadkin](mailto:[email protected])
+78 -78
View File
@@ -1,78 +1,78 @@
var fs = require('fs');
var path = require('path');
var pify = require('pify');
var stat = pify(fs.stat);
var readFile = pify(fs.readFile);
var resolve = path.resolve;
var cache = Object.create(null);
function convert(content, encoding) {
if (Buffer.isEncoding(encoding)) {
return content.toString(encoding);
}
return content;
}
module.exports = function (path, encoding) {
path = resolve(path);
return stat(path).then(function (stats) {
var item = cache[path];
if (item && item.mtime.getTime() === stats.mtime.getTime()) {
return convert(item.content, encoding);
}
return readFile(path).then(function (data) {
cache[path] = {
mtime: stats.mtime,
content: data
};
return convert(data, encoding);
});
}).catch(function (err) {
cache[path] = null;
return Promise.reject(err);
});
};
module.exports.sync = function (path, encoding) {
path = resolve(path);
try {
var stats = fs.statSync(path);
var item = cache[path];
if (item && item.mtime.getTime() === stats.mtime.getTime()) {
return convert(item.content, encoding);
}
var data = fs.readFileSync(path);
cache[path] = {
mtime: stats.mtime,
content: data
};
return convert(data, encoding);
} catch (err) {
cache[path] = null;
throw err;
}
};
module.exports.get = function (path, encoding) {
path = resolve(path);
if (cache[path]) {
return convert(cache[path].content, encoding);
}
return null;
};
module.exports.clear = function () {
cache = Object.create(null);
};
var fs = require('fs');
var path = require('path');
var pify = require('pify');
var stat = pify(fs.stat);
var readFile = pify(fs.readFile);
var resolve = path.resolve;
var cache = Object.create(null);
function convert(content, encoding) {
if (Buffer.isEncoding(encoding)) {
return content.toString(encoding);
}
return content;
}
module.exports = function (path, encoding) {
path = resolve(path);
return stat(path).then(function (stats) {
var item = cache[path];
if (item && item.mtime.getTime() === stats.mtime.getTime()) {
return convert(item.content, encoding);
}
return readFile(path).then(function (data) {
cache[path] = {
mtime: stats.mtime,
content: data
};
return convert(data, encoding);
});
}).catch(function (err) {
cache[path] = null;
return Promise.reject(err);
});
};
module.exports.sync = function (path, encoding) {
path = resolve(path);
try {
var stats = fs.statSync(path);
var item = cache[path];
if (item && item.mtime.getTime() === stats.mtime.getTime()) {
return convert(item.content, encoding);
}
var data = fs.readFileSync(path);
cache[path] = {
mtime: stats.mtime,
content: data
};
return convert(data, encoding);
} catch (err) {
cache[path] = null;
throw err;
}
};
module.exports.get = function (path, encoding) {
path = resolve(path);
if (cache[path]) {
return convert(cache[path].content, encoding);
}
return null;
};
module.exports.clear = function () {
cache = Object.create(null);
};
+70 -70
View File
@@ -1246,76 +1246,76 @@ yargsParser.camelCase = camelCase;
yargsParser.decamelize = decamelize;
yargsParser.looksLikeNumber = looksLikeNumber;
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __addDisposableResource(env, value, async) {
if (value !== null && value !== void 0) {
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
var dispose, inner;
if (async) {
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
dispose = value[Symbol.asyncDispose];
}
if (dispose === void 0) {
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
dispose = value[Symbol.dispose];
if (async) inner = dispose;
}
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
env.stack.push({ value: value, dispose: dispose, async: async });
}
else if (async) {
env.stack.push({ async: true });
}
return value;
}
var _SuppressedError = typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
function __disposeResources(env) {
function fail(e) {
env.error = env.hasError ? new _SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
env.hasError = true;
}
var r, s = 0;
function next() {
while (r = env.stack.pop()) {
try {
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
if (r.dispose) {
var result = r.dispose.call(r.value);
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
}
else s |= 1;
}
catch (e) {
fail(e);
}
}
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
if (env.hasError) throw env.error;
}
return next();
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __addDisposableResource(env, value, async) {
if (value !== null && value !== void 0) {
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
var dispose, inner;
if (async) {
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
dispose = value[Symbol.asyncDispose];
}
if (dispose === void 0) {
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
dispose = value[Symbol.dispose];
if (async) inner = dispose;
}
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
env.stack.push({ value: value, dispose: dispose, async: async });
}
else if (async) {
env.stack.push({ async: true });
}
return value;
}
var _SuppressedError = typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
function __disposeResources(env) {
function fail(e) {
env.error = env.hasError ? new _SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
env.hasError = true;
}
var r, s = 0;
function next() {
while (r = env.stack.pop()) {
try {
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
if (r.dispose) {
var result = r.dispose.call(r.value);
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
}
else s |= 1;
}
catch (e) {
fail(e);
}
}
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
if (env.hasError) throw env.error;
}
return next();
}
const toZeroIfInfinity = value => Number.isFinite(value) ? value : 0;
+77 -77
View File
@@ -263,83 +263,83 @@ pify$1.all = pify$1;
var pifyExports = pify$2.exports;
var fs = require$$0__default;
var path$3 = require$$0;
var pify = pifyExports;
var stat = pify(fs.stat);
var readFile = pify(fs.readFile);
var resolve = path$3.resolve;
var cache = Object.create(null);
function convert(content, encoding) {
if (Buffer.isEncoding(encoding)) {
return content.toString(encoding);
}
return content;
}
readCache$1.exports = function (path, encoding) {
path = resolve(path);
return stat(path).then(function (stats) {
var item = cache[path];
if (item && item.mtime.getTime() === stats.mtime.getTime()) {
return convert(item.content, encoding);
}
return readFile(path).then(function (data) {
cache[path] = {
mtime: stats.mtime,
content: data
};
return convert(data, encoding);
});
}).catch(function (err) {
cache[path] = null;
return Promise.reject(err);
});
};
readCache$1.exports.sync = function (path, encoding) {
path = resolve(path);
try {
var stats = fs.statSync(path);
var item = cache[path];
if (item && item.mtime.getTime() === stats.mtime.getTime()) {
return convert(item.content, encoding);
}
var data = fs.readFileSync(path);
cache[path] = {
mtime: stats.mtime,
content: data
};
return convert(data, encoding);
} catch (err) {
cache[path] = null;
throw err;
}
};
readCache$1.exports.get = function (path, encoding) {
path = resolve(path);
if (cache[path]) {
return convert(cache[path].content, encoding);
}
return null;
};
readCache$1.exports.clear = function () {
cache = Object.create(null);
var fs = require$$0__default;
var path$3 = require$$0;
var pify = pifyExports;
var stat = pify(fs.stat);
var readFile = pify(fs.readFile);
var resolve = path$3.resolve;
var cache = Object.create(null);
function convert(content, encoding) {
if (Buffer.isEncoding(encoding)) {
return content.toString(encoding);
}
return content;
}
readCache$1.exports = function (path, encoding) {
path = resolve(path);
return stat(path).then(function (stats) {
var item = cache[path];
if (item && item.mtime.getTime() === stats.mtime.getTime()) {
return convert(item.content, encoding);
}
return readFile(path).then(function (data) {
cache[path] = {
mtime: stats.mtime,
content: data
};
return convert(data, encoding);
});
}).catch(function (err) {
cache[path] = null;
return Promise.reject(err);
});
};
readCache$1.exports.sync = function (path, encoding) {
path = resolve(path);
try {
var stats = fs.statSync(path);
var item = cache[path];
if (item && item.mtime.getTime() === stats.mtime.getTime()) {
return convert(item.content, encoding);
}
var data = fs.readFileSync(path);
cache[path] = {
mtime: stats.mtime,
content: data
};
return convert(data, encoding);
} catch (err) {
cache[path] = null;
throw err;
}
};
readCache$1.exports.get = function (path, encoding) {
path = resolve(path);
if (cache[path]) {
return convert(cache[path].content, encoding);
}
return null;
};
readCache$1.exports.clear = function () {
cache = Object.create(null);
};
var readCacheExports = readCache$1.exports;
+330 -1
View File
@@ -8,12 +8,18 @@
"name": "czech-clubs-logos-frontend",
"version": "1.0.0",
"dependencies": {
"gsap": "^3.12.5"
"gsap": "^3.12.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.0",
"vite": "^5.2.11"
}
},
@@ -527,6 +533,12 @@
"node": ">=14"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
"dev": true
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz",
@@ -835,6 +847,219 @@
"win32"
]
},
"node_modules/@swc/core": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.3.tgz",
"integrity": "sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.25"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.15.3",
"@swc/core-darwin-x64": "1.15.3",
"@swc/core-linux-arm-gnueabihf": "1.15.3",
"@swc/core-linux-arm64-gnu": "1.15.3",
"@swc/core-linux-arm64-musl": "1.15.3",
"@swc/core-linux-x64-gnu": "1.15.3",
"@swc/core-linux-x64-musl": "1.15.3",
"@swc/core-win32-arm64-msvc": "1.15.3",
"@swc/core-win32-ia32-msvc": "1.15.3",
"@swc/core-win32-x64-msvc": "1.15.3"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.3.tgz",
"integrity": "sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.3.tgz",
"integrity": "sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.3.tgz",
"integrity": "sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.3.tgz",
"integrity": "sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.3.tgz",
"integrity": "sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.3.tgz",
"integrity": "sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.3.tgz",
"integrity": "sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.3.tgz",
"integrity": "sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.3.tgz",
"integrity": "sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.3.tgz",
"integrity": "sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"dev": true
},
"node_modules/@swc/types": {
"version": "0.1.25",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
"dev": true,
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -842,6 +1067,44 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true
},
"node_modules/@types/react": {
"version": "18.3.27",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/@vitejs/plugin-react-swc": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz",
"integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==",
"dev": true,
"dependencies": {
"@rolldown/pluginutils": "1.0.0-beta.27",
"@swc/core": "^1.12.11"
},
"peerDependencies": {
"vite": "^4 || ^5 || ^6 || ^7"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -1148,6 +1411,12 @@
"node": ">=4"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -1499,6 +1768,11 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -1519,6 +1793,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -1912,6 +2197,29 @@
],
"license": "MIT"
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -2033,6 +2341,14 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -2300,6 +2616,19 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+7 -1
View File
@@ -9,12 +9,18 @@
"preview": "vite preview"
},
"dependencies": {
"gsap": "^3.12.5"
"gsap": "^3.12.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.0",
"vite": "^5.2.11"
}
}
+975
View File
@@ -0,0 +1,975 @@
import React, { useEffect, useRef, useState } from 'react'
import gsap from 'gsap'
import { TopNav, SiteFooter } from './layout'
const API_BASE_URL = '/api'
const FACR_API_URL = 'https://facr.tdvorak.dev'
type Club = {
id: string
name: string
type?: string
website?: string
logo_url?: string
}
type ExistingLogo = {
id: string
}
type ClubResult = Club & {
existingLogo?: boolean
logoUrl?: string
}
type SelectedFile = {
file: File
ext: string
name: string
description: string
}
type Notification = {
message: string
type: 'success' | 'error' | 'info'
} | null
const ClubLogoImage: React.FC<{ src?: string; alt: string }> = ({ src, alt }) => {
const [errored, setErrored] = useState(false)
if (!src || errored) {
return (
<svg
className="w-8 h-8 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
)
}
return (
<img
src={src}
alt={alt}
className="max-w-full max-h-full object-contain"
onError={() => setErrored(true)}
/>
)
}
const AdminApp: React.FC = () => {
const [clubSearchQuery, setClubSearchQuery] = useState('')
const [clubs, setClubs] = useState<ClubResult[]>([])
const [clubSearchLoading, setClubSearchLoading] = useState(false)
const [searchError, setSearchError] = useState<string | null>(null)
const [clubUuid, setClubUuid] = useState('')
const [clubName, setClubName] = useState('')
const [clubType, setClubType] = useState<'football' | 'futsal'>('football')
const [clubWebsite, setClubWebsite] = useState('')
const [uploadVisible, setUploadVisible] = useState(false)
const [isEditMode, setIsEditMode] = useState(false)
const [websiteSearchLoading, setWebsiteSearchLoading] = useState(false)
const [websiteSearchUrl, setWebsiteSearchUrl] = useState<string | null>(null)
const [logoUrlInput, setLogoUrlInput] = useState('')
const [loadFromUrlLoading, setLoadFromUrlLoading] = useState(false)
const [selectedFiles, setSelectedFiles] = useState<SelectedFile[]>([])
const [uploading, setUploading] = useState(false)
const [uploadProgress, setUploadProgress] = useState<{ uploaded: number; total: number } | null>(
null,
)
const [dragOver, setDragOver] = useState(false)
const [notification, setNotification] = useState<Notification>(null)
const searchTimeoutRef = useRef<number | null>(null)
const searchResultsRef = useRef<HTMLDivElement | null>(null)
const uploadSectionRef = useRef<HTMLElement | null>(null)
const filesPreviewAreaRef = useRef<HTMLDivElement | null>(null)
const fileInputRef = useRef<HTMLInputElement | null>(null)
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const showNotification = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
setNotification({ message, type })
window.setTimeout(() => setNotification(null), 3000)
}
const searchClubs = async (query: string) => {
setClubSearchLoading(true)
setSearchError(null)
try {
const response = await fetch(`${API_BASE_URL}/clubs/search?q=${encodeURIComponent(query)}`)
if (!response.ok) throw new Error('API nedostupné')
const contentType = response.headers.get('content-type') || ''
if (!contentType.includes('application/json')) throw new Error('API vrátilo neplatnou odpověď')
const clubsData = (await response.json()) as Club[]
let existingLogos: ExistingLogo[] = []
try {
const logosResponse = await fetch(`${API_BASE_URL}/logos`)
if (logosResponse.ok) {
const logosContentType = logosResponse.headers.get('content-type') || ''
if (logosContentType.includes('application/json')) {
const logosData = (await logosResponse.json()) as ExistingLogo[]
existingLogos = logosData || []
}
}
} catch {
// optional
}
const enriched: ClubResult[] = clubsData.map((club) => {
const existingLogo = existingLogos.find((l) => l.id === club.id)
let logoUrl = ''
if (existingLogo) logoUrl = `${API_BASE_URL}/logos/${club.id}`
else if (club.logo_url) logoUrl = club.logo_url
return { ...club, existingLogo: Boolean(existingLogo), logoUrl: logoUrl || undefined }
})
setClubs(enriched)
} catch (error: any) {
if (!String(error?.message || '').includes('<!DOCTYPE')) {
console.warn('Search failed:', error?.message || error)
}
setClubs([])
setSearchError('Hledání dočasně nedostupné')
} finally {
setClubSearchLoading(false)
}
}
const handleSelectClub = (club: ClubResult) => {
setClubUuid(club.id)
setClubName(club.name)
setClubType((club.type as 'football' | 'futsal') || 'football')
setClubWebsite(club.website || '')
setUploadVisible(true)
window.setTimeout(() => {
if (uploadSectionRef.current) {
uploadSectionRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' })
gsap.from(uploadSectionRef.current, {
duration: 0.5,
opacity: 0,
y: 20,
ease: 'power2.out',
})
}
}, 0)
showNotification(`Vybráno: ${club.name}`, 'success')
}
const handleSearchWebsite = () => {
const name = clubName.trim()
if (!name) {
showNotification('Nejprve zadejte název klubu', 'error')
return
}
setWebsiteSearchLoading(true)
try {
const searchQuery = encodeURIComponent(`${name} český fotbal oficiální web`)
const searchUrl = `https://www.google.com/search?q=${searchQuery}`
setWebsiteSearchUrl(searchUrl)
} catch (error) {
console.error('Website search error:', error)
} finally {
setWebsiteSearchLoading(false)
}
}
const handleFilesSelect = (files: File[]) => {
const validFiles: SelectedFile[] = []
for (const file of files) {
const ext = file.name.split('.').pop()?.toLowerCase() || ''
if (ext === 'svg' || ext === 'png' || ext === 'pdf') {
validFiles.push({ file, ext, name: '', description: '' })
}
}
if (validFiles.length === 0) {
showNotification('Vyberte prosím SVG, PNG nebo PDF soubory', 'error')
return
}
setSelectedFiles(validFiles)
}
const handleFileMetadataChange = (
index: number,
field: 'name' | 'description',
value: string,
) => {
setSelectedFiles((prev) => {
const next = [...prev]
if (!next[index]) return prev
next[index] = { ...next[index], [field]: value }
return next
})
}
const handleRemoveFile = (index: number) => {
setSelectedFiles((prev) => {
const next = [...prev]
next.splice(index, 1)
if (next.length === 0 && fileInputRef.current) fileInputRef.current.value = ''
return next
})
}
const handleLoadFromUrl = async () => {
const url = logoUrlInput.trim()
if (!url) {
showNotification('Zadejte prosím URL obrázku', 'error')
return
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
showNotification('URL musí začínat http:// nebo https://', 'error')
return
}
setLoadFromUrlLoading(true)
try {
const response = await fetch(url)
if (!response.ok) throw new Error('Nelze načíst obrázek')
const blob = await response.blob()
let ext = 'png'
const contentType = response.headers.get('content-type') || ''
if (contentType.includes('svg')) ext = 'svg'
else if (contentType.includes('pdf')) ext = 'pdf'
else if (contentType.includes('png')) ext = 'png'
else {
const urlExt = url.split('.').pop()?.toLowerCase().split('?')[0]
if (urlExt && ['svg', 'png', 'pdf'].includes(urlExt)) ext = urlExt
}
const filename = `logo-${Date.now()}.${ext}`
const file = new File([blob], filename, { type: blob.type })
handleFilesSelect([file])
showNotification('Obrázek úspěšně načten z URL', 'success')
} catch (error: any) {
console.error('Load from URL error:', error)
showNotification(`Chyba načítání: ${error?.message || 'Chyba'}`, 'error')
} finally {
setLoadFromUrlLoading(false)
}
}
const resetFormAfterUpload = () => {
setClubUuid('')
setClubName('')
setClubType('football')
setClubWebsite('')
setSelectedFiles([])
setUploadVisible(false)
setClubSearchQuery('')
setClubs([])
setWebsiteSearchUrl(null)
setLogoUrlInput('')
if (fileInputRef.current) fileInputRef.current.value = ''
}
const uploadLogos = async (
uuid: string,
clubNameValue: string,
clubTypeValue: string,
clubWebsiteValue: string,
filesData: SelectedFile[],
) => {
setUploading(true)
setUploadProgress({ uploaded: 0, total: filesData.length })
try {
let uploadedCount = 0
for (let i = 0; i < filesData.length; i++) {
const fileData = filesData[i]
const formData = new FormData()
formData.append('file', fileData.file)
formData.append('club_name', clubNameValue)
if (clubTypeValue) formData.append('club_type', clubTypeValue)
if (clubWebsiteValue) formData.append('club_website', clubWebsiteValue)
if (i > 0) {
formData.append('variant', 'true')
if (fileData.name) formData.append('variant_name', fileData.name)
if (fileData.description) formData.append('variant_description', fileData.description)
} else {
if (fileData.name) formData.append('variant_name', fileData.name || 'Hlavní')
if (fileData.description) formData.append('variant_description', fileData.description)
}
const response = await fetch(`${API_BASE_URL}/logos/${uuid}`, {
method: 'POST',
body: formData,
})
if (!response.ok) {
let message = 'Upload failed'
try {
const errorData = await response.json()
if (errorData && errorData.error) message = errorData.error
} catch {
// ignore
}
throw new Error(message)
}
uploadedCount++
setUploadProgress({ uploaded: uploadedCount, total: filesData.length })
}
showNotification(
`${uploadedCount} ${uploadedCount === 1 ? 'logo' : 'loga'} úspěšně nahráno pro ${
clubNameValue || uuid
}! ✓`,
'success',
)
window.setTimeout(() => {
resetFormAfterUpload()
}, 2000)
} catch (error: any) {
console.error('Upload error:', error)
showNotification(`Nahrání selhalo: ${error?.message || 'Chyba'}`, 'error')
} finally {
setUploading(false)
setUploadProgress(null)
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const uuid = clubUuid.trim()
const name = clubName.trim()
const type = clubType
const website = clubWebsite.trim()
if (!uuid) {
showNotification('Nejprve vyberte klub', 'error')
return
}
if (selectedFiles.length === 0) {
showNotification('Vyberte prosím soubor loga', 'error')
return
}
if (!uuidRegex.test(uuid)) {
showNotification('Neplatný formát UUID', 'error')
return
}
await uploadLogos(uuid, name, type, website, selectedFiles)
}
useEffect(() => {
console.log('🇨🇿 České Kluby Loga API - Administrace')
console.log('Backend API:', API_BASE_URL)
console.log('FAČR API:', FACR_API_URL)
}, [])
useEffect(() => {
try {
const params = new URLSearchParams(window.location.search)
const editId = params.get('id')
if (editId) {
setIsEditMode(true)
setClubUuid(editId)
setUploadVisible(true)
showNotification('Režim úprav pro existující logo', 'info')
;(async () => {
try {
const resp = await fetch(`${API_BASE_URL}/logos/${editId}/json`)
if (resp.ok) {
const contentType = resp.headers.get('content-type') || ''
if (contentType.includes('application/json')) {
const data = await resp.json()
if (data.club_name) setClubName(data.club_name)
if (data.club_type) setClubType(data.club_type)
if (data.club_website) setClubWebsite(data.club_website)
}
}
} catch {
// non-fatal
}
})()
}
} catch {
// ignore
}
}, [])
useEffect(() => {
const timer = window.setTimeout(() => {
showNotification('Administrace: Vyhledejte kluby a nahrajte loga', 'info')
}, 1000)
return () => window.clearTimeout(timer)
}, [])
useEffect(() => {
if (searchTimeoutRef.current) window.clearTimeout(searchTimeoutRef.current)
const query = clubSearchQuery.trim()
if (query.length < 2) {
setClubs([])
setSearchError(null)
return
}
searchTimeoutRef.current = window.setTimeout(() => {
searchClubs(query)
}, 300)
}, [clubSearchQuery])
useEffect(() => {
if (!searchResultsRef.current || clubs.length === 0) return
const items = searchResultsRef.current.querySelectorAll('.club-result')
if (!items.length) return
gsap.from(items, {
duration: 0.4,
opacity: 0,
y: 20,
stagger: 0.08,
ease: 'power2.out',
})
}, [clubs])
useEffect(() => {
if (!filesPreviewAreaRef.current || selectedFiles.length === 0) return
const items = filesPreviewAreaRef.current.querySelectorAll('[data-file-index]')
if (!items.length) return
gsap.from(items, {
duration: 0.4,
opacity: 0,
y: 10,
stagger: 0.05,
ease: 'power2.out',
})
}, [selectedFiles])
return (
<>
{/* Navigation */}
<TopNav active="admin" />
{/* Admin Header */}
<header className="border-b border-dark-border bg-dark-card">
<div className="container mx-auto px-6 py-8">
<h1 className="text-3xl font-bold gradient-text mb-2">Administrace</h1>
<p className="text-gray-400">Vyhledejte kluby a nahrajte jejich loga</p>
</div>
</header>
<main className="container mx-auto px-6 py-12">
{/* Club Search Section */}
<section className="mb-12">
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 className="text-2xl font-bold mb-2">Krok 1: Vyhledat klub</h2>
<p className="text-sm text-gray-400 mb-4">
Začněte psát název klubu nebo města, poté vyberte správný klub ze seznamu.
</p>
<div className="relative mb-6">
<span className="pointer-events-none absolute inset-y-0 left-3 flex items-center text-gray-500">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M21 21l-4.35-4.35M11 5a6 6 0 100 12 6 6 0 000-12z"
/>
</svg>
</span>
<input
type="text"
id="clubSearch"
placeholder="Hledat české kluby (např. Sparta, Slavia)..."
className="w-full bg-dark-bg border border-dark-border rounded-lg px-4 pl-9 py-3 text-sm text-white focus:outline-none focus:border-accent-blue transition-smooth"
value={clubSearchQuery}
onChange={(e) => setClubSearchQuery(e.target.value)}
/>
</div>
{/* Search Results */}
<div id="searchResults" className="space-y-3" ref={searchResultsRef}>
{clubSearchLoading && (
<div className="text-center py-4">
<div className="spinner mx-auto" />
</div>
)}
{!clubSearchLoading && searchError && (
<div className="text-center py-4 text-yellow-400">
<p className="mb-2">Hledání dočasně nedostupné</p>
<p className="text-xs text-gray-400">Zkontrolujte, zda běží backend server</p>
</div>
)}
{!clubSearchLoading &&
!searchError &&
clubSearchQuery.trim().length >= 2 &&
clubs.length === 0 && (
<div className="text-center py-8 text-gray-400">
<p>Žádné kluby nenalezeny</p>
</div>
)}
{!clubSearchLoading &&
!searchError &&
clubs.map((club) => (
<div
key={club.id}
className="club-result bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer"
>
<div className="flex items-center gap-4">
<div className="flex-shrink-0 w-16 h-16 flex items-center justify-center bg-dark-border/30 rounded-lg p-2">
<ClubLogoImage src={club.logoUrl} alt={club.name} />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-lg truncate">{club.name}</h3>
<p className="text-sm text-gray-400">{club.type || 'football'}</p>
<p className="text-xs text-gray-500 font-mono mt-1 truncate">{club.id}</p>
{club.website && (
<p className="text-xs text-blue-400 mt-1 truncate">{club.website}</p>
)}
{club.existingLogo && (
<p className="text-xs text-green-400 mt-1">Logo již nahráno</p>
)}
</div>
<div className="flex flex-col gap-2 flex-shrink-0">
{club.existingLogo && (
<a
href={`/logo.html?id=${club.id}`}
className="px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-smooth text-sm text-center"
onClick={(e) => e.stopPropagation()}
>
Detail
</a>
)}
<button
type="button"
className="select-club px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth text-sm"
onClick={(e) => {
e.stopPropagation()
handleSelectClub(club)
}}
>
Vybrat
</button>
</div>
</div>
</div>
))}
</div>
</div>
</section>
{/* Upload Section */}
<section
id="uploadSection"
ref={uploadSectionRef}
className={uploadVisible ? '' : 'hidden'}
>
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 className="text-2xl font-bold mb-2">Krok 2: Nahrát logo</h2>
<p className="text-sm text-gray-400 mb-4">
Zkontrolujte údaje o klubu a nahrajte hlavní logo i případné varianty.
</p>
{clubUuid && (
<div className="mb-4 rounded-lg border border-dark-border bg-dark-bg/60 px-4 py-3 text-sm md:flex md:items-center md:justify-between md:gap-4">
<div className="min-w-0">
{isEditMode && (
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-accent-blue">
Režim úprav existujícího loga
</p>
)}
<p className="font-semibold truncate">
{clubName || 'Vybraný klub'}
</p>
<p className="text-xs text-gray-400 truncate">
{clubType === 'futsal' ? 'Futsal' : 'Fotbal'}
{clubWebsite ? `${clubWebsite}` : ''}
</p>
</div>
<div className="mt-3 flex flex-wrap gap-2 md:mt-0 md:ml-4">
<span className="inline-flex items-center rounded-full border border-dark-border px-3 py-1 text-xs text-gray-400 font-mono max-w-full truncate">
{clubUuid}
</span>
<a
href={`/logo.html?id=${clubUuid}`}
className="inline-flex items-center rounded-full border border-dark-border px-3 py-1 text-xs text-accent-blue hover:border-accent-blue transition-smooth"
>
Detail loga
</a>
</div>
</div>
)}
<form id="uploadForm" className="space-y-6" onSubmit={handleSubmit}>
{/* Club UUID (Read-only) */}
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
UUID Klubu <span className="text-red-500">*</span>
</label>
<input
type="text"
id="clubUuid"
readOnly
className="w-full bg-dark-bg/50 border border-dark-border rounded-lg px-4 py-3 text-gray-400 cursor-not-allowed"
value={clubUuid}
/>
</div>
{/* Club Name (Optional) */}
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Název Klubu{' '}
<span className="text-gray-500 text-xs">(volitelné)</span>
</label>
<input
type="text"
id="clubName"
placeholder="AC Sparta Praha"
className="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
value={clubName}
onChange={(e) => setClubName(e.target.value)}
/>
<p className="text-xs text-gray-500 mt-1">
Volitelné: Pokud název neuvedete, doplníme jej automaticky dle
FAČR (podle UUID)
</p>
</div>
{/* Club Type */}
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Typ Klubu
</label>
<select
id="clubType"
className="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
value={clubType}
onChange={(e) => setClubType(e.target.value as 'football' | 'futsal')}
>
<option value="football">Fotbal</option>
<option value="futsal">Futsal</option>
</select>
</div>
{/* Club Website with Search */}
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Web Klubu
<button
type="button"
id="searchWebsite"
className="ml-2 text-accent-blue hover:text-blue-400 text-xs disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleSearchWebsite}
disabled={websiteSearchLoading}
>
{websiteSearchLoading ? (
<span className="inline-flex items-center">
<div className="spinner inline-block w-4 h-4" />
</span>
) : (
'Hledat online'
)}
</button>
</label>
<input
type="url"
id="clubWebsite"
placeholder="https://www.sparta.cz"
className="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
value={clubWebsite}
onChange={(e) => setClubWebsite(e.target.value)}
/>
<div
id="websiteSearchResults"
className={`mt-2 ${websiteSearchUrl ? '' : 'hidden'}`}
>
{websiteSearchUrl && (
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
<p className="text-sm text-gray-400 mb-2">Vyhledat web klubu:</p>
<a
href={websiteSearchUrl}
target="_blank"
rel="noreferrer"
className="text-accent-blue hover:text-blue-400 text-sm"
>
Hledat "{clubName || 'klub'}" na Google
</a>
<p className="text-xs text-gray-500 mt-2">
Zkopírujte URL oficiálního webu a vložte jej výše
</p>
</div>
)}
</div>
</div>
{/* File Upload Area */}
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Soubor Loga <span className="text-red-500">*</span>
</label>
{/* URL Upload */}
<div className="mb-3">
<input
type="url"
id="logoUrl"
placeholder="Nebo vložte URL obrázku (https://...)"
className="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
value={logoUrlInput}
onChange={(e) => setLogoUrlInput(e.target.value)}
/>
<button
type="button"
id="loadFromUrl"
className="mt-2 px-4 py-2 bg-blue-600 rounded-lg hover:bg-blue-700 transition-smooth text-sm disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleLoadFromUrl}
disabled={loadFromUrlLoading}
>
{loadFromUrlLoading ? (
<span className="inline-flex items-center">
<div className="spinner inline-block w-4 h-4" />
</span>
) : (
'Načíst z URL'
)}
</button>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-dark-border" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-dark-card text-gray-400">nebo</span>
</div>
</div>
<div
id="uploadArea"
className={`upload-area rounded-lg p-12 text-center cursor-pointer border-2 border-dashed border-dark-border hover:border-accent-blue transition-smooth mt-3 ${
dragOver ? 'dragover border-accent-blue' : ''
}`}
onClick={() => fileInputRef.current?.click()}
onDragOver={(e) => {
e.preventDefault()
setDragOver(true)
}}
onDragLeave={(e) => {
e.preventDefault()
setDragOver(false)
}}
onDrop={(e) => {
e.preventDefault()
setDragOver(false)
const files = Array.from(e.dataTransfer.files || [])
if (files.length > 0) {
handleFilesSelect(files)
}
}}
>
<svg
style={{ width: 75, paddingTop: 20 }}
className="mx-auto h-12 w-12 text-gray-400 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<p className="text-lg mb-2">
Přetáhněte logo sem nebo{' '}
<span className="text-accent-blue font-semibold">
procházet
</span>
</p>
<p className="text-sm text-gray-500">
SVG, PNG nebo PDF Preferováno průhledné pozadí
</p>
<p className="text-xs text-gray-600 mt-2">
SVG a PDF soubory budou automaticky převedeny na PNG
</p>
<input
type="file"
id="fileInput"
accept=".svg,.png,.pdf"
className="hidden"
multiple
ref={fileInputRef}
onChange={(e) => {
const files = Array.from(e.target.files || [])
if (files.length > 0) {
handleFilesSelect(files)
}
}}
/>
</div>
<p className="text-xs text-gray-500 mt-2">
Můžete vybrat více souborů najednou pro nahrání variant
</p>
</div>
{/* Files Preview */}
<div
id="filesPreviewArea"
ref={filesPreviewAreaRef}
className={selectedFiles.length > 0 ? '' : 'hidden'}
>
<h3 className="text-lg font-semibold mb-3">Vybrané soubory</h3>
<div id="filesPreviewList" className="space-y-3">
{selectedFiles.map((fileObj, index) => {
const sizeKB = (fileObj.file.size / 1024).toFixed(2)
const isPrimary = index === 0
const icon =
fileObj.ext === 'svg' ? 'SVG' : fileObj.ext === 'pdf' ? 'PDF' : 'PNG'
return (
<div
key={index}
className="bg-dark-bg rounded-lg p-4 border border-dark-border"
data-file-index={index}
>
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-16 h-16 bg-dark-border/30 rounded flex items-center justify-center">
<span className="text-2xl">{icon}</span>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<h4 className="font-semibold">{fileObj.file.name}</h4>
{isPrimary && (
<span className="px-2 py-0.5 bg-accent-blue rounded text-xs">
Hlavní
</span>
)}
</div>
<p className="text-xs text-gray-400 mb-3">
{fileObj.ext.toUpperCase()} {sizeKB} KB
</p>
<div className="space-y-2">
<input
type="text"
placeholder="Název varianty (volitelné)"
value={fileObj.name}
onChange={(e) =>
handleFileMetadataChange(index, 'name', e.target.value)
}
className="w-full bg-dark-card border border-dark-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-accent-blue transition-smooth"
/>
<input
type="text"
placeholder="Popis (volitelné)"
value={fileObj.description}
onChange={(e) =>
handleFileMetadataChange(index, 'description', e.target.value)
}
className="w-full bg-dark-card border border-dark-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-accent-blue transition-smooth"
/>
</div>
</div>
<button
type="button"
onClick={() => handleRemoveFile(index)}
className="flex-shrink-0 p-2 text-red-400 hover:text-red-300 transition-smooth"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
)
})}
</div>
</div>
{/* Upload Button */}
<button
type="submit"
id="uploadSubmit"
className="w-full px-6 py-4 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth disabled:opacity-50 disabled:cursor-not-allowed text-lg"
disabled={uploading}
>
{uploading && uploadProgress ? (
<span className="inline-flex items-center justify-center gap-2">
<div className="spinner mx-auto" />
<span>
{uploadProgress.uploaded}/{uploadProgress.total}
</span>
</span>
) : (
isEditMode ? 'Aktualizovat logo' : 'Nahrát logo'
)}
</button>
{/* Requirements Notice */}
<div className="bg-red-900/20 border border-red-800 rounded-lg p-4 text-sm">
<p className="font-semibold text-red-400 mb-2">
Požadavky na nahrání:
</p>
<ul className="list-disc list-inside space-y-1 text-red-300/80">
<li>
Název klubu je volitelný (doplníme dle FAČR podle UUID)
</li>
<li>UUID klubu musí být platné</li>
<li>Akceptovány pouze SVG, PNG a PDF soubory</li>
<li>Doporučeno průhledné pozadí</li>
</ul>
</div>
</form>
</div>
</section>
</main>
{/* Footer */}
<SiteFooter caption="České Kluby Loga API" />
{notification && (
<div
className={`fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 text-white font-medium ${
notification.type === 'success'
? 'bg-accent-green'
: notification.type === 'error'
? 'bg-red-500'
: 'bg-accent-blue'
}`}
>
{notification.message}
</div>
)}
</>
)
}
export default AdminApp
+348
View File
@@ -0,0 +1,348 @@
import React, { useEffect, useState } from 'react'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { TopNav, SiteFooter } from './layout'
const API_BASE_URL = '/api'
type Logo = {
id: string
club_name: string
club_city?: string
club_type?: string
has_svg?: boolean
has_png?: boolean
}
gsap.registerPlugin(ScrollTrigger)
function useHomeAnimations() {
useEffect(() => {
gsap.from('.hero-content', {
duration: 1,
opacity: 0,
y: 50,
ease: 'power3.out',
delay: 0.2,
})
gsap.utils.toArray<HTMLElement>('.feature-card').forEach((card, index) => {
gsap.from(card, {
scrollTrigger: {
trigger: card,
start: 'top 80%',
toggleActions: 'play none none reverse',
},
duration: 0.6,
opacity: 0,
y: 30,
delay: index * 0.1,
ease: 'power2.out',
})
})
}, [])
}
function useRecentLogos() {
const [logos, setLogos] = useState<Logo[]>([])
const [filtered, setFiltered] = useState<Logo[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [search, setSearch] = useState('')
useEffect(() => {
const load = async () => {
try {
const resp = await fetch(`${API_BASE_URL}/logos?sort=recent&limit=8`)
if (!resp.ok) throw new Error('Failed to fetch recent logos')
const data: Logo[] = await resp.json()
setLogos(data)
setFiltered(data)
} catch (e) {
console.error(e)
setError('Načtení log selhalo')
} finally {
setLoading(false)
}
}
load()
}, [])
useEffect(() => {
const q = search.trim().toLowerCase()
if (!q) {
setFiltered(logos)
return
}
setFiltered(
logos.filter((logo) => logo.club_name.toLowerCase().includes(q))
)
}, [search, logos])
return {
logos,
filtered,
loading,
error,
search,
setSearch,
}
}
const App: React.FC = () => {
useHomeAnimations()
const { filtered, loading, error, search, setSearch } = useRecentLogos()
return (
<>
<TopNav active="home" />
<header className="relative overflow-hidden border-b border-dark-border">
<div className="absolute inset-0 bg-gradient-to-br from-blue-600/10 to-green-600/10" />
<div className="container mx-auto px-6 py-20 relative z-10">
<div className="text-center hero-content max-w-4xl mx-auto">
<h1 className="text-5xl md:text-7xl font-bold mb-6">
<span className="gradient-text">České Kluby Loga CDN</span>
</h1>
<p className="text-xl text-gray-400 mb-8">
Vysoce kvalitní loga českých fotbalových a futsalových klubů s
průhledným pozadím. Založeno na UUID, API-first, připraveno pro
produkci.
</p>
<div className="flex flex-wrap gap-4 justify-center">
<button
onClick={() => (window.location.href = '/logos.html')}
className="px-8 py-4 bg-accent-blue rounded-lg font-semibold hover:bg-blue-600 transition-smooth text-lg"
>
Procházet loga
</button>
<a
href="/admin.html"
className="px-8 py-4 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth text-lg"
>
Nahrát logo
</a>
</div>
</div>
</div>
</header>
<section className="container mx-auto px-6 py-16" id="logoGallery">
<div className="bg-dark-card border border-dark-border rounded-3xl px-6 py-6 md:px-8 md:py-8 shadow-sm">
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<h2 className="text-3xl font-bold mb-1">Dostupná loga klubů</h2>
<p className="text-sm text-gray-500">
Nejnovější nahraná loga z registru, připravená pro vaše aplikace.
</p>
</div>
<div className="w-full md:w-auto flex flex-col items-stretch gap-3 md:flex-row md:items-center md:gap-4">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Filtrovat podle názvu klubu..."
className="w-full md:w-64 bg-dark-bg border border-dark-border rounded-lg px-4 py-2.5 text-sm text-white focus:outline-none focus:border-accent-blue transition-smooth"
/>
<button
type="button"
onClick={() => (window.location.href = '/logos.html')}
className="inline-flex items-center justify-center px-4 py-2.5 text-sm rounded-lg border border-dark-border bg-dark-bg hover:border-accent-blue transition-smooth"
>
Zobrazit všechna loga
</button>
</div>
</div>
{loading && (
<div className="text-center py-12">
<div className="spinner mx-auto" />
<p className="mt-4 text-gray-400">Načítání log klubů...</p>
</div>
)}
{!loading && error && (
<div className="text-center py-12 text-red-400">{error}</div>
)}
{!loading && !error && filtered.length === 0 && (
<div className="text-center py-12">
<p className="text-lg text-gray-400 mb-4">
Zatím nebyla nahrána žádná loga
</p>
<a
href="/admin.html"
className="px-6 py-3 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth inline-block text-sm"
>
Nahrát první logo
</a>
</div>
)}
{!loading && !error && filtered.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4 md:gap-6">
{filtered.map((logo) => {
const logoUrl = `${API_BASE_URL}/logos/${logo.id}`
return (
<button
key={logo.id}
type="button"
onClick={() =>
(window.location.href = `/logo.html?id=${logo.id}`)
}
className="logo-card bg-dark-card rounded-xl p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer group text-left"
>
<div className="aspect-square bg-dark-bg rounded-lg flex items-center justify-center mb-3 overflow-hidden">
<img
src={logoUrl}
alt={logo.club_name}
className="max-w-full max-h-full object-contain p-2 group-hover:scale-110 transition-transform duration-300"
loading="lazy"
onError={(e) => {
const el = e.currentTarget.parentElement
if (!el) return
el.innerHTML =
"<svg class='w-8 h-8 text-gray-500' fill='none' stroke='currentColor' viewBox='0 0 24 24'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z'></path></svg>"
}}
/>
</div>
<h3 className="font-semibold text-sm truncate mb-1">
{logo.club_name}
</h3>
<p className="text-xs text-gray-400 truncate">
{logo.club_city
? `${logo.club_city} b7 ${logo.club_type || 'fotbal'}`
: logo.club_type || 'fotbal'}
</p>
<div className="flex gap-1 mt-2">
{logo.has_svg && (
<span className="px-2 py-0.5 bg-blue-500/10 text-blue-500 rounded text-[11px] font-medium">
SVG
</span>
)}
{logo.has_png && (
<span className="px-2 py-0.5 bg-green-500/10 text-green-500 rounded text-[11px] font-medium">
PNG
</span>
)}
</div>
</button>
)
})}
</div>
)}
</div>
</section>
<section className="bg-dark-card border-y border-dark-border py-16">
<div className="container mx-auto px-6">
<h2 className="text-3xl font-bold mb-8 text-center">
Rychlá Referenční API
</h2>
<div className="grid md:grid-cols-2 gap-6 max-w-4xl mx-auto">
<div className="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">
GET
</span>
<div className="flex-1">
<p className="font-mono text-sm mb-2">/logos</p>
<p className="text-gray-400 text-sm">
Zobrazit všechna dostupná loga
</p>
</div>
</div>
</div>
<div className="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">
GET
</span>
<div className="flex-1">
<p className="font-mono text-sm mb-2">/logos/:id</p>
<p className="text-gray-400 text-sm">
Získat logo podle UUID (PNG/SVG)
</p>
</div>
</div>
</div>
<div className="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">
GET
</span>
<div className="flex-1">
<p className="font-mono text-sm mb-2">/logos/:id/json</p>
<p className="text-gray-400 text-sm">Získat metadata loga</p>
</div>
</div>
</div>
<div className="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-accent-green/20 text-accent-green rounded-md text-sm font-mono">
POST
</span>
<div className="flex-1">
<p className="font-mono text-sm mb-2">/logos/:id</p>
<p className="text-gray-400 text-sm">Nahrát nové logo</p>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="container mx-auto px-6 py-16">
<h2 className="text-3xl font-bold mb-12 text-center">Funkce</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
<div className="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<h3 className="text-xl font-semibold mb-2">Integrace s FAČR</h3>
<p className="text-gray-400">
Přímá integrace s oficiálním českým fotbalovým registrem
</p>
</div>
<div className="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<h3 className="text-xl font-semibold mb-2">SVG &amp; PNG</h3>
<p className="text-gray-400">
Nahrajte SVG, PNG se vygeneruje automaticky
</p>
</div>
<div className="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<h3 className="text-xl font-semibold mb-2">Založeno na UUID</h3>
<p className="text-gray-400">
Konzistentní identifikace napříč všemi platformami
</p>
</div>
<div className="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<h3 className="text-xl font-semibold mb-2">Připraveno pro CDN</h3>
<p className="text-gray-400">Rychlé, cachovatelné, produkční API</p>
</div>
<div className="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<h3 className="text-xl font-semibold mb-2">Bohatá Metadata</h3>
<p className="text-gray-400">Název klubu, město, typ, web v ceně</p>
</div>
<div className="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<h3 className="text-xl font-semibold mb-2">Připraveno pro Docker</h3>
<p className="text-gray-400">
Nasazení jedním příkazem s Docker Compose
</p>
</div>
</div>
</section>
<SiteFooter
caption="České Kluby Loga API | Vytvořeno pro český fotbal"
secondary="Poháněno FAČR Scraper API | Open Source MIT Licence"
/>
</>
)
}
export default App
+537
View File
@@ -0,0 +1,537 @@
import React from 'react'
import { TopNav, SiteFooter } from './layout'
const DocsApp: React.FC = () => {
return (
<>
{/* Navigation */}
<TopNav active="docs" />
{/* Header */}
<header className="border-b border-dark-border bg-dark-card">
<div className="container mx-auto px-6 py-12">
<h1 className="text-4xl font-bold gradient-text mb-3">API dokumentace</h1>
<p className="text-xl text-gray-400">
Kompletní referenční příručka pro České Kluby Loga API
</p>
<div className="mt-6 flex gap-4 items-center flex-wrap">
<div>
<span className="text-sm text-gray-400 mr-2">Frontend (prod):</span>
<code className="bg-dark-bg px-4 py-2 rounded text-accent-blue">
https://loga.sportcreative.eu
</code>
</div>
<div>
<span className="text-sm text-gray-400 mr-2">Backend API (prod):</span>
<code className="bg-dark-bg px-4 py-2 rounded text-accent-green">
https://logoapi.sportcreative.eu
</code>
</div>
</div>
<p className="text-sm text-gray-400 mt-3">
Ve vývojovém prostředí používejte relativní cesty (např.{' '}
<code className="text-accent-blue">/logos</code>), Vite proxy je
přesměruje na backend
</p>
</div>
</header>
{/* Main Content */}
<main className="container mx-auto px-6 py-12 space-y-16 max-w-5xl">
{/* Quick Start */}
<section className="mb-16">
<h2 className="text-3xl font-bold mb-6">Rychlý start</h2>
<div className="bg-gradient-to-br from-accent-green/10 to-accent-blue/10 rounded-xl p-6 border-2 border-accent-green/30">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
Nahrání loga klubu - Základní příkaz
</h3>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`curl -X POST https://logoapi.sportcreative.eu/logos/{club-uuid} \
-F "[email protected]" \
-F "club_name=Název Klubu"`}
</code>
</pre>
<div className="mt-4 space-y-2">
<p className="text-sm text-gray-300">
<strong className="text-accent-green">Povinné:</strong> Club UUID v
URL, soubor loga (SVG/PNG/PDF), název klubu
</p>
<p className="text-sm text-gray-300">
<strong className="text-accent-blue">Volitelné:</strong> club_type,
club_website, club_city
</p>
</div>
</div>
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mt-6">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
Stažení loga klubu
</h3>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`# Produkční API
curl https://logoapi.sportcreative.eu/logos/{uuid}
# Přes frontend (loga.sportcreative.eu)
curl https://loga.sportcreative.eu/api/logos/{uuid}`}
</code>
</pre>
<p className="text-gray-400 mt-3 text-sm">
Vrátí PNG obrázek loga (SVG jako fallback)
</p>
</div>
</section>
{/* Endpoints */}
<section className="mb-16">
<h2 className="text-3xl font-bold mb-6">Endpointy</h2>
{/* List Logos */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<div className="flex items-center gap-3 mb-4">
<span className="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">
GET
</span>
<code className="text-lg">/logos</code>
</div>
<p className="text-gray-400 mb-4">Seznam všech nahraných log</p>
<div className="bg-dark-bg rounded-lg p-4">
<h4 className="text-sm font-semibold text-gray-400 mb-2">
Response 200:
</h4>
<pre className="text-sm overflow-x-auto">
<code>{`[
{
"id": "uuid-here",
"club_name": "AC Sparta Praha",
"club_type": "football",
"has_svg": true,
"has_png": true,
"logo_url": "https://logoapi.sportcreative.eu/logos/uuid-here",
"created_at": "2024-01-01T12:00:00Z"
}
]`}</code>
</pre>
</div>
</div>
{/* Get Logo File */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<div className="flex items-center gap-3 mb-4">
<span className="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">
GET
</span>
<code className="text-lg">/logos/:id</code>
</div>
<p className="text-gray-400 mb-4">
Získání souboru loga (PNG preferováno, SVG jako fallback)
</p>
<h4 className="text-sm font-semibold mb-2">Query Parameters (volitelné):</h4>
<div className="bg-dark-bg rounded-lg p-4 mb-4">
<code className="text-sm">format</code>{' '}
<span className="text-gray-500">string</span> - "png" nebo "svg"
</div>
<div className="bg-dark-bg rounded-lg p-4">
<h4 className="text-sm font-semibold text-gray-400 mb-2">
Response 200:
</h4>
<p className="text-sm text-gray-400">
Binární data obrázku (image/png nebo image/svg+xml)
</p>
</div>
</div>
{/* Get Logo Metadata */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<div className="flex items-center gap-3 mb-4">
<span className="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">
GET
</span>
<code className="text-lg">/logos/:id/json</code>
</div>
<p className="text-gray-400 mb-4">
Získání metadat loga ve formátu JSON
</p>
<div className="bg-dark-bg rounded-lg p-4">
<h4 className="text-sm font-semibold text-gray-400 mb-2">
Response 200:
</h4>
<pre className="text-sm overflow-x-auto">
<code>{`{
"id": "uuid-here",
"club_name": "AC Sparta Praha",
"club_type": "football",
"club_website": "https://sparta.cz",
"has_svg": true,
"has_png": true,
"primary_format": "png",
"logo_url": "https://logoapi.sportcreative.eu/logos/uuid-here",
"logo_url_svg": "https://logoapi.sportcreative.eu/logos/uuid-here?format=svg",
"logo_url_png": "https://logoapi.sportcreative.eu/logos/uuid-here?format=png",
"file_size_svg": 12345,
"file_size_png": 54321,
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}`}</code>
</pre>
</div>
</div>
{/* Upload Logo */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6 border-2 border-accent-green/40">
<div className="flex items-center gap-3 mb-4">
<span className="px-3 py-1 bg-accent-green/20 text-accent-green rounded font-mono text-sm">
POST
</span>
<code className="text-lg">/logos/:id</code>
</div>
<p className="text-gray-400 mb-4">
Nahrání nového loga klubu s kompletními daty (ID klubu, název, logo
soubory)
</p>
<h4 className="text-sm font-semibold mb-2">URL Parameters:</h4>
<div className="bg-dark-bg rounded-lg p-4 mb-4">
<code className="text-sm">:id</code>{' '}
<span className="text-red-400">*</span>{' '}
<span className="text-gray-500">UUID</span> - Jedinečné ID klubu
(např.{' '}
<code className="text-xs">
550e8400-e29b-41d4-a716-446655440000
</code>
)
</div>
<h4 className="text-sm font-semibold mb-2">Content-Type:</h4>
<div className="bg-dark-bg rounded-lg p-4 mb-4">
<code className="text-sm">multipart/form-data</code>
</div>
<h4 className="text-sm font-semibold mb-2">Form Data (Povinné pole):</h4>
<div className="bg-dark-bg rounded-lg p-4 mb-4 space-y-3">
<div className="border-l-2 border-red-400 pl-3">
<code className="text-sm font-semibold text-red-400">file</code>{' '}
<span className="text-red-400">*</span>{' '}
<span className="text-gray-500">file (SVG nebo PNG)</span>
<p className="text-xs text-gray-500 mt-1">
Soubor loga. Podporované formáty: SVG (doporučeno), PNG, PDF
</p>
</div>
<div className="border-l-2 border-red-400 pl-3">
<code className="text-sm font-semibold text-red-400">
club_name
</code>{' '}
<span className="text-red-400">*</span>{' '}
<span className="text-gray-500">string</span>
<p className="text-xs text-gray-500 mt-1">
Název klubu (např. "AC Sparta Praha")
</p>
</div>
</div>
<h4 className="text-sm font-semibold mb-2">Form Data (Volitelné):</h4>
<div className="bg-dark-bg rounded-lg p-4 mb-4 space-y-3">
<div className="border-l-2 border-blue-400 pl-3">
<code className="text-sm">club_type</code>{' '}
<span className="text-gray-500">string</span>
<p className="text-xs text-gray-500 mt-1">
Typ klubu: <code>"football"</code> (výchozí) nebo{' '}
<code>"futsal"</code>
</p>
</div>
<div className="border-l-2 border-blue-400 pl-3">
<code className="text-sm">club_website</code>{' '}
<span className="text-gray-500">string</span>
<p className="text-xs text-gray-500 mt-1">
URL webové stránky klubu (např. "https://sparta.cz")
</p>
</div>
<div className="border-l-2 border-blue-400 pl-3">
<code className="text-sm">club_city</code>{' '}
<span className="text-gray-500">string</span>
<p className="text-xs text-gray-500 mt-1">
Město klubu (např. "Praha")
</p>
</div>
</div>
<div className="bg-dark-bg rounded-lg p-4 mb-4">
<h4 className="text-sm font-semibold text-gray-400 mb-2">
Response 200 (Úspěch):
</h4>
<pre className="text-sm overflow-x-auto">
<code>{`{
"success": true,
"id": "550e8400-e29b-41d4-a716-446655440000",
"club_name": "AC Sparta Praha",
"has_svg": true,
"has_png": true,
"size_svg": 12543,
"size_png": 45210,
"message": "logo uploaded successfully"
}`}</code>
</pre>
</div>
<div className="bg-red-900/20 rounded-lg p-4 border border-red-600/30">
<h4 className="text-sm font-semibold text-red-400 mb-2">
Response 400 (Chyba):
</h4>
<pre className="text-sm overflow-x-auto">
<code>{`{
"error": "club_name is required"
}`}</code>
</pre>
<p className="text-xs text-gray-400 mt-2">
Možné chyby: <code>"no file provided"</code>,{' '}
<code>"invalid UUID format"</code>,{' '}
<code>"only .svg, .png and .pdf files are allowed"</code>
</p>
</div>
</div>
</section>
{/* Examples */}
<section className="mb-16">
<h2 className="text-3xl font-bold mb-6">
Příklady použití nahrání loga
</h2>
{/* cURL Example */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
cURL (terminal)
</h3>
<div className="space-y-4">
<div>
<h4 className="text-sm font-semibold mb-2 text-accent-green">
Minimální nahrání (pouze povinná pole):
</h4>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`curl -X POST https://logoapi.sportcreative.eu/logos/550e8400-e29b-41d4-a716-446655440000 \
-F "file=@sparta_logo.svg" \
-F "club_name=AC Sparta Praha"`}
</code>
</pre>
</div>
<div>
<h4 className="text-sm font-semibold mb-2 text-accent-blue">
Kompletní nahrání (všechna data):
</h4>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`curl -X POST https://logoapi.sportcreative.eu/logos/550e8400-e29b-41d4-a716-446655440000 \
-F "file=@sparta_logo.svg" \
-F "club_name=AC Sparta Praha" \
-F "club_type=football" \
-F "club_website=https://sparta.cz" \
-F "club_city=Praha"`}
</code>
</pre>
</div>
<div>
<h4 className="text-sm font-semibold mb-2 text-gray-400">
Nahrání PNG místo SVG:
</h4>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`curl -X POST https://logoapi.sportcreative.eu/logos/550e8400-e29b-41d4-a716-446655440000 \
-F "file=@sparta_logo.png" \
-F "club_name=AC Sparta Praha"`}
</code>
</pre>
</div>
</div>
</div>
{/* JavaScript Example */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
JavaScript (Fetch API)
</h3>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`// Funkce pro nahrání loga s kompletními daty
async function uploadClubLogo(clubId, file, clubData) {
const formData = new FormData();
// Povinná pole
formData.append('file', file);
formData.append('club_name', clubData.name);
// Volitelná pole
if (clubData.type) formData.append('club_type', clubData.type);
if (clubData.website) formData.append('club_website', clubData.website);
if (clubData.city) formData.append('club_city', clubData.city);
const response = await fetch(
'https://logoapi.sportcreative.eu/logos/' + clubId,
{
method: 'POST',
body: formData,
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
return await response.json();
}
// Použití s file input
const fileInput = document.getElementById('logoFile');
const clubId = '550e8400-e29b-41d4-a716-446655440000';
const result = await uploadClubLogo(clubId, fileInput.files[0], {
name: 'AC Sparta Praha',
type: 'football',
website: 'https://sparta.cz',
city: 'Praha',
});
console.log('Upload successful:', result);`}
</code>
</pre>
</div>
{/* Python Example */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
Python (requests)
</h3>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`import requests
def upload_club_logo(club_id, file_path, club_name, **optional_data):
"""
Nahraje logo klubu s kompletními daty
Args:
club_id: UUID klubu
file_path: Cesta k souboru loga
club_name: Název klubu (povinný)
**optional_data: club_type, club_website, club_city
"""
with open(file_path, 'rb') as f:
files = {'file': f}
data = {'club_name': club_name}
data.update(optional_data)
response = requests.post(
f"https://logoapi.sportcreative.eu/logos/{club_id}",
files=files,
data=data,
)
response.raise_for_status()
return response.json()
# Použití
result = upload_club_logo(
club_id='550e8400-e29b-41d4-a716-446655440000',
file_path='sparta_logo.svg',
club_name='AC Sparta Praha',
club_type='football',
club_website='https://sparta.cz',
club_city='Praha',
)
print(f"Upload úspěšný: {result['message']}")
print(f"Has SVG: {result['has_svg']}, Has PNG: {result['has_png']}")`}
</code>
</pre>
</div>
{/* PowerShell Example */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
PowerShell
</h3>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`# Nahrání loga s kompletními daty
$clubId = "550e8400-e29b-41d4-a716-446655440000"
$logoFile = "C:\\logos\\sparta_logo.svg"
$form = @{
file = Get-Item -Path $logoFile
club_name = "AC Sparta Praha"
club_type = "football"
club_website = "https://sparta.cz"
club_city = "Praha"
}
$result = Invoke-RestMethod -Uri "https://logoapi.sportcreative.eu/logos/$clubId" -Method Post -Form $form
Write-Host "Upload úspěšný: $($result.message)" -ForegroundColor Green
Write-Host "Club: $($result.club_name)" -ForegroundColor Cyan`}
</code>
</pre>
</div>
</section>
{/* Error Codes */}
<section>
<h2 className="text-3xl font-bold mb-6">Chybové kódy</h2>
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<div className="space-y-4">
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-green-600/20 text-green-400 rounded text-sm font-mono">
200
</span>
<div>
<h4 className="font-semibold">OK</h4>
<p className="text-gray-400 text-sm">Požadavek úspěšně dokončen</p>
</div>
</div>
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">
400
</span>
<div>
<h4 className="font-semibold">Bad Request</h4>
<p className="text-gray-400 text-sm">
Neplatné parametry nebo chybějící povinná pole
</p>
</div>
</div>
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">
404
</span>
<div>
<h4 className="font-semibold">Not Found</h4>
<p className="text-gray-400 text-sm">Logo nebo klub nenalezen</p>
</div>
</div>
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">
500
</span>
<div>
<h4 className="font-semibold">Internal Server Error</h4>
<p className="text-gray-400 text-sm">Interní chyba serveru</p>
</div>
</div>
</div>
</div>
</section>
</main>
{/* Footer */}
<SiteFooter caption="České Kluby Loga API" />
</>
)
}
export default DocsApp
+495
View File
@@ -0,0 +1,495 @@
import React, { useEffect, useState } from 'react'
import gsap from 'gsap'
import { TopNav, SiteFooter } from './layout'
const API_BASE_URL = 'https://logoapi.sportcreative.eu'
type LogoVariant = {
url: string
name?: string
description?: string
format: string
size?: number
}
type LogoDetail = {
id: string
club_name: string
club_city?: string
club_type?: string
club_website?: string
has_svg?: boolean
has_png?: boolean
file_size_svg?: number
file_size_png?: number
created_at?: string
variants?: LogoVariant[]
}
type Notification = {
message: string
type: 'success' | 'error' | 'info'
} | null
function formatFileSize(bytes?: number): string {
if (!bytes) return 'N/A'
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
}
function formatDate(dateString?: string): string {
if (!dateString) return 'N/A'
const date = new Date(dateString)
return date.toLocaleDateString('cs-CZ', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
const LogoDetailApp: React.FC = () => {
const [logo, setLogo] = useState<LogoDetail | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [notification, setNotification] = useState<Notification>(null)
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const id = params.get('id')
if (!id) {
setError('Logo s tímto UUID neexistuje')
setLoading(false)
return
}
const loadLogo = async () => {
try {
const response = await fetch(`${API_BASE_URL}/logos/${id}/json`)
if (!response.ok) {
throw new Error('Logo not found')
}
const data: LogoDetail = await response.json()
setLogo(data)
} catch (e) {
console.error('Error loading logo:', e)
setError('Logo s tímto UUID neexistuje')
} finally {
setLoading(false)
}
}
loadLogo()
}, [])
useEffect(() => {
if (!logo) return
gsap.from('.logo-detail-section', {
duration: 0.6,
opacity: 0,
y: 20,
stagger: 0.1,
ease: 'power2.out',
})
}, [logo])
const handleCopy = (text: string, label: string) => {
if (!text) return
navigator.clipboard
.writeText(text)
.then(() => {
setNotification({
message: `URL zkopírováno: ${label}`,
type: 'success',
})
})
.catch(() => {
setNotification({
message: 'Chyba při kopírování',
type: 'error',
})
})
setTimeout(() => {
setNotification(null)
}, 3000)
}
const logoId = logo?.id
const previewUrl = logoId ? `${API_BASE_URL}/logos/${logoId}` : ''
const formats = logo
? [
logo.has_png && {
name: 'PNG',
url: `${API_BASE_URL}/logos/${logoId}?format=png`,
size: formatFileSize(logo.file_size_png),
icon: 'PNG',
color: 'bg-blue-600',
},
logo.has_svg && {
name: 'SVG',
url: `${API_BASE_URL}/logos/${logoId}?format=svg`,
size: formatFileSize(logo.file_size_svg),
icon: 'SVG',
color: 'bg-green-600',
},
].filter(Boolean) as {
name: string
url: string
size: string
icon: string
color: string
}[]
: []
const apiUrlDefault = logoId ? `${API_BASE_URL}/logos/${logoId}` : ''
const apiUrlJson = logoId ? `${API_BASE_URL}/logos/${logoId}/json` : ''
return (
<>
{/* Navigation */}
<TopNav active="logos" />
{/* Main Content */}
<main className="container mx-auto px-6 py-12">
{loading && (
<div className="text-center py-12">
<div className="spinner mx-auto mb-4" />
<p className="text-gray-400">Načítání...</p>
</div>
)}
{!loading && error && (
<div className="text-center py-12">
<svg
className="mx-auto h-16 w-16 text-red-400 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<h2 className="text-2xl font-bold mb-2">Logo nenalezeno</h2>
<p className="text-gray-400 mb-4">Logo s tímto UUID neexistuje</p>
<a
href="/"
className="px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth inline-block"
>
Zpět na hlavní stránku
</a>
</div>
)}
{!loading && !error && logo && (
<div className="space-y-8 logo-detail-section">
{/* Header */}
<div className="mb-8 space-y-3">
<div className="text-sm text-gray-500">
<a
href="/logos.html"
className="hover:text-accent-blue transition-smooth"
>
Všechna loga
</a>
<span> / </span>
<span className="text-gray-300">{logo.club_name}</span>
</div>
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div className="min-w-0">
<h1 className="text-4xl font-bold gradient-text mb-2 truncate">
{logo.club_name}
</h1>
<div className="flex flex-wrap items-center gap-2 text-sm text-gray-400">
<span>{logo.club_type || 'fotbal'}</span>
{logo.club_city && (
<>
<span></span>
<span>{logo.club_city}</span>
</>
)}
{logo.club_website && (
<>
<span></span>
<a
href={logo.club_website}
target="_blank"
rel="noreferrer"
className="hover:text-accent-blue"
>
Oficiální web
</a>
</>
)}
{logo.created_at && (
<>
<span></span>
<span>Nahráno {formatDate(logo.created_at)}</span>
</>
)}
</div>
</div>
{logoId && (
<div className="flex flex-col gap-2 md:items-end">
<a
href={`/admin.html?id=${logoId}`}
className="px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth text-sm"
>
Upravit logo
</a>
<a
href="/logos.html"
className="text-xs text-gray-400 hover:text-accent-blue transition-smooth"
>
Zpět na seznam log
</a>
</div>
)}
</div>
</div>
{/* Logo Preview */}
<section className="mb-8 logo-detail-section">
<div className="bg-dark-card rounded-xl p-8 border border-dark-border">
<h2 className="text-2xl font-bold mb-6">Náhled loga</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Light Background */}
<div className="logo-preview-surface logo-preview-light rounded-lg p-8 flex items-center justify-center min-h-[300px]">
{previewUrl && (
<div className="logo-preview-inner">
<img
src={previewUrl}
alt={logo.club_name}
className="max-w-full max-h-64 object-contain"
/>
</div>
)}
</div>
{/* Dark Background */}
<div className="logo-preview-surface logo-preview-dark rounded-lg p-8 flex items-center justify-center min-h-[300px]">
{previewUrl && (
<div className="logo-preview-inner">
<img
src={previewUrl}
alt={logo.club_name}
className="max-w-full max-h-64 object-contain"
/>
</div>
)}
</div>
</div>
</div>
</section>
{/* Available Formats */}
<section className="mb-8 logo-detail-section">
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 className="text-2xl font-bold mb-6">Dostupné formáty</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{formats.map((format) => (
<a
key={format.name}
href={format.url}
download
className="block bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth"
>
<div className="flex items-center justify-between mb-3">
<span className="text-2xl">{format.icon}</span>
<span
className={`${format.color} px-2 py-1 rounded text-xs font-semibold`}
>
{format.name}
</span>
</div>
<h3 className="font-semibold mb-1">{format.name} Format</h3>
<p className="text-sm text-gray-400">{format.size}</p>
<div className="mt-3 flex items-center text-accent-blue text-sm">
<svg
className="w-4 h-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
Stáhnout
</div>
</a>
))}
</div>
</div>
</section>
{/* Variants */}
{logo.variants && logo.variants.length > 0 && (
<section className="mb-8 logo-detail-section">
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 className="text-2xl font-bold mb-6">Varianty loga</h2>
<div className="space-y-4">
{logo.variants.map((variant) => (
<div
key={variant.url}
className="bg-dark-bg rounded-lg p-4 border border-dark-border"
>
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-20 h-20 bg-white rounded flex items-center justify-center p-2">
<img
src={variant.url}
alt={variant.name || 'Varianta'}
className="max-w-full max-h-full object-contain"
/>
</div>
<div className="flex-1">
<h3 className="font-semibold mb-1">
{variant.name || 'Varianta'}
</h3>
{variant.description && (
<p className="text-sm text-gray-400 mb-2">
{variant.description}
</p>
)}
<div className="flex items-center gap-3 text-xs text-gray-500">
<span>{variant.format.toUpperCase()}</span>
<span></span>
<span>{formatFileSize(variant.size)}</span>
</div>
</div>
<a
href={variant.url}
download
className="px-3 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth text-sm"
>
</a>
</div>
</div>
))}
</div>
</div>
</section>
)}
{/* Metadata */}
<section className="mb-8 logo-detail-section">
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 className="text-2xl font-bold mb-6">Informace</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="text-sm font-medium text-gray-400 mb-2">UUID</h3>
<p className="font-mono text-sm bg-dark-bg rounded px-3 py-2">
{logo.id}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-400 mb-2">
Typ Klubu
</h3>
<p className="text-sm bg-dark-bg rounded px-3 py-2">
{logo.club_type || 'fotbal'}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-400 mb-2">
Webová Stránka
</h3>
<p className="text-sm bg-dark-bg rounded px-3 py-2">
{logo.club_website ? (
<a
href={logo.club_website}
target="_blank"
rel="noreferrer"
className="text-accent-blue hover:underline"
>
{logo.club_website}
</a>
) : (
'N/A'
)}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-400 mb-2">
Datum Nahrání
</h3>
<p className="text-sm bg-dark-bg rounded px-3 py-2">
{formatDate(logo.created_at)}
</p>
</div>
</div>
</div>
</section>
{/* API Usage */}
<section className="logo-detail-section">
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 className="text-2xl font-bold mb-6">Použití API</h2>
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-gray-400 mb-2">
GET Logo (PNG preferováno)
</h3>
<div className="bg-dark-bg rounded px-4 py-3 font-mono text-sm flex items-center justify-between">
<code>{apiUrlDefault}</code>
<button
type="button"
onClick={() => handleCopy(apiUrlDefault, 'GET /logos/:id')}
className="px-3 py-1 bg-accent-blue rounded text-xs hover:bg-blue-600 transition-smooth"
>
Kopírovat
</button>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-gray-400 mb-2">
GET Logo s Metadaty (JSON)
</h3>
<div className="bg-dark-bg rounded px-4 py-3 font-mono text-sm flex items-center justify-between">
<code>{apiUrlJson}</code>
<button
type="button"
onClick={() => handleCopy(apiUrlJson, 'GET /logos/:id/json')}
className="px-3 py-1 bg-accent-blue rounded text-xs hover:bg-blue-600 transition-smooth"
>
Kopírovat
</button>
</div>
</div>
</div>
</div>
</section>
</div>
)}
</main>
{/* Footer */}
<SiteFooter caption="České Kluby Loga API" />
{notification && (
<div
className={`fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 text-white font-medium ${
notification.type === 'success'
? 'bg-accent-green'
: notification.type === 'error'
? 'bg-red-500'
: 'bg-accent-blue'
}`}
>
{notification.message}
</div>
)}
</>
)
}
export default LogoDetailApp
+363
View File
@@ -0,0 +1,363 @@
import React, { useCallback, useEffect, useState } from 'react'
import { TopNav, SiteFooter } from './layout'
const API_BASE_URL = '/api'
const PAGE_SIZE = 20
type Logo = {
id: string
club_name: string
club_city?: string
club_type?: string
has_svg?: boolean
has_png?: boolean
}
const LogosApp: React.FC = () => {
const [logos, setLogos] = useState<Logo[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [query, setQuery] = useState('')
const [debouncedQuery, setDebouncedQuery] = useState('')
const [typeFilter, setTypeFilter] = useState<'all' | 'football' | 'futsal'>('all')
const [sort, setSort] = useState<'recent' | 'name'>('recent')
const hasActiveFilter =
query.trim().length > 0 || typeFilter !== 'all' || sort !== 'recent'
const resetFilters = () => {
setQuery('')
setTypeFilter('all')
setSort('recent')
setPage(1)
}
useEffect(() => {
const handle = window.setTimeout(() => setDebouncedQuery(query), 300)
return () => window.clearTimeout(handle)
}, [query])
const loadPage = useCallback(
async (reset: boolean) => {
if (loading) return
setLoading(true)
setError(null)
try {
const nextPage = reset ? 1 : page
const url = new URL(`${API_BASE_URL}/logos`, window.location.origin)
url.searchParams.set('sort', sort)
url.searchParams.set('limit', String(PAGE_SIZE))
url.searchParams.set('page', String(nextPage))
if (debouncedQuery) url.searchParams.set('q', debouncedQuery)
if (typeFilter !== 'all') url.searchParams.set('type', typeFilter)
const resp = await fetch(url.toString().replace(window.location.origin, ''))
if (!resp.ok) throw new Error('Failed to fetch logos')
const data: Logo[] = await resp.json()
if (reset) {
setLogos(data)
} else {
setLogos((prev) => [...prev, ...data])
}
if (Array.isArray(data) && data.length === PAGE_SIZE) {
setHasMore(true)
setPage(nextPage + 1)
} else {
setHasMore(false)
}
} catch (e) {
if (reset) {
setError('Načtení log selhalo')
setLogos([])
setHasMore(false)
}
} finally {
setLoading(false)
}
},
[loading, page, debouncedQuery, typeFilter, sort]
)
useEffect(() => {
loadPage(true)
}, [loadPage, debouncedQuery])
const handleDelete = async (id: string) => {
const ok = window.confirm('Smazat toto logo?')
if (!ok) return
try {
const resp = await fetch(`${API_BASE_URL}/logos/${id}`, { method: 'DELETE' })
if (!resp.ok) throw new Error('Delete failed')
setLogos((prev) => prev.filter((l) => l.id !== id))
} catch (_) {
window.alert('Mazání selhalo')
}
}
const isInitialLoading = logos.length === 0 && loading
const showEmpty = !loading && !error && logos.length === 0
return (
<>
<TopNav active="logos" />
<header className="border-b border-dark-border bg-dark-card">
<div className="container mx-auto px-6 py-8">
<h1 className="text-3xl font-bold gradient-text mb-2">Všechna Loga</h1>
<p className="text-gray-400">
Procházejte všechna dostupná loga, vyhledávejte a spravujte
</p>
</div>
</header>
<main className="container mx-auto px-6 py-12">
<div className="mb-6 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div className="w-full md:max-w-lg space-y-2">
<label className="text-xs font-medium text-gray-400 uppercase tracking-wide">
Vyhledat loga
</label>
<div className="relative">
<span className="pointer-events-none absolute inset-y-0 left-3 flex items-center text-gray-500">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M21 21l-4.35-4.35M11 5a6 6 0 100 12 6 6 0 000-12z"
/>
</svg>
</span>
<input
type="text"
value={query}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setQuery(e.target.value)
}
placeholder="Hledat mezi všemi logy..."
className="w-full bg-dark-card border border-dark-border rounded-lg px-4 pl-9 py-3 text-sm text-white focus:outline-none focus:border-accent-blue transition-smooth"
/>
</div>
<p className="text-[11px] text-gray-500">
Filtruje podle názvu klubu, můžete kombinovat s typem klubu a řazením.
</p>
</div>
<div className="w-full md:w-auto space-y-2">
<div className="flex flex-wrap items-center gap-2 justify-between md:justify-end text-[11px] text-gray-500">
<span>{PAGE_SIZE} log na stránku</span>
<span>
řazeno: {sort === 'recent' ? 'nejnovější' : 'podle názvu'}
</span>
</div>
<div className="flex flex-wrap items-center gap-2 justify-start md:justify-end">
<div className="inline-flex flex-wrap gap-1 rounded-full bg-dark-card/60 border border-dark-border px-1 py-1">
<button
type="button"
onClick={() => {
setPage(1)
setTypeFilter('all')
}}
className={`px-3 py-1.5 rounded-full border text-xs ${
typeFilter === 'all'
? 'bg-accent-blue/10 text-accent-blue border-accent-blue'
: 'bg-transparent border-transparent text-gray-400 hover:bg-dark-bg/80'
}`}
>
Vše
</button>
<button
type="button"
onClick={() => {
setPage(1)
setTypeFilter('football')
}}
className={`px-3 py-1.5 rounded-full border text-xs ${
typeFilter === 'football'
? 'bg-accent-blue/10 text-accent-blue border-accent-blue'
: 'bg-transparent border-transparent text-gray-400 hover:bg-dark-bg/80'
}`}
>
Fotbal
</button>
<button
type="button"
onClick={() => {
setPage(1)
setTypeFilter('futsal')
}}
className={`px-3 py-1.5 rounded-full border text-xs ${
typeFilter === 'futsal'
? 'bg-accent-blue/10 text-accent-blue border-accent-blue'
: 'bg-transparent border-transparent text-gray-400 hover:bg-dark-bg/80'
}`}
>
Futsal
</button>
</div>
<select
value={sort}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
setPage(1)
setSort(e.target.value as 'recent' | 'name')
}}
className="bg-dark-card border border-dark-border rounded-lg px-3 py-2 text-xs text-white focus:outline-none focus:border-accent-blue transition-smooth"
>
<option value="recent">Nejnovější</option>
<option value="name">Název (AZ)</option>
</select>
{hasActiveFilter && (
<button
type="button"
onClick={resetFilters}
className="text-[11px] text-gray-400 hover:text-accent-blue underline-offset-2 hover:underline"
>
Vymazat filtry
</button>
)}
</div>
</div>
</div>
{isInitialLoading && (
<div className="text-center py-12">
<div className="spinner mx-auto" />
<p className="mt-4 text-gray-400">Načítání log...</p>
</div>
)}
{!isInitialLoading && error && (
<div className="text-center py-12 text-red-400">{error}</div>
)}
{showEmpty && (
<div className="text-center py-16">
<p className="text-xl text-gray-400 mb-2">Žádná loga nenalezena</p>
{hasActiveFilter ? (
<p className="text-sm text-gray-500 mb-4">
Zkuste upravit vyhledávání nebo typ klubu, případně vymažte filtry.
</p>
) : (
<p className="text-sm text-gray-500 mb-4">
Zatím zde nejsou žádná loga.
</p>
)}
{hasActiveFilter && (
<button
type="button"
onClick={resetFilters}
className="px-4 py-2 bg-dark-card border border-dark-border rounded-lg text-sm text-gray-200 hover:bg-dark-border transition-smooth mr-2"
>
Vymazat filtry
</button>
)}
<a
href="/admin.html"
className="inline-flex items-center justify-center px-4 py-2 bg-accent-green rounded-lg text-sm font-medium hover:bg-green-600 transition-smooth"
>
Nahrát nové logo
</a>
</div>
)}
{!showEmpty && logos.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-6">
{logos.map((logo: Logo) => {
const logoUrl = `${API_BASE_URL}/logos/${logo.id}`
return (
<div
key={logo.id}
className="logo-card bg-dark-card rounded-xl p-4 border border-dark-border hover:border-accent-blue transition-smooth group"
>
<button
type="button"
onClick={() =>
(window.location.href = `/logo.html?id=${logo.id}`)
}
className="aspect-square bg-dark-bg rounded-lg flex items-center justify-center mb-3 overflow-hidden cursor-pointer w-full"
>
<img
src={logoUrl}
alt={logo.club_name}
className="max-w-full max-h-full object-contain p-2 group-hover:scale-110 transition-transform duration-300"
loading="lazy"
onError={(e) => {
const el = e.currentTarget.parentElement
if (!el) return
el.innerHTML =
"<svg class='w-8 h-8 text-gray-500' fill='none' stroke='currentColor' viewBox='0 0 24 24'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z'></path></svg>"
}}
/>
</button>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() =>
(window.location.href = `/logo.html?id=${logo.id}`)
}
className="flex-1 min-w-0 text-left cursor-pointer"
>
<h3 className="font-semibold text-sm truncate mb-0.5">
{logo.club_name}
</h3>
<p className="text-xs text-gray-400 truncate">
{logo.club_city
? `${logo.club_city} b7 ${logo.club_type || 'fotbal'}`
: logo.club_type || 'fotbal'}
</p>
<div className="mt-1 flex gap-1">
{logo.has_svg && (
<span className="px-2 py-0.5 bg-blue-500/10 text-blue-500 rounded text-[11px] font-medium">
SVG
</span>
)}
{logo.has_png && (
<span className="px-2 py-0.5 bg-green-500/10 text-green-500 rounded text-[11px] font-medium">
PNG
</span>
)}
</div>
</button>
<button
type="button"
onClick={() => handleDelete(logo.id)}
className="delete-logo px-3 py-1.5 text-xs bg-red-600 rounded hover:bg-red-500 transition-smooth"
>
Smazat
</button>
</div>
</div>
)
})}
</div>
)}
{hasMore && !isInitialLoading && (
<div className="text-center mt-10">
<button
type="button"
onClick={() => loadPage(false)}
className="px-6 py-3 bg-dark-card border border-dark-border rounded-lg hover:bg-dark-border transition-smooth disabled:opacity-50 disabled:cursor-not-allowed"
disabled={loading}
>
{loading ? 'Načítání...' : 'Načíst další'}
</button>
</div>
)}
</main>
<SiteFooter caption="České Kluby Loga API" />
</>
)
}
export default LogosApp
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import './style.css'
import './theme.js'
import AdminApp from './AdminApp'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<AdminApp />
</React.StrictMode>
)
+176 -19
View File
@@ -1,4 +1,5 @@
import './style.css'
import './theme.js'
import gsap from 'gsap'
// Configuration
@@ -10,8 +11,61 @@ const FACR_API_URL = 'https://facr.tdvorak.dev'
const clubSearch = document.getElementById('clubSearch')
const searchResults = document.getElementById('searchResults')
const uploadSection = document.getElementById('uploadSection')
const clubSportFilterButtons = document.querySelectorAll('[data-club-sport-filter]')
const selectedClubSummary = document.getElementById('selectedClubSummary')
const selectedClubNameEl = document.getElementById('selectedClubName')
const selectedClubTypeEl = document.getElementById('selectedClubType')
const selectedClubCityEl = document.getElementById('selectedClubCity')
const selectedClubWebsiteEl = document.getElementById('selectedClubWebsite')
const selectedClubLogoEl = document.getElementById('selectedClubLogo')
let searchTimeout
let activeIndex = -1
let lastClubs = []
let clubSportFilter = 'all'
function normalizeText(s) {
return (s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
}
function highlight(text, query) {
const t = String(text || '')
const nq = normalizeText(query)
if (!nq) return t
const nt = normalizeText(t)
const idx = nt.indexOf(nq)
if (idx === -1) return t
let i = 0, oi = 0, start = -1, end = -1
while (oi < t.length && i <= idx + nq.length) {
const ch = t[oi]
const n = normalizeText(ch)
if (i === idx) start = oi
if (n) i += n.length
oi += 1
if (i >= idx + nq.length) { end = oi; break }
}
if (start === -1 || end === -1) return t
return t.slice(0, start) + '<span class="bg-accent-blue/20">' + t.slice(start, end) + '</span>' + t.slice(end)
}
function updateActive() {
const items = searchResults.querySelectorAll('.club-result')
items.forEach((el, i) => {
if (i === activeIndex) el.classList.add('ring-2', 'ring-accent-blue')
else el.classList.remove('ring-2', 'ring-accent-blue')
})
}
function updateClubSportFilterButtons() {
if (!clubSportFilterButtons || !clubSportFilterButtons.length) return
clubSportFilterButtons.forEach(btn => {
const value = (btn.dataset.clubSportFilter || 'all').toLowerCase()
const isActive = value === clubSportFilter
btn.classList.toggle('bg-accent-blue', isActive)
btn.classList.toggle('text-white', isActive)
btn.classList.toggle('bg-dark-bg', !isActive)
btn.classList.toggle('text-gray-300', !isActive)
})
}
clubSearch.addEventListener('input', (e) => {
clearTimeout(searchTimeout)
@@ -27,6 +81,27 @@ clubSearch.addEventListener('input', (e) => {
}, 300)
})
clubSearch.addEventListener('keydown', (e) => {
const total = searchResults.querySelectorAll('.club-result').length
if (!total) return
if (e.key === 'ArrowDown') {
e.preventDefault()
activeIndex = (activeIndex + 1) % total
updateActive()
} else if (e.key === 'ArrowUp') {
e.preventDefault()
activeIndex = (activeIndex - 1 + total) % total
updateActive()
} else if (e.key === 'Enter') {
e.preventDefault()
if (activeIndex >= 0 && activeIndex < total) {
const item = searchResults.querySelectorAll('.club-result')[activeIndex]
const btn = item.querySelector('.select-club')
if (btn) btn.click(); else item.click()
}
}
})
async function searchClubs(query) {
searchResults.innerHTML = '<div class="text-center py-4"><div class="spinner mx-auto"></div></div>'
@@ -44,7 +119,8 @@ async function searchClubs(query) {
}
const clubs = await response.json()
await displaySearchResults(clubs)
lastClubs = Array.isArray(clubs) ? clubs : []
await displaySearchResults(lastClubs)
} catch (error) {
// Suppress console spam from HTML responses
@@ -53,7 +129,7 @@ async function searchClubs(query) {
}
searchResults.innerHTML = `
<div class="text-center py-4 text-yellow-400">
<p class="mb-2">⚠️ Hledání dočasně nedostupné</p>
<p class="mb-2">Hledání dočasně nedostupné</p>
<p class="text-xs text-gray-400">Zkontrolujte, zda běží backend server</p>
</div>
`
@@ -85,7 +161,22 @@ async function displaySearchResults(clubs) {
// Silently fail - this is optional data
}
searchResults.innerHTML = clubs.map(club => {
const q = clubSearch.value.trim()
const nq = normalizeText(q)
let filtered = Array.isArray(clubs) ? clubs : []
if (nq) {
filtered = filtered.filter(c => {
const name = normalizeText(c.name)
const city = normalizeText(c.city)
const id = String(c.id || '').toLowerCase()
return name.includes(nq) || city.includes(nq) || id.includes(q.toLowerCase())
})
}
if (clubSportFilter && clubSportFilter !== 'all') {
filtered = filtered.filter(c => (c.type || '').toLowerCase() === clubSportFilter)
}
activeIndex = -1
searchResults.innerHTML = filtered.map(club => {
// Check if we have this logo in our API
const existingLogo = existingLogos.find(l => l.id === club.id)
@@ -120,19 +211,20 @@ async function displaySearchResults(clubs) {
`
}
const clubData = { ...club, display_logo_url: logoUrl }
return `
<div class="club-result bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer" data-club='${JSON.stringify(club)}' data-logo-url='${logoUrl}'>
<div class="club-result bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer" data-club='${JSON.stringify(clubData)}'>
<div class="flex items-center gap-4">
${logoHtml}
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-lg truncate">${club.name}</h3>
<h3 class="font-semibold text-lg truncate">${highlight(club.name, q)}</h3>
<p class="text-sm text-gray-400">${club.type || 'football'}</p>
<p class="text-xs text-gray-500 font-mono mt-1 truncate">${club.id}</p>
${club.website ? `<p class="text-xs text-blue-400 mt-1 truncate">🌐 ${club.website}</p>` : ''}
${existingLogo ? '<p class="text-xs text-green-400 mt-1">Logo již nahráno</p>' : ''}
${club.website ? `<p class="text-xs text-blue-400 mt-1 truncate">${club.website}</p>` : ''}
${existingLogo ? '<p class="text-xs text-green-400 mt-1">Logo již nahráno</p>' : ''}
</div>
<div class="flex flex-col gap-2 flex-shrink-0">
${existingLogo ? `<a href="/logo.html?id=${club.id}" class="px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-smooth text-sm text-center" onclick="event.stopPropagation()">👁️ Detail</a>` : ''}
${existingLogo ? `<a href="/logo.html?id=${club.id}" class="px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-smooth text-sm text-center" onclick="event.stopPropagation()">Detail</a>` : ''}
<button class="select-club px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth text-sm">
Vybrat
</button>
@@ -168,6 +260,27 @@ function selectClub(club) {
document.getElementById('clubName').value = club.name
document.getElementById('clubType').value = club.type || 'football'
document.getElementById('clubWebsite').value = club.website || ''
// Update summary card
if (selectedClubSummary && selectedClubNameEl && selectedClubTypeEl && selectedClubCityEl && selectedClubWebsiteEl && selectedClubLogoEl) {
selectedClubNameEl.textContent = club.name || ''
selectedClubTypeEl.textContent = (club.type || 'football').toUpperCase()
selectedClubCityEl.textContent = club.city || ''
if (club.website) {
selectedClubWebsiteEl.innerHTML = `<a href="${club.website}" target="_blank" class="hover:underline">${club.website}</a>`
} else {
selectedClubWebsiteEl.textContent = ''
}
const displayLogo = club.display_logo_url || club.logo_url || ''
if (displayLogo) {
selectedClubLogoEl.innerHTML = `<img src="${displayLogo}" alt="${club.name || ''}" class="max-w-full max-h-full object-contain rounded-md">`
} else {
selectedClubLogoEl.textContent = '🏟️'
}
selectedClubSummary.classList.remove('hidden')
}
// Show upload section
uploadSection.classList.remove('hidden')
@@ -186,6 +299,23 @@ function selectClub(club) {
showNotification(`Vybráno: ${club.name}`, 'success')
}
if (clubSportFilterButtons && clubSportFilterButtons.length) {
updateClubSportFilterButtons()
clubSportFilterButtons.forEach(btn => {
btn.addEventListener('click', () => {
const value = (btn.dataset.clubSportFilter || 'all').toLowerCase()
if (value === clubSportFilter) return
clubSportFilter = value
updateClubSportFilterButtons()
if (lastClubs.length) {
displaySearchResults(lastClubs)
} else if (clubSearch.value.trim().length >= 2) {
searchClubs(clubSearch.value.trim())
}
})
})
}
// ==================== Website Search ====================
const searchWebsiteBtn = document.getElementById('searchWebsite')
@@ -210,7 +340,7 @@ searchWebsiteBtn.addEventListener('click', async () => {
<div class="bg-dark-bg rounded-lg p-3 border border-dark-border">
<p class="text-sm text-gray-400 mb-2">Vyhledat web klubu:</p>
<a href="${searchUrl}" target="_blank" class="text-accent-blue hover:text-blue-400 text-sm">
🔍 Hledat "${clubName}" na Google
Hledat "${clubName}" na Google
</a>
<p class="text-xs text-gray-500 mt-2">Zkopírujte URL oficiálního webu a vložte jej výše</p>
</div>
@@ -220,7 +350,7 @@ searchWebsiteBtn.addEventListener('click', async () => {
} catch (error) {
console.error('Website search error:', error)
} finally {
searchWebsiteBtn.innerHTML = '🔍 Hledat Online'
searchWebsiteBtn.innerHTML = 'Hledat Online'
searchWebsiteBtn.disabled = false
}
})
@@ -310,7 +440,7 @@ function displayFilesPreview() {
<div class="bg-dark-bg rounded-lg p-4 border border-dark-border" data-file-index="${index}">
<div class="flex items-start gap-4">
<div class="flex-shrink-0 w-16 h-16 bg-dark-border/30 rounded flex items-center justify-center">
<span class="text-2xl">${fileObj.ext === 'svg' ? '📐' : fileObj.ext === 'pdf' ? '📄' : '🖼️'}</span>
<span class="text-2xl">${fileObj.ext === 'svg' ? 'SVG' : fileObj.ext === 'pdf' ? 'PDF' : 'PNG'}</span>
</div>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
@@ -385,11 +515,6 @@ uploadForm.addEventListener('submit', async (e) => {
return
}
if (!clubName) {
showNotification('Název klubu je povinný', 'error')
return
}
if (selectedFiles.length === 0) {
showNotification('Vyberte prosím soubor loga', 'error')
return
@@ -449,7 +574,7 @@ async function uploadLogos(uuid, clubName, clubType, clubWebsite, filesData) {
submitBtn.innerHTML = `<div class="spinner mx-auto"></div> ${uploadedCount}/${filesData.length}`
}
showNotification(`${uploadedCount} ${uploadedCount === 1 ? 'logo' : 'loga'} úspěšně nahráno pro ${clubName}!`, 'success')
showNotification(`${uploadedCount} ${uploadedCount === 1 ? 'logo' : 'loga'} úspěšně nahráno pro ${clubName}!`, 'success')
// Reset form after delay
setTimeout(() => {
@@ -504,10 +629,42 @@ function showNotification(message, type = 'info') {
// ==================== Initialize ====================
console.log('🇨🇿 České Kluby Loga API - Administrace')
console.log('České Kluby Loga API - Administrace')
console.log('Backend API:', API_BASE_URL)
console.log('FAČR API:', FACR_API_URL)
// Prefill editing when navigated with ?id=<uuid>
try {
const params = new URLSearchParams(window.location.search)
const editId = params.get('id')
if (editId) {
// Fill UUID and show upload section
const uuidInput = document.getElementById('clubUuid')
uuidInput.value = editId
uploadSection.classList.remove('hidden')
uploadSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
showNotification('Režim úprav pro existující logo', 'info')
// Load metadata to prefill fields
;(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) document.getElementById('clubName').value = data.club_name
if (data.club_type) document.getElementById('clubType').value = data.club_type
if (data.club_website) document.getElementById('clubWebsite').value = data.club_website
}
}
} catch (e) {
// Non-fatal
}
})()
}
} catch (_) {}
// Load from URL functionality
const loadFromUrlBtn = document.getElementById('loadFromUrl')
const logoUrlInput = document.getElementById('logoUrl')
@@ -559,7 +716,7 @@ loadFromUrlBtn.addEventListener('click', async () => {
showNotification(`Chyba načítání: ${error.message}`, 'error')
} finally {
loadFromUrlBtn.disabled = false
loadFromUrlBtn.innerHTML = '📥 Načíst z URL'
loadFromUrlBtn.innerHTML = 'Načíst z URL'
}
})
+4
View File
@@ -0,0 +1,4 @@
import './style.css'
import './theme.js'
console.log('🇨🇿 České Kluby Loga API - API Docs')
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import './style.css'
import './theme.js'
import DocsApp from './DocsApp'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<DocsApp />
</React.StrictMode>
)
+29 -33
View File
@@ -1,4 +1,5 @@
import './style.css'
import './theme.js'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
@@ -44,27 +45,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 +109,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 +157,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'
})
}
@@ -198,13 +194,13 @@ function showNotification(message, type = 'info') {
// ==================== Initialize ====================
console.log('🇨🇿 Czech Clubs Logos API - Home')
console.log('Czech Clubs Logos API - Home')
console.log('Backend API:', API_BASE_URL)
// Load logos on page load
loadLogos()
loadRecentLogos()
// Show welcome notification
setTimeout(() => {
showNotification('Welcome to Czech Clubs Logos API! 🇨🇿', 'info')
showNotification('Welcome to Czech Clubs Logos API!', 'info')
}, 1000)
+79
View File
@@ -0,0 +1,79 @@
import React from 'react'
type TopNavProps = {
active?: 'home' | 'logos' | 'docs' | 'admin'
}
export const TopNav: React.FC<TopNavProps> = ({ active }) => {
return (
<nav className="border-b border-dark-border bg-white/80 dark:bg-dark-card/80 backdrop-blur-md sticky top-0 z-50">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-3">
<div className="flex items-center justify-between gap-4">
<a href="/" className="flex items-center gap-2">
<div className="h-8 w-8 rounded-xl bg-gradient-to-br from-accent-blue to-accent-green flex items-center justify-center text-xs font-bold text-white">
CL
</div>
<span className="text-lg sm:text-xl font-semibold tracking-tight">
České Kluby Loga
</span>
</a>
<div className="flex items-center gap-3">
<div className="hidden md:flex gap-1 rounded-full bg-dark-bg/40 dark:bg-dark-bg/60 px-1 py-1 border border-dark-border/80">
<a
href="/"
className={`nav-link ${active === 'home' ? 'nav-link--active' : ''}`}
>
Domů
</a>
<a
href="/logos.html"
className={`nav-link ${active === 'logos' ? 'nav-link--active' : ''}`}
>
Všechna loga
</a>
<a
href="/api-docs.html"
className={`nav-link ${active === 'docs' ? 'nav-link--active' : ''}`}
>
API Docs
</a>
<a
href="/admin.html"
className={`nav-link ${active === 'admin' ? 'nav-link--active' : ''}`}
>
Admin
</a>
</div>
<button
type="button"
data-theme-toggle
className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-dark-border bg-dark-bg/60 hover:bg-dark-border transition-smooth text-xs"
>
<span className="sr-only">Přepnout téma</span>
<span data-theme-icon></span>
</button>
</div>
</div>
</div>
</nav>
)
}
type SiteFooterProps = {
caption?: string
secondary?: string
}
export const SiteFooter: React.FC<SiteFooterProps> = ({
caption = 'České Kluby Loga API',
secondary,
}) => {
return (
<footer className="border-t border-dark-border mt-20">
<div className="container mx-auto px-6 py-8 text-center text-gray-400 text-sm">
<p>{caption}</p>
{secondary && <p className="text-xs sm:text-sm mt-2">{secondary}</p>}
</div>
</footer>
)
}
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import './style.css'
import './theme.js'
import LogoDetailApp from './LogoDetailApp'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<LogoDetailApp />
</React.StrictMode>
)
+8 -5
View File
@@ -1,8 +1,9 @@
import './style.css'
import './theme.js'
import gsap from 'gsap'
// Configuration
const API_BASE_URL = window.location.hostname === 'localhost' ? '/api' : 'http://localhost:8080'
const API_BASE_URL = 'https://logoapi.sportcreative.eu'
// Get UUID from URL
const urlParams = new URLSearchParams(window.location.search)
@@ -43,6 +44,8 @@ function displayLogoDetails(logo) {
logoDetail.classList.remove('hidden')
// Club Info
const editBtn = document.getElementById('editButton')
if (editBtn) editBtn.href = `/admin.html?id=${logoId}`
document.getElementById('clubName').textContent = logo.club_name
document.getElementById('clubMeta').textContent = `${logo.club_type || 'fotbal'}`
@@ -60,7 +63,7 @@ function displayLogoDetails(logo) {
name: 'PNG',
url: `${API_BASE_URL}/logos/${logoId}?format=png`,
size: formatFileSize(logo.file_size_png),
icon: '🖼️',
icon: 'PNG',
color: 'bg-blue-600'
})
}
@@ -70,7 +73,7 @@ function displayLogoDetails(logo) {
name: 'SVG',
url: `${API_BASE_URL}/logos/${logoId}?format=svg`,
size: formatFileSize(logo.file_size_svg),
icon: '📐',
icon: 'SVG',
color: 'bg-green-600'
})
}
@@ -137,7 +140,7 @@ function displayLogoDetails(logo) {
document.getElementById('uploadDate').textContent = formatDate(logo.created_at)
// API URLs
const baseUrl = window.location.origin
const baseUrl = API_BASE_URL
document.getElementById('apiUrlDefault').textContent = `${baseUrl}/logos/${logo.id}`
document.getElementById('apiUrlJson').textContent = `${baseUrl}/logos/${logo.id}/json`
@@ -214,5 +217,5 @@ function showNotification(message, type = 'info') {
}, 3000)
}
console.log('🇨🇿 České Kluby Loga API - Detail Loga')
console.log('České Kluby Loga API - Detail loga')
console.log('Logo ID:', logoId)
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import './style.css'
import './theme.js'
import LogosApp from './LogosApp'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<LogosApp />
</React.StrictMode>
)
+161
View File
@@ -0,0 +1,161 @@
import './style.css'
import './theme.js'
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')
const sportFilterButtons = document.querySelectorAll('[data-sport-filter]')
let page = 1
const limit = 20
let query = ''
let isLoading = false
let hasMore = true
let sport = 'all'
async function loadPage(reset = false) {
if (isLoading) return
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)
if (sport && sport !== 'all') url.searchParams.set('sport', sport)
const resp = await fetch(url.toString().replace(window.location.origin, ''))
if (!resp.ok) throw new Error('Failed to fetch logos')
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)
}
function updateSportFilterButtons() {
if (!sportFilterButtons || !sportFilterButtons.length) return
sportFilterButtons.forEach(btn => {
const value = btn.dataset.sportFilter || 'all'
const isActive = value === sport
btn.classList.toggle('bg-accent-blue', isActive)
btn.classList.toggle('text-white', isActive)
btn.classList.toggle('bg-dark-card', !isActive)
btn.classList.toggle('text-gray-300', !isActive)
})
}
grid.addEventListener('click', async (e) => {
const delBtn = e.target.closest('.delete-logo')
if (delBtn) {
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)
})
if (sportFilterButtons && sportFilterButtons.length) {
updateSportFilterButtons()
sportFilterButtons.forEach(btn => {
btn.addEventListener('click', () => {
const value = btn.dataset.sportFilter || 'all'
if (value === sport) return
sport = value
updateSportFilterButtons()
loadPage(true)
})
})
}
loadMoreBtn.addEventListener('click', () => {
if (hasMore) loadPage(false)
})
loadPage(true)
+84 -16
View File
@@ -1,4 +1,5 @@
import './style.css'
import './theme.js'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
@@ -100,6 +101,42 @@ uploadBtn.addEventListener('click', () => {
const searchInput = document.getElementById('searchInput')
const searchResults = document.getElementById('searchResults')
let searchTimeout
let activeIndex = -1
let currentClubs = []
function normalizeText(s) {
return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
}
function highlight(text, query) {
const t = String(text)
const nq = normalizeText(query)
if (!nq) return t
const nt = normalizeText(t)
const idx = nt.indexOf(nq)
if (idx === -1) return t
let i = 0, oi = 0, start = -1, end = -1
while (oi < t.length && i <= idx + nq.length) {
const ch = t[oi]
const n = normalizeText(ch)
if (i === idx) start = oi
if (n) i += n.length
oi += 1
if (i >= idx + nq.length) { end = oi; break }
}
if (start === -1 || end === -1) return t
return t.slice(0, start) + '<span class="bg-accent-blue/20">' + t.slice(start, end) + '</span>' + t.slice(end)
}
function updateActive() {
const items = searchResults.querySelectorAll('.club-result')
items.forEach((el, i) => {
if (i === activeIndex) {
el.classList.add('ring-2', 'ring-accent-blue')
} else {
el.classList.remove('ring-2', 'ring-accent-blue')
}
})
}
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout)
@@ -110,12 +147,31 @@ searchInput.addEventListener('input', (e) => {
return
}
// Debounce search
searchTimeout = setTimeout(() => {
searchClubs(query)
}, 300)
})
searchInput.addEventListener('keydown', (e) => {
const total = searchResults.querySelectorAll('.club-result').length
if (!total) return
if (e.key === 'ArrowDown') {
e.preventDefault()
activeIndex = (activeIndex + 1) % total
updateActive()
} else if (e.key === 'ArrowUp') {
e.preventDefault()
activeIndex = (activeIndex - 1 + total) % total
updateActive()
} else if (e.key === 'Enter') {
e.preventDefault()
if (activeIndex >= 0 && activeIndex < total) {
const item = searchResults.querySelectorAll('.club-result')[activeIndex]
item.click()
}
}
})
async function searchClubs(query) {
searchResults.innerHTML = '<div class="text-center py-4"><div class="spinner mx-auto"></div></div>'
@@ -129,12 +185,19 @@ async function searchClubs(query) {
}
const data = await response.json()
displaySearchResults(data)
const nq = normalizeText(query)
const filtered = data.filter(c => {
const name = normalizeText(c.name || '')
const city = normalizeText(c.city || '')
const id = String(c.id || '').toLowerCase()
return name.includes(nq) || city.includes(nq) || id.includes(query.toLowerCase())
})
displaySearchResults(filtered, query)
} catch (error) {
console.log('Backend not available, showing demo data')
// Demo data when backend is not ready
displaySearchResults(getDemoClubs(query))
const demo = getDemoClubs(query)
displaySearchResults(demo, query)
}
}
@@ -166,12 +229,15 @@ function getDemoClubs(query) {
}
]
return demoClubs.filter(club =>
club.name.toLowerCase().includes(query.toLowerCase())
)
const nq = normalizeText(query)
return demoClubs.filter(club => {
const name = normalizeText(club.name)
const city = normalizeText(club.city)
return name.includes(nq) || city.includes(nq)
})
}
function displaySearchResults(clubs) {
function displaySearchResults(clubs, query) {
if (clubs.length === 0) {
searchResults.innerHTML = `
<div class="text-center py-8 text-gray-400">
@@ -181,12 +247,14 @@ function displaySearchResults(clubs) {
return
}
searchResults.innerHTML = clubs.map(club => `
<div class="club-result bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer">
activeIndex = -1
currentClubs = clubs
searchResults.innerHTML = clubs.map((club, idx) => `
<div class="club-result bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer" data-index="${idx}">
<div class="flex items-center justify-between">
<div class="flex-1">
<h3 class="font-semibold text-lg">${club.name}</h3>
<p class="text-sm text-gray-400">${club.city || 'N/A'}${club.type || 'football'}</p>
<h3 class="font-semibold text-lg">${highlight(club.name, query)}</h3>
<p class="text-sm text-gray-400">${highlight(club.city || 'N/A', query)}${club.type || 'football'}</p>
<p class="text-xs text-gray-500 font-mono mt-1">${club.id}</p>
</div>
<button
@@ -217,7 +285,7 @@ function displaySearchResults(clubs) {
// Visual feedback
const originalText = btn.textContent
btn.textContent = 'Copied!'
btn.textContent = 'Copied!'
setTimeout(() => {
btn.textContent = originalText
}, 2000)
@@ -351,7 +419,7 @@ async function uploadLogo(uuid, file) {
throw new Error('Upload failed')
}
showNotification('Logo uploaded successfully!', 'success')
showNotification('Logo uploaded successfully!', 'success')
// Reset form
setTimeout(() => {
@@ -414,11 +482,11 @@ function showNotification(message, type = 'info') {
// ==================== Initialize ====================
console.log('🇨🇿 Czech Clubs Logos API Frontend')
console.log('Czech Clubs Logos API Frontend')
console.log('Backend API:', API_BASE_URL)
console.log('FAČR API:', FACR_API_URL)
// Show a welcome notification
setTimeout(() => {
showNotification('Welcome to Czech Clubs Logos API! 🇨🇿', 'info')
showNotification('Welcome to Czech Clubs Logos API!', 'info')
}, 1000)
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import './style.css'
import './theme.js'
import App from './App'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
+153 -1
View File
@@ -86,7 +86,7 @@ body {
/* Loading spinner */
.spinner {
border: 3px solid rgba(255, 255, 255, 0.1);
border: 3px solid rgba(148, 163, 184, 0.25);
border-radius: 50%;
border-top-color: #3b82f6;
width: 40px;
@@ -97,3 +97,155 @@ body {
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Theme-aware tokens (light first, dark via .dark) */
.bg-dark-bg {
background-color: #f3f4f6;
}
.dark .bg-dark-bg {
background-color: #0a0e1a;
}
.bg-dark-card {
background-color: #ffffff;
}
.dark .bg-dark-card {
background-color: #131823;
}
.bg-dark-border {
background-color: #e5e7eb;
}
.dark .bg-dark-border {
background-color: #1f2937;
}
.border-dark-border {
border-color: #e5e7eb;
}
.dark .border-dark-border {
border-color: #1f2937;
}
body {
background-color: #f9fafb;
}
.dark body {
background-color: #020617;
}
.bg-dark-card .text-white,
.bg-dark-bg .text-white {
color: #020617;
}
.dark .bg-dark-card .text-white,
.dark .bg-dark-bg .text-white {
color: #f9fafb;
}
.nav-link {
font-size: 0.875rem;
font-weight: 500;
border-radius: 9999px;
padding-inline: 0.9rem;
padding-block: 0.5rem;
color: #6b7280;
}
.dark .nav-link {
color: #9ca3af;
}
.nav-link:hover {
background-color: rgba(15, 23, 42, 0.04);
color: #111827;
}
.dark .nav-link:hover {
background-color: rgba(148, 163, 184, 0.12);
color: #e5e7eb;
}
.nav-link--active {
background-color: rgba(59, 130, 246, 0.12);
color: #1d4ed8;
}
.dark .nav-link--active {
background-color: rgba(59, 130, 246, 0.18);
color: #bfdbfe;
}
.logo-card {
position: relative;
overflow: hidden;
}
.logo-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: 0.75rem;
border: 1px solid transparent;
pointer-events: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.logo-card:hover::before {
border-color: rgba(59, 130, 246, 0.4);
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.16);
}
.dark .logo-card:hover::before {
border-color: rgba(59, 130, 246, 0.6);
box-shadow: 0 22px 55px rgba(15, 23, 42, 0.9);
}
.logo-card:hover {
transform: translateY(-2px);
}
.logo-preview-surface {
position: relative;
overflow: hidden;
}
.logo-preview-surface::before {
content: '';
position: absolute;
inset: 0;
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
opacity: 0.4;
}
.logo-preview-light {
background-color: #f9fafb;
}
.logo-preview-light::before {
background-image:
linear-gradient(45deg, #f3f4f6 25%, transparent 25%, transparent 75%, #f3f4f6 75%, #f3f4f6),
linear-gradient(45deg, #e5e7eb 25%, transparent 25%, transparent 75%, #e5e7eb 75%, #e5e7eb);
}
.logo-preview-dark {
background-color: #020617;
}
.logo-preview-dark::before {
background-image:
linear-gradient(45deg, #111827 25%, transparent 25%, transparent 75%, #111827 75%, #111827),
linear-gradient(45deg, #020617 25%, transparent 25%, transparent 75%, #020617 75%, #020617);
}
.logo-preview-inner {
position: relative;
z-index: 1;
}
+58
View File
@@ -0,0 +1,58 @@
const STORAGE_KEY = 'clublogos-theme'
function getPreferredTheme() {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored === 'light' || stored === 'dark') return stored
} catch (_) {}
return 'light'
}
function applyTheme(theme) {
const root = document.documentElement
if (theme === 'dark') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}
function initThemeToggle() {
const toggles = document.querySelectorAll('[data-theme-toggle]')
if (!toggles.length) return
const current = document.documentElement.classList.contains('dark') ? 'dark' : 'light'
updateToggleIcons(current)
toggles.forEach((btn) => {
btn.addEventListener('click', () => {
const isDark = document.documentElement.classList.contains('dark')
const next = isDark ? 'light' : 'dark'
applyTheme(next)
updateToggleIcons(next)
try {
localStorage.setItem(STORAGE_KEY, next)
} catch (_) {}
})
})
}
function updateToggleIcons(theme) {
const isDark = theme === 'dark'
document.querySelectorAll('[data-theme-toggle]').forEach((btn) => {
const iconSpan = btn.querySelector('[data-theme-icon]')
if (iconSpan) {
iconSpan.textContent = isDark ? '☾' : '☀'
}
})
}
const initial = getPreferredTheme()
applyTheme(initial)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initThemeToggle)
} else {
initThemeToggle()
}
+1 -1
View File
@@ -1,7 +1,7 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./*.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
darkMode: 'class',
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
+4 -1
View File
@@ -1,7 +1,9 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { resolve } from 'path'
export default defineConfig({
plugins: [react()],
root: './',
server: {
port: 3000,
@@ -22,7 +24,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')
}
}
}
View File
+1199
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nodemon\bin\nodemon.js" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nodemon\bin\nodemon.js" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\touch\bin\nodetouch.js" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\touch\bin\nodetouch.js" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\prebuild-install\bin.js" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\prebuild-install\bin.js" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rc\cli.js" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rc\cli.js" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\semver\bin\semver.js" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\semver\bin\semver.js" %*
+17 -17
View File
@@ -1,17 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\svgo\bin\svgo" %*
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\svgo\bin\svgo" %*
+7 -7
View File
@@ -1,8 +1,8 @@
The MIT License (MIT)
Copyright (c) 2015 Dmitry Ivanov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The MIT License (MIT)
Copyright (c) 2015 Dmitry Ivanov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+11 -11
View File
@@ -1,11 +1,11 @@
A JSON with color names and its values. Based on http://dev.w3.org/csswg/css-color/#named-colors.
[![NPM](https://nodei.co/npm/color-name.png?mini=true)](https://nodei.co/npm/color-name/)
```js
var colors = require('color-name');
colors.red //[255,0,0]
```
<a href="LICENSE"><img src="https://upload.wikimedia.org/wikipedia/commons/0/0c/MIT_logo.svg" width="120"/></a>
A JSON with color names and its values. Based on http://dev.w3.org/csswg/css-color/#named-colors.
[![NPM](https://nodei.co/npm/color-name.png?mini=true)](https://nodei.co/npm/color-name/)
```js
var colors = require('color-name');
colors.red //[255,0,0]
```
<a href="LICENSE"><img src="https://upload.wikimedia.org/wikipedia/commons/0/0c/MIT_logo.svg" width="120"/></a>
+152 -152
View File
@@ -1,152 +1,152 @@
'use strict'
module.exports = {
"aliceblue": [240, 248, 255],
"antiquewhite": [250, 235, 215],
"aqua": [0, 255, 255],
"aquamarine": [127, 255, 212],
"azure": [240, 255, 255],
"beige": [245, 245, 220],
"bisque": [255, 228, 196],
"black": [0, 0, 0],
"blanchedalmond": [255, 235, 205],
"blue": [0, 0, 255],
"blueviolet": [138, 43, 226],
"brown": [165, 42, 42],
"burlywood": [222, 184, 135],
"cadetblue": [95, 158, 160],
"chartreuse": [127, 255, 0],
"chocolate": [210, 105, 30],
"coral": [255, 127, 80],
"cornflowerblue": [100, 149, 237],
"cornsilk": [255, 248, 220],
"crimson": [220, 20, 60],
"cyan": [0, 255, 255],
"darkblue": [0, 0, 139],
"darkcyan": [0, 139, 139],
"darkgoldenrod": [184, 134, 11],
"darkgray": [169, 169, 169],
"darkgreen": [0, 100, 0],
"darkgrey": [169, 169, 169],
"darkkhaki": [189, 183, 107],
"darkmagenta": [139, 0, 139],
"darkolivegreen": [85, 107, 47],
"darkorange": [255, 140, 0],
"darkorchid": [153, 50, 204],
"darkred": [139, 0, 0],
"darksalmon": [233, 150, 122],
"darkseagreen": [143, 188, 143],
"darkslateblue": [72, 61, 139],
"darkslategray": [47, 79, 79],
"darkslategrey": [47, 79, 79],
"darkturquoise": [0, 206, 209],
"darkviolet": [148, 0, 211],
"deeppink": [255, 20, 147],
"deepskyblue": [0, 191, 255],
"dimgray": [105, 105, 105],
"dimgrey": [105, 105, 105],
"dodgerblue": [30, 144, 255],
"firebrick": [178, 34, 34],
"floralwhite": [255, 250, 240],
"forestgreen": [34, 139, 34],
"fuchsia": [255, 0, 255],
"gainsboro": [220, 220, 220],
"ghostwhite": [248, 248, 255],
"gold": [255, 215, 0],
"goldenrod": [218, 165, 32],
"gray": [128, 128, 128],
"green": [0, 128, 0],
"greenyellow": [173, 255, 47],
"grey": [128, 128, 128],
"honeydew": [240, 255, 240],
"hotpink": [255, 105, 180],
"indianred": [205, 92, 92],
"indigo": [75, 0, 130],
"ivory": [255, 255, 240],
"khaki": [240, 230, 140],
"lavender": [230, 230, 250],
"lavenderblush": [255, 240, 245],
"lawngreen": [124, 252, 0],
"lemonchiffon": [255, 250, 205],
"lightblue": [173, 216, 230],
"lightcoral": [240, 128, 128],
"lightcyan": [224, 255, 255],
"lightgoldenrodyellow": [250, 250, 210],
"lightgray": [211, 211, 211],
"lightgreen": [144, 238, 144],
"lightgrey": [211, 211, 211],
"lightpink": [255, 182, 193],
"lightsalmon": [255, 160, 122],
"lightseagreen": [32, 178, 170],
"lightskyblue": [135, 206, 250],
"lightslategray": [119, 136, 153],
"lightslategrey": [119, 136, 153],
"lightsteelblue": [176, 196, 222],
"lightyellow": [255, 255, 224],
"lime": [0, 255, 0],
"limegreen": [50, 205, 50],
"linen": [250, 240, 230],
"magenta": [255, 0, 255],
"maroon": [128, 0, 0],
"mediumaquamarine": [102, 205, 170],
"mediumblue": [0, 0, 205],
"mediumorchid": [186, 85, 211],
"mediumpurple": [147, 112, 219],
"mediumseagreen": [60, 179, 113],
"mediumslateblue": [123, 104, 238],
"mediumspringgreen": [0, 250, 154],
"mediumturquoise": [72, 209, 204],
"mediumvioletred": [199, 21, 133],
"midnightblue": [25, 25, 112],
"mintcream": [245, 255, 250],
"mistyrose": [255, 228, 225],
"moccasin": [255, 228, 181],
"navajowhite": [255, 222, 173],
"navy": [0, 0, 128],
"oldlace": [253, 245, 230],
"olive": [128, 128, 0],
"olivedrab": [107, 142, 35],
"orange": [255, 165, 0],
"orangered": [255, 69, 0],
"orchid": [218, 112, 214],
"palegoldenrod": [238, 232, 170],
"palegreen": [152, 251, 152],
"paleturquoise": [175, 238, 238],
"palevioletred": [219, 112, 147],
"papayawhip": [255, 239, 213],
"peachpuff": [255, 218, 185],
"peru": [205, 133, 63],
"pink": [255, 192, 203],
"plum": [221, 160, 221],
"powderblue": [176, 224, 230],
"purple": [128, 0, 128],
"rebeccapurple": [102, 51, 153],
"red": [255, 0, 0],
"rosybrown": [188, 143, 143],
"royalblue": [65, 105, 225],
"saddlebrown": [139, 69, 19],
"salmon": [250, 128, 114],
"sandybrown": [244, 164, 96],
"seagreen": [46, 139, 87],
"seashell": [255, 245, 238],
"sienna": [160, 82, 45],
"silver": [192, 192, 192],
"skyblue": [135, 206, 235],
"slateblue": [106, 90, 205],
"slategray": [112, 128, 144],
"slategrey": [112, 128, 144],
"snow": [255, 250, 250],
"springgreen": [0, 255, 127],
"steelblue": [70, 130, 180],
"tan": [210, 180, 140],
"teal": [0, 128, 128],
"thistle": [216, 191, 216],
"tomato": [255, 99, 71],
"turquoise": [64, 224, 208],
"violet": [238, 130, 238],
"wheat": [245, 222, 179],
"white": [255, 255, 255],
"whitesmoke": [245, 245, 245],
"yellow": [255, 255, 0],
"yellowgreen": [154, 205, 50]
};
'use strict'
module.exports = {
"aliceblue": [240, 248, 255],
"antiquewhite": [250, 235, 215],
"aqua": [0, 255, 255],
"aquamarine": [127, 255, 212],
"azure": [240, 255, 255],
"beige": [245, 245, 220],
"bisque": [255, 228, 196],
"black": [0, 0, 0],
"blanchedalmond": [255, 235, 205],
"blue": [0, 0, 255],
"blueviolet": [138, 43, 226],
"brown": [165, 42, 42],
"burlywood": [222, 184, 135],
"cadetblue": [95, 158, 160],
"chartreuse": [127, 255, 0],
"chocolate": [210, 105, 30],
"coral": [255, 127, 80],
"cornflowerblue": [100, 149, 237],
"cornsilk": [255, 248, 220],
"crimson": [220, 20, 60],
"cyan": [0, 255, 255],
"darkblue": [0, 0, 139],
"darkcyan": [0, 139, 139],
"darkgoldenrod": [184, 134, 11],
"darkgray": [169, 169, 169],
"darkgreen": [0, 100, 0],
"darkgrey": [169, 169, 169],
"darkkhaki": [189, 183, 107],
"darkmagenta": [139, 0, 139],
"darkolivegreen": [85, 107, 47],
"darkorange": [255, 140, 0],
"darkorchid": [153, 50, 204],
"darkred": [139, 0, 0],
"darksalmon": [233, 150, 122],
"darkseagreen": [143, 188, 143],
"darkslateblue": [72, 61, 139],
"darkslategray": [47, 79, 79],
"darkslategrey": [47, 79, 79],
"darkturquoise": [0, 206, 209],
"darkviolet": [148, 0, 211],
"deeppink": [255, 20, 147],
"deepskyblue": [0, 191, 255],
"dimgray": [105, 105, 105],
"dimgrey": [105, 105, 105],
"dodgerblue": [30, 144, 255],
"firebrick": [178, 34, 34],
"floralwhite": [255, 250, 240],
"forestgreen": [34, 139, 34],
"fuchsia": [255, 0, 255],
"gainsboro": [220, 220, 220],
"ghostwhite": [248, 248, 255],
"gold": [255, 215, 0],
"goldenrod": [218, 165, 32],
"gray": [128, 128, 128],
"green": [0, 128, 0],
"greenyellow": [173, 255, 47],
"grey": [128, 128, 128],
"honeydew": [240, 255, 240],
"hotpink": [255, 105, 180],
"indianred": [205, 92, 92],
"indigo": [75, 0, 130],
"ivory": [255, 255, 240],
"khaki": [240, 230, 140],
"lavender": [230, 230, 250],
"lavenderblush": [255, 240, 245],
"lawngreen": [124, 252, 0],
"lemonchiffon": [255, 250, 205],
"lightblue": [173, 216, 230],
"lightcoral": [240, 128, 128],
"lightcyan": [224, 255, 255],
"lightgoldenrodyellow": [250, 250, 210],
"lightgray": [211, 211, 211],
"lightgreen": [144, 238, 144],
"lightgrey": [211, 211, 211],
"lightpink": [255, 182, 193],
"lightsalmon": [255, 160, 122],
"lightseagreen": [32, 178, 170],
"lightskyblue": [135, 206, 250],
"lightslategray": [119, 136, 153],
"lightslategrey": [119, 136, 153],
"lightsteelblue": [176, 196, 222],
"lightyellow": [255, 255, 224],
"lime": [0, 255, 0],
"limegreen": [50, 205, 50],
"linen": [250, 240, 230],
"magenta": [255, 0, 255],
"maroon": [128, 0, 0],
"mediumaquamarine": [102, 205, 170],
"mediumblue": [0, 0, 205],
"mediumorchid": [186, 85, 211],
"mediumpurple": [147, 112, 219],
"mediumseagreen": [60, 179, 113],
"mediumslateblue": [123, 104, 238],
"mediumspringgreen": [0, 250, 154],
"mediumturquoise": [72, 209, 204],
"mediumvioletred": [199, 21, 133],
"midnightblue": [25, 25, 112],
"mintcream": [245, 255, 250],
"mistyrose": [255, 228, 225],
"moccasin": [255, 228, 181],
"navajowhite": [255, 222, 173],
"navy": [0, 0, 128],
"oldlace": [253, 245, 230],
"olive": [128, 128, 0],
"olivedrab": [107, 142, 35],
"orange": [255, 165, 0],
"orangered": [255, 69, 0],
"orchid": [218, 112, 214],
"palegoldenrod": [238, 232, 170],
"palegreen": [152, 251, 152],
"paleturquoise": [175, 238, 238],
"palevioletred": [219, 112, 147],
"papayawhip": [255, 239, 213],
"peachpuff": [255, 218, 185],
"peru": [205, 133, 63],
"pink": [255, 192, 203],
"plum": [221, 160, 221],
"powderblue": [176, 224, 230],
"purple": [128, 0, 128],
"rebeccapurple": [102, 51, 153],
"red": [255, 0, 0],
"rosybrown": [188, 143, 143],
"royalblue": [65, 105, 225],
"saddlebrown": [139, 69, 19],
"salmon": [250, 128, 114],
"sandybrown": [244, 164, 96],
"seagreen": [46, 139, 87],
"seashell": [255, 245, 238],
"sienna": [160, 82, 45],
"silver": [192, 192, 192],
"skyblue": [135, 206, 235],
"slateblue": [106, 90, 205],
"slategray": [112, 128, 144],
"slategrey": [112, 128, 144],
"snow": [255, 250, 250],
"springgreen": [0, 255, 127],
"steelblue": [70, 130, 180],
"tan": [210, 180, 140],
"teal": [0, 128, 128],
"thistle": [216, 191, 216],
"tomato": [255, 99, 71],
"turquoise": [64, 224, 208],
"violet": [238, 130, 238],
"wheat": [245, 222, 179],
"white": [255, 255, 255],
"whitesmoke": [245, 245, 245],
"yellow": [255, 255, 0],
"yellowgreen": [154, 205, 50]
};
+28 -28
View File
@@ -1,28 +1,28 @@
{
"name": "color-name",
"version": "1.1.4",
"description": "A list of color names and its values",
"main": "index.js",
"files": [
"index.js"
],
"scripts": {
"test": "node test.js"
},
"repository": {
"type": "git",
"url": "[email protected]:colorjs/color-name.git"
},
"keywords": [
"color-name",
"color",
"color-keyword",
"keyword"
],
"author": "DY <[email protected]>",
"license": "MIT",
"bugs": {
"url": "https://github.com/colorjs/color-name/issues"
},
"homepage": "https://github.com/colorjs/color-name"
}
{
"name": "color-name",
"version": "1.1.4",
"description": "A list of color names and its values",
"main": "index.js",
"files": [
"index.js"
],
"scripts": {
"test": "node test.js"
},
"repository": {
"type": "git",
"url": "[email protected]:colorjs/color-name.git"
},
"keywords": [
"color-name",
"color",
"color-keyword",
"keyword"
],
"author": "DY <[email protected]>",
"license": "MIT",
"bugs": {
"url": "https://github.com/colorjs/color-name/issues"
},
"homepage": "https://github.com/colorjs/color-name"
}
+53 -53
View File
@@ -7,59 +7,59 @@ var path = require('path');
var fs = require('fs');
var https = require('https');
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
var packageJson = process$1.env.npm_package_json;
+46 -2
View File
@@ -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':
+1 -1
View File
@@ -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",
+118 -118
View File
@@ -1,119 +1,119 @@
🇨🇿 Czech Clubs Logos API
A fullstack project for serving high-quality, transparent background logos of Czech football & futsal clubs.
Logos are mapped by FAČR UUIDs (from facr.tdvorak.dev
) to ensure consistency across projects.
✨ Features
⚽ Fetch Czech clubs metadata from FAČR Scraper API.
🖼️ Upload & store full-quality transparent logos (SVG/PNG).
🔄 Reuse FAČR UUID as a unique identifier.
🌐 Serve logos through a simple CDN-style API.
📝 Optional metadata (club name, city, colors, competition).
📦 Self-hosted with Go backend + CDN storage.
🔧 Tech Stack
Backend: Golang (Gin or Fiber)
Storage:
Local /logos/{id}.svg
Or cloud (Supabase / Cloudflare R2 / S3)
Database: SQLite/Postgres (to link UUID ↔ metadata ↔ logo file)
External API: facr.tdvorak.dev
📂 Project Structure
czech-clubs-logos-api/
│── backend/
│ ├── main.go # Go API entrypoint
│ ├── routes.go # API routes
│ ├── facr_client.go # Client for facr.tdvorak.dev
│ ├── handlers/
│ │ ├── upload.go # Handle logo upload
│ │ ├── logos.go # Serve logos
│ │ └── clubs.go # Proxy FAČR API
│ └── storage/
│ └── local.go # Logo saving/loading
│── logos/ # Stored club logos (UUID.svg/png)
│── frontend/ # (optional, for admin upload UI)
│── db.sqlite # Database (UUID ↔ metadata)
│── go.mod
│── README.md
🚀 API Endpoints
Search clubs
Proxy FAČR search to help find correct UUID:
GET /clubs/search?q=sparta
→ proxies facr.tdvorak.dev/club/search?q=sparta
Get club info
GET /clubs/:id
→ proxies facr.tdvorak.dev/club/{id}
Upload logo
Upload a full-quality SVG/PNG logo mapped to a FAČR UUID:
POST /logos/:id
FormData: file=@slavia.svg
→ Saves to ./logos/{id}.svg
→ Stores metadata in DB
Get logo
Serve logo by FAČR UUID:
GET /logos/:id
→ returns {id}.svg/png
Get logo with metadata
GET /logos/:id/json
{
"id": "00000000-0000-0000-0000-000000000000",
"club": "AC Sparta Praha",
"type": "football",
"logo_url": "https://cdn.example.com/logos/00000000-0000-0000-0000-000000000000.svg"
}
📊 Example Workflow
Search for a club:
GET /clubs/search?q=Slavia
→ returns UUID: 11111111-2222-3333-4444-555555555555
Upload logo for club:
POST /logos/11111111-2222-3333-4444-555555555555
file=@slavia.svg
Get logo:
GET /logos/11111111-2222-3333-4444-555555555555
→ returns full quality transparent SVG
🔮 Future Ideas
✍️ Web admin panel for uploading logos.
🎨 Auto background remover (e.g., remove.bg API).
🔎 Logo search by club name (maps internally to UUID).
📦 Publish as NPM package (@czech-football/logos) or Go module.
🇨🇿 Czech Clubs Logos API
A fullstack project for serving high-quality, transparent background logos of Czech football & futsal clubs.
Logos are mapped by FAČR UUIDs (from facr.tdvorak.dev
) to ensure consistency across projects.
✨ Features
⚽ Fetch Czech clubs metadata from FAČR Scraper API.
🖼️ Upload & store full-quality transparent logos (SVG/PNG).
🔄 Reuse FAČR UUID as a unique identifier.
🌐 Serve logos through a simple CDN-style API.
📝 Optional metadata (club name, city, colors, competition).
📦 Self-hosted with Go backend + CDN storage.
🔧 Tech Stack
Backend: Golang (Gin or Fiber)
Storage:
Local /logos/{id}.svg
Or cloud (Supabase / Cloudflare R2 / S3)
Database: SQLite/Postgres (to link UUID ↔ metadata ↔ logo file)
External API: facr.tdvorak.dev
📂 Project Structure
czech-clubs-logos-api/
│── backend/
│ ├── main.go # Go API entrypoint
│ ├── routes.go # API routes
│ ├── facr_client.go # Client for facr.tdvorak.dev
│ ├── handlers/
│ │ ├── upload.go # Handle logo upload
│ │ ├── logos.go # Serve logos
│ │ └── clubs.go # Proxy FAČR API
│ └── storage/
│ └── local.go # Logo saving/loading
│── logos/ # Stored club logos (UUID.svg/png)
│── frontend/ # (optional, for admin upload UI)
│── db.sqlite # Database (UUID ↔ metadata)
│── go.mod
│── README.md
🚀 API Endpoints
Search clubs
Proxy FAČR search to help find correct UUID:
GET /clubs/search?q=sparta
→ proxies facr.tdvorak.dev/club/search?q=sparta
Get club info
GET /clubs/:id
→ proxies facr.tdvorak.dev/club/{id}
Upload logo
Upload a full-quality SVG/PNG logo mapped to a FAČR UUID:
POST /logos/:id
FormData: file=@slavia.svg
→ Saves to ./logos/{id}.svg
→ Stores metadata in DB
Get logo
Serve logo by FAČR UUID:
GET /logos/:id
→ returns {id}.svg/png
Get logo with metadata
GET /logos/:id/json
{
"id": "00000000-0000-0000-0000-000000000000",
"club": "AC Sparta Praha",
"type": "football",
"logo_url": "https://cdn.example.com/logos/00000000-0000-0000-0000-000000000000.svg"
}
📊 Example Workflow
Search for a club:
GET /clubs/search?q=Slavia
→ returns UUID: 11111111-2222-3333-4444-555555555555
Upload logo for club:
POST /logos/11111111-2222-3333-4444-555555555555
file=@slavia.svg
Get logo:
GET /logos/11111111-2222-3333-4444-555555555555
→ returns full quality transparent SVG
🔮 Future Ideas
✍️ Web admin panel for uploading logos.
🎨 Auto background remover (e.g., remove.bg API).
🔎 Logo search by club name (maps internally to UUID).
📦 Publish as NPM package (@czech-football/logos) or Go module.
👉 This way, youll have a FAČR-aware logo CDN that anyone can integrate into websites, apps, or your SportCreative projects.