fix: remove underline on protocol links, fix back button navigation

- Add text-decoration:none and color:inherit to .protocol-pill so
  <a> tags don't show underlines or link colors
- Guard hash setting in showDetail/showCollectionDetail/showUser:
  only set location.hash if it's not already the target, preventing
  the redundant hashchange that was breaking the back button
This commit is contained in:
Protocolbot
2026-07-08 13:55:10 -06:00
parent e514e8aca3
commit 2278dae455
2 changed files with 17 additions and 60 deletions
+14 -44
View File
@@ -300,12 +300,12 @@ function renderProtocolGrid(protocols) {
grid.innerHTML = protocols.map(function(p) { grid.innerHTML = protocols.map(function(p) {
var sourceLabel = p.source || (p.author ? '@' + p.author : ''); var sourceLabel = p.source || (p.author ? '@' + p.author : '');
var tags = (p.tags || []).map(function(t) { return '<span class="tag">' + escapeHtml(t) + '</span>'; }).join(''); var tags = (p.tags || []).map(function(t) { return '<span class="tag">' + escapeHtml(t) + '</span>'; }).join('');
return '<a class="protocol-pill" href="#protocols/' + p.slug + '">' + return '<div class="protocol-pill" onclick="showDetail(\'' + p.slug + '\')">' +
'<div class="info"><h3>' + escapeHtml(p.title) + '</h3>' + '<div class="info"><h3>' + escapeHtml(p.title) + '</h3>' +
'<p>' + escapeHtml(p.description || '') + '</p>' + '<p>' + escapeHtml(p.description || '') + '</p>' +
(tags ? '<div class="tag-row">' + tags + '</div>' : '') + '</div>' + (tags ? '<div class="tag-row">' + tags + '</div>' : '') + '</div>' +
'<div class="meta"><div class="steps">' + (p.steps || []).length + ' steps</div>' + '<div class="meta"><div class="steps">' + (p.steps || []).length + ' steps</div>' +
'<div>' + escapeHtml(sourceLabel) + '</div></div></a>'; '<div>' + escapeHtml(sourceLabel) + '</div></div></div>';
}).join(''); }).join('');
} }
@@ -313,7 +313,7 @@ function renderProtocolGrid(protocols) {
// --- Detail --- // --- Detail ---
async function showDetail(slug) { async function showDetail(slug) {
location.hash = 'protocols/' + slug; if (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'));
@@ -410,7 +410,6 @@ async function deleteProtocol(slug) {
try { try {
await api('/protocols/' + slug, { method: 'DELETE' }); await api('/protocols/' + slug, { method: 'DELETE' });
toast('Protocol deleted'); toast('Protocol deleted');
currentRoute = '';
navigate('library'); navigate('library');
} catch (e) { toast(e.message, true); } } catch (e) { toast(e.message, true); }
} }
@@ -566,9 +565,9 @@ async function loadCollections() {
} }
grid.innerHTML = collections.map(function(c) { grid.innerHTML = collections.map(function(c) {
var sourceLabel = c.author ? '@' + c.author : (c.source || ''); var sourceLabel = c.author ? '@' + c.author : (c.source || '');
return '<a class="protocol-pill collection" href="#collections/' + 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></a>'; '<div class="meta"><div class="steps">' + (c.item_count || 0) + ' protocols</div><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>';
@@ -714,7 +713,7 @@ async function saveCollection(e) {
} }
async function showCollectionDetail(slug) { async function showCollectionDetail(slug) {
location.hash = 'collections/' + slug; if (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'));
@@ -730,11 +729,11 @@ async function showCollectionDetail(slug) {
if (item.type === 'protocol') { if (item.type === 'protocol') {
try { try {
var p = await api('/protocols/' + item.slug); var p = await api('/protocols/' + item.slug);
itemsHtml += '<a class="protocol-pill" href="#protocols/' + p.slug + '">' + itemsHtml += '<div class="protocol-pill" onclick="showDetail(\'' + p.slug + '\')">' +
'<div class="info"><h3>' + escapeHtml(p.title) + '</h3>' + '<div class="info"><h3>' + escapeHtml(p.title) + '</h3>' +
'<p>' + escapeHtml(p.description || '') + '</p>' + '<p>' + escapeHtml(p.description || '') + '</p>' +
(p.tags && p.tags.length ? '<div class="tag-row">' + p.tags.map(function(t) { return '<span class="tag">' + escapeHtml(t) + '</span>'; }).join('') + '</div>' : '') + '</div>' + (p.tags && p.tags.length ? '<div class="tag-row">' + p.tags.map(function(t) { return '<span class="tag">' + escapeHtml(t) + '</span>'; }).join('') + '</div>' : '') + '</div>' +
'<div class="meta"><div class="steps">' + (p.steps || []).length + ' steps</div></div></a>'; '<div class="meta"><div class="steps">' + (p.steps || []).length + ' steps</div></div></div>';
} catch (e2) { } catch (e2) {
itemsHtml += '<div class="protocol-pill"><div class="info"><h3>' + escapeHtml(item.slug) + '</h3><p>Protocol not found</p></div></div>'; itemsHtml += '<div class="protocol-pill"><div class="info"><h3>' + escapeHtml(item.slug) + '</h3><p>Protocol not found</p></div></div>';
} }
@@ -781,13 +780,13 @@ async function deleteCollection(slug) {
try { try {
await api('/collections/' + slug, { method: 'DELETE' }); await api('/collections/' + slug, { method: 'DELETE' });
toast('Collection deleted'); toast('Collection deleted');
currentRoute = '';
navigate('collections'); navigate('collections');
} catch (e) { toast(e.message, true); } } catch (e) { toast(e.message, true); }
} }
// --- User Profile --- // --- User Profile ---
async function showUser(username) { async function showUser(username) {
if (location.hash !== '#users/' + username && location.hash !== '#user/' + 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'));
@@ -804,54 +803,25 @@ async function showUser(username) {
(data.user.bio ? '<div class="bio">' + escapeHtml(data.user.bio) + '</div>' : '') + '</div>' + (data.user.bio ? '<div class="bio">' + escapeHtml(data.user.bio) + '</div>' : '') + '</div>' +
(p.length ? '<div class="detail-section"><h2>Protocols (' + p.length + ')</h2>' + (p.length ? '<div class="detail-section"><h2>Protocols (' + p.length + ')</h2>' +
p.map(function(proto) { p.map(function(proto) {
return '<a class="protocol-pill" href="#protocols/' + proto.slug + '">' + return '<div class="protocol-pill" onclick="showDetail(\'' + proto.slug + '\')">' +
'<div class="info"><h3>' + escapeHtml(proto.title) + '</h3>' + '<div class="info"><h3>' + escapeHtml(proto.title) + '</h3>' +
'<p>' + escapeHtml(proto.description||'') + '</p>' + '<p>' + escapeHtml(proto.description||'') + '</p>' +
(proto.tags && proto.tags.length ? '<div class="tag-row">' + proto.tags.map(function(t) { return '<span class="tag">' + escapeHtml(t) + '</span>'; }).join('') + '</div>' : '') + '</div>' + (proto.tags && proto.tags.length ? '<div class="tag-row">' + proto.tags.map(function(t) { return '<span class="tag">' + escapeHtml(t) + '</span>'; }).join('') + '</div>' : '') + '</div>' +
'<div class="meta"><div class="steps">' + (proto.steps || []).length + ' steps</div></div></a>'; '<div class="meta"><div class="steps">' + (proto.steps || []).length + ' steps</div></div></div>';
}).join('') + }).join('') +
'</div>' : '') + '</div>' : '') +
(c.length ? '<div class="detail-section"><h2>Collections (' + c.length + ')</h2>' + (c.length ? '<div class="detail-section"><h2>Collections (' + c.length + ')</h2>' +
c.map(function(col) { c.map(function(col) {
return '<a class="protocol-pill collection" href="#collections/' + col.slug + '">' + return '<div class="protocol-pill collection" onclick="showCollectionDetail(\'' + col.slug + '\')"><div class="icon">[+]</div>' +
'<div class="info"><h3>' + escapeHtml(col.title) + '</h3><p>' + escapeHtml(col.description||'') + '</p></div>' + '<div class="info"><h3>' + escapeHtml(col.title) + '</h3><p>' + escapeHtml(col.description||'') + '</p></div></div>';
'<div class="meta"><div class="steps">' + (col.item_count || 0) + ' protocols</div></div></a>';
}).join('') + }).join('') +
'</div>' : ''); '</div>' : '');
// Add password change option if viewing own profile document.getElementById('detailActions').innerHTML = '';
var actionsHtml = '';
if (currentUser && currentUser.username === username) {
actionsHtml = '<button class="btn" onclick="showChangePassword()">Change Password</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);
}
}
// --- Toast --- // --- Toast ---
let toastTimer = null; let toastTimer = null;
function toast(msg, isError) { function toast(msg, isError) {
+2 -15
View File
@@ -232,8 +232,9 @@ header nav a.active { color: var(--accent-bright); border-color: rgba(0,170,255,
background: var(--surface); border: 1px solid var(--border-light); background: var(--surface); border: 1px solid var(--border-light);
border-radius: 16px; padding: 14px 20px; display: flex; align-items: flex-start; border-radius: 16px; padding: 14px 20px; display: flex; align-items: flex-start;
gap: 14px; cursor: pointer; transition: all 0.15s; box-shadow: var(--shadow); gap: 14px; cursor: pointer; transition: all 0.15s; box-shadow: var(--shadow);
text-decoration: none; color: inherit;
} }
.protocol-pill:hover { border-color: var(--accent); box-shadow: var(--shadow-hover); transform: translateX(2px); } .protocol-pill:hover { border-color: var(--accent); box-shadow: var(--shadow-hover); transform: translateX(2px); text-decoration: none; }
.protocol-pill .icon { .protocol-pill .icon {
width: 38px; height: 38px; border-radius: 10px; background: var(--panel); width: 38px; height: 38px; border-radius: 10px; background: var(--panel);
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
@@ -528,17 +529,3 @@ header nav a.active { color: var(--accent-bright); border-color: rgba(0,170,255,
.detail-header, .detail-section { padding: 16px 14px; border-radius: 12px; } .detail-header, .detail-section { padding: 16px 14px; border-radius: 12px; }
} }
/* Footer */
.footer {
text-align: center;
padding: 16px;
font-family: var(--mono);
font-size: 11px;
color: var(--ink-dim);
}
.footer a {
color: var(--accent);
text-decoration: none;
}
.footer a:hover { text-decoration: underline; }