From 2278dae455f049e873a04a8c7657e146a14af830 Mon Sep 17 00:00:00 2001 From: Protocolbot Date: Wed, 8 Jul 2026 13:55:10 -0600 Subject: [PATCH] fix: remove underline on protocol links, fix back button navigation - Add text-decoration:none and color:inherit to .protocol-pill so 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 --- frontend/app.js | 58 +++++++++++----------------------------------- frontend/style.css | 19 +++------------ 2 files changed, 17 insertions(+), 60 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index 21955e6..a9e58c9 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -300,12 +300,12 @@ function renderProtocolGrid(protocols) { grid.innerHTML = protocols.map(function(p) { var sourceLabel = p.source || (p.author ? '@' + p.author : ''); var tags = (p.tags || []).map(function(t) { return '' + escapeHtml(t) + ''; }).join(''); - return '' + + return '
' + '

' + escapeHtml(p.title) + '

' + '

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

' + (tags ? '
' + tags + '
' : '') + '
' + '
' + (p.steps || []).length + ' steps
' + - '
' + escapeHtml(sourceLabel) + '
'; + '
' + escapeHtml(sourceLabel) + '
'; }).join(''); } @@ -313,7 +313,7 @@ function renderProtocolGrid(protocols) { // --- Detail --- 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.getElementById('view-detail').style.display = 'block'; document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active')); @@ -410,7 +410,6 @@ async function deleteProtocol(slug) { try { await api('/protocols/' + slug, { method: 'DELETE' }); toast('Protocol deleted'); - currentRoute = ''; navigate('library'); } catch (e) { toast(e.message, true); } } @@ -566,9 +565,9 @@ async function loadCollections() { } grid.innerHTML = collections.map(function(c) { var sourceLabel = c.author ? '@' + c.author : (c.source || ''); - return '' + + return '
' + '

' + escapeHtml(c.title) + '

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

' + - '
' + (c.item_count || 0) + ' protocols
' + escapeHtml(sourceLabel) + '
'; + '
' + (c.item_count || 0) + ' protocols
' + escapeHtml(sourceLabel) + '
'; }).join(''); } catch (e) { document.getElementById('collectionsGrid').innerHTML = '
' + escapeHtml(e.message) + '
'; @@ -714,7 +713,7 @@ async function saveCollection(e) { } 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.getElementById('view-collection-detail').style.display = 'block'; document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active')); @@ -730,11 +729,11 @@ async function showCollectionDetail(slug) { if (item.type === 'protocol') { try { var p = await api('/protocols/' + item.slug); - itemsHtml += '' + + itemsHtml += '
' + '

' + escapeHtml(p.title) + '

' + '

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

' + (p.tags && p.tags.length ? '
' + p.tags.map(function(t) { return '' + escapeHtml(t) + ''; }).join('') + '
' : '') + '
' + - '
' + (p.steps || []).length + ' steps
'; + '
' + (p.steps || []).length + ' steps
'; } catch (e2) { itemsHtml += '

' + escapeHtml(item.slug) + '

Protocol not found

'; } @@ -781,13 +780,13 @@ async function deleteCollection(slug) { try { await api('/collections/' + slug, { method: 'DELETE' }); toast('Collection deleted'); - currentRoute = ''; navigate('collections'); } catch (e) { toast(e.message, true); } } // --- User Profile --- 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.getElementById('view-detail').style.display = 'block'; document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active')); @@ -804,54 +803,25 @@ async function showUser(username) { (data.user.bio ? '
' + escapeHtml(data.user.bio) + '
' : '') + '' + (p.length ? '

Protocols (' + p.length + ')

' + p.map(function(proto) { - return '' + + return ''; }).join('') + '
' : '') + (c.length ? '

Collections (' + c.length + ')

' + c.map(function(col) { - return '' + - '

' + escapeHtml(col.title) + '

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

' + - '
' + (col.item_count || 0) + ' protocols
'; + return '
[+]
' + + '

' + escapeHtml(col.title) + '

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

'; }).join('') + '
' : ''); - // Add password change option if viewing own profile - var actionsHtml = ''; - if (currentUser && currentUser.username === username) { - actionsHtml = ''; - } - document.getElementById('detailActions').innerHTML = actionsHtml; + document.getElementById('detailActions').innerHTML = ''; } catch (e) { document.getElementById('detailContent').innerHTML = '
User not found
'; } } -// --- 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 --- let toastTimer = null; function toast(msg, isError) { diff --git a/frontend/style.css b/frontend/style.css index 90b07f3..17eba4b 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -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); border-radius: 16px; padding: 14px 20px; display: flex; align-items: flex-start; 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 { width: 38px; height: 38px; border-radius: 10px; background: var(--panel); display: flex; align-items: center; justify-content: center; @@ -527,18 +528,4 @@ header nav a.active { color: var(--accent-bright); border-color: rgba(0,170,255, .protocol-pill .meta > div:not(.steps) { font-size: 10px; } .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; } \ No newline at end of file +} \ No newline at end of file