package router import ( "bookhoard/internal/database" "bookhoard/templates" "bytes" "encoding/json" "net/http" "strings" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v5" ) func registerFiltersRoutes(cfg *Config) { e := cfg.Echo // JWT middleware for protected routes jwtMiddleware := createJWTMiddleware(cfg) protected := e.Group("/api", jwtMiddleware) // Saved filters routes filters := protected.Group("/saved-filters") filters.GET("", cfg.FiltersHandler.GetSavedFilters) filters.GET("/:id", cfg.FiltersHandler.GetSavedFilterByID) // POST route with HTMX support filters.POST("", func(c *echo.Context) error { // Check if HTMX request (expects HTML) if c.Request().Header.Get("HX-Request") == "true" { return handleCreateFilterHTML(c, cfg) } // Otherwise return JSON (API clients) return cfg.FiltersHandler.CreateSavedFilter(c) }) filters.PUT("/:id", cfg.FiltersHandler.UpdateSavedFilter) filters.DELETE("/:id", cfg.FiltersHandler.DeleteSavedFilter) } // handleCreateFilterHTML renders HTML response for HTMX requests func handleCreateFilterHTML(c *echo.Context, cfg *Config) error { user, ok := c.Get("user").(database.Users) if !ok { return c.String(http.StatusUnauthorized, "Unauthorized") } userUUID := uuid.UUID(user.ID.Bytes) // Parse form data var req struct { Name string `form:"filter_name"` ResourceType string `form:"resource_type"` } if err := c.Bind(&req); err != nil { return c.String(http.StatusBadRequest, "Invalid request body") } if req.Name == "" || req.ResourceType == "" { return c.String(http.StatusBadRequest, "name and resource_type are required") } // Collect filter data from individual form fields (via hx-include) filtersMap := collectFilterFormData(c) // Convert to JSON for database storage filtersJSON, err := json.Marshal(filtersMap) if err != nil { return c.String(http.StatusInternalServerError, "Failed to encode filters") } // Create filter using service layer filter, err := cfg.Queries.CreateSavedFilter(c.Request().Context(), database.CreateSavedFilterParams{ UserID: pgtype.UUID{Bytes: userUUID, Valid: true}, Name: req.Name, ResourceType: req.ResourceType, Filters: filtersJSON, }) if err != nil { errorMsg := err.Error() // Check for duplicate name error (wrapped or unwrapped) if strings.Contains(errorMsg, "filter with name") && strings.Contains(errorMsg, "already exists") { if c.Request().Header.Get("HX-Request") == "true" { return c.HTML(http.StatusConflict, `
⚠️ Filter with this name already exists
`) } return c.String(http.StatusConflict, "Filter with this name already exists") } return c.String(http.StatusInternalServerError, "failed to create filter: "+errorMsg) } // Render filter item HTML to buffer var buf bytes.Buffer filterID := uuid.Must(uuid.FromBytes(filter.ID.Bytes[:])) err = templates.FilterItem( filterID.String(), filter.Name, ).Render(c.Request().Context(), &buf) if err != nil { return c.HTML(http.StatusInternalServerError, `
Render error
`) } return c.HTML(http.StatusCreated, buf.String()) } // collectFilterFormData extracts filter values from form fields func collectFilterFormData(c *echo.Context) map[string]string { filters := make(map[string]string) // Collect individual form fields if q := c.FormValue("q"); q != "" { filters["q"] = q } if author := c.FormValue("author_filter"); author != "" { filters["author_filter"] = author } if tags := c.FormValue("tags_filter"); tags != "" { filters["tags_filter"] = tags } if series := c.FormValue("series_filter"); series != "" { filters["series_filter"] = series } if language := c.FormValue("language_filter"); language != "" { filters["language_filter"] = language } if yearMin := c.FormValue("year_min"); yearMin != "" { filters["year_min"] = yearMin } if yearMax := c.FormValue("year_max"); yearMax != "" { filters["year_max"] = yearMax } if hasCover := c.FormValue("has_cover"); hasCover != "" { filters["has_cover"] = hasCover } if libraryID := c.FormValue("library_id"); libraryID != "" { filters["library_id"] = libraryID } if sort := c.FormValue("sort"); sort != "" { filters["sort"] = sort } if limit := c.FormValue("limit"); limit != "" { filters["limit"] = limit } if offset := c.FormValue("offset"); offset != "" { filters["offset"] = offset } return filters }