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)
|
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() {
|
func main() {
|
||||||
cfg := config.LoadConfig()
|
cfg := config.LoadConfig()
|
||||||
|
|
||||||
@@ -144,7 +174,6 @@ func main() {
|
|||||||
protected = e.Group("/api", jwtMiddleware)
|
protected = e.Group("/api", jwtMiddleware)
|
||||||
|
|
||||||
// Setup ebook handler routes first (so we can use it for library scan)
|
// 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.GET("/auth/profile", authHandler.GetProfile)
|
||||||
protected.PUT("/auth/profile", authHandler.UpdateProfile)
|
protected.PUT("/auth/profile", authHandler.UpdateProfile)
|
||||||
@@ -522,6 +551,63 @@ func main() {
|
|||||||
return c.HTML(http.StatusOK, buf.String())
|
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
|
// Queue Management route (protected) - SSR version
|
||||||
protected.GET("/queue", func(c echo.Context) error {
|
protected.GET("/queue", func(c echo.Context) error {
|
||||||
userID := c.Get("user_id").(string)
|
userID := c.Get("user_id").(string)
|
||||||
@@ -572,6 +658,127 @@ func main() {
|
|||||||
return c.HTML(http.StatusOK, buf.String())
|
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
|
// Start server
|
||||||
log.Printf("Starting server on port %s", cfg.ServerPort)
|
log.Printf("Starting server on port %s", cfg.ServerPort)
|
||||||
e.Logger.Fatal(e.Start(":" + cfg.ServerPort))
|
e.Logger.Fatal(e.Start(":" + cfg.ServerPort))
|
||||||
|
|||||||
Reference in New Issue
Block a user