gfds
This commit is contained in:
+2
-1
@@ -27,6 +27,7 @@
|
||||
<a href="/admin/approve" class="adm-stat adm-stat-link @if(totals.pending > 0) is-hot @endif"><span class="adm-stat-num">{{ totals.pending }}</span><span class="adm-stat-label">Pending approval</span></a>
|
||||
<a href="/mod/trash" class="adm-stat adm-stat-link"><span class="adm-stat-num">{{ dash.trash }}</span><span class="adm-stat-label">Soft deleted</span></a>
|
||||
<a href="/mod/reports" class="adm-stat adm-stat-link @if(dash.open_reports > 0) is-hot @endif"><span class="adm-stat-num">{{ dash.open_reports }}</span><span class="adm-stat-label">Open reports</span></a>
|
||||
<a href="/admin/tokens#invite-requests" class="adm-stat adm-stat-link @if(dash.invite_requests > 0) is-hot @endif"><span class="adm-stat-num">{{ dash.invite_requests }}</span><span class="adm-stat-label">Token requests</span></a>
|
||||
<a href="/admin/users" class="adm-stat adm-stat-link"><span class="adm-stat-num">{{ dash.users }}</span><span class="adm-stat-label">Users <small>+{{ dash.anon_users }} anon</small></span></a>
|
||||
</div>
|
||||
@endif
|
||||
@@ -72,7 +73,7 @@
|
||||
</a>
|
||||
<a href="/admin/tokens" class="adm-tool">
|
||||
<i class="fa-solid fa-ticket"></i>
|
||||
<span class="adm-tool-text"><strong>Invite Tokens</strong><span>Create and revoke invites</span></span>
|
||||
<span class="adm-tool-text"><strong>Invite Tokens @if(dash.invite_requests > 0)<span class="adm-count">{{ dash.invite_requests }}</span>@endif</strong><span>Create and revoke invites, review token requests</span></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
+166
-9
@@ -8,6 +8,34 @@
|
||||
<button id="generate-token" class="btn-upload" style="width: auto; padding: 10px 20px;">Generate New Token</button>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════ Invite Requests ═══════════════════ -->
|
||||
<div id="invite-requests" style="margin-bottom: 30px; scroll-margin-top: calc(var(--navbar-h, 50px) + 16px);">
|
||||
<h3 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
|
||||
<i class="fa-solid fa-inbox" style="color: var(--accent);"></i> Invite Requests
|
||||
<span id="invite-req-count" style="display: none; background: #ff6b6b; color: white; font-size: 0.7em; padding: 2px 8px; border-radius: 10px; font-weight: 600;"></span>
|
||||
</h3>
|
||||
<div class="upload-form">
|
||||
<table class="responsive-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Reason</th>
|
||||
<th>Status</th>
|
||||
<th>Submitted</th>
|
||||
<th>Reviewed By</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="invite-req-list">
|
||||
<tr><td colspan="6" style="text-align: center; color: var(--text-muted);">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
|
||||
<i class="fa-solid fa-ticket" style="color: var(--accent);"></i> Token List
|
||||
</h3>
|
||||
<div class="upload-form">
|
||||
<table class="responsive-table">
|
||||
<thead>
|
||||
@@ -29,14 +57,137 @@
|
||||
|
||||
<script>
|
||||
window.f0ckDebug = window.f0ckDebug || (() => {});
|
||||
const loadTokens = async () => {
|
||||
|
||||
// ═══════════════════ Invite Requests ═══════════════════
|
||||
window.loadInviteRequests = async () => {
|
||||
const tbody = document.getElementById('invite-req-list');
|
||||
try {
|
||||
const res = await fetch('/api/v2/admin/invite-requests');
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
const countBadge = document.getElementById('invite-req-count');
|
||||
if (!tbody) return;
|
||||
|
||||
const pendingCount = data.requests.filter(r => r.status === 'pending').length;
|
||||
if (countBadge) {
|
||||
if (pendingCount > 0) {
|
||||
countBadge.textContent = pendingCount;
|
||||
countBadge.style.display = '';
|
||||
} else {
|
||||
countBadge.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (data.requests.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: var(--text-muted);">No invite requests yet.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = data.requests.map(r => {
|
||||
|
||||
const reason = r.reason ? '<span title="' + r.reason.replace(/"/g, '"') + '">' + (r.reason.length > 60 ? r.reason.slice(0, 60) + '…' : r.reason) + '</span>' : '<span style="color: var(--text-muted); font-style: italic;">No reason given</span>';
|
||||
|
||||
let statusBadge;
|
||||
if (r.status === 'pending') {
|
||||
statusBadge = '<span style="color: #ffc107; font-weight: 600;"><i class="fa-solid fa-clock"></i> Pending</span>';
|
||||
} else if (r.status === 'approved') {
|
||||
statusBadge = '<span style="color: #51cf66; font-weight: 600;"><i class="fa-solid fa-circle-check"></i> Approved</span>';
|
||||
} else if (r.status === 'revoked') {
|
||||
statusBadge = '<span style="color: #e67700; font-weight: 600;"><i class="fa-solid fa-ban"></i> Revoked</span>';
|
||||
} else {
|
||||
statusBadge = '<span style="color: #ff6b6b; font-weight: 600;"><i class="fa-solid fa-circle-xmark"></i> Denied</span>';
|
||||
}
|
||||
|
||||
let actions = '';
|
||||
if (r.status === 'pending') {
|
||||
actions =
|
||||
'<div style="display: flex; gap: 6px; flex-wrap: wrap;">' +
|
||||
'<button onclick="approveInviteReq(' + r.id + ')" class="btn-upload" style="padding: 5px 12px; font-size: 0.8em; background: #51cf66; border-color: #51cf66;"><i class="fa-solid fa-check"></i> Approve</button>' +
|
||||
'<button onclick="denyInviteReq(' + r.id + ')" class="btn-remove" style="padding: 5px 12px; font-size: 0.8em;"><i class="fa-solid fa-xmark"></i> Deny</button>' +
|
||||
'</div>';
|
||||
} else {
|
||||
actions = '<span style="color: var(--text-muted); font-size: 0.85em;">' + (r.reviewed_at ? new Date(r.reviewed_at).toLocaleString() : '') + '</span>';
|
||||
}
|
||||
|
||||
const username = r.requester_login || r.requester_name || (r.fingerprint ? r.fingerprint.slice(0, 12) + '…' : '—');
|
||||
const userCell = r.requester_login
|
||||
? '<a href="/user/' + r.requester_login + '" style="color: #5bc0be; text-decoration: none; font-weight: 600;">' + username + '</a>'
|
||||
: '<span style="color: var(--text-muted);">' + username + '</span>';
|
||||
|
||||
return '<tr' + (r.status === 'pending' ? ' style="background: rgba(255,193,7,0.04);"' : '') + '>' +
|
||||
'<td data-label="User">' + userCell + '</td>' +
|
||||
'<td data-label="Reason" style="max-width: 250px;">' + reason + '</td>' +
|
||||
'<td data-label="Status">' + statusBadge + '</td>' +
|
||||
'<td data-label="Submitted">' + new Date(r.created_at).toLocaleString() + '</td>' +
|
||||
'<td data-label="Reviewed By">' + (r.reviewed_by_name || '—') + '</td>' +
|
||||
'<td data-label="Actions">' + actions + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
} else {
|
||||
if (tbody) tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: var(--text-muted);">Failed to load invite requests.</td></tr>';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load invite requests:', e);
|
||||
if (tbody) tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: var(--text-muted);">Failed to load invite requests.</td></tr>';
|
||||
}
|
||||
};
|
||||
|
||||
window.approveInviteReq = async (id) => {
|
||||
if (!confirm('Approve this invite request? A token will be generated and sent to the user.')) return;
|
||||
try {
|
||||
const res = await fetch('/api/v2/admin/invite-requests/approve', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
||||
},
|
||||
body: JSON.stringify({ id })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
window.loadInviteRequests();
|
||||
window.loadTokens();
|
||||
} else {
|
||||
alert('Failed: ' + (data.msg || 'Unknown error'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
window.denyInviteReq = async (id) => {
|
||||
if (!confirm('Deny this invite request?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/v2/admin/invite-requests/deny', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
||||
},
|
||||
body: JSON.stringify({ id })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
window.loadInviteRequests();
|
||||
} else {
|
||||
alert('Failed: ' + (data.msg || 'Unknown error'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// ═══════════════════ Tokens ═══════════════════
|
||||
window.loadTokens = async () => {
|
||||
const tbody = document.getElementById('token-list');
|
||||
try {
|
||||
window.f0ckDebug('Loading tokens...');
|
||||
const res = await fetch('/api/v2/admin/tokens');
|
||||
const data = await res.json();
|
||||
window.f0ckDebug('Tokens data:', data);
|
||||
if (data.success) {
|
||||
const tbody = document.getElementById('token-list');
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = data.tokens.map(t =>
|
||||
'<tr>' +
|
||||
@@ -57,11 +208,16 @@
|
||||
'</td>' +
|
||||
'</tr>'
|
||||
).join('');
|
||||
} else {
|
||||
if (tbody) tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; color: var(--text-muted);">Failed to load tokens.</td></tr>';
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (tbody) tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; color: var(--text-muted);">Failed to load tokens.</td></tr>';
|
||||
}
|
||||
};
|
||||
|
||||
const generateToken = async () => {
|
||||
window.generateToken = async () => {
|
||||
window.f0ckDebug('Generating...');
|
||||
try {
|
||||
const res = await fetch('/api/v2/admin/tokens/create', {
|
||||
@@ -71,7 +227,7 @@
|
||||
const data = await res.json();
|
||||
window.f0ckDebug('Gen result:', data);
|
||||
if (data.success) {
|
||||
loadTokens();
|
||||
window.loadTokens();
|
||||
} else {
|
||||
alert('Failed: ' + data.msg);
|
||||
}
|
||||
@@ -81,7 +237,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
const deleteToken = async (id) => {
|
||||
window.deleteToken = async (id) => {
|
||||
if (!confirm('Delete this token?')) return;
|
||||
const res = await fetch('/api/v2/admin/tokens/delete', {
|
||||
method: 'POST',
|
||||
@@ -93,13 +249,14 @@
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
loadTokens();
|
||||
window.loadTokens();
|
||||
}
|
||||
};
|
||||
|
||||
const btnGenToken = document.getElementById('generate-token');
|
||||
if (btnGenToken) btnGenToken.addEventListener('click', generateToken);
|
||||
loadTokens();
|
||||
if (btnGenToken) btnGenToken.addEventListener('click', window.generateToken);
|
||||
window.loadTokens();
|
||||
window.loadInviteRequests();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
.status-active { background: linear-gradient(135deg, #28a745, #20c997); color: #fff; }
|
||||
.status-pending { background: linear-gradient(135deg, #f08c00, #ffc107); color: #000; }
|
||||
.status-banned { background: linear-gradient(135deg, #e03131, #f03e3e); color: #fff; }
|
||||
.status-system { background: #5c0a0a; color: #ffb3b3; border: 1px solid #8b1a1a; }
|
||||
|
||||
.method-tag {
|
||||
font-size: 0.7rem;
|
||||
@@ -329,7 +330,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 0 15px;">
|
||||
<!-- .container: the admin skin's 1200px column, same width and gutters as the back bar above -->
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 25px; gap: 20px; flex-wrap: wrap;">
|
||||
<div>
|
||||
<h2 style="margin: 0; font-weight: 800; letter-spacing: -0.5px;">User Management</h2>
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
<td data-label="Status" id="status-cell-{{ u.id || 'ghost-' + u.login }}">
|
||||
@if(!u.id)
|
||||
<span class="status-badge" style="background: rgba(255,255,255,0.05); color: #888; border: 1px dashed rgba(255,255,255,0.1);">Legacy</span>
|
||||
@elseif(u.login === 'deleted_user')
|
||||
{{-- System ghost that deleted users' content is reassigned to; it is banned only so it can't log in --}}
|
||||
<span class="status-badge status-system" title="System account: holds content of deleted users">System</span>
|
||||
@else
|
||||
@if(u.banned)
|
||||
<span class="status-badge status-banned">Banned</span>
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
@else
|
||||
<button class="notif-page-tab active" data-tab="system">{{ t('nav.notif_tab_system') }}</button>
|
||||
@endif
|
||||
@if(session && session.admin)
|
||||
<button class="notif-page-tab @if(activeTab === 'admin') active @endif" data-tab="admin">Admin</button>
|
||||
@endif
|
||||
</div>
|
||||
<div id="notifications-container" class="posts notifications-list-full" data-page="{{ pagination.page }}" data-tab="{{ activeTab }}">
|
||||
@include(snippets/notifications-list)
|
||||
@@ -55,16 +58,18 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Mark all read
|
||||
// Mark all read (current tab only)
|
||||
const btn = document.getElementById('mark-all-read-page');
|
||||
if (btn) {
|
||||
btn.onclick = async () => {
|
||||
const res = await fetch('/api/notifications/read', { method: 'POST' });
|
||||
const activeTabEl = document.querySelector('.notif-page-tab.active');
|
||||
const tab = activeTabEl ? activeTabEl.dataset.tab : container.dataset.tab;
|
||||
const res = await fetch('/api/notifications/read?tab=' + encodeURIComponent(tab), { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
document.querySelectorAll('.notif-item.unread').forEach(el => el.classList.remove('unread'));
|
||||
if (window.NotificationSystemInstance) {
|
||||
window.NotificationSystemInstance.markAllReadUI();
|
||||
window.NotificationSystemInstance.markAllReadUI(tab);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
@include(snippets/header)
|
||||
<div class="pagewrapper">
|
||||
<div id="main">
|
||||
<style>
|
||||
/* No blurred item backdrop here: the #bg canvas keeps the last item's image after AJAX navigation.
|
||||
Lives in this page's <style>, so it is gone again as soon as another page is loaded. */
|
||||
body:has(#main > .container.adm.up) #bg { opacity: 0 !important; visibility: hidden !important; }
|
||||
</style>
|
||||
<div class="container adm up">
|
||||
|
||||
<header class="adm-header">
|
||||
|
||||
+91
-5
@@ -1,14 +1,100 @@
|
||||
@include(snippets/header)
|
||||
<div class="pagewrapper">
|
||||
<div id="main" class="ranking-page">
|
||||
<div class="topf0ckers">
|
||||
<h3>{{ t('ranking.title') }}</h3>
|
||||
</div>
|
||||
<style>
|
||||
/* Ranking: style guide (square, flat, theme colours, no glow). Scoped to .ranking-page so it also
|
||||
applies inside the page modal, which lifts this #main (styles included) over the current page. */
|
||||
.ranking-page {
|
||||
--rk-accent: var(--accent, #0096ff);
|
||||
--rk-text: var(--text-color, var(--white, #fff));
|
||||
--rk-muted: var(--text-muted, #8a8f98);
|
||||
--rk-surface: rgba(255, 255, 255, 0.04);
|
||||
--rk-border: rgba(255, 255, 255, 0.1);
|
||||
--rk-gold: #ffd700; --rk-silver: #c0c0c0; --rk-bronze: #cd7f32;
|
||||
}
|
||||
.ranking-page *, .ranking-page *::before, .ranking-page *::after { border-radius: 0 !important; }
|
||||
|
||||
/* Podium: flat tiles, medal colour as frame + label, no bounce, no glow */
|
||||
.ranking-page .ranking-podium { background: none; gap: 16px; margin: 0 0 20px; padding: 0; align-items: stretch; }
|
||||
.ranking-page .podium-item {
|
||||
flex: 1 1 0; max-width: 240px; padding: 22px 14px 16px; box-sizing: border-box;
|
||||
background: var(--rk-surface); border: 1px solid var(--rk-border); border-top: 3px solid var(--medal, var(--rk-accent));
|
||||
justify-content: flex-start; transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
.ranking-page .podium-item:hover { transform: none; background: rgba(255, 255, 255, 0.06); border-color: var(--medal, var(--rk-accent)); }
|
||||
.ranking-page .podium-item.rank-1 { --medal: var(--rk-gold); transform: none; padding-top: 40px; }
|
||||
.ranking-page .podium-item.rank-1:hover { transform: none; }
|
||||
.ranking-page .podium-item.rank-2 { --medal: var(--rk-silver); }
|
||||
.ranking-page .podium-item.rank-3 { --medal: var(--rk-bronze); }
|
||||
.ranking-page .podium-avatar,
|
||||
.ranking-page :is(.rank-1, .rank-2, .rank-3) .podium-avatar {
|
||||
width: 96px; height: 96px; border: 2px solid var(--medal, var(--rk-accent)); box-shadow: none; background: #000;
|
||||
}
|
||||
.ranking-page .rank-1 .podium-avatar { width: 120px; height: 120px; }
|
||||
.ranking-page .podium-crown { top: -34px; filter: none; transform: translateX(-50%); font-size: 1.6em; }
|
||||
.ranking-page .podium-avatar-wrapper { margin-bottom: 18px; }
|
||||
.ranking-page .podium-rank-label,
|
||||
.ranking-page :is(.rank-1, .rank-2, .rank-3) .podium-rank-label {
|
||||
bottom: -11px; padding: 2px 10px; font-size: 0.85em; font-weight: 800; letter-spacing: 0.06em;
|
||||
background: var(--medal, var(--rk-accent)); color: #000; box-shadow: none;
|
||||
}
|
||||
.ranking-page .podium-name { font-size: 1.15em; text-shadow: none; color: var(--rk-text); overflow-wrap: anywhere; }
|
||||
.ranking-page .podium-count,
|
||||
.ranking-page :is(.rank-1, .rank-2, .rank-3) .podium-count {
|
||||
color: var(--rk-muted); font-size: 0.72em; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.ranking-page .clean-table { background: transparent; border: 1px solid var(--rk-border); border-collapse: collapse; }
|
||||
.ranking-page .clean-table th {
|
||||
padding: 10px 12px; font-size: 0.72em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em;
|
||||
color: var(--rk-muted); background: var(--rk-surface); border-bottom: 1px solid var(--rk-border);
|
||||
}
|
||||
.ranking-page .clean-table td { padding: 10px 12px; border-bottom: 1px solid var(--rk-border); color: var(--rk-text); line-height: 1.4; }
|
||||
.ranking-page .clean-table tbody tr { transition: background 0.12s; }
|
||||
.ranking-page .clean-table tbody tr:hover { background: var(--rk-surface); }
|
||||
.ranking-page .rank-cell { color: var(--rk-accent) !important; font-variant-numeric: tabular-nums; }
|
||||
.ranking-page .count-cell, .ranking-page .stats-table td:last-child, .ranking-page .f0cks-table td:last-child { font-variant-numeric: tabular-nums; }
|
||||
.ranking-page .rank-avatar { border: 1px solid var(--rk-border); }
|
||||
|
||||
/* Side panels */
|
||||
.ranking-page .section-small { gap: 20px; }
|
||||
.ranking-page .stats-box-simple {
|
||||
background: var(--rk-surface); border: 1px solid var(--rk-border); box-shadow: none; padding: 16px; box-sizing: border-box;
|
||||
}
|
||||
.ranking-page .stats-box-simple h3 {
|
||||
display: flex; align-items: center; gap: 8px; margin: 0 0 4px; padding: 0 0 10px;
|
||||
font-size: 0.78em; font-weight: 800; letter-spacing: 0.12em; color: var(--rk-muted);
|
||||
border-bottom: 1px solid var(--rk-border);
|
||||
}
|
||||
.ranking-page .stats-box-simple h3::before { content: ''; width: 3px; height: 10px; background: var(--rk-accent); flex-shrink: 0; }
|
||||
.ranking-page .stats-box-simple .clean-table { border: 0; margin-top: 0; }
|
||||
.ranking-page .stats-box-simple .clean-table td { padding: 8px 0; }
|
||||
.ranking-page .stats-box-simple .clean-table tbody tr:hover { background: transparent; }
|
||||
.ranking-page .stats-box-simple .clean-table tbody tr:last-child td { border-bottom: 0; }
|
||||
/* Both columns start on the same top line */
|
||||
.ranking-page .ranking-wrapper { gap: 28px; margin-top: 0; align-items: flex-start; }
|
||||
.ranking-page .section-big > .clean-table:first-child { margin-top: 0; }
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.ranking-page .ranking-podium { flex-direction: row; align-items: stretch; gap: 10px; padding-top: 0; }
|
||||
.ranking-page .podium-item { padding: 18px 8px 12px; }
|
||||
.ranking-page .podium-item.rank-1 { padding-top: 34px; }
|
||||
.ranking-page .podium-avatar, .ranking-page :is(.rank-1, .rank-2, .rank-3) .podium-avatar { width: 64px; height: 64px; }
|
||||
.ranking-page .rank-1 .podium-avatar { width: 80px; height: 80px; }
|
||||
.ranking-page .podium-item.rank-1 { order: 2 !important; }
|
||||
.ranking-page .podium-item.rank-2 { order: 1 !important; }
|
||||
.ranking-page .podium-item.rank-3 { order: 3 !important; }
|
||||
.ranking-page .podium-name { font-size: 0.95em; }
|
||||
}
|
||||
@media (max-width: 420px) {
|
||||
.ranking-page .ranking-podium { flex-direction: column; align-items: stretch; gap: 26px; }
|
||||
.ranking-page .podium-item { max-width: none; order: unset !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="ranking-wrapper">
|
||||
<div class="section-big">
|
||||
<h3>{{ t('ranking.top_contributors') }}</h3>
|
||||
|
||||
@if(list.length >= 3)
|
||||
<div class="ranking-podium">
|
||||
@for(let i = 0; i < 3; i++)
|
||||
|
||||
@@ -1107,6 +1107,9 @@
|
||||
@else
|
||||
<button class="notif-tab active" data-tab="system">{{ t('nav.notif_tab_system') }} <span class="notif-tab-badge" id="scroller-notif-tab-badge-system" style="display:none">0</span></button>
|
||||
@endif
|
||||
@if(typeof session !== 'undefined' && session && session.admin)
|
||||
<button class="notif-tab" data-tab="admin"><i class="fa-solid fa-shield-halved" style="font-size: 0.85em;"></i> Admin <span class="notif-tab-badge" id="scroller-notif-tab-badge-admin" style="display:none">0</span></button>
|
||||
@endif
|
||||
</div>
|
||||
<button id="scroller-mark-all-read" title="{{ t('nav.mark_all_read') }}"><i class="fa-solid fa-check-double"></i></button>
|
||||
</div>
|
||||
|
||||
+982
-253
File diff suppressed because it is too large
Load Diff
@@ -220,7 +220,8 @@
|
||||
}
|
||||
#report-modal .report-modal-content * { border-radius: 0 !important; }
|
||||
#report-modal .rm-header { position: relative; flex-shrink: 0; padding: 20px 64px 14px 24px; border-bottom: 1px solid var(--rm-border); }
|
||||
#report-modal .rm-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 16px 24px; }
|
||||
#report-modal .rm-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 16px 24px; scrollbar-width: none !important; -ms-overflow-style: none; }
|
||||
#report-modal .rm-body::-webkit-scrollbar { display: none; width: 0; height: 0; }
|
||||
#report-modal .rm-body > :last-child { margin-bottom: 0; }
|
||||
#report-modal .rm-header h3 { margin: 0; font-size: 1.25em; font-weight: 700; color: var(--rm-text); }
|
||||
#report-modal .rm-header p { margin: 6px 0 0; font-size: 0.85em; line-height: 1.5; color: var(--rm-muted); }
|
||||
@@ -417,11 +418,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="global-sidebar-right-footer">
|
||||
@if(session)
|
||||
<a href="/ranking" data-page-modal title="{{ t('footer.ranking') }}" aria-label="{{ t('footer.ranking') }}"><i class="fa-solid fa-users-between-lines"></i></a>
|
||||
@endif
|
||||
<a href="/rules" data-page-modal title="{{ t('footer.rules') }}" aria-label="{{ t('footer.rules') }}"><i class="fa-solid fa-scale-balanced"></i></a>
|
||||
<a href="/about" data-page-modal title="{{ t('footer.about') }}" aria-label="{{ t('footer.about') }}"><i class="fa-solid fa-info"></i></a>
|
||||
<a href="/privacy" data-page-modal title="Privacy" aria-label="Privacy"><i class="fa-solid fa-shield-halved"></i></a>
|
||||
<div id="help-button" style="color: var(--accent); font-weight: bold; opacity: 0.7;" title="Keyboard Shortcuts" aria-label="Keyboard Shortcuts"><i class="fa-solid fa-question"></i></div>
|
||||
<div id="help-button" style="color: var(--accent); font-weight: bold; opacity: 0.7;" title="Keyboard Shortcuts" aria-label="Keyboard Shortcuts"><i class="fa-solid fa-keyboard"></i></div>
|
||||
@if(!/^\/\d$/.test(url.pathname))
|
||||
{{-- id kept: f0ckm.js / sidebar-activity.js hook the AJAX random transition on #nav-random --}}
|
||||
<a href="/random" id="nav-random" title="{{ t('nav.random') }}" aria-label="{{ t('nav.random') }}"><i class="fa-solid fa-shuffle"></i></a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -653,7 +660,8 @@
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (status) { status.textContent = json.msg || ''; status.className = json.success ? 'flash-success' : 'flash-error'; }
|
||||
if (json.success) setTimeout(() => { close(registerModal); open(loginModal); }, 2000);
|
||||
if (json.success && json.upgraded) setTimeout(() => { location.reload(); }, 1500);
|
||||
else if (json.success) setTimeout(() => { close(registerModal); open(loginModal); }, 2000);
|
||||
} catch (err) {
|
||||
if (status) { status.textContent = 'Network error.'; status.className = 'flash-error'; }
|
||||
} finally { btn.disabled = false; btn.textContent = 'Create Account'; }
|
||||
|
||||
@@ -347,7 +347,12 @@
|
||||
#info-modal .infobox-modal-content .infobox-modal-footer { flex-shrink: 0; }
|
||||
#info-modal .infobox-modal-content .infobox-modal-body {
|
||||
flex: 1 1 auto; min-height: 0; max-height: none; overflow-y: auto; overscroll-behavior: contain;
|
||||
/* Scrolls (wheel, touch, keyboard) but shows no scrollbar in any browser.
|
||||
NOTE: no backticks or dollar-brace in views, the template engine compiles them as JS template literals */
|
||||
scrollbar-width: none !important;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
#info-modal .infobox-modal-content .infobox-modal-body::-webkit-scrollbar { display: none; width: 0; height: 0; }
|
||||
@media (max-width: 600px) {
|
||||
#info-modal { padding: 0 !important; align-items: stretch !important; }
|
||||
#info-modal .infobox-modal-content {
|
||||
|
||||
+39
-37
@@ -35,9 +35,6 @@
|
||||
<a href="/abyss" title="{{ t('nav.abyss') }}"><i class="fa-solid fa-dice-d6"></i></a>
|
||||
@endif
|
||||
<a href="#" id="nav-search-btn" title="{{ t('nav.search') }}"><i class="fa-solid fa-magnifying-glass"></i></a>
|
||||
@if(!/^\/\d$/.test(url.pathname))
|
||||
<a href="/random" id="nav-random" title="{{ t('nav.random') }}"><i class="fa-solid fa-shuffle"></i></a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +45,7 @@
|
||||
<div class="nav-user-dropdown">
|
||||
<button class="nav-user-btn nav-avatar-btn" id="nav-user-toggle">
|
||||
@if(session.is_anon)
|
||||
<span class="nav-avatar-icon" style="font-size: 1.2em; margin-right: 6px; display: inline-flex; align-items: center; cursor: pointer;" onclick="event.stopPropagation(); const u = '{{ session.login.toLowerCase() }}'; if(typeof window.loadPageAjax==='function'){ window.loadPageAjax('/user/' + u, true); } else { window.location.href='/user/' + u; }" title="{{ session.login }}"><i class="fa-solid fa-user-secret"></i></span>
|
||||
<img class="nav-avatar-img" src="/a/default.png" onclick="event.stopPropagation(); const u = '{{ session.login.toLowerCase() }}'; if(typeof window.loadPageAjax==='function'){ window.loadPageAjax('/user/' + u, true); } else { window.location.href='/user/' + u; }" title="{{ session.login }}">
|
||||
@elseif(session.avatar_file)
|
||||
<img class="nav-avatar-img" src="/a/{{ session.avatar_file }}" onerror="this.src='/a/default.png'" onclick="event.stopPropagation(); if(typeof window.loadPageAjax==='function'){ window.loadPageAjax('/user/{!! session.user.toLowerCase() !!}', true); } else { window.location.href='/user/{!! session.user.toLowerCase() !!}'; }" title="{{ session.user }}">
|
||||
@elseif(session.avatar && session.avatar > 0)
|
||||
@@ -68,12 +65,12 @@
|
||||
<a href="/user/{!! session.user.toLowerCase() !!}/halls">{{ t('nav.my_halls') }}</a>
|
||||
@endif
|
||||
@endif
|
||||
<a href="/pending">Pending Uploads</a>
|
||||
<a href="/pending" class="nav-menu-pending">Pending@if(typeof session.own_pending_count === 'number')<span class="nav-menu-count @if(session.own_pending_count === 0)is-zero@endif" title="{{ session.own_pending_count }} upload(s) awaiting approval">{{ session.own_pending_count }}</span>@endif</a>
|
||||
@if(enable_anonymous_access && session.is_anon)
|
||||
<a href="#" id="nav-user-anon-identity-btn" style="white-space: nowrap;" onclick="event.preventDefault(); if(window.f0ckAnonPasskey) { window.f0ckAnonPasskey.openModal(); } else { const m = document.getElementById('anon-passkey-modal'); if (m) m.style.display='flex'; }">Passkey Identity</a>
|
||||
@endif
|
||||
<a href="/user/{{ (session.is_anon && session.login ? session.login : session.user).toLowerCase() }}/favs" class="mobile-only">{{ t('nav.favs') }}</a>
|
||||
<a href="/settings" class="mobile-only">{{ t('nav.settings') }}</a>
|
||||
<a href="/settings" class="mobile-only" data-page-modal title="{{ t('nav.settings') }}">{{ t('nav.settings') }}</a>
|
||||
<div class="nav-user-divider"></div>
|
||||
<a href="/logout" class="mobile-only">{{ t('nav.logout') }}</a>
|
||||
</div>
|
||||
@@ -94,6 +91,9 @@
|
||||
@else
|
||||
<button class="notif-tab active" data-tab="system">{{ t('nav.notif_tab_system') }} <span class="notif-tab-badge" id="notif-tab-badge-system" style="display:none">0</span></button>
|
||||
@endif
|
||||
@if(session && session.admin)
|
||||
<button class="notif-tab" data-tab="admin">Admin <span class="notif-tab-badge" id="notif-tab-badge-admin" style="display:none">0</span></button>
|
||||
@endif
|
||||
</div>
|
||||
<button id="mark-all-read" title="{{ t('nav.mark_all_read') }}"><i class="fa-solid fa-check-double"></i></button>
|
||||
</div>
|
||||
@@ -129,7 +129,7 @@
|
||||
@endif
|
||||
|
||||
<!-- Quick Settings -->
|
||||
<a href="/settings" title="Settings" class="desktop-only"><i class="fa-solid fa-gear"></i></a>
|
||||
<a href="/settings" title="Settings" class="desktop-only" data-page-modal><i class="fa-solid fa-gear"></i></a>
|
||||
|
||||
<!-- Filter -->
|
||||
@if(!session.is_anon || (enable_anonymous_access && anon_permissions.filter))
|
||||
@@ -171,9 +171,9 @@
|
||||
|
||||
</nav>
|
||||
|
||||
@if(session.admin || session._is_impersonating)
|
||||
@if(session.admin || session._is_impersonating || session._preview_role)
|
||||
<!-- Admin Bar (Bottom, hotkey-activated) -->
|
||||
<div class="admin-bar" id="admin-bar">
|
||||
<div class="admin-bar" id="admin-bar" data-preview-role="{{ session._preview_role || 'admin' }}">
|
||||
@if(session._is_impersonating)
|
||||
<!-- Impersonation Banner -->
|
||||
<div class="admin-bar-impersonate-notice">
|
||||
@@ -190,10 +190,6 @@
|
||||
@else
|
||||
<!-- Normal Admin Bar -->
|
||||
<div class="admin-bar-inner">
|
||||
<div class="admin-bar-brand">
|
||||
<i class="fa-solid fa-shield-halved"></i>
|
||||
<span>Admin</span>
|
||||
</div>
|
||||
|
||||
<div class="admin-bar-links">
|
||||
<a href="/admin" class="admin-bar-link" title="Admin panel">
|
||||
@@ -235,7 +231,7 @@
|
||||
@endif
|
||||
|
||||
<!-- Floating exit-preview pill (visible only during role preview) -->
|
||||
@if(session.admin)
|
||||
@if(session.admin || session._preview_role)
|
||||
<div id="admin-preview-exit-pill" style="display:none;">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
<span id="admin-preview-exit-label">Previewing as User</span>
|
||||
@@ -279,9 +275,6 @@
|
||||
@if(abyss_enabled)
|
||||
<a href="/abyss" title="Abyss"><i class="fa-solid fa-dice-d6"></i></a>
|
||||
@endif
|
||||
@if(!/^\/\d$/.test(url.pathname))
|
||||
<a href="/random" id="nav-random" title="Random"><i class="fa-solid fa-shuffle"></i></a>
|
||||
@endif
|
||||
<a href="#" id="nav-search-btn" title="Search"><i class="fa-solid fa-magnifying-glass"></i></a>
|
||||
|
||||
</div>
|
||||
@@ -301,7 +294,7 @@
|
||||
@if(enable_anonymous_access)
|
||||
|
||||
<a href="#" id="nav-anon-identity-btn" @if(!session || !session.is_anon) style="display:none;" @endif onclick="event.preventDefault(); if(window.f0ckAnonPasskey) { window.f0ckAnonPasskey.openModal(); } else { const m = document.getElementById('anon-passkey-modal'); if (m) m.style.display='flex'; }"><i class="fa-solid fa-fingerprint"></i> Passkey Identity</a>
|
||||
<a href="/settings" id="nav-anon-settings-btn" @if(!session || !session.is_anon) style="display:none;" @endif><i class="fa-solid fa-gear"></i> Settings</a>
|
||||
<a href="/settings" id="nav-anon-settings-btn" data-page-modal title="Settings" @if(!session || !session.is_anon) style="display:none;" @endif><i class="fa-solid fa-gear"></i> Settings</a>
|
||||
@endif
|
||||
<a href="#" id="nav-login-btn">{{ t('nav.login') }}</a>
|
||||
@if(registration_open)
|
||||
@@ -1082,6 +1075,9 @@ async function loginWithPasskey() {
|
||||
<div class="shortcut-item"><span><kbd>Ctrl</kbd> + <kbd>Enter</kbd></span><span>{{ t('shortcuts.send_comment') }}</span></div>
|
||||
<div class="shortcut-item"><span><kbd>s</kbd></span><span>{{ t('shortcuts.flash_yank') }}</span></div>
|
||||
@if(session)
|
||||
<div class="shortcut-item"><span><kbd>Shift</kbd> + <kbd>S</kbd></span><span>{{ t('shortcuts.open_settings') }}</span></div>
|
||||
@endif
|
||||
@if(session)
|
||||
<div class="shortcut-item"><span><kbd>e</kbd></span><span>{{ t('shortcuts.tag_exclude') }}</span></div>
|
||||
<div class="shortcut-item"><span><kbd>i</kbd></span><span>{{ t('shortcuts.tag_input') }}</span></div>
|
||||
@endif
|
||||
@@ -1241,8 +1237,8 @@ async function loginWithPasskey() {
|
||||
<button class="modal-close" id="drag-modal-close" title="Cancel Upload">×</button>
|
||||
<div class="modal-body">
|
||||
<div class="upload-container" style="padding: 0; animation: none; opacity: 1;">
|
||||
<div class="upload-title">{{ t('upload.title') }}</div>
|
||||
|
||||
<div class="upload-title">
|
||||
<span class="upload-title-text">{{ t('upload.title') }}</span>
|
||||
<div class="upload-limit-info">
|
||||
@if(session.uploads_remaining === undefined)
|
||||
<span class="limit-unlimited"><i class="fa-solid fa-infinity"></i> {{ t('upload.unlimited') }}</span>
|
||||
@@ -1252,6 +1248,7 @@ async function loginWithPasskey() {
|
||||
<span class="limit-remaining"><i class="fa-solid fa-upload"></i> {{ session.uploads_remaining }}/{{ upload_limit }} {{ t('upload.remaining') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include(snippets/upload-form)
|
||||
</div>
|
||||
@@ -1261,7 +1258,7 @@ async function loginWithPasskey() {
|
||||
|
||||
@endif
|
||||
|
||||
@if(recaptcha_enabled && !session)
|
||||
@if(recaptcha_enabled && (!session || session.is_anon))
|
||||
<script>
|
||||
(function() {
|
||||
var _rcModalWidgetId = null;
|
||||
@@ -1300,7 +1297,7 @@ async function loginWithPasskey() {
|
||||
<script src="https://www.google.com/recaptcha/api.js?onload=onRecaptchaModalReady&render=explicit" async defer></script>
|
||||
@endif
|
||||
|
||||
@if(session && (session.admin || session._is_impersonating))
|
||||
@if(session && (session.admin || session._is_impersonating || session._preview_role))
|
||||
<style>
|
||||
/* ── Admin Bar ──────────────────────────────────────────────────────────────── */
|
||||
.admin-bar {
|
||||
@@ -1730,7 +1727,8 @@ body.preview-as-user .admin-delete-btn,
|
||||
body.preview-as-user .admin-edit-btn,
|
||||
body.preview-as-user .admin-pin-btn,
|
||||
body.preview-as-user .delete-btn,
|
||||
body.preview-as-user .poll-delete-btn,
|
||||
body.preview-as-user .poll-delete-btn { display: none !important; }
|
||||
/* Title stays visible but loses its click-to-edit */
|
||||
body.preview-as-user .item_title_text { display: inline !important; cursor: default !important; pointer-events: none !important; }
|
||||
body.preview-as-user .item_title_text.no-title { display: none !important; }
|
||||
|
||||
@@ -2012,8 +2010,10 @@ body.preview-as-mod .btn-infobox-action { display: none !important; }
|
||||
}
|
||||
|
||||
/* ── Admin bar: Preview As role toggle ──────────────────────────────────── */
|
||||
// The role is applied server-side (cookie preview_role, see src/index.mjs): templates, f0ckSession and
|
||||
// admin/mod-guarded routes all behave as that role. Switching sets the cookie and reloads.
|
||||
(function() {
|
||||
const STORAGE_KEY = 'f0ck_preview_role';
|
||||
const COOKIE = 'preview_role';
|
||||
const pills = document.querySelectorAll('.admin-bar-preview-pill');
|
||||
const exitPill = document.getElementById('admin-preview-exit-pill');
|
||||
const exitLabel = document.getElementById('admin-preview-exit-label');
|
||||
@@ -2040,26 +2040,28 @@ body.preview-as-mod .btn-infobox-action { display: none !important; }
|
||||
}
|
||||
}
|
||||
|
||||
// Init from localStorage
|
||||
const saved = localStorage.getItem(STORAGE_KEY) || 'admin';
|
||||
applyRole(saved);
|
||||
function switchRole(role) {
|
||||
const secure = location.protocol === 'https:' ? '; Secure' : '';
|
||||
// No template literals here: the view engine would evaluate the placeholders server-side
|
||||
document.cookie = role === 'admin'
|
||||
? COOKIE + '=; path=/; max-age=0; SameSite=Lax' + secure
|
||||
: COOKIE + '=' + role + '; path=/; max-age=86400; SameSite=Lax' + secure;
|
||||
location.reload();
|
||||
}
|
||||
|
||||
// The server tells which role this page was rendered as
|
||||
try { localStorage.removeItem('f0ck_preview_role'); } catch {} // old client-only preview
|
||||
const current = (adminBar && adminBar.dataset.previewRole) || 'admin';
|
||||
applyRole(roleLabels[current] ? current : 'admin');
|
||||
|
||||
// Pill click
|
||||
pills.forEach(pill => {
|
||||
pill.addEventListener('click', () => {
|
||||
const role = pill.dataset.role;
|
||||
localStorage.setItem(STORAGE_KEY, role);
|
||||
applyRole(role);
|
||||
if (role !== current) switchRole(role);
|
||||
});
|
||||
});
|
||||
|
||||
// Exit button
|
||||
if (exitBtn) {
|
||||
exitBtn.addEventListener('click', () => {
|
||||
localStorage.setItem(STORAGE_KEY, 'admin');
|
||||
applyRole('admin');
|
||||
});
|
||||
}
|
||||
if (exitBtn) exitBtn.addEventListener('click', () => switchRole('admin'));
|
||||
})();
|
||||
|
||||
})();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@each(notifications as n)
|
||||
@if(n.type === 'approve')
|
||||
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-type="{{ n.type }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};};"/>
|
||||
</div>
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</a>
|
||||
@elseif(n.type === 'admin_pending')
|
||||
<a href="/mod/approve" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<a href="/mod/approve" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-type="{{ n.type }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};"/>
|
||||
</div>
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
</a>
|
||||
@elseif(n.type === 'report')
|
||||
<a href="/mod/reports" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<a href="/mod/reports" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-type="{{ n.type }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};};"/>
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
</a>
|
||||
@elseif(n.type === 'deny')
|
||||
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-type="{{ n.type }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
</a>
|
||||
@elseif(n.type === 'item_deleted')
|
||||
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-type="{{ n.type }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
|
||||
</div>
|
||||
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
</a>
|
||||
@elseif(n.type === 'upload_comment')
|
||||
<a href="/{{ n.item_slug || n.item_id }}#c{{ n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<a href="/{{ n.item_slug || n.item_id }}#c{{ n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-type="{{ n.type }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumbnail" onerror="this.style.display='none'">
|
||||
</div>
|
||||
@@ -72,7 +72,7 @@
|
||||
</div>
|
||||
</a>
|
||||
@elseif(n.type === 'upload_success')
|
||||
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-type="{{ n.type }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.style.display='none'" />
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@
|
||||
</div>
|
||||
</a>
|
||||
@elseif(n.type === 'upload_error')
|
||||
<a href="{{ n.item_id ? '/' + (n.item_slug || n.item_id) : '#' }}" class="notif-item {{ n.is_read ? '' : 'unread' }} {{ n.item_id ? 'notif-with-thumb' : '' }}" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<a href="{{ n.item_id ? '/' + (n.item_slug || n.item_id) : '#' }}" class="notif-item {{ n.is_read ? '' : 'unread' }} {{ n.item_id ? 'notif-with-thumb' : '' }}" data-id="{{ n.id }}" data-type="{{ n.type }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
@if(n.item_id)
|
||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
|
||||
@@ -104,7 +104,7 @@
|
||||
</div>
|
||||
</a>
|
||||
@elseif(n.type === 'warning')
|
||||
<div class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<div class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-type="{{ n.type }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<div class="notif-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-lighter);color:var(--danger);font-size:1.5em;"><i class="fa-solid fa-triangle-exclamation"></i></div>
|
||||
<div class="notif-content">
|
||||
<div class="notif-user"><strong>{{ t('notifications.system') }}</strong></div>
|
||||
@@ -116,7 +116,7 @@
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<a href="/{{ n.item_slug || n.item_id }}#c{{ n.comment_id || n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
<a href="/{{ n.item_slug || n.item_id }}#c{{ n.comment_id || n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-type="{{ n.type }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
|
||||
@if(n.item_id)
|
||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.style.display='none'" />
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<button type="button" class="pgm-close" title="Close" aria-label="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pgm-body"></div>
|
||||
<div class="pgm-body" tabindex="-1"></div>
|
||||
</div>
|
||||
<style>
|
||||
#page-modal {
|
||||
@@ -17,10 +17,21 @@
|
||||
flex-direction: column;
|
||||
background: var(--bg); color: var(--text-color, #fff);
|
||||
}
|
||||
/* The Onara viewer (10050) lifts the sidebar (10060) and its drag zone (10059) above itself; keep the same
|
||||
order: page over Onara, still under the sidebar controls and the navbar (10070) */
|
||||
body.onara-modal-open #page-modal { z-index: 10055; }
|
||||
#page-modal .pgm-header {
|
||||
flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
||||
padding: 12px 20px; border-bottom: 1px solid var(--accent);
|
||||
}
|
||||
/* Height matched to the sidebar tab row (page-modal.js measures it); the close button scales with it */
|
||||
#page-modal.pgm-header-synced .pgm-header {
|
||||
box-sizing: border-box; height: var(--pgm-header-h); padding-top: 0; padding-bottom: 0;
|
||||
}
|
||||
#page-modal.pgm-header-synced .pgm-close {
|
||||
width: calc(var(--pgm-header-h) - 12px); height: calc(var(--pgm-header-h) - 12px);
|
||||
font-size: 1.3em;
|
||||
}
|
||||
#page-modal .pgm-title {
|
||||
margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
font-size: 1em; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em;
|
||||
@@ -37,7 +48,14 @@
|
||||
#page-modal .pgm-body {
|
||||
flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
/* Scrollbar hidden, scrolling kept (wheel, touch, keyboard). Firefox only honours scrollbar-width;
|
||||
!important so a site-wide scrollbar style can't bring it back */
|
||||
scrollbar-width: none !important;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
#page-modal .pgm-body::-webkit-scrollbar { display: none; width: 0; height: 0; }
|
||||
/* Focusable so arrow keys / PageUp / PageDown / Space scroll it once clicked into, without an outline */
|
||||
#page-modal .pgm-body:focus { outline: none; }
|
||||
#page-modal .pgm-loading { padding: 60px 20px; text-align: center; color: var(--text-muted, #aaa); }
|
||||
#page-modal .pgm-loading a { color: var(--accent); }
|
||||
</style>
|
||||
|
||||
@@ -22,11 +22,7 @@
|
||||
<div class="drop-zone" id="upload-form-drop-zone">
|
||||
<input type="file" class="file-input" name="file" accept="{{ allowed_mimes }}" multiple>
|
||||
<div class="drop-zone-prompt">
|
||||
<p class="drop-zone-title" style="font-size: 1.1rem; font-weight: 500;">{{ t('upload.drop_here') }}</p>
|
||||
<p style="font-size: 0.9rem; opacity: 0.6;">(max {{ max_file_size }})@if(session.admin) <span style="color: var(--accent);">{{ t('upload.admin_boost') }}</span>@endif</p>
|
||||
<p class="drop-album-hint" style="margin-top: 6px; font-size: 0.85rem; color: var(--accent); opacity: 0.95;">
|
||||
<i class="fa-solid fa-layer-group"></i> {{ t('album.drop_hint') || 'Select or drop multiple files (subf0cks) to create an Album' }}
|
||||
</p>
|
||||
<p class="drop-zone-title" style="font-size: 1.1rem; font-weight: 500;">{{ t('upload.drop_here') }} <span class="drop-zone-max" style="font-size: 0.8em; font-weight: 400; opacity: 0.6;">(max {{ max_file_size }})</span>@if(session.admin) <span style="font-size: 0.8em; font-weight: 400; color: var(--accent);">{{ t('upload.admin_boost') }}</span>@endif</p>
|
||||
</div>
|
||||
<!-- Preview Container -->
|
||||
<div class="file-preview" style="display: none;">
|
||||
@@ -180,6 +176,9 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Footer: upload button, progress and status. Pinned to the bottom of the upload dropdown
|
||||
while the form above it scrolls (f0ckm.css #upload-drag-modal .upload-footer) -->
|
||||
<div class="upload-footer">
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn-upload" disabled>
|
||||
<span class="btn-text">{{ t('upload.select_file') }}</span>
|
||||
@@ -195,4 +194,5 @@
|
||||
</div>
|
||||
|
||||
<div class="upload-status"></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+3
-2
@@ -3,8 +3,8 @@
|
||||
<div id="main">
|
||||
<link rel="stylesheet" href="/s/css/upload.css">
|
||||
<div class="upload-container" style="opacity: 0;" data-mimes='{!! mimes_json !!}'>
|
||||
<div class="upload-title">{{ t('upload.title') }}</div>
|
||||
|
||||
<div class="upload-title">
|
||||
<span class="upload-title-text">{{ t('upload.title') }}</span>
|
||||
<div class="upload-limit-info">
|
||||
@if(uploads_remaining === null)
|
||||
<span class="limit-unlimited"><i class="fa-solid fa-infinity"></i> {{ t('upload_page.limit_unlimited') }}</span>
|
||||
@@ -14,6 +14,7 @@
|
||||
<span class="limit-remaining"><i class="fa-solid fa-upload"></i> {{ t('upload_page.limit_remaining').replace('{remaining}', uploads_remaining).replace('{limit}', upload_limit) }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session)
|
||||
@include(snippets/upload-form)
|
||||
|
||||
Reference in New Issue
Block a user