/** * App — Hash router, component lifecycle, navigation, event bus */ const App = (() => { let currentComponent = null; let appEl = null; const routes = [ { pattern: /^\/$/, handler: showHome }, { pattern: /^\/exercises$/, handler: showExercises }, { pattern: /^\/exercise\/(.+)$/, handler: showExercise }, { pattern: /^\/workspace$/, handler: showWorkspace }, { pattern: /^\/workspace\/matrix$/, handler: showMatrixBuilder }, { pattern: /^\/workspace\/system$/, handler: showSystemSolver } ]; // ── Event Bus ── const bus = {}; function on(event, fn) { (bus[event] = bus[event] || []).push(fn); } function emit(event, data) { (bus[event] || []).forEach(fn => fn(data)); } // ── Theme ── function initTheme() { const saved = localStorage.getItem('algebra-theme'); if (saved === 'dark') { document.documentElement.setAttribute('data-theme', 'dark'); } } function toggleTheme() { const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; if (isDark) { document.documentElement.removeAttribute('data-theme'); localStorage.setItem('algebra-theme', 'light'); } else { document.documentElement.setAttribute('data-theme', 'dark'); localStorage.setItem('algebra-theme', 'dark'); } } // ── Navigation ── function navigate(path) { window.location.hash = '#' + path; } function getHash() { return window.location.hash.slice(1) || '/'; } function matchRoute(hash) { for (const route of routes) { const match = hash.match(route.pattern); if (match) { return { handler: route.handler, params: match.slice(1) }; } } return { handler: showHome, params: [] }; } function updateActiveNav() { const hash = getHash(); document.querySelectorAll('.navbar__link').forEach(link => { const route = link.getAttribute('data-route'); link.classList.remove('navbar__link--active'); if (route === 'home' && (hash === '/' || hash === '')) { link.classList.add('navbar__link--active'); } else if (route === 'exercises' && hash.startsWith('/exercise')) { link.classList.add('navbar__link--active'); } else if (route === 'workspace' && hash.startsWith('/workspace')) { link.classList.add('navbar__link--active'); } }); } // ── Component Lifecycle ── function destroyCurrent() { if (currentComponent && typeof currentComponent.destroy === 'function') { currentComponent.destroy(); } currentComponent = null; if (appEl) appEl.innerHTML = ''; } function mountComponent(componentObj, initArgs) { destroyCurrent(); currentComponent = componentObj; componentObj.init(appEl, initArgs); } // ── Page Handlers ── function showHome() { destroyCurrent(); appEl.innerHTML = `

Álgebra Lineal Interactivo

Practicá los ejercicios de Vectores, Recta, Plano, Matrices, Determinantes y Sistemas de Ecuaciones. Paso a paso, con verificación automática.

📚
Ejercicios
68 ejercicios de los capítulos 1 a 3 con solución paso a paso
🧮
Taller
Constructor de matrices y resolvedor de sistemas interactivos
`; // Click handlers for home cards appEl.querySelectorAll('.home-card[data-nav]').forEach(card => { card.addEventListener('click', () => navigate(card.dataset.nav)); }); } function showExercises() { mountComponent(ExerciseBrowser); } function showExercise(params) { mountComponent(ExerciseViewer, { id: params[0] }); } function showWorkspace() { destroyCurrent(); appEl.innerHTML = `

Taller de Cálculo

Operá con matrices y resolvé sistemas de ecuaciones

`; setupWorkspaceTabs(); // Default to matrix const wsContent = document.getElementById('workspaceContent'); MatrixBuilder.init(wsContent); currentComponent = MatrixBuilder; } function setupWorkspaceTabs() { const tabs = appEl.querySelectorAll('.workspace__tab'); tabs.forEach(tab => { tab.addEventListener('click', () => { tabs.forEach(t => t.classList.remove('workspace__tab--active')); tab.classList.add('workspace__tab--active'); const ws = tab.dataset.ws; const wsContent = document.getElementById('workspaceContent'); if (!wsContent) return; if (currentComponent && typeof currentComponent.destroy === 'function') { currentComponent.destroy(); } wsContent.innerHTML = ''; if (ws === 'matrix') { MatrixBuilder.init(wsContent); currentComponent = MatrixBuilder; } else if (ws === 'system') { SystemSolver.init(wsContent); currentComponent = SystemSolver; } }); }); } function showMatrixBuilder() { navigate('/workspace'); } function showSystemSolver() { navigate('/workspace'); } // ── Router ── function handleRoute() { const hash = getHash(); const { handler, params } = matchRoute(hash); updateActiveNav(); // Fade out if (appEl) { appEl.style.opacity = '0'; appEl.style.transition = 'opacity 150ms ease-out'; } setTimeout(() => { handler(params); emit('routechange', hash); // Fade in if (appEl) { appEl.style.opacity = '1'; } }, 150); } // ── Init ── function init() { appEl = document.getElementById('app'); if (!appEl) { console.error('[App] #app element not found'); return; } initTheme(); // Init particles background if (typeof Particles !== 'undefined' && Particles.init) { Particles.init(); } // Theme toggle const themeBtn = document.getElementById('themeToggle'); if (themeBtn) { themeBtn.addEventListener('click', toggleTheme); } // Hash change listener window.addEventListener('hashchange', handleRoute); // Initial route handleRoute(); } // Boot if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } return { navigate, on, emit, getCurrentComponent: () => currentComponent }; })();