feat(dashboard): dynamic scan-complete refresh without page reload
When a scan completes, dynamically update the dashboard carousels instead of requiring a full page reload: - Listen for bookhoard:scan-complete custom event dispatched by header - Fetch updated sections from /api/dashboard/sections - Diff existing book cards by data-media-item-id attribute - Prepend new items to carousel tracks (afterbegin) to match API sort order - Create entirely new section DOM for sections that don't yet exist on page - Remove 'No items' placeholder when items are added - Scroll carousel to left (scrollLeft=0) so newly prepended items are visible Also: - Extract renderSectionHTML() helper from renderDashboardCollections() for reuse - Add data-media-item-id attribute to book card template for DOM diffing - Add diagnostic console.log statements for debugging scan-complete flow
This commit is contained in:
+95
-16
@@ -236,19 +236,8 @@ async function switchLibrary(libraryId: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function renderDashboardCollections(sections: SectionData[]): void {
|
||||
const container = document.getElementById(
|
||||
"collections-container",
|
||||
) as HTMLElement;
|
||||
if (!container) return;
|
||||
|
||||
// Preserve wood paneling attribute
|
||||
const currentWood = container.getAttribute("data-wood");
|
||||
|
||||
// Replace content while still invisible (opacity-0 from switchLibrary)
|
||||
container.innerHTML = sections
|
||||
.map(
|
||||
(section) => `
|
||||
function renderSectionHTML(section: SectionData): string {
|
||||
return `
|
||||
<div class="dashboard-collection mb-8" data-collection-id="${section.id}">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -299,9 +288,20 @@ function renderDashboardCollections(sections: SectionData[]): void {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDashboardCollections(sections: SectionData[]): void {
|
||||
const container = document.getElementById(
|
||||
"collections-container",
|
||||
) as HTMLElement;
|
||||
if (!container) return;
|
||||
|
||||
// Preserve wood paneling attribute
|
||||
const currentWood = container.getAttribute("data-wood");
|
||||
|
||||
// Replace content while still invisible (opacity-0 from switchLibrary)
|
||||
container.innerHTML = sections.map((section) => renderSectionHTML(section)).join("");
|
||||
|
||||
// Step 5: Fade in new content (300ms for smoother entrance)
|
||||
container.classList.remove("duration-150");
|
||||
@@ -329,6 +329,7 @@ function renderBookCard(book: BookInfo): string {
|
||||
<a href="/media/${book.media_item_id}">
|
||||
<div class="book-card flex-shrink-0 w-36 rounded-lg overflow-hidden snap-start cursor-pointer
|
||||
transition-transform duration-200 hover:scale-105"
|
||||
data-media-item-id="${book.media_item_id}"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="View ${book.title}">
|
||||
@@ -505,6 +506,84 @@ 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 (!libraryId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/dashboard/sections?library_id=${libraryId}`,
|
||||
{
|
||||
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 }))));
|
||||
|
||||
const container = document.getElementById(
|
||||
"collections-container",
|
||||
) as HTMLElement;
|
||||
if (!container) {
|
||||
console.log("[dashboard] no collections-container found");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const section of data.sections as SectionData[]) {
|
||||
const track = document.getElementById(
|
||||
`carousel-track-${section.id}`,
|
||||
);
|
||||
|
||||
if (!track) {
|
||||
console.log("[dashboard] creating new section:", section.id, section.title);
|
||||
container.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
renderSectionHTML(section),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = new Set(
|
||||
Array.from(track.querySelectorAll<HTMLElement>("[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<HTMLElement>(
|
||||
'.text-center.py-8',
|
||||
);
|
||||
if (placeholder) {
|
||||
placeholder.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[dashboard] scan-complete handler error:", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { initDashboard };
|
||||
|
||||
Reference in New Issue
Block a user