import { Alpine } from "./alpine"; import { apiPost, apiPut } from "./api"; import { showToast } from "./toast"; import { initLibrarySwitcher, switchWithTransition } from "./library-switcher"; const SCROLL_AMOUNT = 300; function scrollCarousel(collectionId: string, direction: number): void { const track = document.getElementById( `carousel-track-${collectionId}`, ) as HTMLElement; if (!track) return; const scrollAmount = direction * SCROLL_AMOUNT; track.scrollBy({ left: scrollAmount, behavior: "smooth" }); } async function openDashboardSettings(): Promise { const librarySelect = document.getElementById( "library-select", ) as HTMLSelectElement; const libraryId = librarySelect?.value; if (!libraryId) { showToast("Select a specific library to customize preferences", "error"); 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 { showToast("Failed to load library preferences", "error"); console.error("API Error:", response.status, response.statusText); return; } const modal = document.getElementById( "dashboard-settings-modal", ) as HTMLElement; 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); } 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; const isHidden = prefs.hidden_collections.includes(collectionId); if (checkbox) checkbox.checked = !isHidden; const orderIndex = prefs.collection_order.indexOf(collectionId); if (orderIndex !== -1) { orderedItems[orderIndex] = item; } else { orderedItems.push(item); } }); orderedItems.forEach((item) => collectionList.appendChild(item)); } function closeDashboardSettings(): void { const modal = document.getElementById( "dashboard-settings-modal", ) as HTMLElement; modal?.classList.add("hidden"); } async function fetchAndRenderSections(libraryId: string): Promise { const param = libraryId ? `library_id=${libraryId}` : ""; const response = await fetch( `/api/dashboard/sections?${param}`, { headers: { Authorization: `Bearer ${localStorage.getItem("token")}`, "Content-Type": "application/json", }, }, ); if (!response.ok) throw new Error("Failed to load sections"); const data = await response.json(); renderDashboardCollections(data.sections); } 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) { hiddenCollections.push(collectionId); } } }); 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 apiPut("/dashboard/preferences", { library_id: libraryId, hidden_collections: hiddenCollections, collection_order: collectionOrder, items_per_section: parseInt(itemsPerCollection), }); if (response.ok) { showToast("Dashboard settings saved", "success"); closeDashboardSettings(); if (!libraryId) { showToast("Select a specific library to customize preferences", "error"); return; } await fetchAndRenderSections(libraryId); } } catch (error) { showToast("Failed to save settings", "error"); console.error("Save dashboard settings error:", error); } } async function restoreSystemCollection( collectionName: string, collectionTitle: string, ): Promise { if ( !confirm( `Are you sure you want to reset "${collectionTitle}" to its default state? Any customizations will be lost.`, ) ) { return; } try { const response = await apiPost("/dashboard/restore-system-collection", { collection_name: collectionName, }); if (response.ok) { showToast(`"${collectionTitle}" restored to defaults`, "success"); setTimeout(() => window.location.reload(), 1000); } } catch (error) { showToast("Failed to restore system collection", "error"); console.error("Restore system collection error:", error); } } function renderSectionHTML(section: SectionData): string { return `
${section.icon}

${section.title}

${section.description ? `

${section.description}

` : ""}
${section.view_all_url ? `View All →` : ""}
`; } function renderDashboardCollections(sections: SectionData[]): void { const container = document.getElementById( "collections-container", ) as HTMLElement; if (!container) return; const currentWood = container.getAttribute("data-wood"); container.innerHTML = sections.map((section) => renderSectionHTML(section)).join(""); container.classList.remove("duration-150"); container.classList.add("duration-300"); void container.offsetHeight; container.classList.remove("opacity-0"); setTimeout(() => { container.classList.remove("transition-opacity", "duration-300"); }, 300); if (currentWood) { container.setAttribute("data-wood", currentWood); } } const BOOK_OPEN_ICON = '"; function renderBookCard(book: BookInfo): string { const coverUrl = book.cover_image_path || "/static/placeholder-book.svg"; const actionHref = book.has_conflict ? `/media/${book.media_item_id}` : `/readers/${book.media_item_id}`; const actionLabel = book.has_conflict ? `Resolve progress conflict for ${book.title}` : `Read ${book.title}`; const actionTitle = book.has_conflict ? "Resolve progress conflict" : "Read"; return ` `; } async function reloadPage(): Promise { const librarySelect = document.getElementById( "library-select", ) as HTMLSelectElement; const libraryId = librarySelect?.value || ""; await switchWithTransition("collections-container", () => fetchAndRenderSections(libraryId), ); } function updateItemsCount( input: HTMLInputElement, displayTarget: string, ): void { const displayElement = document.getElementById(displayTarget) as HTMLElement; if (displayElement) { displayElement.textContent = input.value; } } function initDragAndDrop(): void { const collectionList = document.getElementById( "collection-list", ) as HTMLElement; if (!collectionList) return; let draggedItem: HTMLElement | null = null; collectionList.addEventListener("dragstart", (e: Event) => { const target = e.target as HTMLElement; if (target.classList.contains("collection-item")) { draggedItem = target; target.style.opacity = "0.5"; } }); collectionList.addEventListener("dragend", (e: Event) => { const target = e.target as HTMLElement; if (target.classList.contains("collection-item")) { target.style.opacity = "1"; draggedItem = null; } }); collectionList.addEventListener("dragover", (e: Event) => { e.preventDefault(); const target = e.target as HTMLElement; if ( target.classList.contains("collection-item") && target !== draggedItem && draggedItem ) { const rect = target.getBoundingClientRect(); const midY = rect.top + rect.height / 2; if ((e as DragEvent).clientY < midY) { target.parentNode?.insertBefore(draggedItem, target); } else { if (target.parentNode) { target.parentNode.insertBefore(draggedItem, target.nextSibling); } } } }); } function initDashboard() { initDragAndDrop(); initLibrarySwitcher({ onSwitch: (libraryId) => switchWithTransition("collections-container", () => fetchAndRenderSections(libraryId), ), }); 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 "reload-page": reloadPage(); 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"); if (displayTarget) updateItemsCount(input, displayTarget); break; } } }); 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); 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 })))); 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("[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(); } } } } catch (err) { console.error("[dashboard] scan-complete handler error:", err); } }); } export { initDashboard }; Alpine.data("dashboard", () => ({ initDashboard, }));