feat: add HTMX support for saving filters with HTML response handling
Router changes for saved filters API: - Add CreateFilterHTML handler to return HTML for HTMX requests - Extract CreateFilterHTML function to handle filter creation logic - Add wrapper for POST /api/saved-filters to detect HTMX requests - HTMX requests: Return HTML via CreateFilterHTML - Regular requests: Return JSON via existing handler - Add collectFilterFormData helper to gather form data from #filter-form - Improve error handling for duplicate filter names (409 Conflict) - HTML responses include error messages for better UX This enables the save filter modal to work without page refresh, providing a smoother user experience with immediate visual feedback.
This commit is contained in:
+125
-1
@@ -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, `<div hx-swap-oob="innerHTML:#filter-save-error" style="color: #ef4444; padding: 0.75rem; border-radius: 0.5rem; margin-bottom: 1rem;">⚠️ Filter with this name already exists</div>`)
|
||||
}
|
||||
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, `<div style="color: red;">Render error</div>`)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user