From be4230266e49c95c7859dacac230b5e9b369bb7e Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Mon, 2 Mar 2026 13:11:35 -0500 Subject: [PATCH] feat(collections): Add library-aware filtering to collection detail pages - Add library_id parameter to BuildSections and getViewAllURL functions - Update dashboard handler to pass libraryID when building sections - Add library_id query param support to collection detail page handler - When library_id is provided, filter collection items by that library - When no library_id, show all books (backward compatible) - Reuses GetCollectionItemsForDashboard query for filtered results - Preserves context when navigating from dashboard to collection detail --- internal/handlers/dashboard.go | 11 +++-- internal/router/frontend.go | 76 ++++++++++++++++++++++++++-------- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/internal/handlers/dashboard.go b/internal/handlers/dashboard.go index 5c199e5..aa653d5 100644 --- a/internal/handlers/dashboard.go +++ b/internal/handlers/dashboard.go @@ -61,7 +61,7 @@ func (h *DashboardHandler) GetSections(c echo.Context) error { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load dashboard sections"}) } - sectionData := BuildSections(sections) + sectionData := BuildSections(sections, libraryID) return c.JSON(http.StatusOK, map[string]interface{}{"sections": sectionData}) } @@ -154,7 +154,7 @@ func (h *DashboardHandler) RestoreSystemCollection(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"message": "System collection restored to defaults"}) } -func BuildSections(sections []services.DashboardSection) []SectionData { +func BuildSections(sections []services.DashboardSection, currentLibraryID string) []SectionData { var result []SectionData for _, ds := range sections { @@ -177,7 +177,7 @@ func BuildSections(sections []services.DashboardSection) []SectionData { Description: ds.Description, Icon: ds.Icon, Items: bookCards, - ViewAllURL: getViewAllURL(ds.CollectionID.String()), + ViewAllURL: getViewAllURL(ds.CollectionID.String(), currentLibraryID), Priority: ds.Priority, }) } @@ -185,8 +185,11 @@ func BuildSections(sections []services.DashboardSection) []SectionData { return result } -func getViewAllURL(collectionID string) string { +func getViewAllURL(collectionID string, libraryID string) string { if collectionID != "" { + if libraryID != "" { + return "/collections/" + collectionID + "?library_id=" + libraryID + } return "/collections/" + collectionID } return "" diff --git a/internal/router/frontend.go b/internal/router/frontend.go index cbcb871..6fac606 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -191,8 +191,8 @@ func registerFrontendRoutes(cfg *Config) { } } - sectionData := handlers.BuildSections(visibleSections) - allSectionsData := handlers.BuildSections(allSections) + sectionData := handlers.BuildSections(visibleSections, libraryID) + allSectionsData := handlers.BuildSections(allSections, libraryID) var buf bytes.Buffer err = templates.Dashboard(user, sectionData, allSectionsData, libData, libraryID, prefs.HiddenCollections, limit, errorMsg).Render(c.Request().Context(), &buf) @@ -358,24 +358,66 @@ func registerFrontendRoutes(cfg *Config) { } } } else { - // User collection - fetch collection items - collItems, err := cfg.Queries.GetCollectionItems(c.Request().Context(), pgtype.UUID{Bytes: collUUID, Valid: true}) - if err != nil { - books = []handlers.BookInfo{} - } + // User collection - check if library_id filter is present + libraryID := c.QueryParam("library_id") - // Convert to BookInfo format - bookCards := make([]handlers.BookInfo, len(collItems)) - for i, item := range collItems { - itemUUID, _ := uuid.FromBytes(item.MediaItemID.Bytes[0:16]) - bookCards[i] = handlers.BookInfo{ - MediaItemID: itemUUID.String(), - Title: item.Title, - Author: getText(item.Author), - CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), + if libraryID != "" { + // Filter by library - reuse dashboard query + libUUID, err := uuid.Parse(libraryID) + if err != nil { + return renderErrorPage(c, "Invalid library ID", "invalid_library_id") } + + // Use GetCollectionItemsForDashboard for library-filtered results + collItems, err := cfg.Queries.GetCollectionItemsForDashboard(c.Request().Context(), + database.GetCollectionItemsForDashboardParams{ + CollectionID: pgtype.UUID{Bytes: collUUID, Valid: true}, + LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true}, + Limit: 1000, + }) + if err != nil { + books = []handlers.BookInfo{} + } else { + // Convert to BookInfo format (non-excluded only) + var validItems []database.GetCollectionItemsForDashboardRow + for _, item := range collItems { + if !item.Excluded.Valid || !item.Excluded.Bool { + validItems = append(validItems, item) + } + } + + bookCards := make([]handlers.BookInfo, len(validItems)) + for i, item := range validItems { + itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16]) + bookCards[i] = handlers.BookInfo{ + MediaItemID: itemUUID.String(), + Title: item.Title, + Author: getText(item.Author), + CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), + } + } + books = bookCards + } + } else { + // No library filter - show all books in collection + collItems, err := cfg.Queries.GetCollectionItems(c.Request().Context(), pgtype.UUID{Bytes: collUUID, Valid: true}) + if err != nil { + books = []handlers.BookInfo{} + } + + // Convert to BookInfo format + bookCards := make([]handlers.BookInfo, len(collItems)) + for i, item := range collItems { + itemUUID, _ := uuid.FromBytes(item.MediaItemID.Bytes[0:16]) + bookCards[i] = handlers.BookInfo{ + MediaItemID: itemUUID.String(), + Title: item.Title, + Author: getText(item.Author), + CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), + } + } + books = bookCards } - books = bookCards } // Build collection data colData := templates.CollectionData{