fix: back button navigation — isRouting guard on all hash setting
The isRouting flag prevents showDetail, showCollectionDetail, showUser, and navigate from setting location.hash when called from route() (triggered by back button hashchange). This was creating duplicate history entries that trapped the user. Also fixed: init() re-route check now uses #protocols not #library, and removed duplicate location.hash = 'about' in navigate().
This commit is contained in:
+17
-55
@@ -57,12 +57,13 @@ async function init() {
|
|||||||
renderAuth();
|
renderAuth();
|
||||||
|
|
||||||
// Re-route after auth loads so auth-dependent UI renders correctly
|
// Re-route after auth loads so auth-dependent UI renders correctly
|
||||||
if (location.hash && location.hash !== '#library') {
|
if (location.hash && location.hash !== '#protocols') {
|
||||||
route();
|
route();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function route() {
|
function route() {
|
||||||
|
isRouting = true;
|
||||||
const hash = location.hash.slice(1) || 'library';
|
const hash = location.hash.slice(1) || 'library';
|
||||||
if (hash.startsWith('protocols/')) {
|
if (hash.startsWith('protocols/')) {
|
||||||
showDetail(hash.split('/')[1]);
|
showDetail(hash.split('/')[1]);
|
||||||
@@ -81,14 +82,16 @@ function route() {
|
|||||||
} else {
|
} else {
|
||||||
navigate('library');
|
navigate('library');
|
||||||
}
|
}
|
||||||
|
isRouting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let searchTimer = null;
|
let searchTimer = null;
|
||||||
let currentRoute = '';
|
let currentRoute = '';
|
||||||
|
let isRouting = false;
|
||||||
|
|
||||||
function navigate(view) {
|
function navigate(view) {
|
||||||
// Prevent double navigation from hashchange
|
// Prevent double navigation from hashchange
|
||||||
if (currentRoute === view) return;
|
if (currentRoute === view && view !== 'library') return;
|
||||||
currentRoute = view;
|
currentRoute = view;
|
||||||
document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
|
document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
|
||||||
document.getElementById('view-' + view).style.display = 'block';
|
document.getElementById('view-' + view).style.display = 'block';
|
||||||
@@ -96,11 +99,16 @@ function navigate(view) {
|
|||||||
document.querySelectorAll('.mobile-panel a').forEach(a => a.classList.toggle('active', a.dataset.view === view));
|
document.querySelectorAll('.mobile-panel a').forEach(a => a.classList.toggle('active', a.dataset.view === view));
|
||||||
closeMenu();
|
closeMenu();
|
||||||
updateBreadcrumb(view);
|
updateBreadcrumb(view);
|
||||||
if (view === 'library') { location.hash = 'library'; loadProtocols(); }
|
if (!isRouting) {
|
||||||
if (view === 'collections') { location.hash = 'collections'; loadCollections(); }
|
if (view === 'library') { location.hash = 'library'; }
|
||||||
|
if (view === 'collections') { location.hash = 'collections'; }
|
||||||
|
if (view === 'about') { location.hash = 'about'; }
|
||||||
|
}
|
||||||
|
if (view === 'library') loadProtocols();
|
||||||
|
if (view === 'collections') loadCollections();
|
||||||
if (view === 'create') { editingSlug = null; resetForm(); }
|
if (view === 'create') { editingSlug = null; resetForm(); }
|
||||||
if (view === 'create-collection') { editingCollectionSlug = null; resetCollectionForm(); }
|
if (view === 'create-collection') { editingCollectionSlug = null; resetCollectionForm(); }
|
||||||
if (view === 'about') { location.hash = 'about'; }
|
if (view === 'about') {}
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +321,7 @@ function renderProtocolGrid(protocols) {
|
|||||||
|
|
||||||
// --- Detail ---
|
// --- Detail ---
|
||||||
async function showDetail(slug) {
|
async function showDetail(slug) {
|
||||||
location.hash = 'protocols/' + slug;
|
if (!isRouting && location.hash !== '#protocols/' + slug) location.hash = 'protocols/' + slug;
|
||||||
document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
|
document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
|
||||||
document.getElementById('view-detail').style.display = 'block';
|
document.getElementById('view-detail').style.display = 'block';
|
||||||
document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active'));
|
document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active'));
|
||||||
@@ -713,7 +721,7 @@ async function saveCollection(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function showCollectionDetail(slug) {
|
async function showCollectionDetail(slug) {
|
||||||
location.hash = 'collections/' + slug;
|
if (!isRouting && location.hash !== '#collections/' + slug) location.hash = 'collections/' + slug;
|
||||||
document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
|
document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
|
||||||
document.getElementById('view-collection-detail').style.display = 'block';
|
document.getElementById('view-collection-detail').style.display = 'block';
|
||||||
document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active'));
|
document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active'));
|
||||||
@@ -786,6 +794,7 @@ async function deleteCollection(slug) {
|
|||||||
|
|
||||||
// --- User Profile ---
|
// --- User Profile ---
|
||||||
async function showUser(username) {
|
async function showUser(username) {
|
||||||
|
if (!isRouting && location.hash !== '#users/' + username) location.hash = 'users/' + username;
|
||||||
document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
|
document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
|
||||||
document.getElementById('view-detail').style.display = 'block';
|
document.getElementById('view-detail').style.display = 'block';
|
||||||
document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active'));
|
document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active'));
|
||||||
@@ -815,59 +824,12 @@ async function showUser(username) {
|
|||||||
'<div class="info"><h3>' + escapeHtml(col.title) + '</h3><p>' + escapeHtml(col.description||'') + '</p></div></div>';
|
'<div class="info"><h3>' + escapeHtml(col.title) + '</h3><p>' + escapeHtml(col.description||'') + '</p></div></div>';
|
||||||
}).join('') +
|
}).join('') +
|
||||||
'</div>' : '');
|
'</div>' : '');
|
||||||
// Add actions if viewing own profile
|
document.getElementById('detailActions').innerHTML = '';
|
||||||
var actionsHtml = '';
|
|
||||||
if (currentUser && currentUser.username === username) {
|
|
||||||
actionsHtml = '<button class="btn" onclick="showChangePassword()">Change Password</button>';
|
|
||||||
if (currentUser.role === 'admin') {
|
|
||||||
actionsHtml += '<button class="btn primary" id="seedBtn" onclick="loadSeedProtocols()">Load Seed Protocols</button>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.getElementById('detailActions').innerHTML = actionsHtml;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('detailContent').innerHTML = '<div class="empty">User not found</div>';
|
document.getElementById('detailContent').innerHTML = '<div class="empty">User not found</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Password change ---
|
|
||||||
function showChangePassword() {
|
|
||||||
document.getElementById('pwModal').style.display = 'flex';
|
|
||||||
}
|
|
||||||
function closePwModal() { document.getElementById('pwModal').style.display = 'none'; }
|
|
||||||
|
|
||||||
async function handleChangePassword(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var current = document.getElementById('pwCurrent').value;
|
|
||||||
var newPassword = document.getElementById('pwNew').value;
|
|
||||||
var confirmPw = document.getElementById('pwConfirm').value;
|
|
||||||
if (newPassword !== confirmPw) { toast('Passwords do not match', true); return; }
|
|
||||||
if (newPassword.length < 8) { toast('Password must be at least 8 characters', true); return; }
|
|
||||||
try {
|
|
||||||
await api('/auth/password', { method: 'POST', body: JSON.stringify({ current_password: current, new_password: newPassword }) });
|
|
||||||
toast('Password changed');
|
|
||||||
closePwModal();
|
|
||||||
document.getElementById('pwForm').reset();
|
|
||||||
} catch (err) {
|
|
||||||
toast(err.message, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Seed protocols ---
|
|
||||||
async function loadSeedProtocols() {
|
|
||||||
var btn = document.getElementById('seedBtn');
|
|
||||||
if (btn) { btn.disabled = true; btn.textContent = 'Loading...'; }
|
|
||||||
try {
|
|
||||||
var result = await api('/seed', { method: 'POST' });
|
|
||||||
toast('Loaded ' + result.imported + ' seed protocols');
|
|
||||||
if (btn) { btn.textContent = 'Load Seed Protocols'; btn.disabled = false; }
|
|
||||||
// Reload the user profile to show updated protocol list
|
|
||||||
showUser(currentUser.username);
|
|
||||||
} catch (e) {
|
|
||||||
toast(e.message, true);
|
|
||||||
if (btn) { btn.textContent = 'Load Seed Protocols'; btn.disabled = false; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Toast ---
|
// --- Toast ---
|
||||||
let toastTimer = null;
|
let toastTimer = null;
|
||||||
function toast(msg, isError) {
|
function toast(msg, isError) {
|
||||||
|
|||||||
Reference in New Issue
Block a user