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:
+120
-8
@@ -10,6 +10,7 @@ import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/handlers"
|
||||
"bookhoard/internal/services"
|
||||
"bookhoard/internal/utils"
|
||||
"bookhoard/templates"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@@ -137,27 +138,38 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
libUUID, _ := uuid.Parse(libraryID)
|
||||
userUUID, _ := uuid.Parse(user.ID)
|
||||
|
||||
prefs, _ := cfg.DashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
|
||||
|
||||
prefs, err := cfg.DashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
|
||||
if err != nil {
|
||||
log.Printf("GetDashboardPreferences failed: %v", err)
|
||||
prefs = database.UserDashboardPreferences{
|
||||
HiddenCollections: []string{},
|
||||
CollectionOrder: []string{},
|
||||
ItemsPerSection: pgtype.Int4{Int32: 20, Valid: true},
|
||||
}
|
||||
}
|
||||
limit := 20
|
||||
if prefs.ItemsPerSection.Int32 > 0 {
|
||||
limit = int(prefs.ItemsPerSection.Int32)
|
||||
}
|
||||
var sections []services.DashboardSection
|
||||
sections, err = cfg.DashboardService.GetDashboardSections(
|
||||
|
||||
// Get ALL sections (unfiltered) for the modal
|
||||
allSections, err := cfg.DashboardService.GetDashboardSections(
|
||||
c.Request().Context(),
|
||||
userUUID,
|
||||
libUUID,
|
||||
limit,
|
||||
prefs.CollectionOrder,
|
||||
prefs.HiddenCollections,
|
||||
[]string{}, // No filtering - get all sections
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Dashboard sections query failed: %v", err)
|
||||
sections = []services.DashboardSection{}
|
||||
allSections = []services.DashboardSection{}
|
||||
errorMsg = "Error loading dashboard"
|
||||
}
|
||||
|
||||
// Get only visible sections for the dashboard display
|
||||
visibleSections := cfg.DashboardService.FilterHiddenCollections(allSections, prefs.HiddenCollections)
|
||||
|
||||
userUUID2, _ := uuid.Parse(user.ID)
|
||||
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID2))
|
||||
if err != nil {
|
||||
@@ -179,10 +191,11 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
}
|
||||
}
|
||||
|
||||
sectionData := handlers.BuildSections(sections)
|
||||
sectionData := handlers.BuildSections(visibleSections)
|
||||
allSectionsData := handlers.BuildSections(allSections)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.Dashboard(user, sectionData, libData, libraryID, errorMsg).Render(c.Request().Context(), &buf)
|
||||
err = templates.Dashboard(user, sectionData, allSectionsData, libData, libraryID, prefs.HiddenCollections, limit, errorMsg).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -225,6 +238,105 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// Collection detail page (works for both system and user collections)
|
||||
frontendProtected.GET("/collections/:id", func(c echo.Context) error {
|
||||
user, err := getTemplateUserWithTheme(c, cfg)
|
||||
if err != nil {
|
||||
return renderErrorPage(c, "Error loading user", "user_load_error")
|
||||
}
|
||||
// Parse collection ID from URL
|
||||
collectionID := c.Param("id")
|
||||
collUUID, err := uuid.Parse(collectionID)
|
||||
if err != nil {
|
||||
return renderErrorPage(c, "Invalid collection ID", "invalid_id")
|
||||
}
|
||||
// Fetch collection details
|
||||
collection, err := cfg.Queries.GetCollection(c.Request().Context(), pgtype.UUID{Bytes: collUUID, Valid: true})
|
||||
if err != nil {
|
||||
if err.Error() == "no rows in result set" {
|
||||
return renderErrorPage(c, "Collection not found", "not_found")
|
||||
}
|
||||
return renderErrorPage(c, "Error loading collection", "collection_load_error")
|
||||
}
|
||||
// Fetch books in collection
|
||||
userUUID, _ := uuid.Parse(user.ID)
|
||||
var books []handlers.BookInfo
|
||||
|
||||
if collection.QueryType.Valid && collection.QueryType.String != "" {
|
||||
// System collection - use query type
|
||||
// System collection - need library_id for system collections
|
||||
// Get library_id from query param or default to user's first library
|
||||
libraryID := c.QueryParam("library_id")
|
||||
if libraryID == "" {
|
||||
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
|
||||
if err == nil && len(libraries) > 0 {
|
||||
libUUID, _ := uuid.FromBytes(libraries[0].ID.Bytes[0:16])
|
||||
libraryID = libUUID.String()
|
||||
}
|
||||
}
|
||||
|
||||
libUUID, _ := uuid.Parse(libraryID)
|
||||
dashboardSvc := services.NewDashboardService(cfg.Queries)
|
||||
sections, err := dashboardSvc.GetDashboardSections(c.Request().Context(), userUUID, libUUID, 1000, []string{}, []string{})
|
||||
if err != nil {
|
||||
return renderErrorPage(c, "Error loading books", "books_load_error")
|
||||
}
|
||||
|
||||
// Find the matching section and convert items
|
||||
for _, section := range sections {
|
||||
if section.CollectionID.String() == collectionID {
|
||||
// Convert []database.MediaItems to []handlers.BookInfo
|
||||
bookCards := make([]handlers.BookInfo, len(section.Items))
|
||||
for i, item := range section.Items {
|
||||
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
|
||||
break
|
||||
}
|
||||
}
|
||||
} 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{}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// Build collection data
|
||||
colData := templates.CollectionData{
|
||||
ID: collectionID,
|
||||
Name: collection.Name,
|
||||
Description: collection.Description.String,
|
||||
Color: collection.Color.String,
|
||||
Icon: collection.Icon.String,
|
||||
}
|
||||
// Render the CollectionDetail template
|
||||
var buf bytes.Buffer
|
||||
err = templates.CollectionDetail(user, colData, books).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// Custom Section Builder page
|
||||
frontendProtected.GET("/custom-section", func(c echo.Context) error {
|
||||
user, err := getTemplateUserWithTheme(c, cfg)
|
||||
|
||||
Reference in New Issue
Block a user