feat(admin): phase 1 — hash-router

AdminRouter wraps existing switchTab for deep-linking.

- frontend/js/admin/router.js (new, 102L): parse/navigate/current/on/off, recursion guard via _navigating flag

- admin.html: +1 <script> before admin.js

- admin.js: switchTab(btn, opts) + initAdminRouter IIFE for hashchange dispatch

Backward compat: all 21 onclick=switchTab(this) callsites continue working.

F5 / back / forward / deep-link verified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-16 22:22:20 +03:00
parent 76e376ee04
commit 8a7bed487f
6 changed files with 219 additions and 14 deletions
+44 -1
View File
@@ -54,7 +54,7 @@
/* ─── Tabs ─── */
let questionsInited = false, testsInited = false, assignmentsInited = false, usersInited = false, sessionsInited = false, subjectsInited = false, permissionsInited = false, shopInited = false, gamInited = false, tplInited = false, simsInited = false, gamesInited = false, sublogInited = false;
function switchTab(btn) {
function switchTab(btn, opts) {
if (btn.classList.contains('locked')) {
LS.toast('Этот раздел доступен только администраторам', 'warn');
return;
@@ -77,6 +77,12 @@
if (name === 'sims' && !simsInited) { simsInited = true; loadSimsAdmin(); }
if (name === 'games' && !gamesInited) { gamesInited = true; loadGamesAdmin(); loadFsFeatures(); }
if (name === 'sublog' && !sublogInited) { sublogInited = true; loadSubmissionLog(); }
// Sync URL hash. `silent: true` only suppresses the router's own emit;
// a subsequent hashchange event will not re-fire switchTab thanks to
// the router's _navigating guard.
if (!(opts && opts.fromRouter) && window.AdminRouter) {
AdminRouter.navigate('#' + name, { silent: true });
}
}
/* Переход к вопросам конкретного предмета с открытием формы */
@@ -3546,3 +3552,40 @@
loadStats();
loadAvatarRequests(); // load badge count on page open
if (window.lucide) lucide.createIcons();
/* ─── Hash router wiring ─── */
(function initAdminRouter() {
if (!window.AdminRouter) return;
function activate(route, opts) {
const name = route || 'stats';
const btn = document.querySelector('.admin-nav-item[data-tab="' + name + '"]');
if (!btn) {
console.warn('AdminRouter: unknown route', name);
AdminRouter.navigate('#stats', { replace: true, silent: true });
const fallback = document.querySelector('.admin-nav-item[data-tab="stats"]');
if (fallback) switchTab(fallback, { fromRouter: true });
return;
}
// Skip locked tabs (non-admin clicked an admin-only deep-link).
if (btn.classList.contains('locked')) {
LS.toast('Этот раздел доступен только администраторам', 'warn');
AdminRouter.navigate('#stats', { replace: true, silent: true });
const fallback = document.querySelector('.admin-nav-item[data-tab="stats"]');
if (fallback) switchTab(fallback, { fromRouter: true });
return;
}
switchTab(btn, { fromRouter: true });
}
AdminRouter.on('change', (r) => activate(r.route));
// Initial dispatch: respect existing hash, else default to #stats.
const initial = AdminRouter.current();
if (!initial.route) {
AdminRouter.navigate('#stats', { replace: true, silent: true });
// #stats tab-pane is already .active in markup; no switchTab needed.
} else if (initial.route !== 'stats') {
activate(initial.route);
}
})();