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 - - - - - +