Add inline admin option editor
This commit is contained in:
@@ -124,6 +124,83 @@
|
|||||||
.btn-edit:hover { border-color: var(--amber); color: #fcd34d; }
|
.btn-edit:hover { border-color: var(--amber); color: #fcd34d; }
|
||||||
.btn-row { display: flex; gap: 6px; flex-shrink: 0; }
|
.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 */
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(80px);
|
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(80px);
|
||||||
@@ -142,6 +219,8 @@
|
|||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.option-row { flex-wrap: wrap; }
|
.option-row { flex-wrap: wrap; }
|
||||||
.btn-row { width: 100%; justify-content: flex-end; }
|
.btn-row { width: 100%; justify-content: flex-end; }
|
||||||
|
.editor-grid { grid-template-columns: 1fr; }
|
||||||
|
.editor-actions { justify-content: stretch; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -187,6 +266,44 @@
|
|||||||
<button class="btn-toggle open" id="pollsBtn" onclick="togglePolls()">Close Polls</button>
|
<button class="btn-toggle open" id="pollsBtn" onclick="togglePolls()">Close Polls</button>
|
||||||
</div>
|
</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 -->
|
<!-- Pending Options -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
@@ -213,6 +330,7 @@ const API = '';
|
|||||||
const PWD_KEY = 'cabo_admin_pwd';
|
const PWD_KEY = 'cabo_admin_pwd';
|
||||||
const CORRECT_PWD = 'cabo2026';
|
const CORRECT_PWD = 'cabo2026';
|
||||||
let allData = null;
|
let allData = null;
|
||||||
|
let editingOptionId = null;
|
||||||
|
|
||||||
function toast(msg, type='') {
|
function toast(msg, type='') {
|
||||||
const t = document.getElementById('toast');
|
const t = document.getElementById('toast');
|
||||||
@@ -242,6 +360,7 @@ async function loadData() {
|
|||||||
fetch(API + '/api/options?includeUnapproved=true').then(r => r.json()),
|
fetch(API + '/api/options?includeUnapproved=true').then(r => r.json()),
|
||||||
]);
|
]);
|
||||||
allData = { categories: cats, options: opts };
|
allData = { categories: cats, options: opts };
|
||||||
|
renderEditorCategoryOptions();
|
||||||
renderStats();
|
renderStats();
|
||||||
renderPending();
|
renderPending();
|
||||||
renderAll();
|
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() {
|
function renderStats() {
|
||||||
const voters = new Set();
|
const voters = new Set();
|
||||||
let totalVotes = 0;
|
let totalVotes = 0;
|
||||||
@@ -305,34 +432,45 @@ function renderAll() {
|
|||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editOption(id) {
|
function editOption(id) {
|
||||||
const current = allData.options.find(o => o.id === id);
|
const current = allData.options.find(o => o.id === id);
|
||||||
if (!current) return;
|
if (!current) return;
|
||||||
const nextName = prompt('Option name', current.name);
|
editingOptionId = id;
|
||||||
if (nextName === null) return;
|
document.getElementById('editorCard').classList.remove('hidden');
|
||||||
const nextDesc = prompt('Short description', current.desc || '');
|
document.getElementById('editorName').value = current.name || '';
|
||||||
if (nextDesc === null) return;
|
document.getElementById('editorCategory').value = current.categoryId || '';
|
||||||
const nextDetails = prompt('Details, one per line', Array.isArray(current.details) ? current.details.join('\\n') : '');
|
document.getElementById('editorDesc').value = current.desc || '';
|
||||||
if (nextDetails === null) return;
|
document.getElementById('editorDetails').value = Array.isArray(current.details) ? current.details.join('\n') : '';
|
||||||
const nextUrl = prompt('Website URL', current.url || '');
|
document.getElementById('editorUrl').value = current.url || '';
|
||||||
if (nextUrl === null) return;
|
document.getElementById('editorApproved').checked = Boolean(current.approved);
|
||||||
const nextCategory = prompt('Category id', current.categoryId);
|
document.getElementById('editorStatus').textContent = current.approved ? 'Approved' : 'Pending';
|
||||||
if (nextCategory === null) return;
|
document.getElementById('editorMeta').textContent = `${current.name} • ${current.categoryId} • added by ${current.addedBy || 'unknown'}`;
|
||||||
const nextApproved = confirm('Approve this option now? Click OK to approve, Cancel to keep it pending.');
|
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 {
|
try {
|
||||||
await fetch(API + '/api/options/' + id, {
|
await fetch(API + '/api/options/' + editingOptionId, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: nextName.trim(),
|
name: document.getElementById('editorName').value.trim(),
|
||||||
desc: nextDesc.trim(),
|
desc: document.getElementById('editorDesc').value.trim(),
|
||||||
details: nextDetails,
|
details: document.getElementById('editorDetails').value,
|
||||||
url: nextUrl.trim(),
|
url: document.getElementById('editorUrl').value.trim(),
|
||||||
categoryId: nextCategory.trim(),
|
categoryId: document.getElementById('editorCategory').value,
|
||||||
approved: nextApproved,
|
approved: document.getElementById('editorApproved').checked,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
toast('Option updated', 'success');
|
toast('Option updated', 'success');
|
||||||
|
closeEditor();
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch(e) { toast('Failed to update option', 'error'); }
|
} catch(e) { toast('Failed to update option', 'error'); }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user