From b77da3a289ba946fde834abb76d8fb3c8a35649c Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 20 Mar 2026 22:58:18 -0400 Subject: [PATCH] refactor: migrate dashboard to SSR-first Alpine.js pattern Update dashboard to follow SSR-first Alpine.js guidelines: - Add x-data="dashboard" and x-init="initDashboard()" to body tag - Wrap initialization in initDashboard() function instead of executing at load time - Alpine.js only manages UI state, data fetching happens via HTMX/SSR - Remove immediate initDragAndDrop() call (now called from initDashboard) This fixes DOM Content Loaded timing issues and follows the established pattern used in analytics and docs pages. The dashboard now properly supports: - SSR with initial data rendered server-side - Alpine.js for interactive UI (drag-drop, modals) - HTMX for dynamic updates without page reload - Progressive enhancement (works without JavaScript) --- templates/dashboard.templ | 2 +- templates/dashboard_templ.go | 2 +- web/src/dashboard.ts | 175 ++++++++++++++++++----------------- 3 files changed, 94 insertions(+), 85 deletions(-) diff --git a/templates/dashboard.templ b/templates/dashboard.templ index 7ff1eb1..8f90d61 100644 --- a/templates/dashboard.templ +++ b/templates/dashboard.templ @@ -15,7 +15,7 @@ templ Dashboard(user User, sections []handlers.SectionData, allSections []handle - + @Header(user, "/dashboard")
diff --git a/templates/dashboard_templ.go b/templates/dashboard_templ.go index d655253..1ffc7b6 100644 --- a/templates/dashboard_templ.go +++ b/templates/dashboard_templ.go @@ -34,7 +34,7 @@ func Dashboard(user User, sections []handlers.SectionData, allSections []handler templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Dashboard - Bookhoard") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Dashboard - Bookhoard") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/src/dashboard.ts b/web/src/dashboard.ts index 0297bf5..7563849 100644 --- a/web/src/dashboard.ts +++ b/web/src/dashboard.ts @@ -1,3 +1,4 @@ +import { Alpine } from "./alpine"; import { apiPost, apiPut } from "./api"; import { showToast } from "./toast"; @@ -425,95 +426,103 @@ function initDragAndDrop(): void { }); } -initDragAndDrop(); +function initDashboard() { + initDragAndDrop(); -document.addEventListener("click", (e: Event) => { - const target = e.target as HTMLElement; - const actionElem = target.closest("[data-action]") as HTMLElement; - const action = actionElem?.getAttribute("data-action"); - - switch (action) { - case "scroll-carousel": { - const collectionId = - target.dataset.collectionId || actionElem?.dataset.collectionId; - const direction = parseInt( - target.dataset.direction || actionElem?.dataset.direction || "0", - ); - if (collectionId) scrollCarousel(collectionId, direction); - break; - } - - case "open-dashboard-settings": - openDashboardSettings(); - break; - - case "close-dashboard-settings": - closeDashboardSettings(); - break; - - case "save-dashboard-settings": - saveDashboardSettings(); - break; - - case "restore-system-collection": { - const colName = - actionElem?.dataset.collectionName || target.dataset.collectionName; - const colTitle = - actionElem?.dataset.collectionTitle || - target.dataset.collectionTitle || - "System Collection"; - if (colName) restoreSystemCollection(colName, colTitle); - break; - } - - case "view-book": { - const bookId = target.dataset.bookId || actionElem?.dataset.bookId; - if (bookId) viewBook(bookId); - break; - } - - case "reload-page": - reloadPage(); - break; - - case "switch-library": { - const select = target as HTMLSelectElement; - if (select.value) switchLibrary(select.value); - break; - } - } - - document.addEventListener("input", (e: Event) => { + document.addEventListener("click", (e: Event) => { const target = e.target as HTMLElement; - const actionElem = target.closest("[data-input-action]") as HTMLElement; - const action = actionElem?.getAttribute("data-input-action"); + const actionElem = target.closest("[data-action]") as HTMLElement; + const action = actionElem?.getAttribute("data-action"); switch (action) { - case "update-items-count": { - const input = target as HTMLInputElement; - const displayTarget = input.getAttribute("target"); - if (displayTarget) updateItemsCount(input, displayTarget); + case "scroll-carousel": { + const collectionId = + target.dataset.collectionId || actionElem?.dataset.collectionId; + const direction = parseInt( + target.dataset.direction || actionElem?.dataset.direction || "0", + ); + if (collectionId) scrollCarousel(collectionId, direction); + break; + } + + case "open-dashboard-settings": + openDashboardSettings(); + break; + + case "close-dashboard-settings": + closeDashboardSettings(); + break; + + case "save-dashboard-settings": + saveDashboardSettings(); + break; + + case "restore-system-collection": { + const colName = + actionElem?.dataset.collectionName || target.dataset.collectionName; + const colTitle = + actionElem?.dataset.collectionTitle || + target.dataset.collectionTitle || + "System Collection"; + if (colName) restoreSystemCollection(colName, colTitle); + break; + } + + case "view-book": { + const bookId = target.dataset.bookId || actionElem?.dataset.bookId; + if (bookId) viewBook(bookId); + break; + } + + case "reload-page": + reloadPage(); + break; + + case "switch-library": { + const select = target as HTMLSelectElement; + if (select.value) switchLibrary(select.value); break; } } - }); - // Load saved library on page load - const librarySelect = document.getElementById( - "library-select", - ) as HTMLSelectElement; - if (librarySelect) { - librarySelect.addEventListener("change", (e) => { - const target = e.target as HTMLSelectElement; - if (target.value) { - switchLibrary(target.value); + + document.addEventListener("input", (e: Event) => { + const target = e.target as HTMLElement; + const actionElem = target.closest("[data-input-action]") as HTMLElement; + const action = actionElem?.getAttribute("data-input-action"); + + switch (action) { + case "update-items-count": { + const input = target as HTMLInputElement; + const displayTarget = input.getAttribute("target"); + if (displayTarget) updateItemsCount(input, displayTarget); + break; + } } }); - } - const savedLibrary = localStorage.getItem("selectedLibrary"); - const currentLibrary = new URLSearchParams(window.location.search).get( - "library_id", - ); - if (savedLibrary && savedLibrary !== currentLibrary) { - window.location.href = `/dashboard?library_id=${savedLibrary}`; - } -}); + // Load saved library on page load + const librarySelect = document.getElementById( + "library-select", + ) as HTMLSelectElement; + if (librarySelect) { + librarySelect.addEventListener("change", (e) => { + const target = e.target as HTMLSelectElement; + if (target.value) { + switchLibrary(target.value); + } + }); + } + const savedLibrary = localStorage.getItem("selectedLibrary"); + const currentLibrary = new URLSearchParams(window.location.search).get( + "library_id", + ); + if (savedLibrary && savedLibrary !== currentLibrary) { + window.location.href = `/dashboard?library_id=${savedLibrary}`; + } + }); +} + +export { initDashboard }; + +Alpine.data("dashboard", () => ({ + initDashboard, +}));