Files
bookhoard/internal/handlers/series.go
T
john-okeefe 2fdc8751b4 feat(series): add SeriesHandler, API routes, and SSR browse page
Create SeriesHandler with two API endpoints:
- GET /api/series (paginated series list with covers)
- GET /api/series/books (books in a specific series)
Uses query param ?name=X instead of path param to avoid URL encoding
issues with special characters in series names.

Add GetSeriesCardsData helper returning services.SeriesInfo for use
by the SSR route (avoids handlers→templates import cycle).

Register /api/series routes via registerSeriesRoutes in router.
Add /series SSR route in frontend.go with library-scoped pagination
and error handling, matching the dashboard/bookshelf patterns.

Add SeriesHandler to router Config and instantiate in main.go.

Add SeriesCardData type to templates/types.go.

Add Continue Series as the 5th valid system collection in
dashboard handler and auth handler's CreateDefaultCollectionsForUser.
2026-05-08 20:27:01 -04:00

125 lines
3.4 KiB
Go

package handlers
import (
"bookhoard/internal/database"
"bookhoard/internal/services"
"bookhoard/internal/utils"
"context"
"net/http"
"strconv"
"github.com/google/uuid"
"github.com/labstack/echo/v5"
)
type SeriesHandler struct {
seriesService *services.SeriesService
}
func NewSeriesHandler(db *database.Queries) *SeriesHandler {
return &SeriesHandler{
seriesService: services.NewSeriesService(db),
}
}
func (h *SeriesHandler) GetSeries(c *echo.Context) error {
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"})
}
limit := 20
if l := c.QueryParam("limit"); l != "" {
if v, err := strconv.Atoi(l); err == nil && v > 0 {
limit = v
if limit > 100 {
limit = 100
}
}
}
offset := 0
if o := c.QueryParam("offset"); o != "" {
if v, err := strconv.Atoi(o); err == nil && v >= 0 {
offset = v
}
}
seriesList, total, err := h.seriesService.GetSeriesPage(c.Request().Context(), libUUID, limit, offset)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load series"})
}
type SeriesResponse struct {
Name string `json:"name"`
BookCount int64 `json:"book_count"`
TotalInSeries int `json:"total_in_series"`
CoverPaths []string `json:"cover_paths"`
LastEntryAt string `json:"last_entry_at"`
}
response := make([]SeriesResponse, 0, len(seriesList))
for _, s := range seriesList {
response = append(response, SeriesResponse{
Name: s.Name,
BookCount: s.BookCount,
TotalInSeries: s.TotalInSeries,
CoverPaths: s.CoverPaths,
LastEntryAt: s.LastEntryAt,
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"series": response,
"total": total,
"limit": limit,
"offset": offset,
})
}
func (h *SeriesHandler) GetSeriesBooks(c *echo.Context) error {
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"})
}
seriesName := c.QueryParam("name")
if seriesName == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "name required"})
}
books, err := h.seriesService.GetSeriesBooks(c.Request().Context(), libUUID, seriesName)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load series books"})
}
bookCards := make([]BookInfo, 0, len(books))
for _, item := range books {
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
bookCards = append(bookCards, BookInfo{
MediaItemID: itemUUID.String(),
Title: item.Title,
Author: textToString(item.Author),
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"name": seriesName,
"books": bookCards,
"total": len(bookCards),
})
}
func GetSeriesCardsData(ctx context.Context, db *database.Queries, libraryID uuid.UUID, limit, offset int) ([]services.SeriesInfo, int, error) {
svc := services.NewSeriesService(db)
return svc.GetSeriesPage(ctx, libraryID, limit, offset)
}