import { Alpine } from "./alpine"; import { apiPost, apiPut } from "./api"; import { showToast } from "./toast"; // Dashboard functionality with unified collections architecture // Procedural/imperative style (no OOP) 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("No library selected", "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; // Don't open modal with stale data. } 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); } // 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; modal?.classList.add("hidden"); } 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(); // Fetch updated sections and re-render (like switchLibrary does) if (!libraryId) { showToast("Unable to refresh dashboard - no library selected", "error"); 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("selectedLibrary", libraryId); } else { showToast("Failed to refresh sections", "error"); } } } 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); } } async function switchLibrary(libraryId: string): Promise { const container = document.getElementById( "collections-container", ) as HTMLElement; const loading = document.getElementById("loading-spinner") as HTMLElement; if (!container || !loading) return; 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}`, { 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); localStorage.setItem("selectedLibrary", libraryId); } catch (error) { showToast("Failed to load library", "error"); console.error("Switch library error:", error); } finally { loading.classList.add("hidden"); } } 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) => `
${section.icon}

${section.title}

${section.description ? `

${section.description}

` : ""}
${section.view_all_url ? `View All →` : ""}
`, ) .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); } } function renderBookCard(book: BookInfo): string { const coverUrl = book.cover_image_path || "/static/placeholder-book.svg"; return `
${book.title}

${book.title}

${book.author ? `

${book.author}

` : ""}
`; } // function viewBook(bookId: string): void { // console.log("View book:", bookId); // } async function reloadPage(): Promise { //Get current library from dropdown const librarySelect = document.getElementById( "library-select", ) as HTMLSelectElement; const currentLibraryId = librarySelect?.value; if (!currentLibraryId) { showToast("No library selected", "error"); return; } // Reuse switchLibrary logic - it handles the fade transition await switchLibrary(currentLibraryId); } 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(); 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 "view-book": { // const bookId = target.dataset.bookId || actionElem?.dataset.bookId; // if (bookId) viewBook(bookId); // break; // } case "reload-page": reloadPage(); break; case "switch-library": { const select = target as HTMLSelectElement; 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"); if (displayTarget) updateItemsCount(input, displayTarget); break; } } }); // 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); } }); } }); } export { initDashboard }; Alpine.data("dashboard", () => ({ initDashboard, }));