package router import ( "bookhoard/internal/database" "bookhoard/internal/handlers" "bookhoard/internal/services" "bookhoard/internal/utils" "bookhoard/templates" "bytes" "log" "net/http" "strconv" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v5" ) func registerSearchRoutes(cfg *Config) { e := cfg.Echo // JWT middleware for protected routes jwtMiddleware := createJWTMiddleware(cfg) protected := e.Group("/api", jwtMiddleware) // Search endpoint - returns JSON or HTML based on HX-Request header protected.GET("/media-items/search", func(c *echo.Context) error { // Check if HTMX request (expects HTML) if c.Request().Header.Get("HX-Request") == "true" { return handleSearchHTML(c, cfg) } // Otherwise return JSON (API clients) return cfg.MediaHandler.SearchMediaItems(c) }) protected.POST("/sync/books/query", cfg.MatchingHandler.QueryBooks) } // handleSearchHTML renders HTML response for HTMX requests func handleSearchHTML(c *echo.Context, cfg *Config) error { user, ok := c.Get("user").(database.Users) if !ok { return c.String(http.StatusUnauthorized, "Unauthorized") } // Extract query parameters query := c.QueryParam("q") libraryID := c.QueryParam("library_id") limit, _ := strconv.Atoi(c.QueryParam("limit")) if limit == 0 { limit = 50 } offset, _ := strconv.Atoi(c.QueryParam("offset")) // Validate library_id var libUUID pgtype.UUID if libraryID != "" { lib, err := uuid.Parse(libraryID) if err != nil { return c.String(http.StatusBadRequest, "invalid library_id") } libUUID = pgtype.UUID{Bytes: lib, Valid: true} } else { libUUID = pgtype.UUID{Valid: false} } // Extract filters authorFilter := c.QueryParam("author_filter") seriesFilter := c.QueryParam("series_filter") genreFilter := c.QueryParam("genre_filter") tagsFilter := c.QueryParam("tags_filter") languageFilter := c.QueryParam("language_filter") yearMin, _ := strconv.Atoi(c.QueryParam("year_min")) yearMax, _ := strconv.Atoi(c.QueryParam("year_max")) hasCover := false hasCoverValid := false if _, exists := c.QueryParams()["has_cover"]; exists { hasCover = c.QueryParam("has_cover") == "true" hasCoverValid = true } sortParam := c.QueryParam("sort") if sortParam == "" { sortParam = "title ASC" } // Build search params params := services.SearchParams{ UserID: user.ID, LibraryID: libUUID, SearchQuery: query, AuthorFilter: authorFilter, SeriesFilter: seriesFilter, GenreFilter: genreFilter, TagsFilter: tagsFilter, LanguageFilter: languageFilter, YearMin: yearMin, YearMax: yearMax, HasCover: pgtype.Bool{Bool: hasCover, Valid: hasCoverValid}, Sort: sortParam, Limit: limit, Offset: offset, } // Execute search using handler method results, totalCount, err := cfg.MediaHandler.ExecuteSearch(c.Request().Context(), params) if err != nil { log.Printf("Search error: %v", err) return c.HTML(http.StatusInternalServerError, `
Search error
`) } // Convert to BookInfo bookInfoList := make([]handlers.BookInfo, len(results)) for i, book := range results { bookUUID, _ := uuid.FromBytes(book.ID.Bytes[0:16]) bookLibUUID, _ := uuid.FromBytes(book.LibraryID.Bytes[0:16]) bookInfoList[i] = handlers.BookInfo{ MediaItemID: bookUUID.String(), Title: book.Title, Author: textToString(book.Author), CoverImagePath: utils.ResolveMediaURL(pgtype.UUID{Bytes: bookLibUUID, Valid: true}, book.CoverImagePath), } } // Stamp active conflict flags so cards route the play action correctly bookInfoList = handlers.MarkActiveConflicts(c.Request().Context(), cfg.Queries, user.ID, bookInfoList) // Render using BooksGrid template (or BookPickerGrid for collection picker) var buf bytes.Buffer if c.QueryParam("show_checkbox") == "true" { err = templates.BookPickerGrid(bookInfoList).Render(c.Request().Context(), &buf) } else { err = templates.BooksGrid(bookInfoList, limit, offset, totalCount, libraryID).Render(c.Request().Context(), &buf) } if err != nil { log.Printf("Template render error: %v", err) return c.HTML(http.StatusInternalServerError, `
Render error
`) } return c.HTML(http.StatusOK, buf.String()) } // textToString converts pgtype.Text to string func textToString(t pgtype.Text) string { if t.Valid { return t.String } return "" }