/** * Tools page functionality */ import { FuzzySearch, type SearchableItem } from "../search"; import { fetchData, debounce, escapeHtml } from "../utils"; export interface Tool extends SearchableItem { id: string; name: string; title: string; description: string; category: string; featured: boolean; requirements: string[]; features: string[]; links: { blog?: string; vscode?: string; "vscode-insiders"?: string; "visual-studio"?: string; github?: string; documentation?: string; marketplace?: string; npm?: string; pypi?: string; }; configuration?: { type: string; content: string; }; tags: string[]; } interface ToolsData { items: Tool[]; filters: { categories: string[]; tags: string[]; }; } let allItems: Tool[] = []; let search: FuzzySearch; let currentFilters = { categories: [] as string[], query: "", }; function formatMultilineText(text: string): string { return escapeHtml(text).replace(/\r?\n/g, "
"); } function applyFiltersAndRender(): void { const searchInput = document.getElementById( "search-input" ) as HTMLInputElement; const countEl = document.getElementById("results-count"); const query = searchInput?.value || ""; currentFilters.query = query; let results = query ? search.search(query) : [...allItems]; if (currentFilters.categories.length > 0) { results = results.filter((item) => currentFilters.categories.includes(item.category) ); } renderTools(results, query); let countText = `${results.length} of ${allItems.length} tools`; if (currentFilters.categories.length > 0) { countText += ` (filtered by ${currentFilters.categories.length} categories)`; } if (countEl) countEl.textContent = countText; } function renderTools(tools: Tool[], query = ""): void { const container = document.getElementById("tools-list"); if (!container) return; if (tools.length === 0) { container.innerHTML = `

No tools found

Try a different search term or adjust filters

`; return; } container.innerHTML = tools .map((tool) => { const badges: string[] = []; if (tool.featured) { badges.push('Featured'); } badges.push( `${escapeHtml(tool.category)}` ); const features = tool.features && tool.features.length > 0 ? `

Features

` : ""; const requirements = tool.requirements && tool.requirements.length > 0 ? `

Requirements

` : ""; const tags = tool.tags && tool.tags.length > 0 ? `
${tool.tags .map((t) => `${escapeHtml(t)}`) .join("")}
` : ""; const config = tool.configuration ? `

Configuration

${escapeHtml(tool.configuration.content)}
` : ""; const actions: string[] = []; if (tool.links.blog) { actions.push( `📖 Blog` ); } if (tool.links.marketplace) { actions.push( `🏪 Marketplace` ); } if (tool.links.npm) { actions.push( `📦 npm` ); } if (tool.links.pypi) { actions.push( `🐍 PyPI` ); } if (tool.links.documentation) { actions.push( `📚 Docs` ); } if (tool.links.github) { actions.push( `GitHub` ); } if (tool.links.vscode) { actions.push( `Install in VS Code` ); } if (tool.links["vscode-insiders"]) { actions.push( `VS Code Insiders` ); } if (tool.links["visual-studio"]) { actions.push( `Visual Studio` ); } const actionsHtml = actions.length > 0 ? `
${actions.join("")}
` : ""; const titleHtml = query ? search.highlight(tool.name, query) : escapeHtml(tool.name); const descriptionHtml = formatMultilineText(tool.description); return `

${titleHtml}

${badges.join("")}

${descriptionHtml}

${features} ${requirements} ${config} ${tags} ${actionsHtml}
`; }) .join(""); setupCopyConfigHandlers(); } function setupCopyConfigHandlers(): void { document.querySelectorAll(".copy-config-btn").forEach((btn) => { btn.addEventListener("click", async (e) => { e.stopPropagation(); const button = e.currentTarget as HTMLButtonElement; const config = decodeURIComponent(button.dataset.config || ""); try { await navigator.clipboard.writeText(config); button.classList.add("copied"); const originalHtml = button.innerHTML; button.innerHTML = ` Copied! `; setTimeout(() => { button.classList.remove("copied"); button.innerHTML = originalHtml; }, 2000); } catch (err) { console.error("Failed to copy:", err); } }); }); } export async function initToolsPage(): Promise { const container = document.getElementById("tools-list"); const searchInput = document.getElementById( "search-input" ) as HTMLInputElement; const categoryFilter = document.getElementById( "filter-category" ) as HTMLSelectElement; const clearFiltersBtn = document.getElementById("clear-filters"); if (container) { container.innerHTML = '
Loading tools...
'; } const data = await fetchData("tools.json"); if (!data || !data.items) { if (container) container.innerHTML = '

Failed to load tools

'; return; } // Map items to include title for FuzzySearch allItems = data.items.map((item) => ({ ...item, title: item.name, // FuzzySearch uses title })); search = new FuzzySearch(); search.setItems(allItems); // Populate category filter if (categoryFilter && data.filters.categories) { categoryFilter.innerHTML = '' + data.filters.categories .map( (c) => `` ) .join(""); categoryFilter.addEventListener("change", () => { currentFilters.categories = categoryFilter.value ? [categoryFilter.value] : []; applyFiltersAndRender(); }); } // Search input handler searchInput?.addEventListener( "input", debounce(() => applyFiltersAndRender(), 200) ); // Clear filters clearFiltersBtn?.addEventListener("click", () => { currentFilters = { categories: [], query: "" }; if (categoryFilter) categoryFilter.value = ""; if (searchInput) searchInput.value = ""; applyFiltersAndRender(); }); applyFiltersAndRender(); } // Auto-initialize when DOM is ready document.addEventListener("DOMContentLoaded", initToolsPage);