Files
algebra-webapp/js/app.js
renato97 45582e5f59 feat: interactive linear algebra practice web app
- 68 exercises from UBA FCE chapters 1-3
- Step-by-step solutions with KaTeX rendering
- Theory panels (26 topics) expandable per exercise
- Matrix builder (2x2/3x3/4x4) with 7 operations
- System solver (Gauss, Gauss-Jordan, Cramer, Rouché-Frobenius)
- Glassmorphism UI with dark mode
- Canvas particle background
- ARIA accessibility (keyboard nav, screen reader)
- Zero build step - open index.html directly
2026-05-20 01:26:40 -03:00

251 lines
7.2 KiB
JavaScript

/**
* 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 = `
<div class="home-page">
<div class="home-page__hero">
<h1>Álgebra Lineal Interactivo</h1>
<p>Practicá los ejercicios de Vectores, Recta, Plano, Matrices, Determinantes y Sistemas de Ecuaciones. Paso a paso, con verificación automática.</p>
</div>
<div class="home-page__cards">
<div class="home-card" data-nav="/exercises">
<div class="home-card__icon">📚</div>
<div class="home-card__title">Ejercicios</div>
<div class="home-card__desc">68 ejercicios de los capítulos 1 a 3 con solución paso a paso</div>
</div>
<div class="home-card" data-nav="/workspace">
<div class="home-card__icon">🧮</div>
<div class="home-card__title">Taller</div>
<div class="home-card__desc">Constructor de matrices y resolvedor de sistemas interactivos</div>
</div>
</div>
</div>
`;
// 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 = `
<div class="workspace">
<div class="workspace__header">
<h2>Taller de Cálculo</h2>
<p>Operá con matrices y resolvé sistemas de ecuaciones</p>
</div>
<div class="workspace__tabs">
<button class="workspace__tab workspace__tab--active" data-ws="matrix">Constructor de Matrices</button>
<button class="workspace__tab" data-ws="system">Resolvedor de Sistemas</button>
</div>
<div class="workspace__content" id="workspaceContent"></div>
</div>
`;
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
};
})();