feat: implement Hybrid SSR for bookshelf page

- Add LibraryData type to templates/types.go
- Update bookshelf template to accept libraries parameter
- Render libraries server-side for faster initial page load
- Libraries now populated from server data instead of AJAX fetch
- JavaScript still uses API for dynamic content (bookshelf items)
- Update /bookshelf route to fetch libraries server-side before render
- Properly handle UUID and pgtype.Text conversions
- Maintain API endpoint compatibility for JavaScript calls

This improves initial page load performance while preserving
dynamic functionality via API calls.
This commit is contained in:
2026-02-02 16:46:45 -05:00
parent 7c4a37175b
commit a89d5c599d
4 changed files with 124 additions and 25 deletions
+31 -10
View File
@@ -339,20 +339,41 @@ func main() {
// Bookshelf route (protected) - new default for logged-in users // Bookshelf route (protected) - new default for logged-in users
protected.GET("/bookshelf", func(c echo.Context) error { protected.GET("/bookshelf", func(c echo.Context) error {
userID := c.Get("user_id").(string) user := c.Get("user").(database.Users)
userEmail := c.Get("user_email").(string) userUUID := uuid.UUID(user.ID.Bytes)
userUsername := c.Get("user_username").(string)
userRole := c.Get("user_role").(string)
user := templates.User{ // Fetch libraries server-side for SSR
ID: userID, librariesData, err := libraryHandler.GetUserVisibleLibrariesData(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
Email: userEmail, if err != nil {
Username: userUsername, return c.HTML(http.StatusInternalServerError, "Error loading libraries")
Role: userRole,
} }
// Convert to template format
libraries := make([]templates.LibraryData, len(librariesData))
for i, lib := range librariesData {
libUUID := uuid.UUID(lib.ID.Bytes)
description := ""
if lib.Description.Valid {
description = lib.Description.String
}
libraries[i] = templates.LibraryData{
ID: libUUID.String(),
Name: lib.Name,
Description: description,
TypeName: lib.TypeName,
}
}
userTemplate := templates.User{
ID: userUUID.String(),
Email: user.Email,
Username: user.Username,
Role: user.Role,
}
// Render template WITH libraries data (SSR)
var buf bytes.Buffer var buf bytes.Buffer
err := templates.BookShelf(user).Render(c.Request().Context(), &buf) err = templates.BookShelf(userTemplate, libraries).Render(c.Request().Context(), &buf)
if err != nil { if err != nil {
return err return err
} }
+41 -13
View File
@@ -1,6 +1,6 @@
package templates package templates
templ BookShelf(user User) { templ BookShelf(user User, libraries []LibraryData) {
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -15,14 +15,20 @@ templ BookShelf(user User) {
</head> </head>
<body class="theme-tokyo-night"> <body class="theme-tokyo-night">
@Header(user, "/bookshelf") @Header(user, "/bookshelf")
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:8 py-8">
<!-- Library Selector --> <!-- Library Selector -->
<div class="mb-8"> <div class="mb-8">
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between"> <div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
<div class="flex-1 w-full"> <div class="flex-1 w-full">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Select Library</label> <label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Select Library</label>
<select id="library-select" onchange="selectLibrary()" class="w-full px-4 py-3 border rounded-lg text-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border);"> <select id="library-select" onchange="selectLibrary()" class="w-full px-4 py-3 border rounded-lg text-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border);">
<option value="">Loading libraries...</option> if len(libraries) == 0 {
<option value="">No libraries available</option>
} else {
for _, lib := range libraries {
<option value={ lib.ID }>{ lib.Name }</option>
}
}
</select> </select>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@@ -57,21 +63,43 @@ templ BookShelf(user User) {
<script src="/static/theme.js"></script> <script src="/static/theme.js"></script>
<script> <script>
let libraries = []; // Libraries already loaded server-side
const libraries = [
if len(libraries) > 0 {
for _, lib := range libraries {
{
id: "{ lib.ID }",
name: "{ lib.Name }",
description: "{ lib.Description }",
type_name: "{ lib.TypeName }"
},
}
}
];
let currentLibrary = null; let currentLibrary = null;
let mediaItems = []; let mediaItems = [];
function loadLibraries() { function selectLibrary() {
const loading = document.getElementById('loading');
const librarySelect = document.getElementById('library-select'); const librarySelect = document.getElementById('library-select');
const selectedId = librarySelect.value;
loading.style.display = 'block'; currentLibrary = libraries.find(lib => lib.id === selectedId);
if (currentLibrary) {
loadBookshelf();
}
}
fetch('/api/libraries/visible', { // Auto-select first library if available
headers: { document.addEventListener('DOMContentLoaded', function() {
'Authorization': 'Bearer ' + localStorage.getItem('token'), loadTheme();
'Content-Type': 'application/json' if (libraries.length > 0) {
} const librarySelect = document.getElementById('library-select');
currentLibrary = libraries[0];
librarySelect.value = currentLibrary.id;
loadBookshelf();
} else {
showEmptyState();
}
});
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
File diff suppressed because one or more lines are too long
+7
View File
@@ -38,6 +38,13 @@ type BookData struct {
CoverImagePath string CoverImagePath string
} }
type LibraryData struct {
ID string
Name string
Description string
TypeName string
}
type DeviceData struct { type DeviceData struct {
ID string ID string
DeviceName string DeviceName string