Files
bitsforfree/public/assets/admin.js
T
2026-05-08 18:18:36 -03:00

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;
}