init f0ckm
This commit is contained in:
321
views/mod/approve.html
Normal file
321
views/mod/approve.html
Normal file
@@ -0,0 +1,321 @@
|
||||
@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.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>
|
||||
@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;">
|
||||
<a href="/mod/approve/?id={!! post.id !!}" class="badge badge-success btn-approve-async" style="margin: 0; text-align: center;">Approve</a>
|
||||
<a href="/mod/deny/?id={!! post.id !!}" class="badge badge-danger btn-deny-async" style="margin: 0; text-align: center;">Deny / Delete</a>
|
||||
<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.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>
|
||||
@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;">
|
||||
<a href="/mod/approve/?id={!! post.id !!}" class="badge badge-warning btn-approve-async" style="margin: 0; text-align: center;">Restore</a>
|
||||
@if(session.admin)
|
||||
<a href="/mod/deny/?id={!! post.id !!}" class="badge badge-danger btn-deny-async" style="margin: 0; text-align: center;">Purge</a>
|
||||
@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">« Prev</a>
|
||||
@endif
|
||||
<span>Page {!! page !!} of {!! pages !!}</span>
|
||||
@if(page < pages) <a href="/mod/approve?page={!! page + 1 !!}" class="badge badge-secondary">Next »</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 url = btn.getAttribute('href');
|
||||
const card = btn.closest('.approval-card');
|
||||
showModal('{!! t('mod.confirm_action') !!}', '{!! t('mod.confirm_action') !!}?', async (reason) => {
|
||||
const res = await fetch(url + (url.indexOf('?') > -1 ? '&' : '?') + 'reason=' + encodeURIComponent(reason), {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
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 url = btn.getAttribute('href');
|
||||
const card = btn.closest('.approval-card'); // Updated selector
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
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' });
|
||||
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)
|
||||
236
views/mod/audit.html
Normal file
236
views/mod/audit.html
Normal file
@@ -0,0 +1,236 @@
|
||||
@include(snippets/header)
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<h1>AUDIT LOG</h1>
|
||||
<p>Actions performed by moderators and admins.</p>
|
||||
<hr>
|
||||
<div class="audit-grid" id="audit-grid">
|
||||
@each(logs as entry)
|
||||
<div class="audit-card">
|
||||
<div class="audit-card-header">
|
||||
<div class="audit-card-user">
|
||||
<a href="/user/{!! entry.username !!}">{!! entry.username !!}</a>
|
||||
</div>
|
||||
<div class="audit-card-time">{!! entry.created_at_fmt !!}</div>
|
||||
</div>
|
||||
<div class="audit-card-body">
|
||||
<div class="audit-card-row">
|
||||
<span class="audit-label">Action:</span>
|
||||
<span class="badge badge-secondary badge-action">{!! entry.action !!}</span>
|
||||
</div>
|
||||
<div class="audit-card-row">
|
||||
<span class="audit-label">Target:</span>
|
||||
<span class="audit-value">
|
||||
<strong>{!! entry.target_type !!}</strong>
|
||||
@if(entry.target_type === 'comment')
|
||||
@if(entry.item_id)
|
||||
<a href="/{!! entry.item_id !!}#c{!! entry.target_id !!}">#c{!! entry.target_id !!}</a>
|
||||
@else
|
||||
#c{!! entry.target_id !!}
|
||||
@endif
|
||||
@else
|
||||
@if(entry.target_type === 'item')
|
||||
<a href="/{!! entry.target_id !!}">/{!! entry.target_id !!}</a>
|
||||
@else
|
||||
{!! entry.target_id !!}
|
||||
@endif
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if(entry.reason)
|
||||
<div class="audit-card-row">
|
||||
<span class="audit-label">Reason:</span>
|
||||
<span class="audit-value">{!! entry.reason !!}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(entry.uploader_info)
|
||||
<div class="audit-card-row">
|
||||
<span class="audit-label">Info:</span>
|
||||
<span class="audit-value" style="color: #ffb8b8;">{!! entry.uploader_info !!}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(entry.old_content !== null || entry.new_content !== null || entry.details_json)
|
||||
<div class="audit-card-row" style="margin-top: 5px; flex-direction: column; align-items: stretch;">
|
||||
<span class="audit-label">Changes:</span>
|
||||
<div class="audit-diff">
|
||||
@if(entry.old_content)
|
||||
<div class="diff-removed">- {!! entry.old_content !!}</div>
|
||||
@endif
|
||||
@if(entry.new_content)
|
||||
<div class="diff-added">+ {!! entry.new_content !!}</div>
|
||||
@endif
|
||||
@if(entry.details_json)
|
||||
<div class="audit-details-json"
|
||||
style="font-size: 0.85em; color: #aaa; margin-top: 5px; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 5px;">
|
||||
{!! entry.details_json !!}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endeach
|
||||
</div>
|
||||
|
||||
<div id="audit-loading" style="text-align: center; padding: 20px; display: none;">
|
||||
<span class="loading-spinner">Loading more logs...</span>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@if(typeof pages !== 'undefined' && pages > 1)
|
||||
<div class="pagination-container" id="audit-pagination"
|
||||
style="display: flex; gap: 10px; align-items: center; justify-content: center;">
|
||||
@if(page > 1)
|
||||
<a href="/mod/audit?page={!! page - 1 !!}" class="badge badge-secondary">« Prev</a>
|
||||
@endif
|
||||
<span>Page <span id="current-page-display">{!! page !!}</span> of {!! pages !!}</span>
|
||||
@if(page < pages) <a href="/mod/audit?page={!! page + 1 !!}" id="next-page-link" class="badge badge-secondary">Next »</a>@endif
|
||||
</div>
|
||||
<br>
|
||||
@endif
|
||||
<script>
|
||||
(function () {
|
||||
var grid = document.getElementById('audit-grid');
|
||||
var loader = document.getElementById('audit-loading');
|
||||
var pagination = document.getElementById('audit-pagination');
|
||||
|
||||
var escapeHtml = function (unsafe) {
|
||||
return (unsafe || '')
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
var renderDiff = function (oldText, newText) {
|
||||
var isEmpty = function (t) { return !t || t === 'null' || t === 'undefined'; };
|
||||
if (isEmpty(oldText) && isEmpty(newText)) return '';
|
||||
|
||||
var html = '<div class="audit-diff">';
|
||||
if (!isEmpty(oldText)) html += '<div class="diff-removed">- ' + escapeHtml(oldText) + '</div>';
|
||||
if (!isEmpty(newText)) html += '<div class="diff-added">+ ' + escapeHtml(newText) + '</div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
};
|
||||
|
||||
var currentPage = Number('{{ page }}') || 1;
|
||||
var totalPages = Number('{{ pages }}') || 1;
|
||||
var loading = false;
|
||||
var hasMore = currentPage < totalPages;
|
||||
|
||||
if (pagination) pagination.style.display = 'none';
|
||||
|
||||
window.addEventListener('scroll', function () {
|
||||
if (loading || !hasMore) return;
|
||||
|
||||
var scrollPosition = window.innerHeight + window.scrollY;
|
||||
var threshold = document.documentElement.scrollHeight - 500;
|
||||
|
||||
if (scrollPosition > threshold) {
|
||||
loadMore();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
loading = true;
|
||||
if (loader) loader.style.display = 'block';
|
||||
|
||||
try {
|
||||
var next = currentPage + 1;
|
||||
var res = await fetch('/mod/audit?page=' + next, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
var data = await res.json();
|
||||
|
||||
if (data.success && data.logs && data.logs.length) {
|
||||
data.logs.forEach(function (log) {
|
||||
var card = document.createElement('div');
|
||||
card.className = 'audit-card';
|
||||
|
||||
var reason = log.reason;
|
||||
|
||||
var html = '<div class="audit-card-header">' +
|
||||
'<div class="audit-card-user">' +
|
||||
'<a href="/user/' + encodeURIComponent(log.username) + '">' + log.username + '</a>' +
|
||||
'</div>' +
|
||||
'<div class="audit-card-time">' + (log.created_at || '') + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="audit-card-body">' +
|
||||
'<div class="audit-card-row">' +
|
||||
'<span class="audit-label">Action:</span>' +
|
||||
'<span class="badge badge-secondary badge-action">' + (log.action || '') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="audit-card-row">' +
|
||||
'<span class="audit-label">Target:</span>' +
|
||||
'<span class="audit-value">' +
|
||||
'<strong>' + (log.target_type || '') + '</strong> ';
|
||||
|
||||
if (log.target_type === 'comment') {
|
||||
if (log.item_id) {
|
||||
html += '<a href="/' + log.item_id + '#c' + log.target_id + '">#c' + log.target_id + '</a>';
|
||||
} else {
|
||||
html += '#c' + (log.target_id || '');
|
||||
}
|
||||
} else if (log.target_type === 'item') {
|
||||
html += '<a href="/' + log.target_id + '">/' + log.target_id + '</a>';
|
||||
} else {
|
||||
html += (log.target_id || '');
|
||||
}
|
||||
html += '</span></div>';
|
||||
|
||||
if (reason) {
|
||||
html += '<div class="audit-card-row">' +
|
||||
'<span class="audit-label">Reason:</span>' +
|
||||
'<span class="audit-value">' + escapeHtml(reason) + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
if (log.uploader_info) {
|
||||
html += '<div class="audit-card-row">' +
|
||||
'<span class="audit-label">Info:</span>' +
|
||||
'<span class="audit-value" style="color: #ffb8b8;">' + escapeHtml(log.uploader_info) + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
if (log.old_content || log.new_content || log.details_json) {
|
||||
html += '<div class="audit-card-row" style="margin-top: 5px; flex-direction: column; align-items: stretch;">' +
|
||||
'<span class="audit-label">Changes:</span>' +
|
||||
'<div class="audit-diff">' +
|
||||
(log.old_content ? '<div class="diff-removed">- ' + escapeHtml(log.old_content) + '</div>' : '') +
|
||||
(log.new_content ? '<div class="diff-added">+ ' + escapeHtml(log.new_content) + '</div>' : '') +
|
||||
(log.details_json ? '<div class="audit-details-json" style="font-size: 0.85em; color: #aaa; margin-top: 5px; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 5px;">' + escapeHtml(log.details_json) + '</div>' : '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
card.innerHTML = html;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
currentPage = data.page;
|
||||
hasMore = data.hasMore;
|
||||
if (document.getElementById('current-page-display')) {
|
||||
document.getElementById('current-page-display').innerText = currentPage;
|
||||
}
|
||||
} else {
|
||||
hasMore = false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Audit infinite scroll error:', err);
|
||||
hasMore = false;
|
||||
} finally {
|
||||
loading = false;
|
||||
if (loader) loader.style.display = 'none';
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
@include(snippets/footer)
|
||||
81
views/mod/motd.html
Normal file
81
views/mod/motd.html
Normal file
@@ -0,0 +1,81 @@
|
||||
@include(snippets/header)
|
||||
<div class="pagewrapper">
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<h2>Moderator Message of the Day (MOTD)</h2>
|
||||
<p style="color: #ccc; margin-bottom: 20px;">This message is displayed site-wide. It will automatically be tagged with your name (<code>t. {!! session.display_name || session.user !!}</code>).</p>
|
||||
|
||||
<div class="admin-motd-form">
|
||||
<form id="motd-form" action="/mod/motd" method="POST" onsubmit="event.preventDefault(); saveMotd(this);">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label for="motd-text" style="display: block; margin-bottom: 8px; color: var(--accent);">MOTD Content (Markdown supported)</label>
|
||||
<textarea id="motd-text" name="motd" style="width: 100%; min-height: 200px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); color: #fff; padding: 15px; border-radius: 4px; font-family: inherit; font-size: 1.1em; resize: vertical;">{!! motd !!}</textarea>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<button type="submit" class="btn-primary" style="width: auto; padding: 10px 30px;">Save MOTD</button>
|
||||
<button type="button" class="btn-remove" style="width: auto; padding: 10px 20px;" onclick="document.getElementById('motd-text').value=''; saveMotd(document.getElementById('motd-form'));">Clear MOTD</button>
|
||||
<span id="motd-status" style="margin-left: 10px; font-weight: bold; display: none;"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 40px; padding: 20px; background: rgba(0,0,0,0.2); border-left: 4px solid var(--accent); border-radius: 4px;">
|
||||
<h4 style="color: var(--accent); margin-top: 0;">Preview Tip</h4>
|
||||
<p style="margin-bottom: 0;">You can use <strong>Markdown</strong> syntax like <code># Header</code>, <code>**bold**</code>, <code>[links](url)</code>, or standard HTML. Your username will be appended as a signature.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function saveMotd(form) {
|
||||
const status = document.getElementById('motd-status');
|
||||
const textarea = document.getElementById('motd-text');
|
||||
|
||||
status.textContent = 'Saving...';
|
||||
status.style.color = 'var(--accent)';
|
||||
status.style.display = 'inline';
|
||||
|
||||
try {
|
||||
const res = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
||||
},
|
||||
body: new URLSearchParams(new FormData(form))
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
status.textContent = 'Saved!';
|
||||
status.style.color = '#28a745';
|
||||
|
||||
// Update textarea with the actual saved MOTD (which includes the tag)
|
||||
if (data.motd) {
|
||||
textarea.value = data.motd;
|
||||
}
|
||||
|
||||
if (typeof window.updateMotdUI === 'function') {
|
||||
window['motd_dismissed'] = false; // Force show on save
|
||||
window.updateMotdUI(data.motd || textarea.value);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
status.style.display = 'none';
|
||||
}, 2000);
|
||||
} else {
|
||||
throw new Error(data.msg || 'Unknown error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('MOTD Save Error:', err);
|
||||
status.textContent = 'Error: ' + err.message;
|
||||
status.style.color = '#d9534f';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
@include(snippets/footer)
|
||||
Reference in New Issue
Block a user