diff --git a/frontend/app.js b/frontend/app.js
index bafaba4..c0af4ce 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -107,7 +107,7 @@ function navigate(view) {
// --- Mobile menu ---
function updateBreadcrumb(view) {
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] || '';
}
@@ -297,28 +297,16 @@ function renderProtocolGrid(protocols) {
grid.innerHTML = '
';
return;
}
- grid.innerHTML = protocols.map(renderProtocolPill).join('');
-}
-
-// --- Shared rendering (use everywhere for consistency) ---
-function renderProtocolPill(p) {
- var sourceLabel = p.source || (p.author ? '@' + p.author : '');
- var tags = (p.tags || []).map(function(t) { return '' + escapeHtml(t) + ''; }).join('');
- return '' +
- '' + escapeHtml(p.title) + '
' +
- '
' + escapeHtml(p.description || '') + '
' +
- (tags ? '
' + tags + '
' : '') + '
' +
- '';
-}
-
-function renderCollectionPill(c) {
- var sourceLabel = c.source || (c.author ? '@' + c.author : '');
- return '' +
- '' + escapeHtml(c.title) + '
' +
- '
' + escapeHtml(c.description || '') + '
' +
- '';
+ 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 '' +
+ '
' + escapeHtml(p.title) + '
' +
+ '
' + escapeHtml(p.description || '') + '
' +
+ (tags ? '
' + tags + '
' : '') + '
' +
+ '
';
+ }).join('');
}
// (iconFor removed — icons are no longer used)
@@ -575,7 +563,12 @@ async function loadCollections() {
grid.innerHTML = '';
return;
}
- grid.innerHTML = collections.map(renderCollectionPill).join('');
+ grid.innerHTML = collections.map(function(c) {
+ var sourceLabel = c.author ? '@' + c.author : (c.source || '');
+ return '' +
+ '
' + escapeHtml(c.title) + '
' + escapeHtml(c.description || '') + '
' +
+ '
';
+ }).join('');
} catch (e) {
document.getElementById('collectionsGrid').innerHTML = '' + escapeHtml(e.message) + '
';
}
@@ -736,7 +729,11 @@ async function showCollectionDetail(slug) {
if (item.type === 'protocol') {
try {
var p = await api('/protocols/' + item.slug);
- itemsHtml += renderProtocolPill(p);
+ itemsHtml += '' +
+ '
' + escapeHtml(p.title) + '
' +
+ '
' + escapeHtml(p.description || '') + '
' +
+ (p.tags && p.tags.length ? '
' + p.tags.map(function(t) { return '' + escapeHtml(t) + ''; }).join('') + '
' : '') + '
' +
+ '
';
} catch (e2) {
itemsHtml += '' + escapeHtml(item.slug) + '
Protocol not found
';
}
@@ -803,111 +800,27 @@ async function showUser(username) {
'@' + escapeHtml(data.user.username) + '
' +
(data.user.display_name ? '
' + escapeHtml(data.user.display_name) + '
' : '') +
(data.user.bio ? '
' + escapeHtml(data.user.bio) + '
' : '') + '
' +
- (p.length ? 'Protocols (' + p.length + ')
' +
- p.map(renderProtocolPill).join('') +
- '
' : '') +
- (c.length ? 'Collections (' + c.length + ')
' +
- c.map(renderCollectionPill).join('') +
- '
' : '');
- // Add actions if viewing own profile
- var actionsHtml = '';
- if (currentUser && currentUser.username === username) {
- actionsHtml = '';
- if (currentUser.role === 'admin') {
- actionsHtml += '';
- actionsHtml += '';
- actionsHtml += '';
- }
- }
- document.getElementById('detailActions').innerHTML = actionsHtml;
+ (p.length ? 'Protocols (' + p.length + ')
' +
+ p.map(function(proto) {
+ return '
' +
+ '
' + escapeHtml(proto.title) + '
' +
+ '
' + escapeHtml(proto.description||'') + '
' +
+ (proto.tags && proto.tags.length ? '
' + proto.tags.map(function(t) { return '' + escapeHtml(t) + ''; }).join('') + '
' : '') + '
' +
+ '
';
+ }).join('') +
+ '
' : '') +
+ (c.length ? 'Collections (' + c.length + ')
' +
+ c.map(function(col) {
+ return '
[+]
' +
+ '
' + escapeHtml(col.title) + '
' + escapeHtml(col.description||'') + '
';
+ }).join('') +
+ '
' : '');
+ document.getElementById('detailActions').innerHTML = '';
} catch (e) {
document.getElementById('detailContent').innerHTML = 'User not found
';
}
}
-// --- 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 ---
let toastTimer = null;
function toast(msg, isError) {