Add inline admin option editor
This commit is contained in:
@@ -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'); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user