// Library management functionality for admin/library page // Procedural/imperative style t // nt // OOP) import { Alpine } from "./alpine"; import { apiPut, apiPost, apiGet, handleResponse, handleError, handleVoidResponse, apiDelete, } from "./api"; import { showToast } from "./toast"; interface Library { id: string; name: string; description: string | null; library_type_id: string; type_name: string; created_at: string; updated_at: string; } interface LibraryFolder { id: string; library_id: string; folder_path: string; created_at: string; } interface LibrariesResponse { data: Library[]; } // State (already SSR'd, used for updates) let libraries: Library[] = []; // Reload libraries from API (called after create/delete/update) async function reloadLibraries(): Promise { try { const response = await apiGet("/libraries"); const result = (await handleResponse( response, )) as unknown as LibrariesResponse; libraries = result.data; renderLibraries(); } catch (error) { handleError(error, "Failed to load libraries"); } } // Render libraries list (replaces SSR content after updates) function renderLibraries(): void { const container = document.getElementById("libraries-list"); if (!container) return; if (libraries.length === 0) { container.innerHTML = '

No libraries yet. Create your first library to get started.

'; return; } container.innerHTML = libraries .map( (library) => '
' + '
' + "
" + `

${escapeHtmlLocal(library.name)}

` + (library.description ? `

${escapeHtmlLocal(library.description)}

