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}`; + } });