192 lines
7.5 KiB
HTML
192 lines
7.5 KiB
HTML
@include(snippets/header)
|
|
<div id="main">
|
|
<div class="container">
|
|
<h1>APPROVAL QUEUE</h1>
|
|
<p>Items here are pending approval.</p>
|
|
<table class="table" style="width: 100%">
|
|
<thead>
|
|
<tr>
|
|
<td>Preview</td>
|
|
<td>ID</td>
|
|
<td>Uploader</td>
|
|
<td>Type</td>
|
|
<td>Action</td>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@each(posts as post)
|
|
<tr>
|
|
<td>
|
|
<video controls loop muted preload="metadata" style="max-height: 200px; max-width: 300px;">
|
|
<source src="/b/{{ post.dest }}" type="{{ post.mime }}">
|
|
</video>
|
|
</td>
|
|
<td>{{ post.id }}</td>
|
|
<td>{{ post.username }}</td>
|
|
<td>{{ post.mime }}</td>
|
|
<td>
|
|
<a href="/admin/approve/?id={{ post.id }}" class="badge badge-success">Approve</a>
|
|
<a href="/admin/deny/?id={{ post.id }}" class="badge badge-danger btn-deny-async">Deny /
|
|
Delete</a>
|
|
</td>
|
|
</tr>
|
|
@endeach
|
|
@if(posts.length === 0)
|
|
<tr>
|
|
<td colspan="5">No pending items.</td>
|
|
</tr>
|
|
@endif
|
|
</tbody>
|
|
</table>
|
|
<br>
|
|
@if(typeof pages !== 'undefined' && pages > 1)
|
|
<div class="pagination" style="display: flex; gap: 10px; align-items: center; justify-content: center;">
|
|
@if(page > 1)
|
|
<a href="/admin/approve?page={{ page - 1 }}" class="badge badge-secondary">« Prev</a>
|
|
@endif
|
|
<span>Page {{ page }} of {{ pages }}</span>
|
|
@if(page < pages) <a href="/admin/approve?page={{ page + 1 }}" class="badge badge-secondary">Next
|
|
»</a>
|
|
@endif
|
|
</div>
|
|
<br>
|
|
@endif
|
|
|
|
<div style="text-align: center; margin-bottom: 20px;">
|
|
<button id="btn-deny-all" class="badge badge-danger" onclick="window.handleDenyAll(event)"
|
|
style="font-size: 1.2em; padding: 10px 20px; border: none; cursor: pointer;">Deny All</button>
|
|
</div>
|
|
|
|
<a href="/admin">Back to Admin</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Modal -->
|
|
<div id="custom-modal"
|
|
style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); justify-content: center; align-items: center; z-index: 1000;">
|
|
<div
|
|
style="background: #222; color: #fff; padding: 20px; border-radius: 8px; max-width: 400px; text-align: center; border: 1px solid #444;">
|
|
<h3 id="modal-title" style="margin-top: 0;">Confirm Action</h3>
|
|
<p id="modal-text">Are you sure?</p>
|
|
<div style="display: flex; justify-content: space-around; margin-top: 20px;">
|
|
<button id="modal-cancel" class="badge badge-secondary"
|
|
style="border: none; padding: 10px 20px; cursor: pointer;">Cancel</button>
|
|
<button id="modal-confirm" class="badge badge-danger"
|
|
style="border: none; padding: 10px 20px; cursor: pointer;">Confirm</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Dynamic Button Text
|
|
const btnDenyAllInit = document.getElementById('btn-deny-all');
|
|
if (btnDenyAllInit) {
|
|
const count = document.querySelectorAll('.btn-deny-async').length;
|
|
btnDenyAllInit.innerText = 'Deny All (' + count + ' visible)';
|
|
}
|
|
|
|
const modal = document.getElementById('custom-modal');
|
|
const modalTitle = document.getElementById('modal-title');
|
|
const modalText = document.getElementById('modal-text');
|
|
const btnConfirm = document.getElementById('modal-confirm');
|
|
const btnCancel = document.getElementById('modal-cancel');
|
|
|
|
let pendingAction = null;
|
|
|
|
const showModal = (title, text, action) => {
|
|
modalTitle.innerText = title;
|
|
modalText.innerText = text;
|
|
pendingAction = action;
|
|
modal.style.display = 'flex';
|
|
|
|
btnConfirm.onclick = async () => {
|
|
if (!pendingAction) return;
|
|
btnConfirm.disabled = true;
|
|
btnConfirm.innerText = 'Processing...';
|
|
try {
|
|
await pendingAction();
|
|
closeModal();
|
|
} catch (e) {
|
|
alert('Error: ' + e.message);
|
|
} finally {
|
|
btnConfirm.disabled = false;
|
|
btnConfirm.innerText = 'Confirm';
|
|
}
|
|
};
|
|
};
|
|
|
|
const closeModal = () => {
|
|
modal.style.display = 'none';
|
|
pendingAction = null;
|
|
};
|
|
|
|
if (btnCancel) btnCancel.onclick = closeModal;
|
|
|
|
// Single Deny
|
|
document.querySelectorAll('.btn-deny-async').forEach(btn => {
|
|
btn.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
const url = btn.getAttribute('href');
|
|
const row = btn.closest('tr');
|
|
|
|
showModal('Deny Item', 'Permanently delete this item?', async () => {
|
|
const res = await fetch(url);
|
|
if (res.ok) {
|
|
row.style.opacity = '0';
|
|
setTimeout(() => row.remove(), 300);
|
|
} else {
|
|
throw new Error('Request failed');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// Global handler for Deny All
|
|
window.handleDenyAll = (e) => {
|
|
e.preventDefault();
|
|
console.log('Deny All clicked (Inline)');
|
|
const allBtn = [...document.querySelectorAll('.btn-deny-async')];
|
|
|
|
// Map to {id, element}
|
|
const targets = allBtn.map(b => {
|
|
const href = b.getAttribute('href');
|
|
const match = href ? href.match(/[?&]id=([^&]+)/) : null;
|
|
if (!match && href) console.log('No ID match for href:', href);
|
|
return match ? { id: match[1], btn: b } : null;
|
|
}).filter(item => item);
|
|
|
|
const ids = targets.map(t => t.id);
|
|
console.log('Deny List:', ids);
|
|
|
|
if (ids.length === 0) return alert('No items to deny');
|
|
|
|
showModal('Deny ALL', 'Permanently delete ' + ids.length + ' visible items?', async () => {
|
|
const res = await fetch('/admin/deny-multi', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ ids })
|
|
});
|
|
const json = await res.json();
|
|
if (json.success) {
|
|
closeModal(); // UX Polish: Close modal immediately
|
|
// Visual Removal
|
|
targets.forEach(t => {
|
|
const row = t.btn.closest('tr');
|
|
if (row) {
|
|
row.style.opacity = '0';
|
|
}
|
|
});
|
|
|
|
// Allow transition then reload
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 500);
|
|
} else {
|
|
throw new Error(json.msg || 'Failed');
|
|
}
|
|
});
|
|
};
|
|
});
|
|
</script>
|
|
@include(snippets/footer) |