diff --git a/templates/dashboard.templ b/templates/dashboard.templ index 799f17b..4f967f2 100644 --- a/templates/dashboard.templ +++ b/templates/dashboard.templ @@ -77,11 +77,11 @@ templ CollectionCarousel(section handlers.SectionData) { class="carousel-track flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory px-6 pb-2" style="scrollbar-width: none; -ms-overflow-style: none;" > - for _, item := range section.Items { -
- @BookCard(item) -
- } + for _, item := range section.Items { +
+ @BookCard(item) +
+ } if len(section.Items) == 0 {
@Icon("book", "h-8 w-8 opacity-50") diff --git a/templates/dashboard_templ.go b/templates/dashboard_templ.go index 82f7450..81fe020 100644 --- a/templates/dashboard_templ.go +++ b/templates/dashboard_templ.go @@ -242,7 +242,20 @@ func CollectionCarousel(section handlers.SectionData) templ.Component { return templ_7745c5c3_Err } for _, item := range section.Items { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -250,13 +263,13 @@ func CollectionCarousel(section handlers.SectionData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if len(section.Items) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -264,25 +277,25 @@ func CollectionCarousel(section handlers.SectionData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "

No items in this collection

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "

No items in this collection

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -314,224 +327,224 @@ func BookCard(item handlers.BookInfo) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var12 := templ.GetChildren(ctx) - if templ_7745c5c3_Var12 == nil { - templ_7745c5c3_Var12 = templ.NopComponent + templ_7745c5c3_Var13 := templ.GetChildren(ctx) + if templ_7745c5c3_Var13 == nil { + templ_7745c5c3_Var13 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"block h-full\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if item.CoverImagePath != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\"")") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\"")") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" class=\"w-full h-full object-cover\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\"")") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 131, Col: 17} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if item.Author != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 135, Col: 19} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if item.HasConflict { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if item.HasConflict { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -555,12 +568,12 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [ }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var25 := templ.GetChildren(ctx) - if templ_7745c5c3_Var25 == nil { - templ_7745c5c3_Var25 = templ.NopComponent + templ_7745c5c3_Var26 := templ.GetChildren(ctx) + if templ_7745c5c3_Var26 == nil { + templ_7745c5c3_Var26 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "

Customize Dashboard

Drag to reorder, toggle to show or hide.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, section := range sections { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" data-is-system=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var28 string + templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%v", section.IsSystem)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 185, Col: 58} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\" draggable=\"true\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -607,107 +620,107 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 190, Col: 43} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) + templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 192, Col: 84} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 190, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var30 string + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 192, Col: 84} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if section.IsSystem { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "System") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "System") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if section.IsSystem { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" class=\"btn btn-secondary py-1 px-2.5 text-xs\" title=\"Restore { section.Title } to defaults\">Restore ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, ">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/src/dashboard.ts b/web/src/dashboard.ts index 170ec3a..9f19e9e 100644 --- a/web/src/dashboard.ts +++ b/web/src/dashboard.ts @@ -4,6 +4,7 @@ import { showToast } from "./toast"; import { initLibrarySwitcher, switchWithTransition } from "./library-switcher"; const SCROLL_AMOUNT = 300; +let scanListenerRegistered = false; function scrollCarousel(collectionId: string, direction: number): void { const track = document.getElementById( @@ -290,7 +291,7 @@ function renderBookCard(book: BookInfo): string { : "Read"; return ` -
+
@@ -454,83 +455,69 @@ function initDashboard() { } }); - window.addEventListener("bookhoard:scan-complete", async () => { - console.log("[dashboard] scan-complete event received"); - const librarySelect = document.getElementById( - "library-select", - ) as HTMLSelectElement; - const libraryId = librarySelect?.value || ""; - console.log("[dashboard] libraryId:", libraryId); + if (!scanListenerRegistered) { + scanListenerRegistered = true; + window.addEventListener("bookhoard:scan-complete", async () => { + const librarySelect = document.getElementById( + "library-select", + ) as HTMLSelectElement; + const libraryId = librarySelect?.value || ""; - try { - const param = libraryId ? `library_id=${libraryId}` : ""; - const response = await fetch( - `/api/dashboard/sections?${param}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("token")}`, - "Content-Type": "application/json", + try { + const param = libraryId ? `library_id=${libraryId}` : ""; + const response = await fetch( + `/api/dashboard/sections?${param}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + "Content-Type": "application/json", + }, }, - }, - ); - console.log("[dashboard] fetch status:", response.status); - if (!response.ok) return; - const data = await response.json(); - console.log("[dashboard] sections:", data.sections?.length, JSON.stringify(data.sections?.map((s: SectionData) => ({ id: s.id, items: s.items?.length })))); + ); + if (!response.ok) return; + const data = await response.json(); - const container = document.getElementById( - "collections-container", - ) as HTMLElement; - if (!container) { - console.log("[dashboard] no collections-container found"); - return; - } + const container = document.getElementById( + "collections-container", + ) as HTMLElement; + if (!container) return; - for (const section of data.sections as SectionData[]) { - const track = document.getElementById( - `carousel-track-${section.id}`, + const freshSectionIds = new Set( + (data.sections as SectionData[]).map((s) => s.id), ); - if (!track) { - console.log("[dashboard] creating new section:", section.id, section.title); - container.insertAdjacentHTML( - "beforeend", - renderSectionHTML(section), + for (const section of data.sections as SectionData[]) { + const track = document.getElementById( + `carousel-track-${section.id}`, ); - continue; - } - const existing = new Set( - Array.from(track.querySelectorAll("[data-media-item-id]")).map( - (el) => el.dataset.mediaItemId, - ), - ); - console.log("[dashboard] section:", section.id, "existing:", existing.size, "api items:", section.items.length); - - let added = false; - const newItems: string[] = []; - for (const item of section.items) { - if (existing.has(item.media_item_id)) continue; - newItems.push(renderBookCard(item)); - added = true; - } - - if (added) { - track.insertAdjacentHTML("afterbegin", newItems.join("")); - track.scrollLeft = 0; - - const placeholder = track.querySelector( - '.text-center.py-8', - ); - if (placeholder) { - placeholder.remove(); + if (!track) { + container.insertAdjacentHTML( + "beforeend", + renderSectionHTML(section), + ); + continue; } + + track.innerHTML = section.items.length > 0 + ? section.items.map((item) => renderBookCard(item)).join("") + : '

No items in this collection

'; } + + const existingSections = container.querySelectorAll( + ".dashboard-collection", + ); + existingSections.forEach((sec) => { + const secId = sec.getAttribute("data-collection-id"); + if (secId && !freshSectionIds.has(secId)) { + sec.remove(); + } + }); + } catch (err) { + console.error("[dashboard] scan-complete handler error:", err); } - } catch (err) { - console.error("[dashboard] scan-complete handler error:", err); - } - }); + }); + } } export { initDashboard };