|
|
|
@@ -156,7 +156,73 @@
|
|
|
|
|
<div id="user-pref-show-motd" style="display:none">@if(session.show_motd !== false)true@else false @endif</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@if(session.admin || session._is_impersonating)
|
|
|
|
|
<!-- Admin Bar -->
|
|
|
|
|
<div class="admin-bar" id="admin-bar">
|
|
|
|
|
@if(session._is_impersonating)
|
|
|
|
|
<!-- Impersonation Banner -->
|
|
|
|
|
<div class="admin-bar-impersonate-notice">
|
|
|
|
|
<span class="admin-bar-imp-icon"><i class="fa-solid fa-user-secret"></i></span>
|
|
|
|
|
<span class="admin-bar-imp-text">Viewing as <strong>{!! session.user !!}</strong></span>
|
|
|
|
|
<span class="admin-bar-imp-hint">— all actions are performed as this user</span>
|
|
|
|
|
<button class="admin-bar-exit-btn" id="admin-bar-exit-impersonate" title="Exit impersonation and return to your admin session">
|
|
|
|
|
<i class="fa-solid fa-door-open"></i> Exit
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
@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">
|
|
|
|
|
<i class="fa-solid fa-cog"></i> Admin
|
|
|
|
|
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
|
|
|
|
|
<span class="admin-bar-badge">{{ session.pending_count }}</span>
|
|
|
|
|
@endif
|
|
|
|
|
</a>
|
|
|
|
|
<a href="/admin/approve" class="admin-bar-link" title="Pending approvals">
|
|
|
|
|
<i class="fa-solid fa-clock"></i> Pending
|
|
|
|
|
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
|
|
|
|
|
<span class="admin-bar-badge">{{ session.pending_count }}</span>
|
|
|
|
|
@endif
|
|
|
|
|
</a>
|
|
|
|
|
<a href="/mod" class="admin-bar-link" title="Moderator panel">
|
|
|
|
|
<i class="fa-solid fa-gavel"></i> Mod
|
|
|
|
|
</a>
|
|
|
|
|
<a href="/admin/audit" class="admin-bar-link" title="Audit log">
|
|
|
|
|
<i class="fa-solid fa-scroll"></i> Audit
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Preview As role toggle (right-aligned) -->
|
|
|
|
|
<div class="admin-bar-preview" id="admin-bar-preview-wrap" style="margin-left: auto;">
|
|
|
|
|
<span class="admin-bar-preview-label"><i class="fa-solid fa-eye" style="opacity:0.6;"></i> Preview:</span>
|
|
|
|
|
<div class="admin-bar-preview-pills" id="admin-bar-preview-pills">
|
|
|
|
|
<button class="admin-bar-preview-pill active" data-role="admin" title="View as yourself (admin)">Admin</button>
|
|
|
|
|
<button class="admin-bar-preview-pill" data-role="mod" title="Simulate moderator view">Mod</button>
|
|
|
|
|
<button class="admin-bar-preview-pill" data-role="user" title="Simulate regular user view">User</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
<!-- Floating exit-preview pill (visible only during role preview) -->
|
|
|
|
|
@if(session.admin)
|
|
|
|
|
<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>
|
|
|
|
|
<button id="admin-preview-exit-btn" title="Return to admin view"><i class="fa-solid fa-xmark"></i> Exit</button>
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
|
|
|
|
|
@else
|
|
|
|
|
<!-- not logged in -->
|
|
|
|
|
@if(!private_society)
|
|
|
|
@@ -435,3 +501,569 @@
|
|
|
|
|
</script>
|
|
|
|
|
<script src="https://www.google.com/recaptcha/api.js?onload=onRecaptchaModalReady&render=explicit" async defer></script>
|
|
|
|
|
@endif
|
|
|
|
|
|
|
|
|
|
@if(session && (session.admin || session._is_impersonating))
|
|
|
|
|
<style>
|
|
|
|
|
/* ── Admin Bar ──────────────────────────────────────────────────────────────── */
|
|
|
|
|
.admin-bar {
|
|
|
|
|
width: 100%;
|
|
|
|
|
font-family: var(--font, system-ui, sans-serif);
|
|
|
|
|
font-size: 0.76rem;
|
|
|
|
|
z-index: 900;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.35);
|
|
|
|
|
border-bottom: 1px solid rgba(255, 180, 0, 0.18);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Normal admin bar */
|
|
|
|
|
.admin-bar-inner {
|
|
|
|
|
background: linear-gradient(90deg, #1a1200 0%, #1e1600 40%, #141000 100%);
|
|
|
|
|
border-top: 1px solid rgba(255, 190, 0, 0.22);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
padding: 0 14px;
|
|
|
|
|
height: 30px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-bar-brand {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 5px;
|
|
|
|
|
color: #f5c400;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
letter-spacing: 0.04em;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
font-size: 0.72rem;
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
padding-right: 4px;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-brand i { font-size: 0.78rem; }
|
|
|
|
|
|
|
|
|
|
.admin-bar-links {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 2px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-link {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
color: rgba(255, 210, 80, 0.75) !important;
|
|
|
|
|
text-decoration: none !important;
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
font-size: 0.74rem;
|
|
|
|
|
transition: background 0.15s, color 0.15s;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-link:hover {
|
|
|
|
|
background: rgba(255, 200, 0, 0.12);
|
|
|
|
|
color: #ffd844 !important;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-badge {
|
|
|
|
|
background: #e05020;
|
|
|
|
|
color: #fff;
|
|
|
|
|
border-radius: 9px;
|
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
padding: 0 5px;
|
|
|
|
|
min-width: 16px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
line-height: 16px;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-bar-sep {
|
|
|
|
|
flex: 1;
|
|
|
|
|
height: 14px;
|
|
|
|
|
border-left: 1px solid rgba(255, 200, 0, 0.12);
|
|
|
|
|
margin: 0 4px;
|
|
|
|
|
flex-basis: 0;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* View-as widget */
|
|
|
|
|
.admin-bar-viewas {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 5px;
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-viewas-input-wrap {
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-input {
|
|
|
|
|
background: rgba(255, 200, 0, 0.07);
|
|
|
|
|
border: 1px solid rgba(255, 200, 0, 0.2);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
color: #ffd844;
|
|
|
|
|
font-size: 0.73rem;
|
|
|
|
|
font-family: var(--font, system-ui, sans-serif);
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
width: 160px;
|
|
|
|
|
outline: none;
|
|
|
|
|
transition: border-color 0.15s, width 0.2s;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-input::placeholder { color: rgba(255,210,80,0.35); }
|
|
|
|
|
.admin-bar-input:focus {
|
|
|
|
|
border-color: rgba(255, 200, 0, 0.55);
|
|
|
|
|
width: 210px;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-viewas-btn {
|
|
|
|
|
background: rgba(255, 200, 0, 0.12);
|
|
|
|
|
border: 1px solid rgba(255, 200, 0, 0.3);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
color: #ffd844;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
font-size: 0.72rem;
|
|
|
|
|
transition: background 0.15s, border-color 0.15s;
|
|
|
|
|
height: 22px;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-viewas-btn:hover {
|
|
|
|
|
background: rgba(255, 200, 0, 0.22);
|
|
|
|
|
border-color: rgba(255, 200, 0, 0.55);
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-viewas-btn:disabled {
|
|
|
|
|
opacity: 0.4;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Autocomplete suggestions */
|
|
|
|
|
.admin-viewas-suggestions {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: calc(100% + 3px);
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
background: #1a1600;
|
|
|
|
|
border: 1px solid rgba(255, 200, 0, 0.3);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
box-shadow: 0 6px 20px rgba(0,0,0,0.5);
|
|
|
|
|
z-index: 9999;
|
|
|
|
|
display: none;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
min-width: 180px;
|
|
|
|
|
}
|
|
|
|
|
.admin-viewas-suggestions.open { display: block; }
|
|
|
|
|
.admin-viewas-suggestion {
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
color: rgba(255, 210, 80, 0.85);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 0.76rem;
|
|
|
|
|
transition: background 0.1s;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
.admin-viewas-suggestion:hover,
|
|
|
|
|
.admin-viewas-suggestion.active {
|
|
|
|
|
background: rgba(255, 200, 0, 0.14);
|
|
|
|
|
color: #ffd844;
|
|
|
|
|
}
|
|
|
|
|
.admin-viewas-suggestion i { opacity: 0.5; font-size: 0.72rem; }
|
|
|
|
|
|
|
|
|
|
/* Impersonation banner */
|
|
|
|
|
.admin-bar-impersonate-notice {
|
|
|
|
|
background: linear-gradient(90deg, #1a0800 0%, #200c00 40%, #180a00 100%);
|
|
|
|
|
border-top: 1px solid rgba(255, 120, 0, 0.35);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
padding: 0 14px;
|
|
|
|
|
height: 30px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
animation: admin-imp-pulse 3s ease-in-out infinite;
|
|
|
|
|
}
|
|
|
|
|
@keyframes admin-imp-pulse {
|
|
|
|
|
0%, 100% { border-top-color: rgba(255, 120, 0, 0.35); }
|
|
|
|
|
50% { border-top-color: rgba(255, 160, 0, 0.7); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-bar-imp-icon {
|
|
|
|
|
color: #ff8c00;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
animation: admin-imp-wiggle 4s ease-in-out infinite;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
@keyframes admin-imp-wiggle {
|
|
|
|
|
0%, 90%, 100% { transform: rotate(0deg); }
|
|
|
|
|
92% { transform: rotate(-8deg); }
|
|
|
|
|
96% { transform: rotate(8deg); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-bar-imp-text {
|
|
|
|
|
color: #ffb84d;
|
|
|
|
|
font-size: 0.77rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-imp-text strong { color: #ffcc00; }
|
|
|
|
|
.admin-bar-imp-hint {
|
|
|
|
|
color: rgba(255, 160, 60, 0.55);
|
|
|
|
|
font-size: 0.71rem;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-bar-exit-btn {
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
background: rgba(255, 80, 0, 0.18);
|
|
|
|
|
border: 1px solid rgba(255, 100, 0, 0.45);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
color: #ff8c40;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
padding: 2px 10px;
|
|
|
|
|
font-size: 0.73rem;
|
|
|
|
|
font-family: var(--font, system-ui, sans-serif);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
letter-spacing: 0.02em;
|
|
|
|
|
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 5px;
|
|
|
|
|
height: 22px;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-exit-btn:hover {
|
|
|
|
|
background: rgba(255, 80, 0, 0.35);
|
|
|
|
|
border-color: rgba(255, 120, 0, 0.7);
|
|
|
|
|
color: #ffaa66;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Preview As role pills */
|
|
|
|
|
.admin-bar-preview {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-preview-label {
|
|
|
|
|
color: rgba(255,210,80,0.55);
|
|
|
|
|
font-size: 0.72rem;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-preview-pills {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 2px;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-preview-pill {
|
|
|
|
|
background: rgba(255,200,0,0.07);
|
|
|
|
|
border: 1px solid rgba(255,200,0,0.18);
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
color: rgba(255,210,80,0.55);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 0.71rem;
|
|
|
|
|
font-family: var(--font, system-ui, sans-serif);
|
|
|
|
|
padding: 1px 7px;
|
|
|
|
|
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-preview-pill:hover {
|
|
|
|
|
background: rgba(255,200,0,0.16);
|
|
|
|
|
color: #ffd844;
|
|
|
|
|
border-color: rgba(255,200,0,0.4);
|
|
|
|
|
}
|
|
|
|
|
.admin-bar-preview-pill.active {
|
|
|
|
|
background: rgba(255,200,0,0.22);
|
|
|
|
|
color: #ffd844;
|
|
|
|
|
border-color: rgba(255,200,0,0.55);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Floating exit-preview pill */
|
|
|
|
|
#admin-preview-exit-pill {
|
|
|
|
|
position: fixed;
|
|
|
|
|
bottom: 18px;
|
|
|
|
|
right: 18px;
|
|
|
|
|
z-index: 99999;
|
|
|
|
|
background: #1a1200;
|
|
|
|
|
border: 1px solid rgba(255,200,0,0.45);
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
color: #ffd844;
|
|
|
|
|
font-size: 0.76rem;
|
|
|
|
|
font-family: var(--font, system-ui, sans-serif);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
padding: 6px 14px;
|
|
|
|
|
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
|
|
|
|
|
animation: preview-pill-in 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
@keyframes preview-pill-in {
|
|
|
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
|
|
|
to { opacity: 1; transform: translateY(0); }
|
|
|
|
|
}
|
|
|
|
|
#admin-preview-exit-pill span { font-weight: 600; }
|
|
|
|
|
#admin-preview-exit-btn {
|
|
|
|
|
background: rgba(255,200,0,0.15);
|
|
|
|
|
border: 1px solid rgba(255,200,0,0.35);
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
color: #ffd844;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 0.71rem;
|
|
|
|
|
font-family: var(--font, system-ui, sans-serif);
|
|
|
|
|
padding: 1px 10px;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
transition: background 0.15s;
|
|
|
|
|
}
|
|
|
|
|
#admin-preview-exit-btn:hover { background: rgba(255,200,0,0.3); }
|
|
|
|
|
|
|
|
|
|
/* Body class rules: simulate role views */
|
|
|
|
|
body.preview-as-user .admin-bar,
|
|
|
|
|
body.preview-as-mod .admin-bar { display: none !important; }
|
|
|
|
|
|
|
|
|
|
/* Hide all owner/admin/mod actions in user preview */
|
|
|
|
|
body.preview-as-user #a_delete,
|
|
|
|
|
body.preview-as-user #a_rethumb,
|
|
|
|
|
body.preview-as-user .removetag,
|
|
|
|
|
body.preview-as-user .can-cycle,
|
|
|
|
|
body.preview-as-user .info-title-edit-wrap,
|
|
|
|
|
body.preview-as-user #info-title-save,
|
|
|
|
|
body.preview-as-user #info-title-status,
|
|
|
|
|
body.preview-as-user #info-visibility-edit-btn,
|
|
|
|
|
body.preview-as-user #info-rethumb-btn,
|
|
|
|
|
body.preview-as-user #info-set-expiry-btn,
|
|
|
|
|
body.preview-as-user .infobox-action-btn,
|
|
|
|
|
body.preview-as-user .btn-infobox-action,
|
|
|
|
|
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 [data-is-admin] .admin-only { display: none !important; }
|
|
|
|
|
|
|
|
|
|
/* Hide admin-only actions in mod preview (keep delete — mods can delete) */
|
|
|
|
|
body.preview-as-mod #a_rethumb,
|
|
|
|
|
body.preview-as-mod .removetag,
|
|
|
|
|
body.preview-as-mod #info-rethumb-btn,
|
|
|
|
|
body.preview-as-mod .btn-infobox-action { display: none !important; }
|
|
|
|
|
|
|
|
|
|
@media (max-width: 640px) {
|
|
|
|
|
.admin-bar-imp-hint { display: none; }
|
|
|
|
|
.admin-bar-links a span:not(.admin-bar-badge) { display: none; }
|
|
|
|
|
.admin-bar-input { width: 110px; }
|
|
|
|
|
.admin-bar-input:focus { width: 140px; }
|
|
|
|
|
.admin-bar-preview-label { display: none; }
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
(function() {
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
/* ── Admin bar: View-As (impersonation) ────────────────────────────────── */
|
|
|
|
|
const input = document.getElementById('admin-viewas-input');
|
|
|
|
|
const suggestBox = document.getElementById('admin-viewas-suggestions');
|
|
|
|
|
const viewAsBtn = document.getElementById('admin-viewas-btn');
|
|
|
|
|
const exitBtn = document.getElementById('admin-bar-exit-impersonate');
|
|
|
|
|
|
|
|
|
|
let _suggestTimer = null;
|
|
|
|
|
let _suggestions = [];
|
|
|
|
|
let _activeIdx = -1;
|
|
|
|
|
|
|
|
|
|
function closeSuggestions() {
|
|
|
|
|
if (suggestBox) { suggestBox.innerHTML = ''; suggestBox.classList.remove('open'); }
|
|
|
|
|
_suggestions = [];
|
|
|
|
|
_activeIdx = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderSuggestions(list) {
|
|
|
|
|
if (!suggestBox) return;
|
|
|
|
|
suggestBox.innerHTML = '';
|
|
|
|
|
if (!list || list.length === 0) { suggestBox.classList.remove('open'); return; }
|
|
|
|
|
list.forEach((u, i) => {
|
|
|
|
|
const el = document.createElement('div');
|
|
|
|
|
el.className = 'admin-viewas-suggestion';
|
|
|
|
|
el.innerHTML = '<i class="fa-solid fa-user"></i>' + u.user;
|
|
|
|
|
el.dataset.username = u.user;
|
|
|
|
|
el.addEventListener('mousedown', (e) => { e.preventDefault(); selectSuggestion(u.user); });
|
|
|
|
|
el.addEventListener('mouseenter', () => { setActive(i); });
|
|
|
|
|
suggestBox.appendChild(el);
|
|
|
|
|
});
|
|
|
|
|
_suggestions = list;
|
|
|
|
|
_activeIdx = -1;
|
|
|
|
|
suggestBox.classList.add('open');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setActive(idx) {
|
|
|
|
|
const items = suggestBox ? suggestBox.querySelectorAll('.admin-viewas-suggestion') : [];
|
|
|
|
|
items.forEach((el, i) => el.classList.toggle('active', i === idx));
|
|
|
|
|
_activeIdx = idx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectSuggestion(username) {
|
|
|
|
|
if (input) input.value = username;
|
|
|
|
|
closeSuggestions();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fetchSuggestions(q) {
|
|
|
|
|
if (!q || q.length < 1) { closeSuggestions(); return; }
|
|
|
|
|
fetch('/api/v2/admin/users/search?q=' + encodeURIComponent(q), {
|
|
|
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
|
|
|
}).then(r => r.json()).then(data => {
|
|
|
|
|
if (Array.isArray(data)) renderSuggestions(data);
|
|
|
|
|
}).catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (input) {
|
|
|
|
|
input.addEventListener('input', () => {
|
|
|
|
|
clearTimeout(_suggestTimer);
|
|
|
|
|
const q = input.value.trim();
|
|
|
|
|
if (!q) { closeSuggestions(); return; }
|
|
|
|
|
_suggestTimer = setTimeout(() => fetchSuggestions(q), 200);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
input.addEventListener('keydown', (e) => {
|
|
|
|
|
const items = suggestBox ? suggestBox.querySelectorAll('.admin-viewas-suggestion') : [];
|
|
|
|
|
if (e.key === 'ArrowDown') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setActive(Math.min(_activeIdx + 1, items.length - 1));
|
|
|
|
|
} else if (e.key === 'ArrowUp') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setActive(Math.max(_activeIdx - 1, -1));
|
|
|
|
|
} else if (e.key === 'Enter') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (_activeIdx >= 0 && _suggestions[_activeIdx]) {
|
|
|
|
|
selectSuggestion(_suggestions[_activeIdx].user);
|
|
|
|
|
} else {
|
|
|
|
|
startImpersonation();
|
|
|
|
|
}
|
|
|
|
|
} else if (e.key === 'Escape') {
|
|
|
|
|
closeSuggestions();
|
|
|
|
|
input.blur();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
input.addEventListener('blur', () => {
|
|
|
|
|
setTimeout(closeSuggestions, 150);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startImpersonation() {
|
|
|
|
|
const username = input ? input.value.trim() : '';
|
|
|
|
|
if (!username) return;
|
|
|
|
|
if (viewAsBtn) { viewAsBtn.disabled = true; }
|
|
|
|
|
|
|
|
|
|
const csrf = (typeof window.f0ckSession !== 'undefined' && window.f0ckSession.csrf_token)
|
|
|
|
|
? window.f0ckSession.csrf_token
|
|
|
|
|
: (document.querySelector('meta[name="csrf-token"]')?.content || '');
|
|
|
|
|
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
params.append('username', username);
|
|
|
|
|
if (csrf) params.append('csrf_token', csrf);
|
|
|
|
|
|
|
|
|
|
fetch('/api/v2/admin/impersonate', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
|
|
|
body: params
|
|
|
|
|
}).then(r => r.json()).then(data => {
|
|
|
|
|
if (data.success) {
|
|
|
|
|
window.location.reload();
|
|
|
|
|
} else {
|
|
|
|
|
alert('Impersonation failed: ' + (data.msg || 'Unknown error'));
|
|
|
|
|
if (viewAsBtn) viewAsBtn.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
}).catch(e => {
|
|
|
|
|
alert('Network error: ' + e.message);
|
|
|
|
|
if (viewAsBtn) viewAsBtn.disabled = false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (viewAsBtn) {
|
|
|
|
|
viewAsBtn.addEventListener('click', startImpersonation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Exit impersonation ─────────────────────────────────────────────────── */
|
|
|
|
|
if (exitBtn) {
|
|
|
|
|
exitBtn.addEventListener('click', () => {
|
|
|
|
|
exitBtn.disabled = true;
|
|
|
|
|
exitBtn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Exiting…';
|
|
|
|
|
|
|
|
|
|
const csrf = (typeof window.f0ckSession !== 'undefined' && window.f0ckSession.csrf_token)
|
|
|
|
|
? window.f0ckSession.csrf_token
|
|
|
|
|
: (document.querySelector('meta[name="csrf-token"]')?.content || '');
|
|
|
|
|
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
if (csrf) params.append('csrf_token', csrf);
|
|
|
|
|
|
|
|
|
|
fetch('/api/v2/admin/stop-impersonate', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
|
|
|
body: params
|
|
|
|
|
}).then(() => {
|
|
|
|
|
window.location.reload();
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
window.location.reload();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Admin bar: Preview As role toggle ──────────────────────────────────── */
|
|
|
|
|
(function() {
|
|
|
|
|
const STORAGE_KEY = 'f0ck_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');
|
|
|
|
|
const exitBtn = document.getElementById('admin-preview-exit-btn');
|
|
|
|
|
|
|
|
|
|
const roleLabels = { admin: 'Admin', mod: 'Moderator', user: 'User' };
|
|
|
|
|
|
|
|
|
|
function applyRole(role) {
|
|
|
|
|
document.body.classList.remove('preview-as-user', 'preview-as-mod');
|
|
|
|
|
if (role === 'user') document.body.classList.add('preview-as-user');
|
|
|
|
|
if (role === 'mod') document.body.classList.add('preview-as-mod');
|
|
|
|
|
|
|
|
|
|
// Update active pill
|
|
|
|
|
pills.forEach(p => p.classList.toggle('active', p.dataset.role === role));
|
|
|
|
|
|
|
|
|
|
// Show/hide exit pill
|
|
|
|
|
if (exitPill) {
|
|
|
|
|
if (role !== 'admin') {
|
|
|
|
|
if (exitLabel) exitLabel.textContent = 'Previewing as ' + roleLabels[role];
|
|
|
|
|
exitPill.style.display = 'flex';
|
|
|
|
|
} else {
|
|
|
|
|
exitPill.style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Init from localStorage
|
|
|
|
|
const saved = localStorage.getItem(STORAGE_KEY) || 'admin';
|
|
|
|
|
applyRole(saved);
|
|
|
|
|
|
|
|
|
|
// Pill click
|
|
|
|
|
pills.forEach(pill => {
|
|
|
|
|
pill.addEventListener('click', () => {
|
|
|
|
|
const role = pill.dataset.role;
|
|
|
|
|
localStorage.setItem(STORAGE_KEY, role);
|
|
|
|
|
applyRole(role);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Exit button
|
|
|
|
|
if (exitBtn) {
|
|
|
|
|
exitBtn.addEventListener('click', () => {
|
|
|
|
|
localStorage.setItem(STORAGE_KEY, 'admin');
|
|
|
|
|
applyRole('admin');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
@endif
|
|
|
|
|