` : "") + `${escapeHtmlLocal(library.type_name)}` + "
" + '
' + `` + `` + `` + "
" + "
" + `` + "
", ) .join(""); } // Load user's visible libraries for visibility management async function loadUserVisibility(): Promise { const select = document.getElementById("user-select") as HTMLSelectElement; const userId = select?.value; if (!userId) { const container = document.getElementById("user-libraries"); if (container) { container.innerHTML = '

Please select a user

'; } return; } try { const response = await apiGet("/libraries/visible"); const visibleLibraries = (await handleResponse( response, )) as unknown as Library[]; const container = document.getElementById("user-libraries"); if (!container) return; const visibleIds = new Set(visibleLibraries.map((lib: Library) => lib.id)); container.innerHTML = libraries .map((library) => { const isVisible = visibleIds.has(library.id); return ( '" ); }) .join(""); } catch (error) { handleError(error, "Failed to load user libraries"); } } // Set library visibility for a user async function setLibraryVisibility( userId: string, libraryId: string, isVisible: boolean, ): Promise { // userId is used in the HTML onchange handler but the API gets user from JWT context console.debug( "Setting visibility for user:", userId, "library:", libraryId, "visible:", isVisible, ); try { const response = await apiPost("/libraries/visibility", { library_id: libraryId, is_visible: isVisible, }); await handleVoidResponse(response); showToast("Library visibility updated", "success"); // Refresh visibility controls void loadUserVisibility(); } catch (error) { handleError(error, "Failed to update library visibility"); } } // Create library form handler async function handleCreateLibrarySubmit(event: Event): Promise { event.preventDefault(); const form = event.target as HTMLFormElement; const formData = new FormData(form); const libraryId = (document.getElementById("library-id") as HTMLInputElement) ?.value; const isEdit = !!libraryId; const libraryData = { name: formData.get("name") as string, description: formData.get("description") as string, type: formData.get("type") as string, }; try { const url = isEdit ? `/libraries/${libraryId}` : "/libraries"; const response = isEdit ? await apiPut(url, libraryData) : await apiPost(url, libraryData); if (isEdit) { await handleVoidResponse(response); } else { (await handleResponse(response)) as unknown as { data: Library }; } showToast( isEdit ? "Library updated successfully" : "Library created successfully", "success", ); hideCreateLibraryModal(); form.reset(); const libraryIdInput = document.getElementById( "library-id", ) as HTMLInputElement; if (libraryIdInput) { libraryIdInput.value = ""; } void reloadLibraries(); } catch (error) { handleError( error, isEdit ? "Failed to update library" : "Failed to create library", ); } } // Delete library async function deleteLibrary(libraryId: string): Promise { const library = libraries.find((l) => l.id === libraryId); if (!library) return; showDeleteModal(library); } // Show library folders async function showLibraryFolders(libraryId: string): Promise { const container = document.getElementById(`library-folders-${libraryId}`); if (!container) return; try { const response = await apiGet(`/libraries/${libraryId}/folders`); const folders = (await handleResponse( response, )) as unknown as LibraryFolder[]; container.innerHTML = folders .map( (folder: LibraryFolder) => '
' + `${escapeHtmlLocal(folder.folder_path)}` + `' + "
", ) .join(""); container.innerHTML += '
' + `' + `' + `' + "
"; container.classList.remove("hidden"); } catch (error) { handleError(error, "Failed to load folders"); } } // Add library folder async function addLibraryFolder(libraryId: string): Promise { const input = document.getElementById( `folder-path-${libraryId}`, ) as HTMLInputElement; const folderPath = input?.value.trim(); if (!folderPath) { return; } try { const response = await apiPost(`/libraries/${libraryId}/folders`, { folder_path: folderPath, }); await handleVoidResponse(response); showToast("Folder added successfully", "success"); if (input) { input.value = ""; } void showLibraryFolders(libraryId); // Refresh } catch (error) { handleError(error, "Failed to add folder"); } } // Remove library folder async function removeLibraryFolder( libraryId: string, folderPath: string, ): Promise { if (!confirm(`Remove folder "${folderPath}" from the library?`)) { return; } try { const response = await apiDelete(`/libraries/${libraryId}/folders`, { folder_path: folderPath, }); await handleVoidResponse(response); showToast("Folder removed successfully", "success"); void showLibraryFolders(libraryId); // Refresh } catch (error) { handleError(error, "Failed to remove folder"); } } // Edit library (placeholder - opens modal or navigates to edit page) function editLibrary(libraryId: string): void { const library = libraries.find((l) => l.id === libraryId); if (!library) { showToast("Library not found", "error"); return; } const form = document.getElementById( "create-library-form", ) as HTMLFormElement; if (form) { const nameInput = form.querySelector('[name="name"]') as HTMLInputElement; const descInput = form.querySelector( '[name="description"]', ) as HTMLTextAreaElement; const typeInput = form.querySelector('[name="type"]') as HTMLSelectElement; if (nameInput) nameInput.value = library.name; if (descInput) descInput.value = library.description || ""; if (typeInput) typeInput.value = library.type_name; } const modalTitle = document.querySelector("#create-library-modal h2"); if (modalTitle) { modalTitle.textContent = "Edit Library"; } const libraryIdInput = document.getElementById( "library-id", ) as HTMLInputElement; if (libraryIdInput) { libraryIdInput.value = libraryId; } showCreateLibraryModal(); } // Modal controls function showCreateLibraryModal(): void { const modal = document.getElementById("create-library-modal") as HTMLElement; if (modal) { modal.classList.remove("hidden"); const modalTitle = document.querySelector("#create-library-modal h2"); if (modalTitle) { modalTitle.textContent = "Create Library"; } } } function hideCreateLibraryModal(): void { const modal = document.getElementById("create-library-modal") as HTMLElement; if (modal) { modal.classList.add("hidden"); } } // Delete modal state let libraryToDelete: Library | null = null; function showDeleteModal(library: Library): void { libraryToDelete = library; const modal = document.getElementById("delete-library-modal") as HTMLElement; const content = document.getElementById( "delete-modal-content", ) as HTMLElement; if (modal && content) { const message = `Are you sure you want to delete "${escapeHtmlLocal(library.name)}"? This will remove: • Library metadata from the database • All folder references • All book records from the database ⚠️ Book files on disk will NOT be deleted. This action cannot be undone.`; content.innerHTML = message.replace(/\n/g, "
"); modal.classList.remove("hidden"); } } function hideDeleteModal(): void { const modal = document.getElementById("delete-library-modal") as HTMLElement; if (modal) { modal.classList.add("hidden"); } libraryToDelete = null; } async function confirmDeleteLibrary(): Promise { if (!libraryToDelete) return; const libraryId = libraryToDelete.id; hideDeleteModal(); try { const response = await apiDelete(`/libraries/${libraryId}`); await handleVoidResponse(response); showToast("Library deleted successfully", "success"); await reloadLibraries(); const libraryIdInput = document.getElementById( "library-id", ) as HTMLInputElement | null; if (libraryIdInput) { libraryIdInput.value = ""; } } catch (error) { handleError(error, "Failed to delete library"); } } // Local escape HTML helper function escapeHtmlLocal(text: string): string { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } // Event delegation for handling dynamic button clicks function handleLibraryListClick(event: Event): void { const target = event.target as HTMLElement; const button = target.closest("button") as HTMLElement; if (!button) return; const action = button.dataset.action; const libraryId = button.dataset.libraryId; switch (action) { case "show-folders": if (libraryId) showLibraryFolders(libraryId); break; case "delete": if (libraryId) deleteLibrary(libraryId); break; case "edit": if (libraryId) editLibrary(libraryId); break; case "add-folder": if (libraryId) addLibraryFolder(libraryId); break; case "remove-folder": if (libraryId && button.dataset.folderPath) { removeLibraryFolder(libraryId, button.dataset.folderPath); } break; case "browse-folder": if (button.dataset.inputId) showFolderBrowser(button.dataset.inputId); break; } } function handleFolderBrowserClick(event: Event): void { const target = event.target as HTMLElement; const button = target.closest("button") as HTMLElement; const div = target.closest("div[data-action]") as HTMLElement; if (button) { const action = button.dataset.action; const path = button.dataset.path; switch (action) { case "browse-parent": if (path) navigateFolderBrowser(path); break; case "browse-cancel": hideFolderBrowser(); break; case "browse-select": if (path) selectBrowseFolder(path); break; } } if (div && div.dataset.action === "browse-navigate") { const path = div.dataset.path; if (path) navigateFolderBrowser(path); } } function handleGlobalClick(event: Event): void { const target = event.target as HTMLElement; const button = target.closest("button") as HTMLElement; if (!button) return; const action = button.dataset.action; switch (action) { case "show-create-modal": showCreateLibraryModal(); break; case "hide-create-modal": hideCreateLibraryModal(); break; case "hide-delete-modal": hideDeleteModal(); break; case "confirm-delete": void confirmDeleteLibrary(); break; } } // Folder browser state let currentBrowsePath = ""; let currentBrowseInputId = ""; // Show folder browser modal function showFolderBrowser(inputId: string): void { currentBrowseInputId = inputId; currentBrowsePath = "/"; const modal = document.getElementById("folder-browser-modal") as HTMLElement; if (modal) { modal.classList.remove("hidden"); void loadBrowseDirectories(currentBrowsePath); } } // Load directories for browsing async function loadBrowseDirectories(path: string): Promise { try { const response = await apiGet( `/libraries/browse?path=${encodeURIComponent(path)}`, ); const data = (await handleResponse(response)) as unknown as { current_path: string; parent_path: string; directories: string[]; }; currentBrowsePath = data.current_path; renderBrowseDirectories(data); } catch (error) { handleError(error, "Failed to load directories"); } } // Render browse directories (uses event delegation via data-action attributes) function renderBrowseDirectories(data: { current_path: string; parent_path: string; directories: string[]; }): void { const container = document.getElementById("folder-browser-content"); if (!container) return; let html = `
${ data.parent_path ? `` : "" } ${escapeHtmlLocal(data.current_path)}
`; if (data.directories.length === 0) { html += '

No subdirectories

'; } else { data.directories.forEach((dir) => { const fullPath = data.current_path === "/" ? `/${dir}` : `${data.current_path}/${dir}`; html += `
📁 ${escapeHtmlLocal(dir)}
`; }); } html += `
`; container.innerHTML = html; } // Navigate to subdirectory function navigateFolderBrowser(path: string): void { void loadBrowseDirectories(path); } // Select folder and close browser function selectBrowseFolder(path: string): void { const input = document.getElementById( currentBrowseInputId, ) as HTMLInputElement; if (input) { input.value = path; } hideFolderBrowser(); } // Hide folder browser modal function hideFolderBrowser(): void { const modal = document.getElementById("folder-browser-modal") as HTMLElement; if (modal) { modal.classList.add("hidden"); } } // Initialize page function initializeLibraryAdmin(): void { // Setup event listeners const librariesList = document.getElementById("libraries-list"); if (librariesList) { librariesList.addEventListener("click", handleLibraryListClick); } const folderBrowserModal = document.getElementById("folder-browser-modal"); if (folderBrowserModal) { folderBrowserModal.addEventListener("click", handleFolderBrowserClick); } document.addEventListener("click", handleGlobalClick); // Setup form submission const createLibraryForm = document.getElementById("create-library-form"); if (createLibraryForm) { createLibraryForm.addEventListener("submit", handleCreateLibrarySubmit); } // SSR provides initial library list - no need to fetch on page load // reloadLibraries() is called after create/delete/update operations } // Export functions for global access export { addLibraryFolder, confirmDeleteLibrary, deleteLibrary, editLibrary, handleCreateLibrarySubmit, hideDeleteModal, hideFolderBrowser, initializeLibraryAdmin, loadUserVisibility, navigateFolderBrowser, removeLibraryFolder, selectBrowseFolder, setLibraryVisibility, showDeleteModal, showFolderBrowser, showLibraryFolders, }; Alpine.data("library", () => ({ addLibraryFolder, confirmDeleteLibrary, deleteLibrary, editLibrary, handleCreateLibrarySubmit, hideDeleteModal, hideFolderBrowser, initializeLibraryAdmin, loadUserVisibility, navigateFolderBrowser, removeLibraryFolder, selectBrowseFolder, setLibraryVisibility, showDeleteModal, showFolderBrowser, showLibraryFolders, }));