// Bookshelf functionality - procedural/imperative style import { Alpine } from "./alpine"; import { showToast } from "./toast"; function clearFilters(): void { const filterForm = document.getElementById("filter-form") as HTMLFormElement; if (!filterForm) return; // Reset all form fields const inputs = filterForm.querySelectorAll("input, select"); inputs.forEach((input) => { if (input instanceof HTMLInputElement && input.type === "checkbox") { input.checked = false; } else { (input as HTMLInputElement).value = ""; } }); // Trigger HTMX reload with cleared filters window.htmx.trigger(filterForm, "change"); } // Alpine.js component Alpine.data("bookshelf", () => ({ // Component state showSaveModal: false, filterName: "", showFiltersDropdown: false, // Standalone function references (don't access component state) clearFilters, initBookshelf, // Inline method - opens the modal showSaveFilterModal() { this.showSaveModal = true; }, // Toggle the filters dropdown toggleFiltersDropdown() { this.showFiltersDropdown = !this.showFiltersDropdown; }, // Load a saved filter into the form async loadFilter(event: Event) { const button = event.target as HTMLElement; const filterRow = button.closest("[data-filter-id]"); if (!filterRow) return; const filterId = filterRow?.getAttribute("data-filter-id"); if (!filterId) return; const filterName = button.textContent?.trim() || ""; showToast(`Loading filter: ${filterName}...`, "info"); const token = localStorage.getItem("token"); if (!token) { showToast("Not authenticated", "error"); return; } try { // Fetch filter details from API const response = await fetch(`/api/saved-filters/${filterId}`, { headers: { Authorization: `Bearer ${token}` }, }); if (!response.ok) { if (response.status === 404) { showToast("Filter not found", "error"); } else { showToast("Failed to load filter", "error"); } return; } const filter = await response.json(); // Parse filters JSON (string → object) const filterData: Record = typeof filter.filters === "string" ? JSON.parse(filter.filters) : filter.filters; // Get the hidden filter form const filterForm = document.getElementById( "filter-form", ) as HTMLFormElement; if (!filterForm) { showToast("Filter form not found", "error"); return; } // Clear existing filter values filterForm.innerHTML = ` `; // Populate form fields from filter data Object.entries(filterData).forEach(([key, value]) => { if (value) { // Only set non-empty values const input = document.createElement("input"); input.type = "hidden"; input.name = key; input.value = value; filterForm.appendChild(input); // Also update visible form fields if they exist const visibleField = document.querySelector( `[name="${key}"]`, ) as HTMLInputElement; if (visibleField) { visibleField.value = value; } } }); // Trigger HTMX to apply the filter // Use the first input to trigger the change event const firstInput = filterForm.querySelector("input"); if (firstInput) { window.htmx.trigger(firstInput, "change"); } showToast(`Filter applied: ${filterName}`, "success"); // Close the dropdown after applying this.showFiltersDropdown = false; } catch (error) { console.error("Failed to load filter:", error); showToast("Error loading filter", "error"); } }, // Delete a saved filter async deleteFilter(event: Event) { const button = event.target as HTMLElement; const filterRow = button.closest("[data-filter-id]"); if (!filterRow) return; const filterId = filterRow?.getAttribute("data-filter-id"); if (!filterId) return; if (!confirm("Are you sure you want to delete this filter?")) return; const token = localStorage.getItem("token"); if (!token) { showToast("Not authenticated", "error"); return; } try { const response = await fetch(`/api/saved-filters/${filterId}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { showToast("Filter deleted", "success"); // Remove the element from DOM filterRow?.remove(); } else { showToast("Failed to delete filter", "error"); } } catch (error) { console.error("Failed to delete filter:", error); showToast("Error deleting filter", "error"); } }, // Inline method - saves the filter and closes modal async saveFilter(event: Event) { event.preventDefault(); const token = localStorage.getItem("token"); if (!token) { showToast("Not authenticated", "error"); return; } // Get filter name from component state const filterName = this.filterName; if (!filterName) { showToast("Please enter a filter name", "error"); return; } // Collect filter form data const filterForm = document.getElementById( "filter-form", ) as HTMLFormElement; if (!filterForm) { showToast("Filter form not found", "error"); return; } const formData = new FormData(filterForm); const filterData: Record = {}; formData.forEach((value, key) => { filterData[key] = value.toString(); }); try { const response = await fetch("/api/saved-filters", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ name: filterName, resource_type: "media-items", filters: filterData, }), }); if (response.ok) { showToast("Filter saved successfully", "success"); // Clear the filter name input this.filterName = ""; // Close the modal this.showSaveModal = false; } else { showToast("Failed to save filter", "error"); } } catch (error) { console.error("Failed to save filter:", error); showToast("Error saving filter", "error"); } }, // Autocomplete helper function async fetchFieldValues( field: string, search: string, datalistId: string, ): Promise { const token = localStorage.getItem("token"); if (!token) { console.error("Not authenticated"); return; } if (search.length < 2) return; const currentLibraryId = ( document.getElementById("library-select") as HTMLSelectElement )?.value; if (!currentLibraryId) { console.error("No library selected"); return; } try { const response = await fetch( `/api/media-items/search?${field}=${encodeURIComponent( search, )}&library_id=${currentLibraryId}&limit=50`, { headers: { Authorization: `Bearer ${token}` }, }, ); if (!response.ok) { console.error("Failed to fetch field values"); return; } const data = await response.json(); // Update datalist via DOM manipulation const datalist = document.getElementById(datalistId); if (!datalist) { console.error(`Datalist ${datalistId} not found`); return; } // Clear existing options datalist.innerHTML = ""; // Add new options data.results.forEach((item: { value: string; count: number }) => { const option = document.createElement("option"); option.value = item.value; option.textContent = `${item.value} (${item.count})`; datalist.appendChild(option); }); } catch (error) { console.error("Error fetching field values:", error); } }, // Fetch author values for autocomplete async fetchAuthorValues(input: HTMLInputElement): Promise { await this.fetchFieldValues("author", input.value, "author-datalist"); }, // Fetch tag values for autocomplete async fetchTagValues(input: HTMLInputElement): Promise { await this.fetchFieldValues("tags", input.value, "tags-datalist"); }, // // Fetch genre values for autocomplete // async fetchGenreValues(input: HTMLInputElement): Promise { // await this.fetchFieldValues("genre", input.value, "genre-datalist"); // }, // Fetch series values for autocomplete async fetchSeriesValues(input: HTMLInputElement): Promise { await this.fetchFieldValues("series", input.value, "series-datalist"); }, // Fetch language values for autocomplete async fetchLanguageValues(input: HTMLInputElement): Promise { await this.fetchFieldValues("language", input.value, "language-datalist"); }, })); // Standalone function - initialize the bookshelf (NOT async, like dashboard.ts) function initBookshelf(): void { // Check if books were already rendered server-side const booksGrid = document.getElementById("books-grid"); const hasServerBooks = booksGrid && booksGrid.querySelector("[data-book-id]") !== null; if (!hasServerBooks) { // Only trigger HTMX if no SSR books rendered const librarySelect = document.getElementById( "library-select", ) as HTMLSelectElement; if (librarySelect && librarySelect.value) { const filterForm = document.getElementById( "filter-form", ) as HTMLFormElement; if (filterForm) { // Trigger initial HTMX load window.htmx.trigger(librarySelect, "change"); } } } } export { clearFilters };