feat(router): cookie-aware SSR library resolution with resolveLibrary helper
- helpers.go: Promote getText() from a local closure in frontend.go
to a package-level function so it can be used by resolveLibrary.
Add resolveLibrary(c, cfg, user.ID) helper that:
1. Reads library_id query param (explicit navigation wins)
2. Falls back to selectedLibrary cookie — validates __all__
sentinel or real UUID, rejects garbage values silently
3. Falls back to user's first visible library
Returns LibraryResolution struct with LibraryID, IsAll, LibUUID,
Libraries, and FirstID — eliminating repeated boilerplate across
all SSR routes.
- frontend.go: Replace manual library resolution boilerplate in 5
SSR route handlers (series, tags/detail, bookshelf, dashboard,
collections/:id) with resolveLibrary(). Each route now gets cookie-
aware library selection for free. Collection detail correctly
handles All Libraries mode for both system and user collections.
Dashboard no longer makes a redundant second GetUserVisibleLibraries
call.
This commit is contained in:
+128
-315
@@ -111,14 +111,6 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
// Protected frontend routes (no /api prefix)
|
// Protected frontend routes (no /api prefix)
|
||||||
frontendProtected := e.Group("", jwtMiddleware, ensureUserExistsMiddleware(cfg))
|
frontendProtected := e.Group("", jwtMiddleware, ensureUserExistsMiddleware(cfg))
|
||||||
|
|
||||||
// Helper to extract text from pgtype.Text
|
|
||||||
getText := func(t pgtype.Text) string {
|
|
||||||
if t.Valid {
|
|
||||||
return t.String
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Series browse page
|
// Series browse page
|
||||||
frontendProtected.GET("/series", func(c *echo.Context) error {
|
frontendProtected.GET("/series", func(c *echo.Context) error {
|
||||||
user, err := getTemplateUserWithTheme(c, cfg)
|
user, err := getTemplateUserWithTheme(c, cfg)
|
||||||
@@ -128,36 +120,11 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
|
|
||||||
var errorMsg string
|
var errorMsg string
|
||||||
|
|
||||||
libraryID := c.QueryParam("library_id")
|
libRes := resolveLibrary(c, cfg, user.ID)
|
||||||
userUUID, _ := uuid.Parse(user.ID)
|
libraryID := libRes.LibraryID
|
||||||
if libraryID == "" {
|
libData := libRes.Libraries
|
||||||
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
|
if libRes.IsAll {
|
||||||
if err == nil && len(libraries) > 0 {
|
errorMsg = ""
|
||||||
libUUID, _ := uuid.FromBytes(libraries[0].ID.Bytes[0:16])
|
|
||||||
libraryID = libUUID.String()
|
|
||||||
} else {
|
|
||||||
errorMsg = "No libraries available"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("GetUserVisibleLibraries failed: %v", err)
|
|
||||||
libraries = []database.GetUserVisibleLibrariesRow{}
|
|
||||||
if errorMsg == "" {
|
|
||||||
errorMsg = "Error loading libraries"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
libData := make([]templates.LibraryData, len(libraries))
|
|
||||||
for i, lib := range libraries {
|
|
||||||
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
|
||||||
libData[i] = templates.LibraryData{
|
|
||||||
ID: libUUID.String(),
|
|
||||||
Name: lib.Name,
|
|
||||||
Description: getText(lib.Description),
|
|
||||||
TypeName: lib.TypeName,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
perSeriesPage := 24
|
perSeriesPage := 24
|
||||||
@@ -172,31 +139,28 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
var seriesCards []templates.SeriesCardData
|
var seriesCards []templates.SeriesCardData
|
||||||
totalPages := 1
|
totalPages := 1
|
||||||
|
|
||||||
if libraryID != "" && errorMsg == "" {
|
if errorMsg == "" {
|
||||||
libUUID, err := uuid.Parse(libraryID)
|
seriesList, total, err := handlers.GetSeriesCardsData(c.Request().Context(), cfg.Queries, libRes.LibUUID, perSeriesPage, offset)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
seriesList, total, err := handlers.GetSeriesCardsData(c.Request().Context(), cfg.Queries, libUUID, perSeriesPage, offset)
|
log.Printf("GetSeriesCardsData failed: %v", err)
|
||||||
if err != nil {
|
errorMsg = "Error loading series"
|
||||||
log.Printf("GetSeriesCardsData failed: %v", err)
|
} else {
|
||||||
errorMsg = "Error loading series"
|
totalPages = (total + perSeriesPage - 1) / perSeriesPage
|
||||||
} else {
|
if totalPages < 1 {
|
||||||
totalPages = (total + perSeriesPage - 1) / perSeriesPage
|
totalPages = 1
|
||||||
if totalPages < 1 {
|
}
|
||||||
totalPages = 1
|
seriesCards = make([]templates.SeriesCardData, 0, len(seriesList))
|
||||||
}
|
for _, s := range seriesList {
|
||||||
seriesCards = make([]templates.SeriesCardData, 0, len(seriesList))
|
covers := s.CoverPaths
|
||||||
for _, s := range seriesList {
|
if covers == nil {
|
||||||
covers := s.CoverPaths
|
covers = []string{}
|
||||||
if covers == nil {
|
|
||||||
covers = []string{}
|
|
||||||
}
|
|
||||||
seriesCards = append(seriesCards, templates.SeriesCardData{
|
|
||||||
Name: s.Name,
|
|
||||||
BookCount: s.BookCount,
|
|
||||||
TotalInSeries: s.TotalInSeries,
|
|
||||||
CoverPaths: covers,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
seriesCards = append(seriesCards, templates.SeriesCardData{
|
||||||
|
Name: s.Name,
|
||||||
|
BookCount: s.BookCount,
|
||||||
|
TotalInSeries: s.TotalInSeries,
|
||||||
|
CoverPaths: covers,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,40 +191,23 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
return renderErrorPage(c, "Series name required", "bad_request")
|
return renderErrorPage(c, "Series name required", "bad_request")
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryID := c.QueryParam("library_id")
|
|
||||||
userUUID, _ := uuid.Parse(user.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()
|
|
||||||
} else {
|
|
||||||
errorMsg = "No libraries available"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var bookInfoList []handlers.BookInfo
|
var bookInfoList []handlers.BookInfo
|
||||||
|
|
||||||
if libraryID != "" && errorMsg == "" {
|
svc := services.NewSeriesService(cfg.Queries)
|
||||||
libUUID, err := uuid.Parse(libraryID)
|
books, err := svc.GetSeriesBooks(c.Request().Context(), seriesName)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
svc := services.NewSeriesService(cfg.Queries)
|
log.Printf("GetSeriesBooks failed: %v", err)
|
||||||
books, err := svc.GetSeriesBooks(c.Request().Context(), libUUID, seriesName)
|
errorMsg = "Error loading series books"
|
||||||
if err != nil {
|
} else {
|
||||||
log.Printf("GetSeriesBooks failed: %v", err)
|
bookInfoList = make([]handlers.BookInfo, 0, len(books))
|
||||||
errorMsg = "Error loading series books"
|
for _, item := range books {
|
||||||
} else {
|
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
|
||||||
bookInfoList = make([]handlers.BookInfo, 0, len(books))
|
bookInfoList = append(bookInfoList, handlers.BookInfo{
|
||||||
for _, item := range books {
|
MediaItemID: itemUUID.String(),
|
||||||
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
|
Title: item.Title,
|
||||||
bookInfoList = append(bookInfoList, handlers.BookInfo{
|
Author: textToString(item.Author),
|
||||||
MediaItemID: itemUUID.String(),
|
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
||||||
Title: item.Title,
|
})
|
||||||
Author: textToString(item.Author),
|
|
||||||
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +216,7 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err = templates.BrowseDetail(user, "📚", "Series", seriesName, seriesName, "/series", "All Series", "📚", "This series doesn't have any books in this library yet", bookInfoList, errorMsg).Render(c.Request().Context(), &buf)
|
err = templates.BrowseDetail(user, "📚", "Series", seriesName, seriesName, "/series", "All Series", "📚", "This series doesn't have any books yet", bookInfoList, errorMsg).Render(c.Request().Context(), &buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -289,41 +236,29 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
return renderErrorPage(c, "Tag name required", "bad_request")
|
return renderErrorPage(c, "Tag name required", "bad_request")
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryID := c.QueryParam("library_id")
|
libRes := resolveLibrary(c, cfg, user.ID)
|
||||||
userUUID, _ := uuid.Parse(user.ID)
|
libraryID := libRes.LibraryID
|
||||||
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()
|
|
||||||
} else {
|
|
||||||
errorMsg = "No libraries available"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var bookInfoList []handlers.BookInfo
|
var bookInfoList []handlers.BookInfo
|
||||||
|
|
||||||
if libraryID != "" && errorMsg == "" {
|
if libraryID != "" && errorMsg == "" {
|
||||||
libUUID, err := uuid.Parse(libraryID)
|
books, err := cfg.Queries.GetBooksByTag(c.Request().Context(), database.GetBooksByTagParams{
|
||||||
if err == nil {
|
LibraryID: libRes.LibUUID,
|
||||||
books, err := cfg.Queries.GetBooksByTag(c.Request().Context(), database.GetBooksByTagParams{
|
Column2: tagName,
|
||||||
LibraryID: uuidToPGType(libUUID),
|
})
|
||||||
Column2: tagName,
|
if err != nil {
|
||||||
})
|
log.Printf("GetBooksByTag failed: %v", err)
|
||||||
if err != nil {
|
errorMsg = "Error loading tag books"
|
||||||
log.Printf("GetBooksByTag failed: %v", err)
|
} else {
|
||||||
errorMsg = "Error loading tag books"
|
bookInfoList = make([]handlers.BookInfo, 0, len(books))
|
||||||
} else {
|
for _, item := range books {
|
||||||
bookInfoList = make([]handlers.BookInfo, 0, len(books))
|
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
|
||||||
for _, item := range books {
|
bookInfoList = append(bookInfoList, handlers.BookInfo{
|
||||||
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
|
MediaItemID: itemUUID.String(),
|
||||||
bookInfoList = append(bookInfoList, handlers.BookInfo{
|
Title: item.Title,
|
||||||
MediaItemID: itemUUID.String(),
|
Author: textToString(item.Author),
|
||||||
Title: item.Title,
|
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
||||||
Author: textToString(item.Author),
|
})
|
||||||
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,52 +283,20 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
|
|
||||||
var errorMsg string
|
var errorMsg string
|
||||||
|
|
||||||
// Get library_id from query param or user's first library
|
libRes := resolveLibrary(c, cfg, user.ID)
|
||||||
libraryID := c.QueryParam("library_id")
|
libraryID := libRes.LibraryID
|
||||||
if libraryID == "" {
|
libData := libRes.Libraries
|
||||||
userUUID, _ := uuid.Parse(user.ID)
|
|
||||||
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()
|
|
||||||
} else {
|
|
||||||
errorMsg = "No libraries available"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get libraries for dropdown
|
// Fetch saved filters for SSR
|
||||||
userUUID, _ := uuid.Parse(user.ID)
|
userUUID, _ := uuid.Parse(user.ID)
|
||||||
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("GetUserVisibleLibraries failed: %v", err)
|
|
||||||
libraries = []database.GetUserVisibleLibrariesRow{}
|
|
||||||
if errorMsg == "" {
|
|
||||||
errorMsg = "Error loading libraries"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
libData := make([]templates.LibraryData, len(libraries))
|
|
||||||
for i, lib := range libraries {
|
|
||||||
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
|
||||||
libData[i] = templates.LibraryData{
|
|
||||||
ID: libUUID.String(),
|
|
||||||
Name: lib.Name,
|
|
||||||
Description: getText(lib.Description),
|
|
||||||
TypeName: lib.TypeName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch saved filters for SSR (using existing query)
|
|
||||||
var savedFilters []database.SavedFilters
|
var savedFilters []database.SavedFilters
|
||||||
if libraryID != "" && errorMsg == "" {
|
savedFilters, err = cfg.Queries.GetSavedFilters(c.Request().Context(), database.GetSavedFiltersParams{
|
||||||
savedFilters, err = cfg.Queries.GetSavedFilters(c.Request().Context(), database.GetSavedFiltersParams{
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
ResourceType: "media-items",
|
||||||
ResourceType: "media-items",
|
})
|
||||||
})
|
if err != nil {
|
||||||
if err != nil {
|
log.Printf("GetSavedFilters failed: %v", err)
|
||||||
log.Printf("GetSavedFilters failed: %v", err)
|
savedFilters = []database.SavedFilters{}
|
||||||
savedFilters = []database.SavedFilters{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch first page of books for SSR
|
// Fetch first page of books for SSR
|
||||||
@@ -402,64 +305,51 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
limit := 50
|
limit := 50
|
||||||
offset := 0
|
offset := 0
|
||||||
|
|
||||||
if libraryID != "" && errorMsg == "" {
|
if errorMsg == "" {
|
||||||
libUUID, err := uuid.Parse(libraryID)
|
// Check URL params for pagination
|
||||||
if err == nil {
|
if limitStr := c.QueryParam("limit"); limitStr != "" {
|
||||||
// Check URL params for pagination
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
||||||
if limitStr := c.QueryParam("limit"); limitStr != "" {
|
limit = l
|
||||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
}
|
||||||
limit = l
|
}
|
||||||
|
if offsetStr := c.QueryParam("offset"); offsetStr != "" {
|
||||||
|
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
|
||||||
|
offset = o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params := services.SearchParams{
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
LibraryID: libRes.LibUUID,
|
||||||
|
SearchQuery: "",
|
||||||
|
AuthorFilter: "",
|
||||||
|
SeriesFilter: "",
|
||||||
|
GenreFilter: "",
|
||||||
|
TagsFilter: "",
|
||||||
|
LanguageFilter: "",
|
||||||
|
YearMin: 0,
|
||||||
|
YearMax: 0,
|
||||||
|
HasCover: pgtype.Bool{Valid: false},
|
||||||
|
Sort: "created_at DESC",
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
}
|
||||||
|
var results []database.SearchMediaItemsUnifiedRow
|
||||||
|
results, totalCount, err = cfg.MediaHandler.ExecuteSearch(c.Request().Context(), params)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ExecuteSearch failed: %v", err)
|
||||||
|
} else {
|
||||||
|
bookInfoList = make([]handlers.BookInfo, len(results))
|
||||||
|
for i, book := range results {
|
||||||
|
bookUUID, _ := uuid.FromBytes(book.ID.Bytes[0:16])
|
||||||
|
bookLibUUID, _ := uuid.FromBytes(book.LibraryID.Bytes[0:16])
|
||||||
|
bookInfoList[i] = handlers.BookInfo{
|
||||||
|
MediaItemID: bookUUID.String(),
|
||||||
|
Title: book.Title,
|
||||||
|
Author: textToString(book.Author),
|
||||||
|
CoverImagePath: utils.ResolveMediaURL(pgtype.UUID{Bytes: bookLibUUID, Valid: true}, book.CoverImagePath),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if offsetStr := c.QueryParam("offset"); offsetStr != "" {
|
|
||||||
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
|
|
||||||
offset = o
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Convert user.ID (string) to pgtype.UUID for service layer
|
|
||||||
userUUID, err := uuid.Parse(user.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to parse user ID: %v", err)
|
|
||||||
return renderErrorPage(c, "Error loading user", "user_id_error")
|
|
||||||
}
|
|
||||||
// Build search params (same as search.go:76-91)
|
|
||||||
params := services.SearchParams{
|
|
||||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
|
||||||
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
|
||||||
SearchQuery: "", // Empty for initial SSR load
|
|
||||||
AuthorFilter: "",
|
|
||||||
SeriesFilter: "",
|
|
||||||
GenreFilter: "",
|
|
||||||
TagsFilter: "",
|
|
||||||
LanguageFilter: "",
|
|
||||||
YearMin: 0,
|
|
||||||
YearMax: 0,
|
|
||||||
HasCover: pgtype.Bool{Valid: false},
|
|
||||||
Sort: "created_at DESC",
|
|
||||||
Limit: limit,
|
|
||||||
Offset: offset,
|
|
||||||
}
|
|
||||||
// Execute search using the same handler as API (search.go:93)
|
|
||||||
var results []database.SearchMediaItemsUnifiedRow
|
|
||||||
results, totalCount, err = cfg.MediaHandler.ExecuteSearch(c.Request().Context(), params)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("ExecuteSearch failed: %v", err)
|
|
||||||
// Continue without books - will show empty state
|
|
||||||
} else {
|
|
||||||
// Convert to BookInfo (same as search.go:99-109)
|
|
||||||
bookInfoList = make([]handlers.BookInfo, len(results))
|
|
||||||
for i, book := range results {
|
|
||||||
bookUUID, _ := uuid.FromBytes(book.ID.Bytes[0:16])
|
|
||||||
bookLibUUID, _ := uuid.FromBytes(book.LibraryID.Bytes[0:16])
|
|
||||||
bookInfoList[i] = handlers.BookInfo{
|
|
||||||
MediaItemID: bookUUID.String(),
|
|
||||||
Title: book.Title,
|
|
||||||
Author: textToString(book.Author),
|
|
||||||
CoverImagePath: utils.ResolveMediaURL(pgtype.UUID{Bytes: bookLibUUID, Valid: true}, book.CoverImagePath),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Printf("SSR: fetched %d books for library %s", len(bookInfoList), libraryID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,20 +370,13 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
|
|
||||||
var errorMsg string
|
var errorMsg string
|
||||||
|
|
||||||
libraryID := c.QueryParam("library_id")
|
libRes := resolveLibrary(c, cfg, user.ID)
|
||||||
if libraryID == "" {
|
libraryID := libRes.LibraryID
|
||||||
userUUID, _ := uuid.Parse(user.ID)
|
|
||||||
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)
|
|
||||||
userUUID, _ := uuid.Parse(user.ID)
|
userUUID, _ := uuid.Parse(user.ID)
|
||||||
|
pgLibUUID := libRes.LibUUID
|
||||||
|
|
||||||
prefs, err := cfg.DashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
|
prefs, err := cfg.DashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, pgLibUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("GetDashboardPreferences failed: %v", err)
|
log.Printf("GetDashboardPreferences failed: %v", err)
|
||||||
prefs = database.UserDashboardPreferences{
|
prefs = database.UserDashboardPreferences{
|
||||||
@@ -511,7 +394,7 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
allSections, err := cfg.DashboardService.GetDashboardSections(
|
allSections, err := cfg.DashboardService.GetDashboardSections(
|
||||||
c.Request().Context(),
|
c.Request().Context(),
|
||||||
userUUID,
|
userUUID,
|
||||||
libUUID,
|
pgLibUUID,
|
||||||
limit,
|
limit,
|
||||||
prefs.CollectionOrder,
|
prefs.CollectionOrder,
|
||||||
[]string{}, // No filtering - get all sections
|
[]string{}, // No filtering - get all sections
|
||||||
@@ -525,32 +408,11 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
// Get only visible sections for the dashboard display
|
// Get only visible sections for the dashboard display
|
||||||
visibleSections := cfg.DashboardService.FilterHiddenCollections(allSections, prefs.HiddenCollections)
|
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 {
|
|
||||||
log.Printf("GetUserVisibleLibraries failed: %v", err)
|
|
||||||
libraries = []database.GetUserVisibleLibrariesRow{}
|
|
||||||
if errorMsg == "" {
|
|
||||||
errorMsg = "Error loading libraries"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
libData := make([]templates.LibraryData, len(libraries))
|
|
||||||
for i, lib := range libraries {
|
|
||||||
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
|
||||||
libData[i] = templates.LibraryData{
|
|
||||||
ID: libUUID.String(),
|
|
||||||
Name: lib.Name,
|
|
||||||
Description: getText(lib.Description),
|
|
||||||
TypeName: lib.TypeName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sectionData := handlers.BuildSections(visibleSections, libraryID)
|
sectionData := handlers.BuildSections(visibleSections, libraryID)
|
||||||
allSectionsData := handlers.BuildSections(allSections, libraryID)
|
allSectionsData := handlers.BuildSections(allSections, libraryID)
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err = templates.Dashboard(user, sectionData, allSectionsData, libData, libraryID, prefs.HiddenCollections, limit, errorMsg).Render(c.Request().Context(), &buf)
|
err = templates.Dashboard(user, sectionData, allSectionsData, libRes.Libraries, libraryID, prefs.HiddenCollections, limit, errorMsg).Render(c.Request().Context(), &buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -585,32 +447,8 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userUUID, _ := uuid.Parse(user.ID)
|
|
||||||
libraries, libErr := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
|
|
||||||
if libErr != nil {
|
|
||||||
log.Printf("GetUserVisibleLibraries failed: %v", libErr)
|
|
||||||
libraries = []database.GetUserVisibleLibrariesRow{}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentLibraryID := c.QueryParam("library_id")
|
|
||||||
if currentLibraryID == "" && len(libraries) > 0 {
|
|
||||||
libUUID, _ := uuid.FromBytes(libraries[0].ID.Bytes[0:16])
|
|
||||||
currentLibraryID = libUUID.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
libData := make([]templates.LibraryData, len(libraries))
|
|
||||||
for i, lib := range libraries {
|
|
||||||
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
|
||||||
libData[i] = templates.LibraryData{
|
|
||||||
ID: libUUID.String(),
|
|
||||||
Name: lib.Name,
|
|
||||||
Description: getText(lib.Description),
|
|
||||||
TypeName: lib.TypeName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err = templates.Collection(user, colData, libData, currentLibraryID, errorMsg).Render(c.Request().Context(), &buf)
|
err = templates.Collection(user, colData, errorMsg).Render(c.Request().Context(), &buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -698,20 +536,12 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
userUUID, _ := uuid.Parse(user.ID)
|
userUUID, _ := uuid.Parse(user.ID)
|
||||||
var books []handlers.BookInfo
|
var books []handlers.BookInfo
|
||||||
|
|
||||||
libraryID := c.QueryParam("library_id")
|
libRes := resolveLibrary(c, cfg, user.ID)
|
||||||
|
libraryID := libRes.LibraryID
|
||||||
|
|
||||||
if collection.QueryType.Valid && collection.QueryType.String != "" {
|
if collection.QueryType.Valid && collection.QueryType.String != "" {
|
||||||
if libraryID == "" {
|
|
||||||
libraries, libErr := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
|
|
||||||
if libErr == 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)
|
dashboardSvc := services.NewDashboardService(cfg.Queries)
|
||||||
sections, secErr := dashboardSvc.GetDashboardSections(c.Request().Context(), userUUID, libUUID, 1000, []string{}, []string{})
|
sections, secErr := dashboardSvc.GetDashboardSections(c.Request().Context(), userUUID, libRes.LibUUID, 1000, []string{}, []string{})
|
||||||
if secErr != nil {
|
if secErr != nil {
|
||||||
return renderErrorPage(c, "Error loading books", "books_load_error")
|
return renderErrorPage(c, "Error loading books", "books_load_error")
|
||||||
}
|
}
|
||||||
@@ -733,7 +563,7 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if libraryID != "" {
|
if libraryID != "" && !libRes.IsAll {
|
||||||
libUUID, parseErr := uuid.Parse(libraryID)
|
libUUID, parseErr := uuid.Parse(libraryID)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
return renderErrorPage(c, "Invalid library ID", "invalid_library_id")
|
return renderErrorPage(c, "Invalid library ID", "invalid_library_id")
|
||||||
@@ -743,7 +573,7 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
database.GetCollectionItemsForDashboardParams{
|
database.GetCollectionItemsForDashboardParams{
|
||||||
CollectionID: pgtype.UUID{Bytes: collUUID, Valid: true},
|
CollectionID: pgtype.UUID{Bytes: collUUID, Valid: true},
|
||||||
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
||||||
Limit: 1000,
|
Limit: pgtype.Int4{Int32: 1000, Valid: true},
|
||||||
})
|
})
|
||||||
if collErr != nil {
|
if collErr != nil {
|
||||||
books = []handlers.BookInfo{}
|
books = []handlers.BookInfo{}
|
||||||
@@ -795,25 +625,8 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
Icon: collection.Icon.String,
|
Icon: collection.Icon.String,
|
||||||
}
|
}
|
||||||
|
|
||||||
libraries, libErr := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
|
|
||||||
if libErr != nil {
|
|
||||||
log.Printf("GetUserVisibleLibraries failed: %v", libErr)
|
|
||||||
libraries = []database.GetUserVisibleLibrariesRow{}
|
|
||||||
}
|
|
||||||
|
|
||||||
libData := make([]templates.LibraryData, len(libraries))
|
|
||||||
for i, lib := range libraries {
|
|
||||||
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
|
||||||
libData[i] = templates.LibraryData{
|
|
||||||
ID: libUUID.String(),
|
|
||||||
Name: lib.Name,
|
|
||||||
Description: getText(lib.Description),
|
|
||||||
TypeName: lib.TypeName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err = templates.CollectionDetail(user, colData, books, libraryID, libData).Render(c.Request().Context(), &buf)
|
err = templates.CollectionDetail(user, colData, books, libraryID, libRes.Libraries).Render(c.Request().Context(), &buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package router
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"bookhoard/internal/database"
|
||||||
"bookhoard/templates"
|
"bookhoard/templates"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -86,3 +88,77 @@ func parseUUID(s string) (uuid.UUID, error) {
|
|||||||
func uuidToPGType(u uuid.UUID) pgtype.UUID {
|
func uuidToPGType(u uuid.UUID) pgtype.UUID {
|
||||||
return pgtype.UUID{Bytes: u, Valid: true}
|
return pgtype.UUID{Bytes: u, Valid: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedLibraryCookie = "selectedLibrary"
|
||||||
|
const allLibrariesSentinel = "__all__"
|
||||||
|
|
||||||
|
type LibraryResolution struct {
|
||||||
|
LibraryID string
|
||||||
|
IsAll bool
|
||||||
|
LibUUID pgtype.UUID
|
||||||
|
Libraries []templates.LibraryData
|
||||||
|
FirstID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getText(t pgtype.Text) string {
|
||||||
|
if t.Valid {
|
||||||
|
return t.String
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveLibrary(c *echo.Context, cfg *Config, userUUID string) LibraryResolution {
|
||||||
|
res := LibraryResolution{}
|
||||||
|
|
||||||
|
userU, _ := uuid.Parse(userUUID)
|
||||||
|
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userU))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("GetUserVisibleLibraries failed: %v", err)
|
||||||
|
libraries = []database.GetUserVisibleLibrariesRow{}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Libraries = make([]templates.LibraryData, len(libraries))
|
||||||
|
for i, lib := range libraries {
|
||||||
|
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
||||||
|
res.Libraries[i] = templates.LibraryData{
|
||||||
|
ID: libUUID.String(),
|
||||||
|
Name: lib.Name,
|
||||||
|
Description: getText(lib.Description),
|
||||||
|
TypeName: lib.TypeName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(libraries) > 0 {
|
||||||
|
libUUID, _ := uuid.FromBytes(libraries[0].ID.Bytes[0:16])
|
||||||
|
res.FirstID = libUUID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
libraryID := c.QueryParam("library_id")
|
||||||
|
if libraryID == "" {
|
||||||
|
if cookie, err := c.Cookie(selectedLibraryCookie); err == nil {
|
||||||
|
val, _ := url.QueryUnescape(cookie.Value)
|
||||||
|
if val == allLibrariesSentinel {
|
||||||
|
res.IsAll = true
|
||||||
|
res.LibraryID = ""
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
if _, parseErr := uuid.Parse(val); parseErr == nil {
|
||||||
|
libraryID = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if libraryID == "" {
|
||||||
|
res.LibraryID = res.FirstID
|
||||||
|
if res.LibraryID != "" {
|
||||||
|
parsed, _ := uuid.Parse(res.LibraryID)
|
||||||
|
res.LibUUID = pgtype.UUID{Bytes: parsed, Valid: true}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
res.LibraryID = libraryID
|
||||||
|
parsed, _ := uuid.Parse(libraryID)
|
||||||
|
res.LibUUID = pgtype.UUID{Bytes: parsed, Valid: true}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user