/** * Skills page functionality */ import { createChoices, getChoicesValues, type Choices } from '../choices'; import { FuzzySearch } from '../search'; import { fetchData, debounce, escapeHtml, getGitHubUrl, getRawGitHubUrl } from '../utils'; import { setupModal, openFileModal } from '../modal'; import JSZip from '../jszip'; interface SkillFile { name: string; path: string; } interface Skill { id: string; title: string; description?: string; path: string; skillFile: string; category: string; hasAssets: boolean; assetCount: number; files: SkillFile[]; } interface SkillsData { items: Skill[]; filters: { categories: string[]; }; } const resourceType = 'skill'; let allItems: Skill[] = []; let search = new FuzzySearch(); let categorySelect: Choices; let currentFilters = { categories: [] as string[], hasAssets: false }; 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.categories.length > 0) { results = results.filter(item => currentFilters.categories.includes(item.category)); } if (currentFilters.hasAssets) { results = results.filter(item => item.hasAssets); } renderItems(results, query); const activeFilters: string[] = []; if (currentFilters.categories.length > 0) activeFilters.push(`${currentFilters.categories.length} categor${currentFilters.categories.length > 1 ? 'ies' : 'y'}`); if (currentFilters.hasAssets) activeFilters.push('has assets'); let countText = `${results.length} of ${allItems.length} skills`; if (activeFilters.length > 0) { countText += ` (filtered by ${activeFilters.join(', ')})`; } if (countEl) countEl.textContent = countText; } function renderItems(items: Skill[], query = ''): void { const list = document.getElementById('resource-list'); if (!list) return; if (items.length === 0) { list.innerHTML = '

No skills found

Try a different search term or adjust filters

'; return; } list.innerHTML = items.map(item => `
${query ? search.highlight(item.title, query) : escapeHtml(item.title)}
${escapeHtml(item.description || 'No description')}
${escapeHtml(item.category)} ${item.hasAssets ? `${item.assetCount} asset${item.assetCount === 1 ? '' : 's'}` : ''} ${item.files.length} file${item.files.length === 1 ? '' : 's'}
GitHub
`).join(''); // Add click handlers for opening modal list.querySelectorAll('.resource-item').forEach(el => { el.addEventListener('click', (e) => { // Don't trigger modal if clicking download button or github link if ((e.target as HTMLElement).closest('.resource-actions')) return; const path = (el as HTMLElement).dataset.path; if (path) openFileModal(path, resourceType); }); }); // Add download handlers list.querySelectorAll('.download-skill-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const skillId = (btn as HTMLElement).dataset.skillId; if (skillId) downloadSkill(skillId, btn as HTMLButtonElement); }); }); } async function downloadSkill(skillId: string, btn: HTMLButtonElement): Promise { const skill = allItems.find(item => item.id === skillId); if (!skill || !skill.files || skill.files.length === 0) { alert('No files found for this skill'); return; } const originalContent = btn.innerHTML; btn.disabled = true; btn.innerHTML = ' Preparing...'; try { const zip = new JSZip(); const folder = zip.folder(skill.id); const fetchPromises = skill.files.map(async (file) => { const url = getRawGitHubUrl(file.path); try { const response = await fetch(url); if (!response.ok) return null; const content = await response.text(); return { name: file.name, content }; } catch { return null; } }); const results = await Promise.all(fetchPromises); let addedFiles = 0; for (const result of results) { if (result && folder) { folder.file(result.name, result.content); addedFiles++; } } if (addedFiles === 0) throw new Error('Failed to fetch any files'); const blob = await zip.generateAsync({ type: 'blob' }); const downloadUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; link.download = `${skill.id}.zip`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(downloadUrl); btn.innerHTML = ' Downloaded!'; setTimeout(() => { btn.disabled = false; btn.innerHTML = originalContent; }, 2000); } catch { btn.innerHTML = ' Failed'; setTimeout(() => { btn.disabled = false; btn.innerHTML = originalContent; }, 2000); } } export async function initSkillsPage(): Promise { const list = document.getElementById('resource-list'); const searchInput = document.getElementById('search-input') as HTMLInputElement; const hasAssetsCheckbox = document.getElementById('filter-has-assets') as HTMLInputElement; const clearFiltersBtn = document.getElementById('clear-filters'); const data = await fetchData('skills.json'); if (!data || !data.items) { if (list) list.innerHTML = '

Failed to load data

'; return; } allItems = data.items; search.setItems(allItems); categorySelect = createChoices('#filter-category', { placeholderValue: 'All Categories' }); categorySelect.setChoices(data.filters.categories.map(c => ({ value: c, label: c })), 'value', 'label', true); document.getElementById('filter-category')?.addEventListener('change', () => { currentFilters.categories = getChoicesValues(categorySelect); applyFiltersAndRender(); }); applyFiltersAndRender(); searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200)); hasAssetsCheckbox?.addEventListener('change', () => { currentFilters.hasAssets = hasAssetsCheckbox.checked; applyFiltersAndRender(); }); clearFiltersBtn?.addEventListener('click', () => { currentFilters = { categories: [], hasAssets: false }; categorySelect.removeActiveItems(); if (hasAssetsCheckbox) hasAssetsCheckbox.checked = false; if (searchInput) searchInput.value = ''; applyFiltersAndRender(); }); setupModal(); } // Auto-initialize when DOM is ready document.addEventListener('DOMContentLoaded', initSkillsPage);