117 lines
4.1 KiB
JavaScript
117 lines
4.1 KiB
JavaScript
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;
|
|
}
|