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)
This commit is contained in:
@@ -15,7 +15,7 @@ templ Dashboard(user User, sections []handlers.SectionData, allSections []handle
|
|||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<link href="/static/style.css" rel="stylesheet"/>
|
<link href="/static/style.css" rel="stylesheet"/>
|
||||||
</head>
|
</head>
|
||||||
<body class="theme-{ user.Theme }">
|
<body x-data="dashboard" x-init="initDashboard()" class="theme-{ user.Theme }">
|
||||||
@Header(user, "/dashboard")
|
@Header(user, "/dashboard")
|
||||||
<!-- Sticky Library Selector -->
|
<!-- Sticky Library Selector -->
|
||||||
<div class="sticky top-0 z-40 bg-opacity-95 backdrop-blur border-b" style="background-color: var(--bg-primary);">
|
<div class="sticky top-0 z-40 bg-opacity-95 backdrop-blur border-b" style="background-color: var(--bg-primary);">
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func Dashboard(user User, sections []handlers.SectionData, allSections []handler
|
|||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Dashboard - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ user.Theme }\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Dashboard - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"dashboard\" x-init=\"initDashboard()\" class=\"theme-{ user.Theme }\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-83
@@ -1,3 +1,4 @@
|
|||||||
|
import { Alpine } from "./alpine";
|
||||||
import { apiPost, apiPut } from "./api";
|
import { apiPost, apiPut } from "./api";
|
||||||
import { showToast } from "./toast";
|
import { showToast } from "./toast";
|
||||||
|
|
||||||
@@ -425,95 +426,103 @@ function initDragAndDrop(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initDragAndDrop();
|
function initDashboard() {
|
||||||
|
initDragAndDrop();
|
||||||
|
|
||||||
document.addEventListener("click", (e: Event) => {
|
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) => {
|
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
const actionElem = target.closest("[data-input-action]") as HTMLElement;
|
const actionElem = target.closest("[data-action]") as HTMLElement;
|
||||||
const action = actionElem?.getAttribute("data-input-action");
|
const action = actionElem?.getAttribute("data-action");
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "update-items-count": {
|
case "scroll-carousel": {
|
||||||
const input = target as HTMLInputElement;
|
const collectionId =
|
||||||
const displayTarget = input.getAttribute("target");
|
target.dataset.collectionId || actionElem?.dataset.collectionId;
|
||||||
if (displayTarget) updateItemsCount(input, displayTarget);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
// Load saved library on page load
|
document.addEventListener("input", (e: Event) => {
|
||||||
const librarySelect = document.getElementById(
|
const target = e.target as HTMLElement;
|
||||||
"library-select",
|
const actionElem = target.closest("[data-input-action]") as HTMLElement;
|
||||||
) as HTMLSelectElement;
|
const action = actionElem?.getAttribute("data-input-action");
|
||||||
if (librarySelect) {
|
|
||||||
librarySelect.addEventListener("change", (e) => {
|
switch (action) {
|
||||||
const target = e.target as HTMLSelectElement;
|
case "update-items-count": {
|
||||||
if (target.value) {
|
const input = target as HTMLInputElement;
|
||||||
switchLibrary(target.value);
|
const displayTarget = input.getAttribute("target");
|
||||||
|
if (displayTarget) updateItemsCount(input, displayTarget);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
// Load saved library on page load
|
||||||
const savedLibrary = localStorage.getItem("selectedLibrary");
|
const librarySelect = document.getElementById(
|
||||||
const currentLibrary = new URLSearchParams(window.location.search).get(
|
"library-select",
|
||||||
"library_id",
|
) as HTMLSelectElement;
|
||||||
);
|
if (librarySelect) {
|
||||||
if (savedLibrary && savedLibrary !== currentLibrary) {
|
librarySelect.addEventListener("change", (e) => {
|
||||||
window.location.href = `/dashboard?library_id=${savedLibrary}`;
|
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,
|
||||||
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user