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:
+31
-10
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user