feat: Add collection detail page with /collections/:id route

Add comprehensive collection detail page that works for both system collections
(continue-reading, recently-added, not-started) and user collections.

Backend changes:
- Add new /collections/:id route in internal/router/frontend.go
  - Fetches collection using GetCollection with UUID parameter
  - Determines collection type from QueryType field
  - Resolves library_id for system collections
  - Converts database.MediaItems to handlers.BookInfo for display
  - Renders CollectionDetail template with collection and books data

- Update SectionData struct in internal/handlers/collections.go
  - Add CollectionID string field for view all links

- Update BuildSections() in internal/handlers/dashboard.go
  - Pass CollectionID to SectionData for proper link generation

- Simplify getViewAllURL() in internal/handlers/dashboard.go
  - Return /collections/{collectionID} instead of /section/{type}
  - Works uniformly for both system and user collections

Frontend changes:
- Fix CollectionDetail template in templates/collections.templ
  - Fix broken div nesting causing compilation error
  - Add null check for CoverImagePath to prevent broken images
  - Update aspect ratio to modern aspect-[3/4] syntax
  - Use responsive widths (w-16 sm:w-20) for mobile/desktop
  - Improve card layout with horizontal flex structure
  - Add placeholder image fallback for books without covers
  - Remove erroneous renderBooks() function call

This change aligns with the backend update where system collections are
now pre-made user collections in the database with query_type fields.
All collections can now use the same CollectionDetail template for a
consistent viewing experience.
This commit is contained in:
2026-03-01 00:28:54 -05:00
parent fd608f3e3f
commit 0b666f3fdd
6 changed files with 481 additions and 274 deletions
+45 -17
View File
@@ -40,6 +40,9 @@ func (h *DashboardHandler) GetSections(c echo.Context) error {
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
@@ -83,6 +86,11 @@ func (h *DashboardHandler) UpdatePreferences(c echo.Context) error {
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},
@@ -157,29 +165,49 @@ func BuildSections(sections []services.DashboardSection) []SectionData {
}
result = append(result, SectionData{
ID: ds.CollectionName,
IsSystem: ds.IsSystem,
Title: ds.Title,
Description: ds.Description,
Icon: ds.Icon,
Items: bookCards,
ViewAllURL: getViewAllURL(ds.CollectionName, ds.QueryType),
Priority: ds.Priority,
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(key, queryType string) string {
urls := map[string]string{
"continue-reading": "/section/continue-reading",
"recently-added": "/section/recently-added",
"recently-read": "/history",
"not-started": "/section/not-started",
}
if url, exists := urls[queryType]; exists {
return url
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"})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"hidden_collections": prefs.HiddenCollections,
"collection_order": prefs.CollectionOrder,
"items_per_section": prefs.ItemsPerSection.Int32,
})
}