Files
bookhoard/internal/handlers/dashboard.go
T
john-okeefe a14b9c82ef fix(dashboard): normalize nil slices to empty arrays in preferences API
Ensure consistent JSON responses by converting nil slices to empty arrays
in the GetPreferences handler. This prevents null values from being
returned to the client for hidden_collections and collection_order fields,
making the API response more predictable and easier to consume.
2026-03-01 21:35:31 -05:00

230 lines
6.9 KiB
Go

package handlers
import (
"bookhoard/internal/database"
"bookhoard/internal/services"
"bookhoard/internal/utils"
"net/http"
"strconv"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v4"
)
type DashboardHandler struct {
db *database.Queries
dashboardService *services.DashboardService
}
func NewDashboardHandler(db *database.Queries) *DashboardHandler {
return &DashboardHandler{
db: db,
dashboardService: services.NewDashboardService(db),
}
}
func (h *DashboardHandler) GetSections(c echo.Context) error {
user := c.Get("user").(database.Users)
userUUID := uuid.UUID(user.ID.Bytes)
libraryID := c.QueryParam("library_id")
if libraryID == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id required"})
}
libUUID, err := uuid.Parse(libraryID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
}
prefs, _ := h.dashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
limit := 20
if prefs.ItemsPerSection.Valid {
limit = int(prefs.ItemsPerSection.Int32)
}
if limitStr := c.QueryParam("limit"); limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
limit = l
}
}
sections, err := h.dashboardService.GetDashboardSections(
c.Request().Context(),
userUUID,
libUUID,
limit,
prefs.CollectionOrder,
prefs.HiddenCollections,
)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load dashboard sections"})
}
sectionData := BuildSections(sections)
return c.JSON(http.StatusOK, map[string]interface{}{"sections": sectionData})
}
func (h *DashboardHandler) UpdatePreferences(c echo.Context) error {
user := c.Get("user").(database.Users)
userUUID := uuid.UUID(user.ID.Bytes)
var req struct {
LibraryID string `json:"library_id"`
HiddenCollections []string `json:"hidden_collections"`
CollectionOrder []string `json:"collection_order"`
ItemsPerSection int `json:"items_per_section"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
}
libUUID, err := uuid.Parse(req.LibraryID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
}
// Sanitize preferences (remove duplicates)
cleanHidden, cleanOrder := h.dashboardService.SanitizeDashboardPreferences(req.HiddenCollections, req.CollectionOrder)
req.HiddenCollections = cleanHidden
req.CollectionOrder = cleanOrder
prefs, err := h.dashboardService.UpsertDashboardPreferences(c.Request().Context(), database.UpsertDashboardPreferencesParams{
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
HiddenCollections: req.HiddenCollections,
CollectionOrder: req.CollectionOrder,
ItemsPerSection: pgtype.Int4{Int32: int32(req.ItemsPerSection), Valid: true},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to save preferences"})
}
return c.JSON(http.StatusOK, prefs)
}
func (h *DashboardHandler) RestoreSystemCollection(c echo.Context) error {
user := c.Get("user").(database.Users)
userUUID := uuid.UUID(user.ID.Bytes)
var req struct {
CollectionName string `form:"collection_name" json:"collection_name"`
ResetType string `form:"reset_type" json:"reset_type"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
}
if req.CollectionName == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "collection_name required"})
}
if req.ResetType == "" {
req.ResetType = "full"
}
validCollections := map[string]bool{
"continue-reading": true,
"recently-added": true,
"recently-read": true,
"not-started": true,
}
if !validCollections[req.CollectionName] {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid system collection name"})
}
if req.ResetType != "full" && req.ResetType != "keep_books" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "reset_type must be 'full' or 'keep_books'"})
}
err := h.dashboardService.RestoreSystemCollection(c.Request().Context(), userUUID, req.CollectionName, req.ResetType)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to restore system collection"})
}
// Add redirect header for HTMX requests
if c.Request().Header.Get("HX-Request") == "true" {
c.Response().Header().Set("HX-Redirect", "/collections")
}
return c.JSON(http.StatusOK, map[string]string{"message": "System collection restored to defaults"})
}
func BuildSections(sections []services.DashboardSection) []SectionData {
var result []SectionData
for _, ds := range sections {
bookCards := make([]BookInfo, len(ds.Items))
for i, item := range ds.Items {
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
bookCards[i] = BookInfo{
MediaItemID: itemUUID.String(),
Title: item.Title,
Author: textToString(item.Author),
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
}
}
result = append(result, SectionData{
ID: ds.CollectionName,
CollectionID: ds.CollectionID.String(),
IsSystem: ds.IsSystem,
Title: ds.Title,
Description: ds.Description,
Icon: ds.Icon,
Items: bookCards,
ViewAllURL: getViewAllURL(ds.CollectionID.String()),
Priority: ds.Priority,
})
}
return result
}
func getViewAllURL(collectionID string) string {
if collectionID != "" {
return "/collections/" + collectionID
}
return ""
}
func (h *DashboardHandler) GetPreferences(c echo.Context) error {
user := c.Get("user").(database.Users)
userUUID := uuid.UUID(user.ID.Bytes)
libraryID := c.QueryParam("library_id")
// ✅ Add validation
if libraryID == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id required"})
}
libUUID, err := uuid.Parse(libraryID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
}
prefs, err := h.dashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": "Preferences not found"})
}
// Normalize nil slices to empty arrays for JSON
hiddenCollections := prefs.HiddenCollections
if hiddenCollections == nil {
hiddenCollections = []string{}
}
collectionOrder := prefs.CollectionOrder
if collectionOrder == nil {
collectionOrder = []string{}
}
return c.JSON(http.StatusOK, map[string]interface{}{
"hidden_collections": hiddenCollections,
"collection_order": collectionOrder,
"items_per_section": prefs.ItemsPerSection.Int32,
})
}