esifawafwu
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
{{-- Shared card UI for mod/approve.html and mod/trash.html: styles, confirmation modal, rating toggle and helpers on window.mq (page actions live in each page) --}}
|
||||
<div id="mq-modal" class="mq-modal" style="display:none;">
|
||||
<div class="mq-modal-box">
|
||||
<h3 id="mq-modal-title">Confirm action</h3>
|
||||
<p id="mq-modal-text">Are you sure?</p>
|
||||
<div id="mq-modal-reason-wrap" style="display:none;">
|
||||
<label for="mq-modal-reason">Reason (required)</label>
|
||||
<textarea id="mq-modal-reason" rows="3" placeholder="Enter a reason…"></textarea>
|
||||
</div>
|
||||
<div class="mq-modal-actions">
|
||||
<button type="button" id="mq-modal-cancel" class="mq-btn mq-btn-ghost">Cancel</button>
|
||||
<button type="button" id="mq-modal-confirm" class="mq-btn mq-btn-danger">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Moderation queues — square, flat, theme colours */
|
||||
.mq {
|
||||
--mq-accent: var(--accent, #0096ff);
|
||||
--mq-text: var(--text-color, var(--white, #fff));
|
||||
--mq-muted: var(--text-muted, #8a8f98);
|
||||
--mq-surface: rgba(255, 255, 255, 0.04);
|
||||
--mq-border: rgba(255, 255, 255, 0.1);
|
||||
--mq-danger: #ff5c5c;
|
||||
max-width: 1200px; margin: 0 auto; padding: 24px 16px 60px;
|
||||
text-align: left; color: var(--mq-text); line-height: 1.5;
|
||||
}
|
||||
.mq *, .mq *::before, .mq *::after, .mq-modal *, .mq-modal *::before, .mq-modal *::after { border-radius: 0 !important; }
|
||||
|
||||
.mq-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 18px; }
|
||||
.mq-title { margin: 0; font-size: 1.8em; font-weight: 800; line-height: 1.1; }
|
||||
.mq-subtitle { margin-top: 6px; font-size: 0.85em; color: var(--mq-muted); }
|
||||
|
||||
|
||||
.mq-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
|
||||
.mq-card { display: flex; flex-direction: column; background: var(--mq-surface); border: 1px solid var(--mq-border); transition: opacity 0.3s, border-color 0.15s; }
|
||||
.mq-card:hover { border-color: rgba(255, 255, 255, 0.2); }
|
||||
.mq-media { position: relative; aspect-ratio: 16 / 10; background: #000; display: flex; align-items: center; justify-content: center; overflow: hidden; }
|
||||
.mq-media img, .mq-media video { width: 100%; height: 100%; object-fit: contain; display: block; }
|
||||
.mq-media .embed-responsive { width: 100%; }
|
||||
.mq-trash .mq-media img, .mq-trash .mq-media video { filter: grayscale(50%); }
|
||||
.mq-body { display: flex; flex-direction: column; gap: 8px; padding: 12px; flex: 1; }
|
||||
.mq-meta { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 0.8em; color: var(--mq-muted); }
|
||||
.mq-meta strong { color: var(--mq-text); font-weight: 700; }
|
||||
.mq-reason { padding: 6px 8px; font-size: 0.8em; color: #ffb8b8; background: rgba(255, 92, 92, 0.08); border-left: 2px solid var(--mq-danger); }
|
||||
.mq-tags { display: flex; flex-wrap: wrap; gap: 4px; min-height: 22px; }
|
||||
.mq-tags .badge { margin: 0; }
|
||||
.mq-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: auto; }
|
||||
.mq-actions .mq-btn-rating { grid-column: span 2; }
|
||||
|
||||
.mq-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 34px; margin: 0; padding: 0 12px;
|
||||
font: inherit; font-size: 0.8em; font-weight: 700; cursor: pointer; text-decoration: none; white-space: nowrap;
|
||||
border: 1px solid var(--mq-border); background: transparent; color: var(--mq-text);
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s, opacity 0.15s;
|
||||
}
|
||||
.mq-btn:hover { text-decoration: none; }
|
||||
.mq-btn:disabled { opacity: 0.5; cursor: progress; }
|
||||
.mq-btn-primary { background: var(--mq-accent); border-color: var(--mq-accent); color: #000; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.mq-btn-primary:hover:not(:disabled) { background: transparent; color: var(--mq-accent); }
|
||||
.mq-btn-danger { border-color: rgba(255, 92, 92, 0.5); color: var(--mq-danger); }
|
||||
.mq-btn-danger:hover:not(:disabled) { background: rgba(255, 92, 92, 0.15); }
|
||||
.mq-btn-ghost { color: var(--mq-muted); }
|
||||
.mq-btn-ghost:hover:not(:disabled) { color: var(--mq-text); border-color: rgba(255, 255, 255, 0.25); background: var(--mq-surface); }
|
||||
.mq-btn:focus-visible { outline: 2px solid var(--mq-accent); outline-offset: 2px; }
|
||||
|
||||
.mq-note { margin: 0 0 16px; font-size: 0.85em; color: var(--mq-muted); }
|
||||
.mq-empty { padding: 40px 20px; text-align: center; color: var(--mq-muted); border: 1px dashed var(--mq-border); }
|
||||
.mq-empty strong { display: block; margin-bottom: 4px; color: var(--mq-text); font-size: 1.05em; }
|
||||
|
||||
.mq-pager { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 24px; font-size: 0.85em; color: var(--mq-muted); }
|
||||
|
||||
.mq-modal { position: fixed; inset: 0; z-index: 100100; display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.8); backdrop-filter: blur(4px); padding: 16px; }
|
||||
.mq-modal-box {
|
||||
width: min(420px, 100%); padding: 22px; text-align: left; color: var(--text-color, #fff);
|
||||
background: var(--bg); border: 1px solid var(--accent); box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.mq-modal-box h3 { margin: 0 0 6px; font-size: 1.15em; font-weight: 700; }
|
||||
.mq-modal-box p { margin: 0 0 14px; font-size: 0.88em; color: var(--text-muted, #8a8f98); }
|
||||
.mq-modal-box label { display: block; margin-bottom: 6px; font-size: 0.78em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted, #8a8f98); }
|
||||
.mq-modal-box textarea {
|
||||
width: 100%; box-sizing: border-box; padding: 10px 12px; margin-bottom: 14px; resize: vertical;
|
||||
background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.1); color: inherit; font: inherit; font-size: 0.9em; outline: none;
|
||||
}
|
||||
.mq-modal-box textarea:focus { border-color: var(--accent); }
|
||||
.mq-modal-box textarea.is-invalid { border-color: #ff5c5c; }
|
||||
.mq-modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.mq-modal .mq-btn { height: 38px; padding: 0 16px; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Shared helpers for the moderation card pages. Re-assigned on every (AJAX) page load.
|
||||
window.mq = (() => {
|
||||
const modal = document.getElementById('mq-modal');
|
||||
const title = document.getElementById('mq-modal-title');
|
||||
const text = document.getElementById('mq-modal-text');
|
||||
const reasonWrap = document.getElementById('mq-modal-reason-wrap');
|
||||
const reasonInput = document.getElementById('mq-modal-reason');
|
||||
const btnConfirm = document.getElementById('mq-modal-confirm');
|
||||
const btnCancel = document.getElementById('mq-modal-cancel');
|
||||
|
||||
const csrf = () => (window.f0ckSession && window.f0ckSession.csrf_token) || '';
|
||||
const flash = (msg, type) => { if (window.flashMessage) window.flashMessage(msg, type === 'error' ? 3000 : 2000, type); };
|
||||
|
||||
let pendingAction = null;
|
||||
let needsReason = false;
|
||||
|
||||
const close = () => { modal.style.display = 'none'; pendingAction = null; };
|
||||
const confirm = ({ heading, body, confirmLabel = 'Confirm', withReason = false, action }) => {
|
||||
title.innerText = heading;
|
||||
text.innerText = body;
|
||||
pendingAction = action;
|
||||
needsReason = withReason;
|
||||
btnConfirm.innerText = confirmLabel;
|
||||
btnConfirm.dataset.label = confirmLabel;
|
||||
reasonWrap.style.display = withReason ? 'block' : 'none';
|
||||
reasonInput.value = '';
|
||||
reasonInput.classList.remove('is-invalid');
|
||||
modal.style.display = 'flex';
|
||||
if (withReason) reasonInput.focus();
|
||||
};
|
||||
|
||||
btnConfirm.addEventListener('click', async () => {
|
||||
if (!pendingAction) return;
|
||||
let reason = null;
|
||||
if (needsReason) {
|
||||
reason = reasonInput.value.trim();
|
||||
if (!reason) { reasonInput.classList.add('is-invalid'); reasonInput.focus(); return; }
|
||||
}
|
||||
btnConfirm.disabled = true;
|
||||
btnConfirm.innerText = 'Processing…';
|
||||
try {
|
||||
await pendingAction(reason);
|
||||
close();
|
||||
} catch (e) {
|
||||
flash('Error: ' + e.message, 'error');
|
||||
} finally {
|
||||
btnConfirm.disabled = false;
|
||||
btnConfirm.innerText = btnConfirm.dataset.label || 'Confirm';
|
||||
}
|
||||
});
|
||||
btnCancel.addEventListener('click', close);
|
||||
modal.addEventListener('click', e => { if (e.target === modal) close(); });
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape' && modal.style.display !== 'none') close(); });
|
||||
|
||||
const post = async (url, body) => {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-Token': csrf() },
|
||||
body: JSON.stringify(body || {})
|
||||
});
|
||||
let data = {};
|
||||
try { data = await res.json(); } catch {}
|
||||
if (!res.ok || !data.success) throw new Error(data.msg || ('Request failed (' + res.status + ')'));
|
||||
return data;
|
||||
};
|
||||
|
||||
const removeCard = (card) => {
|
||||
if (!card) return;
|
||||
card.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
card.remove();
|
||||
const grid = document.querySelector('.mq-grid');
|
||||
if (grid && !grid.querySelector('.mq-card')) location.reload();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// Rating toggle (same on both pages)
|
||||
document.querySelectorAll('.btn-rating-toggle-async').forEach(btn => {
|
||||
btn.addEventListener('click', async e => {
|
||||
e.preventDefault();
|
||||
const tagsContainer = btn.closest('.mq-card')?.querySelector('.mq-tags');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch('/api/v2/tags/' + btn.dataset.id + '/toggle', {
|
||||
method: 'PUT',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-Token': csrf() }
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success || !data.tags) throw new Error(data.msg || 'Unknown error');
|
||||
if (tagsContainer) {
|
||||
tagsContainer.innerHTML = '';
|
||||
data.tags.forEach(tag => {
|
||||
let cls = 'badge-light';
|
||||
if (tag.normalized === 'sfw') cls = 'badge-success';
|
||||
else if (tag.normalized === 'nsfw') cls = 'badge-danger';
|
||||
else if (tag.tag.startsWith('>')) cls = 'badge-greentext badge-light';
|
||||
const span = document.createElement('span');
|
||||
span.className = 'badge ' + cls;
|
||||
span.innerText = tag.tag;
|
||||
tagsContainer.appendChild(span);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
flash('Error: ' + err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return { confirm, post, flash, removeCard };
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user