diff --git a/internal/router/filters.go b/internal/router/filters.go index 343a2a7..72e2fa4 100644 --- a/internal/router/filters.go +++ b/internal/router/filters.go @@ -1,5 +1,18 @@ 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 @@ -11,7 +24,118 @@ func registerFiltersRoutes(cfg *Config) { filters := protected.Group("/saved-filters") filters.GET("", cfg.FiltersHandler.GetSavedFilters) filters.GET("/:id", cfg.FiltersHandler.GetSavedFilterByID) - filters.POST("", cfg.FiltersHandler.CreateSavedFilter) + // 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 +}