Add inline admin option editor

This commit is contained in:
TopherMayor
2026-04-30 22:43:09 -07:00
parent 1e36d45976
commit d5a7e85417

View File

@@ -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; }
}
</style>
</head>
@@ -187,6 +266,44 @@
<button class="btn-toggle open" id="pollsBtn" onclick="togglePolls()">Close Polls</button>
</div>
<div class="editor-card hidden" id="editorCard">
<div class="section-header">
<span class="section-title">Edit Option</span>
<span class="badge" id="editorStatus">Draft</span>
</div>
<div class="editor-meta" id="editorMeta">Select an option to edit.</div>
<div class="editor-grid">
<label>
Name
<input id="editorName" type="text" maxlength="80" />
</label>
<label>
Category
<select id="editorCategory"></select>
</label>
<label class="full">
Description
<input id="editorDesc" type="text" maxlength="200" />
</label>
<label class="full">
Details
<textarea id="editorDetails" placeholder="One detail per line"></textarea>
</label>
<label class="full">
Website URL
<input id="editorUrl" type="url" />
</label>
</div>
<label class="checkbox-row">
<input id="editorApproved" type="checkbox" />
Approve this option
</label>
<div class="editor-actions">
<button class="btn btn-secondary" onclick="closeEditor()">Cancel</button>
<button class="btn btn-approve" onclick="saveEditor()">Save changes</button>
</div>
</div>
<!-- Pending Options -->
<div class="section">
<div class="section-header">
@@ -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 => `<option value="${category.id}">${category.name}</option>`)
.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'); }
}