/** * ExerciseBrowser — Topic filter tabs, exercise list, search, navigation to viewer */ const ExerciseBrowser = (() => { let container = null; let exercises = []; let filteredExercises = []; let activeTopic = 'all'; let searchText = ''; let exerciseDataLoaded = {}; let ac = null; let searchDebounceTimer = null; const TOPICS = [ { key: 'all', label: 'Todos', icon: '📚' }, { key: 'vector-ops', label: 'Vectores', icon: '➡️' }, { key: 'line-eq', label: 'Rectas', icon: '📏' }, { key: 'plane-eq', label: 'Planos', icon: '📐' }, { key: 'matrix-ops', label: 'Matrices', icon: '🔲' }, { key: 'determinants', label: 'Determinantes', icon: 'det' }, { key: 'matrix-inverse', label: 'Inversa', icon: '🔄' }, { key: 'systems', label: 'Sistemas', icon: '⚖️' } ]; const CHAPTER_NAMES = { 1: 'Cap. 1 — Vectores, Recta y Plano', 2: 'Cap. 2 — Matrices y Determinantes', 3: 'Cap. 3 — Sistemas de Ecuaciones' }; async function loadExerciseData() { if (Object.keys(exerciseDataLoaded).length > 0) return exerciseDataLoaded; const chapters = [1, 2, 3]; const promises = chapters.map(async (ch) => { try { const resp = await fetch(`data/exercises-cap0${ch}.json`); exerciseDataLoaded[ch] = await resp.json(); } catch (e) { console.warn(`[ExerciseBrowser] Failed to load cap${ch}:`, e); exerciseDataLoaded[ch] = []; } }); await Promise.all(promises); exercises = Object.values(exerciseDataLoaded).flat(); return exerciseDataLoaded; } function getTopicCounts() { const counts = { all: exercises.length }; exercises.forEach(ex => { counts[ex.topic] = (counts[ex.topic] || 0) + 1; }); return counts; } function filterExercises() { filteredExercises = exercises.filter(ex => { const topicMatch = activeTopic === 'all' || ex.topic === activeTopic; if (!searchText) return topicMatch; const search = searchText.toLowerCase(); const textMatch = (ex.statement || '').toLowerCase().includes(search) || (ex.id || '').toLowerCase().includes(search) || (ex.subtopic || '').toLowerCase().includes(search); return topicMatch && textMatch; }); } function renderTopicTabs() { const counts = getTopicCounts(); return `
${TOPICS.map(t => { const isActive = t.key === activeTopic ? ' topic-tab--active' : ''; const count = counts[t.key] || 0; return ``; }).join('')}
`; } function renderExerciseCards() { if (filteredExercises.length === 0) { return `

No se encontraron ejercicios

`; } return `
${filteredExercises.map(ex => { const topicInfo = TOPICS.find(t => t.key === ex.topic) || { label: ex.topic }; const difficultyClass = `difficulty--${ex.difficulty || 'basic'}`; const diffLabel = { basic: 'Básico', intermediate: 'Intermedio', advanced: 'Avanzado' }; return `
${ex.id}
${topicInfo.icon || ''} ${topicInfo.label}
${ex.statement}
${diffLabel[ex.difficulty] || ex.difficulty}
`; }).join('')}
`; } function escapeAttr(str) { return (str || '').replace(/"/g, '"').replace(/'/g, '''); } function render() { if (!container) return; filterExercises(); container.innerHTML = `

Ejercicios

Seleccioná un tema para filtrar los ejercicios

${renderTopicTabs()} ${renderExerciseCards()}
`; // Skip KaTeX on cards — text+math mixing causes spacing loss in math mode. // Full KaTeX rendering happens in exercise-viewer when user opens the exercise. } function renderMathInElements() { // Disabled: KaTeX rendering on cards removed due to text spacing issues. // The statement is shown as plain text; full rendering in exercise-viewer. return; } function handleTopicClick(e) { const tab = e.target.closest('.topic-tab'); if (!tab) return; activeTopic = tab.dataset.topic; render(); } function handleCardClick(e) { const card = e.target.closest('.exercise-card'); if (!card) return; const id = card.dataset.exerciseId; if (id && typeof App !== 'undefined') { App.navigate(`/exercise/${id}`); } } function handleSearch(e) { clearTimeout(searchDebounceTimer); searchDebounceTimer = setTimeout(() => { searchText = e.target.value; filterExercises(); // Re-render only the grid, not the tabs const grid = container.querySelector('.exercise-grid'); const empty = container.querySelector('.exercise-browser__empty'); if (grid || empty) { const parent = grid ? grid.parentElement : empty.parentElement; const targetEl = grid || empty; const temp = document.createElement('div'); temp.innerHTML = renderExerciseCards(); targetEl.replaceWith(temp.firstElementChild); renderMathInElements(); } }, 300); } function init(el) { container = el; ac = new AbortController(); loadExerciseData().then(() => { render(); container.addEventListener('click', handleTopicClick, { signal: ac.signal }); container.addEventListener('click', handleCardClick, { signal: ac.signal }); container.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { const card = e.target.closest('.exercise-card'); if (card) { e.preventDefault(); card.click(); } } }, { signal: ac.signal }); container.addEventListener('input', (e) => { if (e.target.classList.contains('exercise-browser__search')) { handleSearch(e); } }, { signal: ac.signal }); }); } function destroy() { if (ac) { ac.abort(); ac = null; } clearTimeout(searchDebounceTimer); if (container) { container.innerHTML = ''; } container = null; activeTopic = 'all'; searchText = ''; } function getExercises() { return exercises; } return { init, destroy, getExercises, loadExerciseData }; })();