fix: collection source suppression + back button + home click

Collection source:
- Collection list: don't default to @author as source label
- Collection detail: suppress SOURCE when it equals @author
- Collection detail: 'curated by' instead of 'authored by'
- No source shown when field is empty

Back button:
- isRouting guard on route() prevents showDetail, showCollectionDetail,
  showUser, and navigate from setting location.hash when called from
  a hashchange event (back button). This prevents duplicate history
  entries that trap the user.

Home click:
- navigate('library') no longer blocked by currentRoute guard
  (view !== 'library' exception), so clicking the site name always
  returns to the homepage.
This commit is contained in:
Protocolbot
2026-07-13 11:45:48 -06:00
parent 9135e4c58e
commit b2a17eb2b1
+19 -11
View File
@@ -63,6 +63,7 @@ async function init() {
} }
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,15 @@ 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 if (currentRoute === view && view !== 'library') return;
if (currentRoute === view) 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 +98,15 @@ 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'; }
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
@@ -313,7 +319,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'));
@@ -564,10 +570,11 @@ async function loadCollections() {
return; return;
} }
grid.innerHTML = collections.map(function(c) { grid.innerHTML = collections.map(function(c) {
var sourceLabel = c.author ? '@' + c.author : (c.source || ''); var sourceLabel = c.source || '';
return '<div class="protocol-pill collection" onclick="showCollectionDetail(\'' + c.slug + '\')">' + return '<div class="protocol-pill collection" onclick="showCollectionDetail(\'' + c.slug + '\')">' +
'<div class="info"><h3>' + escapeHtml(c.title) + '</h3><p>' + escapeHtml(c.description || '') + '</p></div>' + '<div class="info"><h3>' + escapeHtml(c.title) + '</h3><p>' + escapeHtml(c.description || '') + '</p></div>' +
'<div class="meta"><div class="steps">' + (c.item_count || 0) + ' protocols</div><div>' + escapeHtml(sourceLabel) + '</div></div></div>'; '<div class="meta"><div class="steps">' + (c.item_count || 0) + ' protocols</div>' +
(sourceLabel ? '<div>' + escapeHtml(sourceLabel) + '</div>' : '') + '</div></div>';
}).join(''); }).join('');
} catch (e) { } catch (e) {
document.getElementById('collectionsGrid').innerHTML = '<div class="empty">' + escapeHtml(e.message) + '</div>'; document.getElementById('collectionsGrid').innerHTML = '<div class="empty">' + escapeHtml(e.message) + '</div>';
@@ -713,7 +720,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'));
@@ -739,8 +746,8 @@ async function showCollectionDetail(slug) {
} }
} }
} }
var sourceLine = c.source ? 'SOURCE: ' + escapeHtml(c.source) : ''; var sourceLine = (c.source && c.source !== '@' + (c.author || '')) ? 'SOURCE: ' + escapeHtml(c.source) : '';
var authorLine = c.author ? ' · authored by <a href="#user/' + c.author + '" onclick="showUser(\'' + c.author + '\');return false">@' + escapeHtml(c.author) + '</a>' : ''; var authorLine = c.author ? (sourceLine ? ' · ' : '') + 'curated by <a href="#users/' + c.author + '" onclick="showUser(\'' + c.author + '\');return false">@' + escapeHtml(c.author) + '</a>' : '';
var dateLine = c.created_at ? ' · added ' + formatDate(c.created_at) : ''; var dateLine = c.created_at ? ' · added ' + formatDate(c.created_at) : '';
document.getElementById('collectionDetailContent').innerHTML = document.getElementById('collectionDetailContent').innerHTML =
@@ -786,6 +793,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'));