init
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
const token = location.pathname.split('/').pop();
|
||||
history.replaceState(null, '', '/admin');
|
||||
|
||||
const grid = document.querySelector('#review-grid');
|
||||
const reviewStatus = document.querySelector('#review-status');
|
||||
const reviewCount = document.querySelector('#review-count');
|
||||
const selected = new Set();
|
||||
|
||||
document.querySelector('#refresh-review').addEventListener('click', loadPending);
|
||||
document.querySelector('#approve-selected').addEventListener('click', () => moderateSelected('approve'));
|
||||
document.querySelector('#delete-selected').addEventListener('click', () => moderateSelected('delete'));
|
||||
|
||||
grid.addEventListener('change', (event) => {
|
||||
const checkbox = event.target.closest('[data-select-id]');
|
||||
if (!checkbox) return;
|
||||
if (checkbox.checked) selected.add(checkbox.dataset.selectId);
|
||||
else selected.delete(checkbox.dataset.selectId);
|
||||
});
|
||||
|
||||
grid.addEventListener('click', (event) => {
|
||||
const action = event.target.closest('[data-admin-action]');
|
||||
if (!action) return;
|
||||
moderate([action.dataset.id], action.dataset.adminAction);
|
||||
});
|
||||
|
||||
await loadPending();
|
||||
|
||||
async function loadPending() {
|
||||
selected.clear();
|
||||
reviewStatus.textContent = 'FETCHING_PENDING_QUEUE';
|
||||
const response = await fetch('/api/admin/pending', { headers: adminHeaders() });
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
reviewStatus.textContent = 'REVIEW_AUTH_FAILED';
|
||||
grid.innerHTML = '<div class="empty-state">ADMIN_TOKEN_INVALID</div>';
|
||||
return;
|
||||
}
|
||||
reviewStatus.textContent = 'PENDING_REVIEW';
|
||||
reviewCount.textContent = `${payload.total} ITEMS`;
|
||||
renderPending(payload.memes);
|
||||
}
|
||||
|
||||
function renderPending(memes) {
|
||||
if (memes.length === 0) {
|
||||
grid.innerHTML = '<div class="empty-state">NO_PENDING_MEMES</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.replaceChildren(...memes.map((meme) => {
|
||||
const article = document.createElement('article');
|
||||
article.className = 'meme-card';
|
||||
article.innerHTML = `
|
||||
<div class="card-head">
|
||||
<label class="review-select">
|
||||
<input type="checkbox" data-select-id="${meme.id}">
|
||||
<span>ID: ${shortId(meme.id)}</span>
|
||||
</label>
|
||||
<span class="card-meta age">SCORE ${meme.moderationScore}</span>
|
||||
</div>
|
||||
<div class="image-frame">
|
||||
<img src="/admin-media/${token}/${meme.id}" alt="Pending meme ${shortId(meme.id)}" loading="lazy">
|
||||
</div>
|
||||
<div class="review-body">
|
||||
<p>${escapeText(meme.moderationReason || 'Queued for review.')}</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<div class="stats">
|
||||
<span class="card-stat">${formatBytes(meme.byteSize)}</span>
|
||||
</div>
|
||||
<div class="action-group">
|
||||
<button class="text-action text-action-accent icon-only-action" type="button" data-admin-action="approve" data-id="${meme.id}" aria-label="Approve meme" title="Approve meme"><span class="material-symbols-outlined" aria-hidden="true">check</span></button>
|
||||
<button class="text-action icon-only-action danger-action" type="button" data-admin-action="delete" data-id="${meme.id}" aria-label="Delete meme" title="Delete meme"><span class="material-symbols-outlined" aria-hidden="true">delete</span></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return article;
|
||||
}));
|
||||
}
|
||||
|
||||
async function moderateSelected(action) {
|
||||
if (selected.size === 0) return;
|
||||
await moderate([...selected], action);
|
||||
}
|
||||
|
||||
async function moderate(ids, action) {
|
||||
const endpoint = action === 'approve' ? '/api/admin/approve' : '/api/admin/delete';
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { ...adminHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids })
|
||||
});
|
||||
if (!response.ok) {
|
||||
reviewStatus.textContent = 'REVIEW_ACTION_FAILED';
|
||||
return;
|
||||
}
|
||||
await loadPending();
|
||||
}
|
||||
|
||||
function adminHeaders() {
|
||||
return { 'X-Admin-Token': token };
|
||||
}
|
||||
|
||||
function shortId(id) {
|
||||
return `0x${id.slice(0, 4).toUpperCase()}...${id.slice(-4).toUpperCase()}`;
|
||||
}
|
||||
|
||||
function formatBytes(value) {
|
||||
if (value >= 1024 * 1024) return `${(value / 1024 / 1024).toFixed(1)}MB`;
|
||||
return `${Math.ceil(value / 1024)}KB`;
|
||||
}
|
||||
|
||||
function escapeText(value) {
|
||||
const span = document.createElement('span');
|
||||
span.textContent = value;
|
||||
return span.innerHTML;
|
||||
}
|
||||
Reference in New Issue
Block a user