From d5a7e854178257ec355d30cdd057eb175a300ee9 Mon Sep 17 00:00:00 2001 From: TopherMayor Date: Thu, 30 Apr 2026 22:43:09 -0700 Subject: [PATCH] Add inline admin option editor --- public/admin.html | 176 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 157 insertions(+), 19 deletions(-) diff --git a/public/admin.html b/public/admin.html index dd8578e..c965d4c 100644 --- a/public/admin.html +++ b/public/admin.html @@ -124,6 +124,83 @@ .btn-edit:hover { border-color: var(--amber); color: #fcd34d; } .btn-row { display: flex; gap: 6px; flex-shrink: 0; } + /* Editor */ + .editor-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 18px; + margin-bottom: 24px; + } + .editor-card.hidden { display: none; } + .editor-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin-top: 14px; + } + .editor-grid .full { grid-column: 1 / -1; } + .editor-card label { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 0.72rem; + color: var(--text-muted); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.4px; + } + .editor-card input, + .editor-card select, + .editor-card textarea { + width: 100%; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + padding: 10px 12px; + font-size: 0.88rem; + outline: none; + } + .editor-card textarea { + min-height: 110px; + resize: vertical; + } + .editor-card input:focus, + .editor-card select:focus, + .editor-card textarea:focus { + border-color: var(--accent); + } + .editor-meta { + font-size: 0.78rem; + color: var(--text-muted); + margin-top: 4px; + } + .checkbox-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.82rem; + color: var(--text); + margin-top: 6px; + } + .checkbox-row input { width: auto; } + .editor-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 14px; + } + .btn-secondary { + background: transparent; + border: 1px solid var(--border); + color: var(--text-muted); + } + .btn-secondary:hover { + border-color: var(--text-muted); + color: var(--text); + } + /* Toast */ .toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(80px); @@ -142,6 +219,8 @@ @media (max-width: 600px) { .option-row { flex-wrap: wrap; } .btn-row { width: 100%; justify-content: flex-end; } + .editor-grid { grid-template-columns: 1fr; } + .editor-actions { justify-content: stretch; } } @@ -187,6 +266,44 @@ + +
@@ -213,6 +330,7 @@ const API = ''; const PWD_KEY = 'cabo_admin_pwd'; const CORRECT_PWD = 'cabo2026'; let allData = null; +let editingOptionId = null; function toast(msg, type='') { const t = document.getElementById('toast'); @@ -242,6 +360,7 @@ async function loadData() { fetch(API + '/api/options?includeUnapproved=true').then(r => r.json()), ]); allData = { categories: cats, options: opts }; + renderEditorCategoryOptions(); renderStats(); renderPending(); renderAll(); @@ -251,6 +370,14 @@ async function loadData() { } } +function renderEditorCategoryOptions() { + const select = document.getElementById('editorCategory'); + select.innerHTML = (allData?.categories || []) + .filter(category => category.id !== 'results' && category.id !== 'map') + .map(category => ``) + .join(''); +} + function renderStats() { const voters = new Set(); let totalVotes = 0; @@ -305,34 +432,45 @@ function renderAll() { `).join(''); } -async function editOption(id) { +function editOption(id) { const current = allData.options.find(o => o.id === id); if (!current) return; - const nextName = prompt('Option name', current.name); - if (nextName === null) return; - const nextDesc = prompt('Short description', current.desc || ''); - if (nextDesc === null) return; - const nextDetails = prompt('Details, one per line', Array.isArray(current.details) ? current.details.join('\\n') : ''); - if (nextDetails === null) return; - const nextUrl = prompt('Website URL', current.url || ''); - if (nextUrl === null) return; - const nextCategory = prompt('Category id', current.categoryId); - if (nextCategory === null) return; - const nextApproved = confirm('Approve this option now? Click OK to approve, Cancel to keep it pending.'); + editingOptionId = id; + document.getElementById('editorCard').classList.remove('hidden'); + document.getElementById('editorName').value = current.name || ''; + document.getElementById('editorCategory').value = current.categoryId || ''; + document.getElementById('editorDesc').value = current.desc || ''; + document.getElementById('editorDetails').value = Array.isArray(current.details) ? current.details.join('\n') : ''; + document.getElementById('editorUrl').value = current.url || ''; + document.getElementById('editorApproved').checked = Boolean(current.approved); + document.getElementById('editorStatus').textContent = current.approved ? 'Approved' : 'Pending'; + document.getElementById('editorMeta').textContent = `${current.name} • ${current.categoryId} • added by ${current.addedBy || 'unknown'}`; + document.getElementById('editorName').focus(); + window.scrollTo({ top: 0, behavior: 'smooth' }); +} + +function closeEditor() { + editingOptionId = null; + document.getElementById('editorCard').classList.add('hidden'); +} + +async function saveEditor() { + if (!editingOptionId) return; try { - await fetch(API + '/api/options/' + id, { + await fetch(API + '/api/options/' + editingOptionId, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - name: nextName.trim(), - desc: nextDesc.trim(), - details: nextDetails, - url: nextUrl.trim(), - categoryId: nextCategory.trim(), - approved: nextApproved, + name: document.getElementById('editorName').value.trim(), + desc: document.getElementById('editorDesc').value.trim(), + details: document.getElementById('editorDetails').value, + url: document.getElementById('editorUrl').value.trim(), + categoryId: document.getElementById('editorCategory').value, + approved: document.getElementById('editorApproved').checked, }) }); toast('Option updated', 'success'); + closeEditor(); await loadData(); } catch(e) { toast('Failed to update option', 'error'); } }