fdsafs
This commit is contained in:
+265
-17
@@ -87,6 +87,99 @@
|
||||
.btn-role { background: rgba(180, 100, 255, 0.15); color: #c084fc; border: 1px solid rgba(180, 100, 255, 0.3); }
|
||||
.btn-role:hover { background: rgba(180, 100, 255, 0.25); opacity: 1; }
|
||||
|
||||
.legacy-filter-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 0 14px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
color: #aaa;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
height: 42px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.legacy-filter-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
.legacy-filter-btn.active {
|
||||
background: rgba(var(--accent-rgb, 0, 150, 255), 0.15);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
.legacy-filter-btn input {
|
||||
cursor: pointer;
|
||||
accent-color: var(--accent);
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-select-filter {
|
||||
padding: 0 14px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
color: #ccc;
|
||||
font-size: 0.85rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
height: 42px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.admin-select-filter:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
.admin-select-filter:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.admin-select-filter.active {
|
||||
background: rgba(var(--accent-rgb, 0, 150, 255), 0.15);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.admin-select-filter option {
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
}
|
||||
.admin-select-filter:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-filter-reset {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #aaa;
|
||||
padding: 0 14px;
|
||||
height: 42px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-filter-reset:hover {
|
||||
background: rgba(255, 60, 60, 0.15);
|
||||
border-color: rgba(255, 60, 60, 0.3);
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* Create User Modal */
|
||||
#create-user-modal {
|
||||
display: none;
|
||||
@@ -236,28 +329,60 @@
|
||||
</div>
|
||||
|
||||
<div style="padding: 0 15px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 30px; gap: 20px; flex-wrap: wrap;">
|
||||
<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>
|
||||
<p style="color: #888; margin: 5px 0 0 0;">Administration hub for <span id="total-count">{!! total !!}</span> registered members.</p>
|
||||
<p style="color: #888; margin: 5px 0 0 0;">Administration hub for <span id="total-count">{!! total !!}</span> <span id="total-label">{{ totalLabel }}</span>.</p>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; align-items: center; flex-grow: 1; max-width: 500px;">
|
||||
<div style="flex-grow: 1; position: relative;">
|
||||
<input type="text" id="user-search" placeholder='Search by name or email… use "exact" for exact match'
|
||||
style="width: 100%; padding: 12px 20px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; outline: none; transition: border-color 0.2s;"
|
||||
value="{{ q }}">
|
||||
<div id="search-spinner" style="position: absolute; right: 15px; top: 12px; display: none;">
|
||||
<svg class="spinner" width="20" height="20" viewBox="0 0 50 50" style="animation: rotate 2s linear infinite;">
|
||||
<circle cx="25" cy="25" r="20" fill="none" stroke="var(--accent)" stroke-width="5" stroke-dasharray="90,150" stroke-dashoffset="0" style="stroke-linecap: round;"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-modern btn-create" onclick="openCreateUserModal()" style="padding: 10px 16px; font-size: 0.85rem; white-space: nowrap;" title="Create new user">
|
||||
<div>
|
||||
<button class="btn-modern btn-create" onclick="openCreateUserModal()" style="padding: 11px 18px; font-size: 0.85rem; white-space: nowrap; height: 42px;" title="Create new user">
|
||||
<i class="fa fa-user-plus"></i> Create User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter & Search Toolbar -->
|
||||
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 25px; flex-wrap: wrap;">
|
||||
<div style="flex-grow: 1; min-width: 240px; position: relative;">
|
||||
<input type="text" id="user-search" placeholder='Search by name or email… use "exact" for exact match'
|
||||
style="width: 100%; height: 42px; padding: 0 20px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; outline: none; transition: border-color 0.2s; box-sizing: border-box;"
|
||||
value="{{ q }}">
|
||||
<div id="search-spinner" style="position: absolute; right: 15px; top: 11px; display: none;">
|
||||
<svg class="spinner" width="20" height="20" viewBox="0 0 50 50" style="animation: rotate 2s linear infinite;">
|
||||
<circle cx="25" cy="25" r="20" fill="none" stroke="var(--accent)" stroke-width="5" stroke-dasharray="90,150" stroke-dashoffset="0" style="stroke-linecap: round;"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Filter -->
|
||||
<select id="status-filter" class="admin-select-filter {{ status ? 'active' : '' }}" {{ onlyLegacy ? 'disabled' : '' }} title="Filter by account status">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="banned" {{ status === 'banned' ? 'selected' : '' }}>⛔ Banned</option>
|
||||
<option value="active" {{ status === 'active' ? 'selected' : '' }}>🟢 Active</option>
|
||||
<option value="pending" {{ status === 'pending' ? 'selected' : '' }}>⏳ Pending</option>
|
||||
</select>
|
||||
|
||||
<!-- Role Filter -->
|
||||
<select id="role-filter" class="admin-select-filter {{ role ? 'active' : '' }}" {{ onlyLegacy ? 'disabled' : '' }} title="Filter by user role">
|
||||
<option value="">All Roles</option>
|
||||
<option value="staff" {{ role === 'staff' ? 'selected' : '' }}>🛡️ Staff (All)</option>
|
||||
<option value="admin" {{ role === 'admin' ? 'selected' : '' }}>⚡ Admins</option>
|
||||
<option value="mod" {{ role === 'mod' ? 'selected' : '' }}>🗡️ Moderators</option>
|
||||
<option value="user" {{ role === 'user' ? 'selected' : '' }}>👤 Regular Users</option>
|
||||
</select>
|
||||
|
||||
<!-- Legacy Only Toggle -->
|
||||
<label id="legacy-filter-label" class="legacy-filter-btn {{ onlyLegacy ? 'active' : '' }}" title="Toggle to only show legacy ghost users">
|
||||
<input type="checkbox" id="legacy-filter" {{ onlyLegacy ? 'checked' : '' }}>
|
||||
<span><i class="fa fa-ghost" style="opacity: 0.8; margin-right: 4px;"></i>Only legacy</span>
|
||||
</label>
|
||||
|
||||
<!-- Reset Filters Button -->
|
||||
<button id="reset-filters-btn" class="btn-filter-reset" style="display: {{ (q || status || role || onlyLegacy) ? 'inline-flex' : 'none' }};" title="Reset all filters and search">
|
||||
<i class="fa fa-rotate-left"></i> Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="upload-form">
|
||||
<table class="admin-users-table responsive-table">
|
||||
<thead>
|
||||
@@ -276,7 +401,7 @@
|
||||
</table>
|
||||
<div id="loading-trigger" style="height: 20px; margin-top: 10px;"></div>
|
||||
<div id="no-users-msg" style="display: {{ users.length === 0 ? 'block' : 'none' }}; padding: 40px; text-align: center; color: #666;">
|
||||
No users matched your search.
|
||||
{{ emptyMsg }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -502,28 +627,81 @@
|
||||
var hasMore = {!! hasMore ? 'true' : 'false' !!};
|
||||
var isLoading = false;
|
||||
var searchQuery = '{{ q }}';
|
||||
var onlyLegacy = {!! onlyLegacy ? 'true' : 'false' !!};
|
||||
var currentStatus = '{{ status }}';
|
||||
var currentRole = '{{ role }}';
|
||||
|
||||
var searchInput = document.getElementById('user-search');
|
||||
var statusFilter = document.getElementById('status-filter');
|
||||
var roleFilter = document.getElementById('role-filter');
|
||||
var legacyFilter = document.getElementById('legacy-filter');
|
||||
var legacyFilterLabel = document.getElementById('legacy-filter-label');
|
||||
var resetFiltersBtn = document.getElementById('reset-filters-btn');
|
||||
var totalLabel = document.getElementById('total-label');
|
||||
var tableBody = document.getElementById('user-table-body');
|
||||
var loadingTrigger = document.getElementById('loading-trigger');
|
||||
var noUsersMsg = document.getElementById('no-users-msg');
|
||||
var spinner = document.getElementById('search-spinner');
|
||||
var countSpan = document.getElementById('total-count');
|
||||
|
||||
function updateUrlAndControls() {
|
||||
var urlParams = new URLSearchParams();
|
||||
if (searchQuery) urlParams.set('q', searchQuery);
|
||||
if (onlyLegacy) {
|
||||
urlParams.set('legacy', '1');
|
||||
} else {
|
||||
if (currentStatus) urlParams.set('status', currentStatus);
|
||||
if (currentRole) urlParams.set('role', currentRole);
|
||||
}
|
||||
var newUrl = window.location.pathname + (urlParams.toString() ? ('?' + urlParams.toString()) : '');
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
|
||||
// Update reset button
|
||||
var hasFilter = !!(searchQuery || onlyLegacy || currentStatus || currentRole);
|
||||
if (resetFiltersBtn) resetFiltersBtn.style.display = hasFilter ? 'inline-flex' : 'none';
|
||||
|
||||
// Update control states
|
||||
if (statusFilter) {
|
||||
statusFilter.disabled = onlyLegacy;
|
||||
statusFilter.classList.toggle('active', !!currentStatus && !onlyLegacy);
|
||||
}
|
||||
if (roleFilter) {
|
||||
roleFilter.disabled = onlyLegacy;
|
||||
roleFilter.classList.toggle('active', !!currentRole && !onlyLegacy);
|
||||
}
|
||||
if (legacyFilterLabel) {
|
||||
legacyFilterLabel.classList.toggle('active', onlyLegacy);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUsers(page, q, append) {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
if (!append) spinner.style.display = 'block';
|
||||
|
||||
try {
|
||||
var url = '/admin/users?page=' + page + '&q=' + encodeURIComponent(q);
|
||||
var params = new URLSearchParams();
|
||||
params.set('page', page);
|
||||
if (q) params.set('q', q);
|
||||
if (onlyLegacy) {
|
||||
params.set('legacy', '1');
|
||||
} else {
|
||||
if (currentStatus) params.set('status', currentStatus);
|
||||
if (currentRole) params.set('role', currentRole);
|
||||
}
|
||||
|
||||
var url = '/admin/users?' + params.toString();
|
||||
var res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
|
||||
// Update state from headers
|
||||
var total = res.headers.get('X-Total-Count');
|
||||
var hasMoreHeader = res.headers.get('X-Has-More');
|
||||
var totalLabelHeader = res.headers.get('X-Total-Label');
|
||||
var emptyMsgHeader = res.headers.get('X-Empty-Msg');
|
||||
|
||||
if (total !== null) countSpan.textContent = total;
|
||||
if (totalLabelHeader && totalLabel) totalLabel.textContent = totalLabelHeader;
|
||||
if (emptyMsgHeader && noUsersMsg) noUsersMsg.textContent = emptyMsgHeader;
|
||||
if (hasMoreHeader !== null) hasMore = (hasMoreHeader === 'true');
|
||||
|
||||
var html = await res.text();
|
||||
@@ -548,12 +726,74 @@
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(function() {
|
||||
searchQuery = searchInput.value;
|
||||
searchQuery = searchInput.value.trim();
|
||||
currentPage = 1;
|
||||
updateUrlAndControls();
|
||||
fetchUsers(1, searchQuery, false);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Status Filter Handling
|
||||
if (statusFilter) {
|
||||
statusFilter.addEventListener('change', function() {
|
||||
currentStatus = this.value;
|
||||
if (currentStatus && onlyLegacy) {
|
||||
onlyLegacy = false;
|
||||
if (legacyFilter) legacyFilter.checked = false;
|
||||
}
|
||||
updateUrlAndControls();
|
||||
currentPage = 1;
|
||||
fetchUsers(1, searchQuery, false);
|
||||
});
|
||||
}
|
||||
|
||||
// Role Filter Handling
|
||||
if (roleFilter) {
|
||||
roleFilter.addEventListener('change', function() {
|
||||
currentRole = this.value;
|
||||
if (currentRole && onlyLegacy) {
|
||||
onlyLegacy = false;
|
||||
if (legacyFilter) legacyFilter.checked = false;
|
||||
}
|
||||
updateUrlAndControls();
|
||||
currentPage = 1;
|
||||
fetchUsers(1, searchQuery, false);
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy Only Filter Handling
|
||||
if (legacyFilter) {
|
||||
legacyFilter.addEventListener('change', function() {
|
||||
onlyLegacy = this.checked;
|
||||
if (onlyLegacy) {
|
||||
currentStatus = '';
|
||||
currentRole = '';
|
||||
if (statusFilter) statusFilter.value = '';
|
||||
if (roleFilter) roleFilter.value = '';
|
||||
}
|
||||
updateUrlAndControls();
|
||||
currentPage = 1;
|
||||
fetchUsers(1, searchQuery, false);
|
||||
});
|
||||
}
|
||||
|
||||
// Reset All Filters
|
||||
if (resetFiltersBtn) {
|
||||
resetFiltersBtn.addEventListener('click', function() {
|
||||
searchQuery = '';
|
||||
searchInput.value = '';
|
||||
onlyLegacy = false;
|
||||
currentStatus = '';
|
||||
currentRole = '';
|
||||
if (legacyFilter) legacyFilter.checked = false;
|
||||
if (statusFilter) statusFilter.value = '';
|
||||
if (roleFilter) roleFilter.value = '';
|
||||
updateUrlAndControls();
|
||||
currentPage = 1;
|
||||
fetchUsers(1, '', false);
|
||||
});
|
||||
}
|
||||
|
||||
// Infinite Scroll
|
||||
var observer = new IntersectionObserver(function(entries) {
|
||||
if (entries[0].isIntersecting && hasMore && !isLoading) {
|
||||
@@ -624,6 +864,14 @@
|
||||
if (data.success) {
|
||||
closeCreateUserModal();
|
||||
showFlash(data.msg, 'success');
|
||||
// If we were filtering by legacy or banned, reset filter so admin can see newly created user
|
||||
if (onlyLegacy || currentStatus === 'banned') {
|
||||
onlyLegacy = false;
|
||||
currentStatus = '';
|
||||
if (legacyFilter) legacyFilter.checked = false;
|
||||
if (statusFilter) statusFilter.value = '';
|
||||
updateUrlAndControls();
|
||||
}
|
||||
// Refresh the table from page 1
|
||||
currentPage = 1;
|
||||
fetchUsers(1, searchQuery, false);
|
||||
|
||||
Reference in New Issue
Block a user