diff --git a/frontend/app.js b/frontend/app.js
index 15b11e1..4b1ab86 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -57,13 +57,12 @@ async function init() {
renderAuth();
// Re-route after auth loads so auth-dependent UI renders correctly
- if (location.hash && location.hash !== '#protocols') {
+ if (location.hash && location.hash !== '#library') {
route();
}
}
function route() {
- isRouting = true;
const hash = location.hash.slice(1) || 'library';
if (hash.startsWith('protocols/')) {
showDetail(hash.split('/')[1]);
@@ -82,16 +81,14 @@ 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 && view !== 'library') return;
+ if (currentRoute === view) return;
currentRoute = view;
document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
document.getElementById('view-' + view).style.display = 'block';
@@ -99,16 +96,11 @@ function navigate(view) {
document.querySelectorAll('.mobile-panel a').forEach(a => a.classList.toggle('active', a.dataset.view === view));
closeMenu();
updateBreadcrumb(view);
- 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 === 'library') { location.hash = 'library'; loadProtocols(); }
+ if (view === 'collections') { location.hash = 'collections'; loadCollections(); }
if (view === 'create') { editingSlug = null; resetForm(); }
if (view === 'create-collection') { editingCollectionSlug = null; resetCollectionForm(); }
- if (view === 'about') {}
+ if (view === 'about') { location.hash = 'about'; }
window.scrollTo(0, 0);
}
@@ -321,7 +313,7 @@ function renderProtocolGrid(protocols) {
// --- Detail ---
async function showDetail(slug) {
- if (!isRouting && location.hash !== '#protocols/' + slug) 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'));
@@ -721,7 +713,7 @@ async function saveCollection(e) {
}
async function showCollectionDetail(slug) {
- if (!isRouting && location.hash !== '#collections/' + slug) 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'));
@@ -794,7 +786,6 @@ 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'));
@@ -824,12 +815,105 @@ async function showUser(username) {
'
' + escapeHtml(col.title) + '
' + escapeHtml(col.description||'') + '
';
}).join('') +
'' : '');
- document.getElementById('detailActions').innerHTML = '';
+ // 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;
} 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) {
diff --git a/frontend/index.html b/frontend/index.html
index 0841178..200be03 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,22 +4,15 @@
Protocol Droid
-
-
-
-
-
+