/** * Hooks page functionality */ import { createChoices, getChoicesValues, type Choices } from "../choices"; import { FuzzySearch, type SearchItem } from "../search"; import { fetchData, debounce, showToast, downloadZipBundle, } from "../utils"; import { setupModal, openFileModal } from "../modal"; import { renderHooksHtml, sortHooks, type HookSortOption, type RenderableHook, } from "./hooks-render"; interface Hook extends SearchItem, RenderableHook {} interface HooksData { items: Hook[]; filters: { hooks: string[]; tags: string[]; }; } const resourceType = "hook"; let allItems: Hook[] = []; let search = new FuzzySearch(); let hookSelect: Choices; let tagSelect: Choices; let currentFilters = { hooks: [] as string[], tags: [] as string[], }; let currentSort: HookSortOption = "title"; let resourceListHandlersReady = false; function sortItems(items: Hook[]): Hook[] { return sortHooks(items, currentSort); } function applyFiltersAndRender(): void { const searchInput = document.getElementById( "search-input" ) as HTMLInputElement; const countEl = document.getElementById("results-count"); const query = searchInput?.value || ""; let results = query ? search.search(query) : [...allItems]; if (currentFilters.hooks.length > 0) { results = results.filter((item) => item.hooks.some((h) => currentFilters.hooks.includes(h)) ); } if (currentFilters.tags.length > 0) { results = results.filter((item) => item.tags.some((t) => currentFilters.tags.includes(t)) ); } results = sortItems(results); renderItems(results, query); const activeFilters: string[] = []; if (currentFilters.hooks.length > 0) activeFilters.push( `${currentFilters.hooks.length} hook event${ currentFilters.hooks.length > 1 ? "s" : "" }` ); if (currentFilters.tags.length > 0) activeFilters.push( `${currentFilters.tags.length} tag${ currentFilters.tags.length > 1 ? "s" : "" }` ); let countText = `${results.length} of ${allItems.length} hooks`; if (activeFilters.length > 0) { countText += ` (filtered by ${activeFilters.join(", ")})`; } if (countEl) countEl.textContent = countText; } function renderItems(items: Hook[], query = ""): void { const list = document.getElementById("resource-list"); if (!list) return; list.innerHTML = renderHooksHtml(items, { query, highlightTitle: (title, highlightQuery) => search.highlight(title, highlightQuery), }); } function setupResourceListHandlers(list: HTMLElement | null): void { if (!list || resourceListHandlersReady) return; list.addEventListener("click", (event) => { const target = event.target as HTMLElement; const downloadButton = target.closest( ".download-hook-btn" ) as HTMLButtonElement | null; if (downloadButton) { event.stopPropagation(); const hookId = downloadButton.dataset.hookId; if (hookId) downloadHook(hookId, downloadButton); return; } if (target.closest(".resource-actions")) { return; } const item = target.closest(".resource-item") as HTMLElement | null; const path = item?.dataset.path; if (path) { openFileModal(path, resourceType); } }); resourceListHandlersReady = true; } async function downloadHook( hookId: string, btn: HTMLButtonElement ): Promise { const hook = allItems.find((item) => item.id === hookId); if (!hook) { showToast("Hook not found.", "error"); return; } // Build file list: README.md + all assets const files = [ { name: "README.md", path: hook.readmeFile }, ...hook.assets.map((a) => ({ name: a, path: `${hook.path}/${a}`, })), ]; if (files.length === 0) { showToast("No files found for this hook.", "error"); return; } const originalContent = btn.innerHTML; btn.disabled = true; btn.innerHTML = ' Preparing...'; try { await downloadZipBundle(hook.id, files); btn.innerHTML = ' Downloaded!'; setTimeout(() => { btn.disabled = false; btn.innerHTML = originalContent; }, 2000); } catch (error) { const message = error instanceof Error ? error.message : "Download failed."; showToast(message, "error"); btn.innerHTML = ' Failed'; setTimeout(() => { btn.disabled = false; btn.innerHTML = originalContent; }, 2000); } } export async function initHooksPage(): Promise { const list = document.getElementById("resource-list"); const searchInput = document.getElementById( "search-input" ) as HTMLInputElement; const clearFiltersBtn = document.getElementById("clear-filters"); const sortSelect = document.getElementById( "sort-select" ) as HTMLSelectElement; setupResourceListHandlers(list as HTMLElement | null); const data = await fetchData("hooks.json"); if (!data || !data.items) { if (list) list.innerHTML = '

Failed to load data

'; return; } allItems = data.items; search.setItems(allItems); // Setup hook event filter hookSelect = createChoices("#filter-hook", { placeholderValue: "All Events", }); hookSelect.setChoices( data.filters.hooks.map((h) => ({ value: h, label: h })), "value", "label", true ); document.getElementById("filter-hook")?.addEventListener("change", () => { currentFilters.hooks = getChoicesValues(hookSelect); applyFiltersAndRender(); }); // Setup tag filter tagSelect = createChoices("#filter-tag", { placeholderValue: "All Tags", }); tagSelect.setChoices( data.filters.tags.map((t) => ({ value: t, label: t })), "value", "label", true ); document.getElementById("filter-tag")?.addEventListener("change", () => { currentFilters.tags = getChoicesValues(tagSelect); applyFiltersAndRender(); }); sortSelect?.addEventListener("change", () => { currentSort = sortSelect.value as HookSortOption; applyFiltersAndRender(); }); applyFiltersAndRender(); searchInput?.addEventListener( "input", debounce(() => applyFiltersAndRender(), 200) ); clearFiltersBtn?.addEventListener("click", () => { currentFilters = { hooks: [], tags: [] }; currentSort = "title"; hookSelect.removeActiveItems(); tagSelect.removeActiveItems(); if (searchInput) searchInput.value = ""; if (sortSelect) sortSelect.value = "title"; applyFiltersAndRender(); }); setupModal(); } // Auto-initialize when DOM is ready document.addEventListener("DOMContentLoaded", initHooksPage);