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:
1 parent
7c4a37175b
commit
a89d5c599d
4 files changed
+124
-25
No files matched your search
+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 => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ package templates
|
|||||||
import "github.com/a-h/templ"
|
import "github.com/a-h/templ"
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
func BookShelf(user User) templ.Component {
|
func BookShelf(user User, libraries []LibraryData) templ.Component {
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
@@ -37,7 +37,50 @@ func BookShelf(user User) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8\"><!-- Library Selector --><div class=\"mb-8\"><div class=\"flex flex-col sm:flex-row gap-4 items-center justify-between\"><div class=\"flex-1 w-full\"><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);\"><option value=\"\">Loading libraries...</option></select></div><div class=\"flex gap-2\"><button onclick=\"loadBookshelf()\" class=\"btn-primary px-6 py-3 rounded-lg font-medium\">Refresh</button></div></div></div><!-- Bookshelf Container --><div id=\"bookshelf-container\"><!-- Loading state --><div id=\"loading\" class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"loading-spinner mx-auto mb-4\"></div><p>Loading your library...</p></div><!-- Empty state --><div id=\"empty-state\" class=\"hidden text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📚</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Books Yet</h3><p>Select a library to view your collection</p></div><!-- Books Grid --><div id=\"books-grid\" class=\"hidden\"><!-- Books will be loaded here on shelves --></div></div></div><script src=\"/static/theme.js\"></script><script>\n let libraries = [];\n let currentLibrary = null;\n let mediaItems = [];\n\n function loadLibraries() {\n const loading = document.getElementById('loading');\n const librarySelect = document.getElementById('library-select');\n \n loading.style.display = 'block';\n\n fetch('/api/libraries/visible', {\n headers: {\n 'Authorization': 'Bearer ' + localStorage.getItem('token'),\n 'Content-Type': 'application/json'\n }\n })\n .then(response => response.json())\n .then(data => {\n loading.style.display = 'none';\n libraries = data;\n populateLibrarySelect();\n \n // Auto-select first library if available\n if (libraries.length > 0 && !currentLibrary) {\n currentLibrary = libraries[0];\n librarySelect.value = currentLibrary.id;\n loadBookshelf();\n }\n })\n .catch(error => {\n loading.style.display = 'none';\n console.error('Error loading libraries:', error);\n showToast('Error loading libraries', 'error');\n });\n }\n\n function populateLibrarySelect() {\n const librarySelect = document.getElementById('library-select');\n \n if (libraries.length === 0) {\n librarySelect.innerHTML = '<option value=\"\">No libraries available</option>';\n return;\n }\n\n librarySelect.innerHTML = '<option value=\"\">Select a library...</option>' +\n libraries.map(library => {\n const icon = getLibraryIcon(library.type_name);\n return `<option value=\"${library.id}\">${icon} ${library.name}</option>`;\n }).join('');\n }\n\n function getLibraryIcon(typeName) {\n switch (typeName) {\n case 'ebooks': return '📚';\n case 'comics': return '📖';\n case 'manga': return '🗾';\n default: return '📁';\n }\n }\n\n function selectLibrary() {\n const librarySelect = document.getElementById('library-select');\n const libraryId = librarySelect.value;\n \n currentLibrary = libraries.find(l => l.id === libraryId);\n loadBookshelf();\n }\n\n function loadBookshelf() {\n if (!currentLibrary) {\n showEmptyState();\n return;\n }\n\n const loading = document.getElementById('loading');\n const booksGrid = document.getElementById('books-grid');\n const emptyState = document.getElementById('empty-state');\n\n loading.style.display = 'block';\n booksGrid.classList.add('hidden');\n emptyState.classList.add('hidden');\n\n fetch(`/api/media-items?library_iLine truncated
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:8 py-8\"><!-- Library Selector --><div class=\"mb-8\"><div class=\"flex flex-col sm:flex-row gap-4 items-center justify-between\"><div class=\"flex-1 w-full\"><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);\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(libraries) == 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<option value=\"\">No libraries available</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, lib := range libraries {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<option value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(lib.ID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 29, Col: 58}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 29, Col: 71}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</select></div><div class=\"flex gap-2\"><button onclick=\"loadBookshelf()\" class=\"btn-primary px-6 py-3 rounded-lg font-medium\">Refresh</button></div></div></div><!-- Bookshelf Container --><div id=\"bookshelf-container\"><!-- Loading state --><div id=\"loading\" class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"loading-spinner mx-auto mb-4\"></div><p>Loading your library...</p></div><!-- Empty state --><div id=\"empty-state\" class=\"hidden text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📚</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Books Yet</h3><p>Select a library to view your collection</p></div><!-- Books Grid --><div id=\"books-grid\" class=\"hidden\"><!-- Books will be loaded here on shelves --></div></div></div><script src=\"/static/theme.js\"></script><script>\n // Libraries already loaded server-side\n const libraries = [\n if len(libraries) > 0 {\n for _, lib := range libraries {\n {\n id: \"{ lib.ID }\",\n name: \"{ lib.Name }\",\n description: \"{ lib.Description }\",\n type_name: \"{ lib.TypeName }\"\n },\n }\n }\n ];\n let currentLibrary = null;\n let mediaItems = [];\n\n function selectLibrary() {\n const librarySelect = document.getElementById('library-select');\n const selectedId = librarySelect.value;\n currentLibrary = libraries.find(lib => lib.id === selectedId);\n if (currentLibrary) {\n loadBookshelf();\n }\n }\n\n // Auto-select first library if available\n document.addEventListener('DOMContentLoaded', function() {\n loadTheme();\n if (libraries.length > 0) {\n const librarySelect = document.getElementById('library-select');\n currentLibrary = libraries[0];\n librarySelect.value = currentLibrary.id;\n loadBookshelf();\n } else {\n showEmptyState();\n }\n });\n })\n .then(response => response.json())\n .then(data => {\n loading.style.display = 'none';\n libraries = data;\n populateLibrarySelect();\n \n // Auto-select first library if available\n if (libraries.length > 0 && !currentLibrary) {\n currentLibrary = libraries[0];\n librarySelect.value = currentLibrary.id;\n loadBookshelf();\n }\n })\n .catch(error => {\n loading.style.display = 'none';\n console.error('Error loading libraries:', error);\n showToast('Error loading libraries', 'error');\n });\n }\n\n function populateLibrarySelect() {\n const librarySelect = document.getElementById('library-select');\n \n if (libraries.length === 0) {\n librarySelect.innerHTML = '<option value=\"\">No libraries available</option>';\n return;\n }\n\n librarySelect.innerHTML = '<option value=\"\">Select a library...</option>' +\n libraries.map(library => {\n const icon = getLibraryIcon(library.type_name);\n return `<option value=\"${library.id}\">${icon} ${library.name}</option>`;\n }).join('');\n }\n\n function getLibraryIcon(typeName) {\n switch (typeName) {\n case 'ebooks': return '📚';\n case 'comics': return '📖';\n case 'manga': return '🗾';\n default: return '📁';\n }\n }\n\n function selectLibrary() {\n const librarySelect = document.getElementById('library-select');\n const libraryId = librarySelect.value;\n \n currentLibrary = libraries.find(l => l.id === libraryId);\n loadBookshelf();\n }\n\n function loadBookshelf() {\n if (!currentLibrary) {\n showEmptyState();\n return;\n }\n\n const loading = document.getElementById('loading');\n const booksGrid = document.getElemenLine truncated
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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