diff --git a/backend/data/db.sqlite b/backend/data/db.sqlite new file mode 100644 index 0000000..5cc399a Binary files /dev/null and b/backend/data/db.sqlite differ diff --git a/backend/go.mod b/backend/go.mod index 7a146f0..1d5bfdc 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,6 +3,7 @@ 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 @@ -13,6 +14,7 @@ require ( ) 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 @@ -33,11 +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/crypto v0.22.0 // indirect golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect - golang.org/x/net v0.22.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + 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 ) diff --git a/backend/go.sum b/backend/go.sum index a7210f5..bbacf8f 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -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= @@ -81,23 +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/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/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +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= diff --git a/backend/handlers.go b/backend/handlers.go index 8e55583..1c377fd 100644 --- a/backend/handlers.go +++ b/backend/handlers.go @@ -13,6 +13,9 @@ import ( "strings" "time" + "unicode" + + "github.com/PuerkitoBio/goquery" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/PuerkitoBio/goquery" @@ -25,12 +28,24 @@ import ( // ==================== Club Handlers ==================== func searchClubs(c *gin.Context) { + q := strings.TrimSpace(c.Query("q")) + if q == "" { q := strings.TrimSpace(c.Query("q")) if q == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'q' is required"}) return } + 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)) clubs, err := scrapeFotbalSearch(q) if err != nil || len(clubs) == 0 { nq := removeDiacritics(strings.ToLower(q)) @@ -54,6 +69,8 @@ func getClub(c *gin.Context) { return } + club, err := fetchClubByID(id) + if err != nil || club == nil { club, err := fetchClubByID(id) if err != nil || club == nil { c.JSON(http.StatusNotFound, gin.H{"error": "club not found"}) @@ -317,18 +334,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 { @@ -345,21 +362,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) @@ -474,7 +491,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) @@ -496,6 +513,7 @@ func getLogoWithMetadata(c *gin.Context) { // List all logos func listLogos(c *gin.Context) { q := strings.TrimSpace(c.Query("q")) + sport := strings.ToLower(strings.TrimSpace(c.DefaultQuery("sport", c.DefaultQuery("type", "")))) sortParam := c.DefaultQuery("sort", "name") limitStr := c.Query("limit") pageStr := c.Query("page") @@ -715,16 +733,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() @@ -751,12 +769,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) @@ -764,15 +782,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() @@ -783,17 +801,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() @@ -817,14 +835,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) diff --git a/backend/main.go b/backend/main.go index 5431d1b..ea9a68c 100644 --- a/backend/main.go +++ b/backend/main.go @@ -3,8 +3,8 @@ package main import ( "database/sql" "log" - "os" "net/http" + "os" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" @@ -46,7 +46,7 @@ func main() { 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 }, + AllowOriginFunc: func(origin string) bool { return true }, })) // Routes @@ -89,6 +89,7 @@ func setupRoutes(r *gin.Engine) { clubs := r.Group("/clubs") { clubs.GET("/search", searchClubs) + clubs.GET("/search-with-logos", searchClubsWithLogos) clubs.GET("/:id", getClub) } diff --git a/frontend/src/admin.js b/frontend/src/admin.js index e4cee01..bff4a77 100644 --- a/frontend/src/admin.js +++ b/frontend/src/admin.js @@ -11,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) + '' + t.slice(start, end) + '' + 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) @@ -28,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 = '
${club.type || 'football'}
${club.id}
${club.website ? `${club.website}
` : ''} @@ -169,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 = `${club.website}` + } else { + selectedClubWebsiteEl.textContent = '' + } + + const displayLogo = club.display_logo_url || club.logo_url || '' + if (displayLogo) { + selectedClubLogoEl.innerHTML = `${club.city || 'N/A'} • ${club.type || 'football'}
+${highlight(club.city || 'N/A', query)} • ${club.type || 'football'}
${club.id}