- 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
251 lines
7.2 KiB
JavaScript
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
|
|
};
|
|
})();
|