diff --git a/frontend/app.js b/frontend/app.js index 23c2546..9224335 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -63,6 +63,7 @@ async function init() { } function route() { + isRouting = true; const hash = location.hash.slice(1) || 'library'; if (hash.startsWith('protocols/')) { showDetail(hash.split('/')[1]); @@ -81,14 +82,15 @@ function route() { } else { navigate('library'); } + isRouting = false; } let searchTimer = null; let currentRoute = ''; +let isRouting = false; function navigate(view) { - // Prevent double navigation from hashchange - if (currentRoute === view) return; + if (currentRoute === view && view !== 'library') return; currentRoute = view; document.querySelectorAll('.view').forEach(v => v.style.display = 'none'); document.getElementById('view-' + view).style.display = 'block'; @@ -96,11 +98,15 @@ function navigate(view) { document.querySelectorAll('.mobile-panel a').forEach(a => a.classList.toggle('active', a.dataset.view === view)); closeMenu(); updateBreadcrumb(view); - if (view === 'library') { location.hash = 'library'; loadProtocols(); } - if (view === 'collections') { location.hash = 'collections'; loadCollections(); } + if (!isRouting) { + 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-collection') { editingCollectionSlug = null; resetCollectionForm(); } - if (view === 'about') { location.hash = 'about'; } window.scrollTo(0, 0); } @@ -313,7 +319,7 @@ function renderProtocolGrid(protocols) { // --- Detail --- 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.getElementById('view-detail').style.display = 'block'; document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active')); @@ -564,10 +570,11 @@ async function loadCollections() { return; } grid.innerHTML = collections.map(function(c) { - var sourceLabel = c.author ? '@' + c.author : (c.source || ''); + var sourceLabel = c.source || ''; return '
' + '

' + escapeHtml(c.title) + '

' + escapeHtml(c.description || '') + '

' + - '
' + (c.item_count || 0) + ' protocols
' + escapeHtml(sourceLabel) + '
'; + '
' + (c.item_count || 0) + ' protocols
' + + (sourceLabel ? '
' + escapeHtml(sourceLabel) + '
' : '') + '
'; }).join(''); } catch (e) { document.getElementById('collectionsGrid').innerHTML = '
' + escapeHtml(e.message) + '
'; @@ -713,7 +720,7 @@ async function saveCollection(e) { } 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.getElementById('view-collection-detail').style.display = 'block'; document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active')); @@ -739,8 +746,8 @@ async function showCollectionDetail(slug) { } } } - var sourceLine = c.source ? 'SOURCE: ' + escapeHtml(c.source) : ''; - var authorLine = c.author ? ' · authored by @' + escapeHtml(c.author) + '' : ''; + var sourceLine = (c.source && c.source !== '@' + (c.author || '')) ? 'SOURCE: ' + escapeHtml(c.source) : ''; + var authorLine = c.author ? (sourceLine ? ' · ' : '') + 'curated by @' + escapeHtml(c.author) + '' : ''; var dateLine = c.created_at ? ' · added ' + formatDate(c.created_at) : ''; document.getElementById('collectionDetailContent').innerHTML = @@ -786,6 +793,7 @@ async function deleteCollection(slug) { // --- User Profile --- async function showUser(username) { + if (!isRouting && location.hash !== '#users/' + username) location.hash = 'users/' + username; document.querySelectorAll('.view').forEach(v => v.style.display = 'none'); document.getElementById('view-detail').style.display = 'block'; document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active'));