Files
f0ckm/views/mod/approve.html
2026-05-04 04:24:18 +02:00

350 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@include(snippets/header)
<div class="pagewrapper">
<div id="main">
<div class="container">
<h1>APPROVAL QUEUE</h1>
<p>Items here are pending approval.</p>
@if(pending.length > 0)
<h2>Pending Uploads</h2>
<div class="approval-grid">
@each(pending as post)
<div class="approval-card">
<div class="approval-card-media">
@if(post.mime === 'video/youtube')
<div class="embed-responsive embed-responsive-16by9" style="width: 100%;">
<iframe class="embed-responsive-item" src="https://www.youtube.com/embed/{!! post.dest.replace('yt:', '') !!}" frameborder="0" allowfullscreen></iframe>
</div>
@elseif(post.mime.startsWith('video'))
<video controls loop muted preload="metadata">
<source src="/mod/pending/b/{!! post.dest !!}" type="{!! post.mime !!}">
Your browser does not support the video tag.
</video>
@elseif(post.mime === 'application/pdf')
<div class="embed-responsive embed-responsive-16by9" style="width: 100%;">
<iframe class="embed-responsive-item" src="/mod/pending/b/{!! post.dest !!}#toolbar=0" loading="lazy" style="border: none;"></iframe>
</div>
@else
<img src="/mod/pending/t/{!! post.id !!}.webp" alt="Preview">
@endif
</div>
<div class="approval-card-body">
<div class="approval-card-info">
<div><strong>ID:</strong> {!! post.id !!}</div>
<div><strong>User:</strong> {!! post.username !!}</div>
<div><strong>Type:</strong> {!! post.mime !!}</div>
</div>
<div class="approval-card-tags">
@each(post.tags as tag)
<span class="badge {!! tag.badge !!}">{!! tag.tag !!}</span>
@endeach
</div>
<div class="approval-card-actions" style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<button data-id="{!! post.id !!}" class="badge badge-success btn-approve-async" style="margin: 0; text-align: center; border: none; cursor: pointer;">Approve</button>
<button data-id="{!! post.id !!}" class="badge badge-danger btn-deny-async" style="margin: 0; text-align: center; border: none; cursor: pointer;">Deny / Delete</button>
<a href="/api/v2/tags/{!! post.id !!}/toggle" class="badge btn-rating-toggle-async" style="grid-column: span 2; background: #444; color: #ccc; margin: 0; text-align: center;">Rating</a>
</div>
</div>
</div>
@endeach
</div>
@endif
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 40px; margin-bottom: 20px;">
<h2 style="color: #ff6b6b; margin: 0;">Soft Deleted</h2>
@if(trash.length > 0 && session.admin)
<button id="btn-purge-trash" class="badge badge-danger" style="border: none; padding: 10px 15px; cursor: pointer; font-size: 14px;">Purge All Soft-Deleted</button>
@endif
</div>
<p class="text-muted">These items are in the deleted folder but not purged from DB. Approving them will restore them.</p>
@if(trash.length > 0)
<div class="approval-grid" style="opacity: 0.8;">
@each(trash as post)
<div class="approval-card">
<div class="approval-card-media">
@if(post.mime === 'video/youtube')
<div class="embed-responsive embed-responsive-16by9" style="width: 100%;">
<iframe class="embed-responsive-item" src="https://www.youtube.com/embed/{!! post.dest.replace('yt:', '') !!}" frameborder="0" allowfullscreen></iframe>
</div>
@elseif(post.mime.startsWith('video'))
<video controls loop muted preload="metadata">
<source src="/mod/deleted/b/{!! post.dest !!}" type="{!! post.mime !!}">
Your browser does not support the video tag.
</video>
@elseif(post.mime === 'application/pdf')
<div class="embed-responsive embed-responsive-16by9" style="width: 100%;">
<iframe class="embed-responsive-item" src="/mod/deleted/b/{!! post.dest !!}#toolbar=0" loading="lazy" style="border: none;"></iframe>
</div>
@else
<img src="/mod/deleted/t/{!! post.id !!}.webp" style="filter: grayscale(50%);" alt="Preview">
@endif
</div>
<div class="approval-card-body">
<div class="approval-card-info">
<div><strong>ID:</strong> {!! post.id !!}</div>
<div><strong>User:</strong> {!! post.username !!}</div>
<div><strong>Type:</strong> {!! post.mime !!}</div>
@if(post.delete_reason)
<div style="color: #ffb8b8;"><strong>Reason:</strong> {!! post.delete_reason !!}</div>
@endif
</div>
<div class="approval-card-tags">
@each(post.tags as tag)
<span class="badge {!! tag.badge !!}">{!! tag.tag !!}</span>
@endeach
</div>
<div class="approval-card-actions" style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<button data-id="{!! post.id !!}" class="badge badge-warning btn-approve-async" style="margin: 0; text-align: center; border: none; cursor: pointer;">Restore</button>
@if(session.admin)
<button data-id="{!! post.id !!}" class="badge badge-danger btn-deny-async" style="margin: 0; text-align: center; border: none; cursor: pointer;">Purge</button>
@else
<span></span>
@endif
<a href="/api/v2/tags/{!! post.id !!}/toggle" class="badge btn-rating-toggle-async" style="grid-column: span 2; background: #444; color: #ccc; margin: 0; text-align: center;">Rating</a>
</div>
</div>
</div>
@endeach
</div>
@else
<p style="padding: 20px; border: 1px dashed #444; color: #888;">Trash is empty.</p>
@endif
@if(pending.length === 0 && trash.length === 0)
<div style="text-align: center; padding: 50px;">
<h3>No pending items.</h3>
<p>Go touch grass?</p>
</div>
@endif
<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="/mod/approve?page={!! page - 1 !!}" class="badge badge-secondary">&laquo; Prev</a>
@endif
<span>Page {!! page !!} of {!! pages !!}</span>
@if(page < pages) <a href="/mod/approve?page={!! page + 1 !!}" class="badge badge-secondary">Next &raquo;</a>@endif
</div>
<br>
@endif
<!-- 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 id="modal-reason-container" style="display: none; margin-bottom: 15px; text-align: left;">
<label style="display: block; margin-bottom: 5px; font-size: 0.9em; color: #aaa;">Reason (required):</label>
<textarea id="modal-reason" style="width: 100%; background: #333; color: #fff; border: 1px solid #444; border-radius: 4px; padding: 8px; resize: none;" rows="3" placeholder="Enter reason for denial..."></textarea>
</div>
<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>
(() => {
const modal = document.getElementById('custom-modal');
const modalTitle = document.getElementById('modal-title');
const modalText = document.getElementById('modal-text');
const modalReasonContainer = document.getElementById('modal-reason-container');
const modalReasonInput = document.getElementById('modal-reason');
const btnConfirm = document.getElementById('modal-confirm');
const btnCancel = document.getElementById('modal-cancel');
let pendingAction = null;
const showModal = (title, text, action, showReason = false) => {
modalTitle.innerText = title;
modalText.innerText = text;
pendingAction = action;
if (showReason) {
modalReasonContainer.style.display = 'block';
modalReasonInput.value = '';
modalReasonInput.focus();
} else {
modalReasonContainer.style.display = 'none';
}
modal.style.display = 'flex';
btnConfirm.onclick = async () => {
if (!pendingAction) return;
btnConfirm.disabled = true;
btnConfirm.innerText = 'Processing...';
try {
let reason = null;
if (showReason) {
reason = modalReasonInput.value.trim();
if (!reason) {
modalReasonInput.style.borderColor = '#ff4444';
alert('Please provide a reason for denial');
return;
}
modalReasonInput.style.borderColor = '#444';
}
await pendingAction(reason);
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 itemId = btn.getAttribute('data-id');
const card = btn.closest('.approval-card');
showModal('{!! t('mod.confirm_action') !!}', '{!! t('mod.confirm_action') !!}?', async (reason) => {
const res = await fetch('/mod/deny', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json',
'X-CSRF-Token': window.f0ckSession?.csrf_token || ''
},
body: JSON.stringify({ id: itemId, reason })
});
const data = await res.json();
if (res.ok && data.success) {
if (window.flashMessage) window.flashMessage('{!! t('toast.item_deleted_success') !!}', 2000, 'success');
card.style.opacity = '0';
setTimeout(() => card.remove(), 300);
} else {
if (window.flashMessage) window.flashMessage(data.msg || '{!! t('toast.report_error') !!}', 3000, 'error');
throw new Error(data.msg || 'Request failed');
}
}, true);
});
});
// Single Approve / Restore
document.querySelectorAll('.btn-approve-async').forEach(btn => {
btn.addEventListener('click', async e => {
e.preventDefault();
const itemId = btn.getAttribute('data-id');
const card = btn.closest('.approval-card');
try {
const res = await fetch('/mod/approve', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json',
'X-CSRF-Token': window.f0ckSession?.csrf_token || ''
},
body: JSON.stringify({ id: itemId })
});
const data = await res.json();
if (data.success) {
if (window.flashMessage) window.flashMessage('{!! t('toast.approve_success') !!}', 2000, 'success');
card.style.opacity = '0';
setTimeout(() => card.remove(), 300);
} else {
if (window.flashMessage) window.flashMessage(data.msg || '{!! t('toast.approve_error') !!}', 3000, 'error');
}
} catch (err) {
console.error(err);
if (window.flashMessage) window.flashMessage('{!! t('toast.network_error') !!}', 3000, 'error');
}
});
});
// Rating Toggle
document.querySelectorAll('.btn-rating-toggle-async').forEach(btn => {
btn.addEventListener('click', async e => {
e.preventDefault();
const url = btn.getAttribute('href');
const card = btn.closest('.approval-card');
const tagsContainer = card.querySelector('.approval-card-tags');
btn.style.opacity = '0.5';
btn.style.pointerEvents = 'none';
try {
const res = await fetch(url, {
method: 'PUT',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
});
const data = await res.json();
if (data.success && data.tags) {
// Update tags UI
tagsContainer.innerHTML = '';
data.tags.forEach(tag => {
const span = document.createElement('span');
let badgeClass = 'badge-light';
if (tag.normalized === 'sfw') badgeClass = 'badge-success';
else if (tag.normalized === 'nsfw') badgeClass = 'badge-danger';
else if (tag.tag.startsWith('>')) badgeClass = 'badge-greentext badge-light';
else if (tag.normalized === 'ukraine') badgeClass = 'badge-ukraine badge-light';
else if (/[а-яё]/.test(tag.normalized) || tag.normalized === 'russia') badgeClass = 'badge-russia badge-light';
else if (tag.normalized === 'german') badgeClass = 'badge-german badge-light';
else if (tag.normalized === 'dutch') badgeClass = 'badge-dutch badge-light';
span.className = 'badge ' + badgeClass;
span.innerText = tag.tag;
tagsContainer.appendChild(span);
});
} else {
alert('Error: ' + (data.msg || 'Unknown error'));
}
} catch (err) {
console.error(err);
alert('Network error');
} finally {
btn.style.opacity = '1';
btn.style.pointerEvents = 'auto';
}
});
});
// Purge All Trash
const btnPurgeTrash = document.getElementById('btn-purge-trash');
if (btnPurgeTrash) {
btnPurgeTrash.addEventListener('click', () => {
showModal('Purge Trash', 'Permanently delete ALL items in the trash? This cannot be undone.', async () => {
const res = await fetch('/mod/purge-trash-all', {
method: 'POST',
headers: {
'X-CSRF-Token': window.f0ckSession?.csrf_token || ''
}
});
const data = await res.json();
if (data.success) {
location.reload();
} else {
throw new Error(data.msg || 'Purge failed');
}
});
});
}
})();
</script>
</div>
</div>
</div>
@include(snippets/footer)