From 62d3d50140c3974d8d769017f703f09842214c70 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 1 Mar 2026 00:29:00 -0500 Subject: [PATCH] fix: Dashboard modal and slider library-specific behavior Fix multiple issues with dashboard customization modal and slider not working correctly per library. Frontend changes in web/src/dashboard.ts: - Fix openDashboardSettings() to use current library ID - Add library_id parameter to dashboard preferences API call - Show toast error message on API failure instead of opening modal - Prevent opening modal with stale/inaccurate data - Fix slider query parameter mismatch - Change from 'libraryId' to 'library_id' to match backend API - Fix DOM query from collectionList.querySelector to document.querySelector - Ensure slider targets correct input element - Fix saveDashboardSettings() to refresh current library - Fetch current library data before saving preferences - Use library_id from current library, not from URL - Show toast error message on save failure - Keep modal open on error for user to retry - Add localStorage persistence for selected library - Store selectedLibrary in localStorage after switching - Enables persistence across page refreshes - Improve switchLibrary() with fade transitions - Add fade-out (150ms) before data fetch - Add fade-in (300ms) after rendering new library - Provide smooth visual feedback during library switches - Apply preferences dynamically to modal - Use applyPreferencesToModal() to update slider and toggles - Ensure modal reflects current library's settings Backend changes in internal/router/dashboard.go: - Update GetDashboardPreferences to use library_id query parameter - Matches frontend API call parameter naming Template changes in templates/dashboard.templ: - Remove duplicate renderDashboardCollections() inline script - Functionality now handled by dashboard.ts These fixes ensure that: 1. Dashboard settings work correctly per library 2. Slider reflects and updates the correct library's item limit 3. Toggles show accurate visibility state for each library 4. Library switches provide smooth visual feedback 5. Errors are properly surfaced to users via toast messages --- internal/router/dashboard.go | 1 + templates/dashboard.templ | 41 ++++--- templates/dashboard_templ.go | 113 ++++++++++------- web/src/dashboard.ts | 228 +++++++++++++++++++++++++++-------- 4 files changed, 271 insertions(+), 112 deletions(-) diff --git a/internal/router/dashboard.go b/internal/router/dashboard.go index 6b3da91..18d417c 100644 --- a/internal/router/dashboard.go +++ b/internal/router/dashboard.go @@ -8,6 +8,7 @@ func registerDashboardRoutes(cfg *Config) { dashboard := protected.Group("/dashboard") dashboard.GET("/sections", cfg.DashboardHandler.GetSections) + dashboard.GET("/preferences", cfg.DashboardHandler.GetPreferences) dashboard.PUT("/preferences", cfg.DashboardHandler.UpdatePreferences) dashboard.POST("/restore-system-collection", cfg.DashboardHandler.RestoreSystemCollection) } diff --git a/templates/dashboard.templ b/templates/dashboard.templ index 21c7914..99c5415 100644 --- a/templates/dashboard.templ +++ b/templates/dashboard.templ @@ -5,7 +5,7 @@ import ( "fmt" ) -templ Dashboard(user User, sections []handlers.SectionData, libData []LibraryData, currentLibraryID string, errorMessage string) { +templ Dashboard(user User, sections []handlers.SectionData, allSections []handlers.SectionData, libData []LibraryData, currentLibraryID string, hiddenCollections []string, itemsPerSection int, errorMessage string) { @@ -31,7 +31,6 @@ templ Dashboard(user User, sections []handlers.SectionData, libData []LibraryDat name="library_id" class="px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500" style="background-color: var(--bg-secondary); color: var(--text-primary);" - data-action="switch-library" > for _, lib := range libData { if lib.ID == currentLibraryID { @@ -76,7 +75,7 @@ templ Dashboard(user User, sections []handlers.SectionData, libData []LibraryDat } - @DashboardSettingsModal(sections) + @DashboardSettingsModal(allSections, hiddenCollections, itemsPerSection) @ErrorToast(errorMessage) @@ -158,7 +157,7 @@ templ CollectionCarousel(section handlers.SectionData) { templ BookCard(item handlers.BookInfo) {
if item.CoverImagePath != "" { @@ -186,18 +185,20 @@ templ BookCard(item handlers.BookInfo) { /> }
-

- { item.Title } -

- if item.Author != "" { -

- { item.Author } -

- } +
+

+ { item.Title } +

+ if item.Author != "" { +

+ { item.Author } +

+ } +
} -templ DashboardSettingsModal(sections []handlers.SectionData) { +templ DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections []string, itemsPerSection int) { ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/src/dashboard.ts b/web/src/dashboard.ts index 06c2e30..30a3fa4 100644 --- a/web/src/dashboard.ts +++ b/web/src/dashboard.ts @@ -13,51 +13,103 @@ function scrollCarousel(collectionId: string, direction: number): void { track.scrollBy({ left: scrollAmount, behavior: "smooth" }); } -function openDashboardSettings(): void { +async function openDashboardSettings(): Promise { + const librarySelect = document.getElementById( + "library-select", + ) as HTMLSelectElement; + const libraryId = librarySelect?.value; + if (!libraryId) { + (window as any).showToast.error("No library selected"); + return; + } + + const response = await fetch( + `/api/dashboard/preferences?library_id=${libraryId}`, + { + headers: { Authorization: `Bearer ${localStorage.getItem("token")}` }, + }, + ); + + if (response.ok) { + const prefs = await response.json(); + applyPreferencesToModal(prefs); + } else { + (window as any).showToast.error("Failed to load library preferences"); + console.error("API Error:", response.status, response.statusText); + return; // Don't open modal with stale data. + } + const modal = document.getElementById( "dashboard-settings-modal", ) as HTMLElement; - if (modal) { - modal.classList.remove("hidden"); + modal?.classList.remove("hidden"); +} + +function applyPreferencesToModal(prefs: any): void { + const collectionList = document.getElementById( + "collection-list", + ) as HTMLElement; + if (!collectionList) return; + + const slider = document.querySelector( + '#dashboard-settings-modal input[type="range"]', + ) as HTMLInputElement; + const display = document.getElementById("items-count-display") as HTMLElement; + if (slider && display) { + slider.value = String(prefs.items_per_section); + display.textContent = String(prefs.items_per_section); } + + // Update checkboxes and reorder items + const items = collectionList.querySelectorAll( + "[data-collection-id]", + ) as NodeListOf; + const orderedItems: HTMLElement[] = []; + + items.forEach((item) => { + const collectionId = item.dataset.collectionId; + const checkbox = item.querySelector( + 'input[type="checkbox"]', + ) as HTMLInputElement; + + // Update checkbox state + const isHidden = prefs.hidden_collections.includes(collectionId); + if (checkbox) checkbox.checked = !isHidden; + + // Sort according to collection_order + const orderIndex = prefs.collection_order.indexOf(collectionId); + if (orderIndex !== -1) { + orderedItems[orderIndex] = item; + } else { + orderedItems.push(item); + } + }); + + // Reorder in DOM + orderedItems.forEach((item) => collectionList.appendChild(item)); } function closeDashboardSettings(): void { const modal = document.getElementById( "dashboard-settings-modal", ) as HTMLElement; - if (modal) { - modal.classList.add("hidden"); - } + modal?.classList.add("hidden"); } - -function toggleCollectionVisibility(collectionId: string): void { - const checkbox = document.querySelector( - `input[data-collection-id="${collectionId}"]`, - ) as HTMLInputElement; - if (checkbox) { - checkbox.checked = !checkbox.checked; - } -} - async function saveDashboardSettings(): Promise { const collectionList = document.getElementById( "collection-list", ) as HTMLElement; if (!collectionList) return; - const collectionItems = collectionList.querySelectorAll( "[data-collection-id]", ) as NodeListOf; const hiddenCollections: string[] = []; const collectionOrder: string[] = []; - collectionItems.forEach((item) => { const collectionId = item.dataset.collectionId; const checkbox = item.querySelector( 'input[type="checkbox"]', ) as HTMLInputElement; - if (collectionId) { collectionOrder.push(collectionId); if (checkbox && !checkbox.checked) { @@ -65,31 +117,52 @@ async function saveDashboardSettings(): Promise { } } }); - const itemsPerCollection = (document.querySelector("#items-count-display") as HTMLElement) ?.textContent || "20"; - try { + const librarySelect = document.getElementById( + "library-select", + ) as HTMLSelectElement; + const libraryId = librarySelect?.value || ""; const response = await (window as any).api.put("/dashboard/preferences", { - library_id: - new URLSearchParams(window.location.search).get("library_id") || "", + library_id: libraryId, hidden_collections: hiddenCollections, collection_order: collectionOrder, items_per_section: parseInt(itemsPerCollection), }); - if (response.ok) { (window as any).showToast.success("Dashboard settings saved"); closeDashboardSettings(); - window.location.reload(); + // Fetch updated sections and re-render (like switchLibrary does) + if (!libraryId) { + (window as any).showToast.error( + "Unable to refresh dashboard - no library selected", + ); + return; + } + const sectionResponse = await fetch( + `/api/dashboard/sections?library_id=${libraryId}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + "Content-Type": "application/json", + }, + }, + ); + if (sectionResponse.ok) { + const data = await sectionResponse.json(); + renderDashboardCollections(data.sections); + localStorage.setItem("selectedLibraryId", libraryId); + } else { + (window as any).showToast.error("Failed to refresh sections"); + } } } catch (error) { (window as any).showToast.error("Failed to save settings"); console.error("Save dashboard settings error:", error); } } - async function restoreSystemCollection( collectionName: string, collectionTitle: string, @@ -130,9 +203,17 @@ async function switchLibrary(libraryId: string): Promise { if (!container || !loading) return; - loading.classList.remove("hidden"); - try { + // Step 1: Fade out current content (150ms) + container.classList.add("opacity-0", "transition-opacity", "duration-150"); + + // Wait for fade-out to complete + await new Promise((resolve) => setTimeout(resolve, 150)); + + // Step 2: Show loading spinner + loading.classList.remove("hidden"); + + // Step 3: Fetch new data const response = await fetch( `/api/dashboard/sections?library_id=${libraryId}`, { @@ -149,6 +230,7 @@ async function switchLibrary(libraryId: string): Promise { const data = await response.json(); renderDashboardCollections(data.sections); + localStorage.setItem("selectedLibrary", libraryId); } catch (error) { (window as any).showToast.error("Failed to load library"); console.error("Switch library error:", error); @@ -166,6 +248,7 @@ function renderDashboardCollections(sections: SectionData[]): void { // Preserve wood paneling attribute const currentWood = container.getAttribute("data-wood"); + // Replace content while still invisible (opacity-0 from switchLibrary) container.innerHTML = sections .map( (section) => ` @@ -223,6 +306,20 @@ function renderDashboardCollections(sections: SectionData[]): void { ) .join(""); + // Step 5: Fade in new content (300ms for smoother entrance) + container.classList.remove("duration-150"); + container.classList.add("duration-300"); + + // Trigger reflow to ensure transition happens + void container.offsetHeight; + + // Fade to visible + container.classList.remove("opacity-0"); + + // Clean up transition classes after animation completes + setTimeout(() => { + container.classList.remove("transition-opacity", "duration-300"); + }, 300); if (currentWood) { container.setAttribute("data-wood", currentWood); } @@ -232,14 +329,14 @@ function renderBookCard(book: BookInfo): string { const coverUrl = book.cover_image_path || "/static/placeholder-book.svg"; return ` -
-
${book.title}
-

- ${book.title} -

- ${book.author ? `

${book.author}

` : ""} +
+

+ ${book.title} +

+ ${book.author ? `

${book.author}

` : ""} +
`; } @@ -259,14 +358,29 @@ function viewBook(bookId: string): void { console.log("View book:", bookId); } -function reloadPage(): void { - window.location.reload(); +async function reloadPage(): Promise { + //Get current library from dropdown + const librarySelect = document.getElementById( + "library-select", + ) as HTMLSelectElement; + const currentLibraryId = librarySelect?.value; + + if (!currentLibraryId) { + (window as any).showToast.error("No library selected"); + return; + } + + // Reuse switchLibrary logic - it handles the fade transition + await switchLibrary(currentLibraryId); } -function updateItemsCount(input: HTMLInputElement, targetId: string): void { - const display = document.getElementById(targetId) as HTMLElement; - if (display) { - display.textContent = input.value; +function updateItemsCount( + input: HTMLInputElement, + displayTarget: string, +): void { + const displayElement = document.getElementById(displayTarget) as HTMLElement; + if (displayElement) { + displayElement.textContent = input.value; } } @@ -342,13 +456,6 @@ document.addEventListener("DOMContentLoaded", () => { closeDashboardSettings(); break; - case "toggle-collection-visibility": { - const checkbox = target as HTMLInputElement; - const colId = checkbox.dataset.collectionId; - if (colId) toggleCollectionVisibility(colId); - break; - } - case "save-dashboard-settings": saveDashboardSettings(); break; @@ -379,7 +486,15 @@ document.addEventListener("DOMContentLoaded", () => { if (select.value) switchLibrary(select.value); break; } + } + }); + 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"); @@ -388,4 +503,23 @@ document.addEventListener("DOMContentLoaded", () => { } } }); + // 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}`; + } });