fix: show protocols/ in breadcrumb on main page for consistency

This commit is contained in:
Protocolbot
2026-07-09 09:48:03 -06:00
parent b5ce383147
commit dffd90458d
+38 -125
View File
@@ -107,7 +107,7 @@ function navigate(view) {
// --- Mobile menu --- // --- Mobile menu ---
function updateBreadcrumb(view) { function updateBreadcrumb(view) {
const path = document.getElementById('crumbPath'); const path = document.getElementById('crumbPath');
const map = { library: '', create: 'create', collections: 'collections/', 'create-collection': 'collections/create', about: 'about' }; const map = { library: 'protocols/', create: 'protocols/create', collections: 'collections/', 'create-collection': 'collections/create', about: 'about' };
path.textContent = map[view] || ''; path.textContent = map[view] || '';
} }
@@ -297,28 +297,16 @@ function renderProtocolGrid(protocols) {
grid.innerHTML = '<div class="empty">No protocols yet. <a href="#create" onclick="navigate(\'create\');return false">Create one</a></div>'; grid.innerHTML = '<div class="empty">No protocols yet. <a href="#create" onclick="navigate(\'create\');return false">Create one</a></div>';
return; return;
} }
grid.innerHTML = protocols.map(renderProtocolPill).join(''); grid.innerHTML = protocols.map(function(p) {
} var sourceLabel = p.source || (p.author ? '@' + p.author : '');
var tags = (p.tags || []).map(function(t) { return '<span class="tag">' + escapeHtml(t) + '</span>'; }).join('');
// --- Shared rendering (use everywhere for consistency) --- return '<div class="protocol-pill" onclick="showDetail(\'' + p.slug + '\')">' +
function renderProtocolPill(p) { '<div class="info"><h3>' + escapeHtml(p.title) + '</h3>' +
var sourceLabel = p.source || (p.author ? '@' + p.author : ''); '<p>' + escapeHtml(p.description || '') + '</p>' +
var tags = (p.tags || []).map(function(t) { return '<span class="tag">' + escapeHtml(t) + '</span>'; }).join(''); (tags ? '<div class="tag-row">' + tags + '</div>' : '') + '</div>' +
return '<a class="protocol-pill" href="#protocols/' + p.slug + '">' + '<div class="meta"><div class="steps">' + (p.steps || []).length + ' steps</div>' +
'<div class="info"><h3>' + escapeHtml(p.title) + '</h3>' + '<div>' + escapeHtml(sourceLabel) + '</div></div></div>';
'<p>' + escapeHtml(p.description || '') + '</p>' + }).join('');
(tags ? '<div class="tag-row">' + tags + '</div>' : '') + '</div>' +
'<div class="meta"><div class="steps">' + (p.steps || []).length + ' steps</div>' +
(sourceLabel ? '<div>' + escapeHtml(sourceLabel) + '</div>' : '') + '</div></a>';
}
function renderCollectionPill(c) {
var sourceLabel = c.source || (c.author ? '@' + c.author : '');
return '<a class="protocol-pill collection" href="#collections/' + 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>' +
(sourceLabel ? '<div>' + escapeHtml(sourceLabel) + '</div>' : '') + '</div></a>';
} }
// (iconFor removed — icons are no longer used) // (iconFor removed — icons are no longer used)
@@ -575,7 +563,12 @@ async function loadCollections() {
grid.innerHTML = '<div class="empty">No collections yet. <a href="#" onclick="navigate(\'create-collection\');return false">Create one</a></div>'; grid.innerHTML = '<div class="empty">No collections yet. <a href="#" onclick="navigate(\'create-collection\');return false">Create one</a></div>';
return; return;
} }
grid.innerHTML = collections.map(renderCollectionPill).join(''); grid.innerHTML = collections.map(function(c) {
var sourceLabel = c.author ? '@' + c.author : (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>';
}).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>';
} }
@@ -736,7 +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 += renderProtocolPill(p); itemsHtml += '<div class="protocol-pill" onclick="showDetail(\'' + p.slug + '\')">' +
'<div class="info"><h3>' + escapeHtml(p.title) + '</h3>' +
'<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>' +
'<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>';
} }
@@ -803,111 +800,27 @@ async function showUser(username) {
'<div class="profile-card"><h2>@' + escapeHtml(data.user.username) + '</h2>' + '<div class="profile-card"><h2>@' + escapeHtml(data.user.username) + '</h2>' +
(data.user.display_name ? '<div class="bio">' + escapeHtml(data.user.display_name) + '</div>' : '') + (data.user.display_name ? '<div class="bio">' + escapeHtml(data.user.display_name) + '</div>' : '') +
(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><div class="protocol-grid">' + (p.length ? '<div class="detail-section"><h2>Protocols (' + p.length + ')</h2>' +
p.map(renderProtocolPill).join('') + p.map(function(proto) {
'</div></div>' : '') + return '<div class="protocol-pill" onclick="showDetail(\'' + proto.slug + '\')">' +
(c.length ? '<div class="detail-section"><h2>Collections (' + c.length + ')</h2><div class="protocol-grid">' + '<div class="info"><h3>' + escapeHtml(proto.title) + '</h3>' +
c.map(renderCollectionPill).join('') + '<p>' + escapeHtml(proto.description||'') + '</p>' +
'</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>' +
// Add actions if viewing own profile '<div class="meta"><div class="steps">' + (proto.steps || []).length + ' steps</div></div></div>';
var actionsHtml = ''; }).join('') +
if (currentUser && currentUser.username === username) { '</div>' : '') +
actionsHtml = '<button class="btn" onclick="showChangePassword()">Change Password</button>'; (c.length ? '<div class="detail-section"><h2>Collections (' + c.length + ')</h2>' +
if (currentUser.role === 'admin') { c.map(function(col) {
actionsHtml += '<button class="btn primary" id="seedBtn" onclick="loadSeedProtocols()">Load Seed Protocols</button>'; return '<div class="protocol-pill collection" onclick="showCollectionDetail(\'' + col.slug + '\')"><div class="icon">[+]</div>' +
actionsHtml += '<button class="btn" onclick="document.getElementById(\'bulkImportFile\').click()">Import YAML Files</button>'; '<div class="info"><h3>' + escapeHtml(col.title) + '</h3><p>' + escapeHtml(col.description||'') + '</p></div></div>';
actionsHtml += '<input type="file" id="bulkImportFile" accept=".yaml,.yml" multiple style="display:none" onchange="bulkImportYaml(event)">'; }).join('') +
} '</div>' : '');
} document.getElementById('detailActions').innerHTML = '';
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>';
} }
} }
// --- Import YAML to form ---
async function importYamlToForm(event) {
var file = event.target.files[0];
if (!file) return;
event.target.value = ''; // Reset for re-use
try {
var text = await file.text();
var result = await api('/import', { method: 'POST', body: JSON.stringify({ yaml: text }) });
// Successfully imported — now load it into the form for editing
toast('Imported: ' + result.slug + ' — review and save');
editProtocol(result.slug);
} catch (err) {
toast('Import failed: ' + err.message, true);
}
}
// --- Bulk import YAML files (admin) ---
async function bulkImportYaml(event) {
var files = event.target.files;
if (!files || !files.length) return;
event.target.value = ''; // Reset for re-use
var imported = 0;
var failed = 0;
var errors = [];
for (var i = 0; i < files.length; i++) {
try {
var text = await files[i].text();
await api('/import', { method: 'POST', body: JSON.stringify({ yaml: text }) });
imported++;
} catch (err) {
failed++;
errors.push(files[i].name + ': ' + err.message);
}
}
var msg = 'Imported ' + imported + ' protocol' + (imported !== 1 ? 's' : '');
if (failed > 0) {
msg += ', ' + failed + ' failed: ' + errors.join('; ');
toast(msg, true);
} else {
toast(msg);
}
// Reload profile to show new protocols
if (currentUser) showUser(currentUser.username);
}
// --- 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);
}
}
// --- Seed protocols ---
async function loadSeedProtocols() {
var btn = document.getElementById('seedBtn');
if (btn) { btn.disabled = true; btn.textContent = 'Loading...'; }
try {
var result = await api('/seed', { method: 'POST' });
toast('Loaded ' + result.imported + ' seed protocols');
if (btn) { btn.textContent = 'Load Seed Protocols'; btn.disabled = false; }
showUser(currentUser.username);
} catch (e) {
toast(e.message, true);
if (btn) { btn.textContent = 'Load Seed Protocols'; btn.disabled = false; }
}
}
// --- Toast --- // --- Toast ---
let toastTimer = null; let toastTimer = null;
function toast(msg, isError) { function toast(msg, isError) {