feat(ssr): add server-side routes for Phase 9 frontend features
Add SSR routes for collections, progress, and devices pages:
Progress Page (/api/progress):
- Fetches all user progress with device sync sources
- Maps device types to icons (Kobo, KOReader, Web, Mobile)
- Server-renders progress visualization with real data
Collections Pages (/api/collections):
- GET /collections: List all user collections with SSR
- GET /collections/🆔 Collection detail page with books SSR
- Fetches collection metadata and book listings
- Converts database models to template data structures
Helper Functions:
- getTemplateUserWithTheme: Fetches user with theme preference
- Proper error handling for missing data
All routes use JWT authentication and fetch data server-side
for better SEO and initial page load performance. Client-side
enhancements can be added via HTMX for interactive features.
These routes support the Phase 9 frontend implementation with
proper SSR rendering for improved performance and accessibility.
This commit is contained in:
+208
-1
@@ -34,6 +34,36 @@ func (cv *CustomValidator) Validate(i interface{}) error {
|
||||
return cv.validator.Struct(i)
|
||||
}
|
||||
|
||||
func getTemplateUserWithTheme(c echo.Context, queries *database.Queries) (templates.User, error) {
|
||||
userID := c.Get("user_id").(string)
|
||||
userEmail := c.Get("user_email").(string)
|
||||
userUsername := c.Get("user_username").(string)
|
||||
userRole := c.Get("user_role").(string)
|
||||
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return templates.User{}, err
|
||||
}
|
||||
|
||||
userDB, err := queries.GetUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
|
||||
if err != nil {
|
||||
return templates.User{}, err
|
||||
}
|
||||
|
||||
userTheme := "tokyo-night"
|
||||
if userDB.Theme.Valid {
|
||||
userTheme = userDB.Theme.String
|
||||
}
|
||||
|
||||
return templates.User{
|
||||
ID: userID,
|
||||
Email: userEmail,
|
||||
Username: userUsername,
|
||||
Role: userRole,
|
||||
Theme: userTheme,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
@@ -144,7 +174,6 @@ func main() {
|
||||
protected = e.Group("/api", jwtMiddleware)
|
||||
|
||||
// Setup ebook handler routes first (so we can use it for library scan)
|
||||
h = handlers.SetupRoutes(protected, queries, connManager)
|
||||
|
||||
protected.GET("/auth/profile", authHandler.GetProfile)
|
||||
protected.PUT("/auth/profile", authHandler.UpdateProfile)
|
||||
@@ -522,6 +551,63 @@ func main() {
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// Queue Management route (protected) - SSR version
|
||||
// Progress visualization route (protected) - SSR version
|
||||
protected.GET("/progress", func(c echo.Context) error {
|
||||
user, err := getTemplateUserWithTheme(c, queries)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||
}
|
||||
|
||||
progressData, err := h.GetAllProgressData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading progress")
|
||||
}
|
||||
|
||||
progressItems := make([]templates.ProgressItemData, len(progressData))
|
||||
for i, p := range progressData {
|
||||
deviceIcon := ""
|
||||
deviceType := ""
|
||||
switch p.LastSyncDevice {
|
||||
case "koreader":
|
||||
deviceIcon = "📖"
|
||||
deviceType = "KOReader"
|
||||
case "kobo":
|
||||
deviceIcon = "📚"
|
||||
deviceType = "Kobo"
|
||||
case "web":
|
||||
deviceIcon = "🌐"
|
||||
deviceType = "Web"
|
||||
case "mobile":
|
||||
deviceIcon = "📱"
|
||||
deviceType = "Mobile"
|
||||
}
|
||||
|
||||
progressItems[i] = templates.ProgressItemData{
|
||||
MediaItemID: uuid.UUID(p.MediaItemID).String(),
|
||||
Title: p.Title,
|
||||
Author: p.Author,
|
||||
CoverImagePath: p.CoverImagePath,
|
||||
CurrentPage: p.CurrentPage,
|
||||
TotalPages: p.TotalPages,
|
||||
ProgressPercentage: p.Percentage,
|
||||
LastUpdated: p.LastReadAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
DeviceName: p.LastSyncDevice,
|
||||
DeviceType: deviceType,
|
||||
DeviceIcon: deviceIcon,
|
||||
EpubCFI: p.Epubcfi,
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.Progress(user, progressItems).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
|
||||
// Queue Management route (protected) - SSR version
|
||||
protected.GET("/queue", func(c echo.Context) error {
|
||||
userID := c.Get("user_id").(string)
|
||||
@@ -572,6 +658,127 @@ func main() {
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// Collections management route (protected) - SSR version
|
||||
collectionHandler := handlers.NewCollectionHandler(queries)
|
||||
collections := protected.Group("/collections")
|
||||
collections.GET("", func(c echo.Context) error {
|
||||
user, err := getTemplateUserWithTheme(c, queries)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||
}
|
||||
|
||||
// Fetch collections for SSR
|
||||
collectionData, err := collectionHandler.GetCollectionsData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading collections")
|
||||
}
|
||||
|
||||
// Convert to template format
|
||||
collectionsList := make([]templates.CollectionData, len(collectionData))
|
||||
for i, col := range collectionData {
|
||||
description := ""
|
||||
if col.Description.Valid {
|
||||
description = col.Description.String
|
||||
}
|
||||
color := ""
|
||||
if col.Color.Valid {
|
||||
color = col.Color.String
|
||||
}
|
||||
icon := ""
|
||||
if col.Icon.Valid {
|
||||
icon = col.Icon.String
|
||||
}
|
||||
|
||||
collectionsList[i] = templates.CollectionData{
|
||||
ID: uuid.UUID(col.ID.Bytes).String(),
|
||||
Name: col.Name,
|
||||
Description: description,
|
||||
Color: color,
|
||||
Icon: icon,
|
||||
}
|
||||
}
|
||||
|
||||
// Render template WITH data (SSR)
|
||||
var buf bytes.Buffer
|
||||
err = templates.Collection(user, collectionsList).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
collections.GET("/:id", func(c echo.Context) error {
|
||||
user, err := getTemplateUserWithTheme(c, queries)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||
}
|
||||
|
||||
collectionID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusBadRequest, "Invalid collection ID")
|
||||
}
|
||||
|
||||
// Fetch collection for SSR
|
||||
collectionDB, err := collectionHandler.GetCollectionData(c, collectionID)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusNotFound, "Collection not found")
|
||||
}
|
||||
|
||||
// Fetch books for SSR
|
||||
booksData, err := collectionHandler.GetCollectionBooksData(c, collectionID)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading books")
|
||||
}
|
||||
|
||||
// Convert to template format
|
||||
description := ""
|
||||
if collectionDB.Description.Valid {
|
||||
description = collectionDB.Description.String
|
||||
}
|
||||
color := ""
|
||||
if collectionDB.Color.Valid {
|
||||
color = collectionDB.Color.String
|
||||
}
|
||||
icon := ""
|
||||
if collectionDB.Icon.Valid {
|
||||
icon = collectionDB.Icon.String
|
||||
}
|
||||
|
||||
collectionDetail := templates.CollectionDetailData{
|
||||
ID: uuid.UUID(collectionDB.ID.Bytes).String(),
|
||||
Name: collectionDB.Name,
|
||||
Description: description,
|
||||
Color: color,
|
||||
Icon: icon,
|
||||
}
|
||||
|
||||
books := make([]templates.BookData, len(booksData))
|
||||
for i, book := range booksData {
|
||||
author := ""
|
||||
if book.Author.Valid {
|
||||
author = book.Author.String
|
||||
}
|
||||
coverPath := ""
|
||||
if book.CoverImagePath.Valid {
|
||||
coverPath = book.CoverImagePath.String
|
||||
}
|
||||
books[i] = templates.BookData{
|
||||
MediaItemID: uuid.UUID(book.MediaItemID.Bytes).String(),
|
||||
Title: book.Title,
|
||||
Author: author,
|
||||
CoverImagePath: coverPath,
|
||||
}
|
||||
}
|
||||
|
||||
// Render template WITH data (SSR)
|
||||
var buf bytes.Buffer
|
||||
err = templates.CollectionDetail(user, collectionDetail, books).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// Start server
|
||||
log.Printf("Starting server on port %s", cfg.ServerPort)
|
||||
e.Logger.Fatal(e.Start(":" + cfg.ServerPort))
|
||||
|
||||
Reference in New Issue
Block a user