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() {
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 '<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="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('');
} catch (e) {
document.getElementById('collectionsGrid').innerHTML = '<div class="empty">' + escapeHtml(e.message) + '</div>';
@@ -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 <a href="#user/' + c.author + '" onclick="showUser(\'' + c.author + '\');return false">@' + escapeHtml(c.author) + '</a>' : '';
var sourceLine = (c.source && c.source !== '@' + (c.author || '')) ? 'SOURCE: ' + escapeHtml(c.source) : '';
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) : '';
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'));