mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Update admin views and controllers
This commit is contained in:
@@ -216,149 +216,6 @@ Alpine.data('verifyUser', () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
// User List Scroll Sync
|
||||
Alpine.data('adminUserList', () => ({
|
||||
allChecked: false,
|
||||
|
||||
init() {
|
||||
const top = document.getElementById('topScroll');
|
||||
const bottom = document.getElementById('bottomScroll');
|
||||
const content = document.getElementById('topScrollContent');
|
||||
const table = bottom?.querySelector('table');
|
||||
if (top && bottom && content && table) {
|
||||
const sync = () => { content.style.width = table.scrollWidth + 'px'; };
|
||||
sync();
|
||||
window.addEventListener('resize', sync);
|
||||
top.addEventListener('scroll', function() { if (!top._syncing) { bottom._syncing = true; bottom.scrollLeft = top.scrollLeft; bottom._syncing = false; } });
|
||||
bottom.addEventListener('scroll', function() { if (!bottom._syncing) { top._syncing = true; top.scrollLeft = bottom.scrollLeft; bottom._syncing = false; } });
|
||||
}
|
||||
|
||||
const selectAll = this.$el.querySelector('#selectAllUserList');
|
||||
const form = this.$el.querySelector('#bulkUserActionForm');
|
||||
|
||||
if (selectAll) {
|
||||
selectAll.addEventListener('change', () => this.toggleAll());
|
||||
}
|
||||
|
||||
this.$el.querySelectorAll('.user-list-checkbox').forEach(cb => {
|
||||
cb.addEventListener('change', () => this.onCheckboxChange());
|
||||
});
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', e => this.submitBulkAction(e));
|
||||
}
|
||||
},
|
||||
|
||||
_checkboxes() {
|
||||
return Array.from(this.$el.querySelectorAll('.user-list-checkbox:not(:disabled)'));
|
||||
},
|
||||
|
||||
_checkedBoxes() {
|
||||
return this._checkboxes().filter(cb => cb.checked);
|
||||
},
|
||||
|
||||
_checkedUnverifiedBoxes() {
|
||||
return this._checkedBoxes().filter(cb => cb.dataset.isVerified === '0');
|
||||
},
|
||||
|
||||
_setValidationError(message) {
|
||||
const errEl = document.getElementById('validationError');
|
||||
const errMsg = document.getElementById('validationErrorMessage');
|
||||
if (errEl && errMsg) {
|
||||
errMsg.textContent = message;
|
||||
errEl.classList.remove('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
_clearValidationError() {
|
||||
const errEl = document.getElementById('validationError');
|
||||
if (errEl) errEl.classList.add('hidden');
|
||||
},
|
||||
|
||||
toggleAll() {
|
||||
const selectAll = this.$el.querySelector('#selectAllUserList');
|
||||
this._checkboxes().forEach(cb => { cb.checked = selectAll?.checked ?? false; });
|
||||
this.onCheckboxChange();
|
||||
},
|
||||
|
||||
onCheckboxChange() {
|
||||
const selectAll = this.$el.querySelector('#selectAllUserList');
|
||||
const boxes = this._checkboxes();
|
||||
const checked = this._checkedBoxes();
|
||||
this.allChecked = boxes.length > 0 && checked.length === boxes.length;
|
||||
|
||||
if (selectAll) {
|
||||
selectAll.checked = this.allChecked;
|
||||
selectAll.indeterminate = checked.length > 0 && checked.length < boxes.length;
|
||||
}
|
||||
},
|
||||
|
||||
submitBulkAction(e) {
|
||||
e.preventDefault();
|
||||
this._clearValidationError();
|
||||
|
||||
const form = this.$el.querySelector('#bulkUserActionForm');
|
||||
const action = this.$el.querySelector('#bulkUserAction')?.value;
|
||||
const checked = this._checkedBoxes();
|
||||
const checkedCount = checked.length;
|
||||
|
||||
if (!action) { this._setValidationError('Please select an action.'); return; }
|
||||
if (checkedCount === 0) { this._setValidationError('Please select at least one non-admin active user.'); return; }
|
||||
|
||||
const actions = {
|
||||
delete: {
|
||||
title: 'Soft Delete Users',
|
||||
actionText: 'soft-delete',
|
||||
type: 'danger',
|
||||
confirmText: 'Soft Delete'
|
||||
},
|
||||
verify: {
|
||||
title: 'Verify Users',
|
||||
actionText: 'mark as verified',
|
||||
type: 'success',
|
||||
confirmText: 'Verify'
|
||||
},
|
||||
resend_verification: {
|
||||
title: 'Resend Verification Emails',
|
||||
actionText: 'send account verification emails to',
|
||||
type: 'warning',
|
||||
confirmText: 'Send Emails'
|
||||
}
|
||||
};
|
||||
|
||||
const config = actions[action];
|
||||
if (!config) { this._setValidationError('Please select a valid action.'); return; }
|
||||
|
||||
let targetCount = checkedCount;
|
||||
let message = 'Are you sure you want to ' + config.actionText + ' ' + targetCount + ' user(s)?';
|
||||
|
||||
if (action === 'resend_verification') {
|
||||
const unverifiedCount = this._checkedUnverifiedBoxes().length;
|
||||
const skippedCount = checkedCount - unverifiedCount;
|
||||
|
||||
if (unverifiedCount === 0) {
|
||||
this._setValidationError('Please select at least one not verified user to resend verification email.');
|
||||
return;
|
||||
}
|
||||
|
||||
targetCount = unverifiedCount;
|
||||
message = 'Are you sure you want to send account verification emails to ' + targetCount + ' not verified user(s)?';
|
||||
|
||||
if (skippedCount > 0) {
|
||||
message += ' ' + skippedCount + ' already verified user(s) will be skipped.';
|
||||
}
|
||||
}
|
||||
|
||||
showConfirm({
|
||||
title: config.title,
|
||||
message: message,
|
||||
type: config.type,
|
||||
confirmText: config.confirmText,
|
||||
onConfirm: function() { if (form) form.submit(); }
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// Admin deleted users
|
||||
Alpine.data('adminDeletedUsers', () => ({
|
||||
allChecked: false,
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Alpine.data('adminUserList') - Admin user list bulk selection and scroll sync.
|
||||
*/
|
||||
import Alpine from '@alpinejs/csp';
|
||||
|
||||
Alpine.data('adminUserList', () => ({
|
||||
allChecked: false,
|
||||
|
||||
init() {
|
||||
const top = document.getElementById('topScroll');
|
||||
const bottom = document.getElementById('bottomScroll');
|
||||
const content = document.getElementById('topScrollContent');
|
||||
const table = bottom?.querySelector('table');
|
||||
if (top && bottom && content && table) {
|
||||
const sync = () => { content.style.width = table.scrollWidth + 'px'; };
|
||||
sync();
|
||||
window.addEventListener('resize', sync);
|
||||
top.addEventListener('scroll', function() { if (!top._syncing) { bottom._syncing = true; bottom.scrollLeft = top.scrollLeft; bottom._syncing = false; } });
|
||||
bottom.addEventListener('scroll', function() { if (!bottom._syncing) { top._syncing = true; top.scrollLeft = bottom.scrollLeft; bottom._syncing = false; } });
|
||||
}
|
||||
|
||||
const selectAll = this.$el.querySelector('#selectAllUserList');
|
||||
const form = this.$el.querySelector('#bulkUserActionForm');
|
||||
|
||||
if (selectAll) {
|
||||
selectAll.addEventListener('change', () => this.toggleAll());
|
||||
}
|
||||
|
||||
this.$el.querySelectorAll('.user-list-checkbox').forEach(cb => {
|
||||
cb.addEventListener('change', () => this.onCheckboxChange());
|
||||
});
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', e => this.submitBulkAction(e));
|
||||
}
|
||||
},
|
||||
|
||||
_checkboxes() {
|
||||
return Array.from(this.$el.querySelectorAll('.user-list-checkbox:not(:disabled)'));
|
||||
},
|
||||
|
||||
_checkedBoxes() {
|
||||
return this._checkboxes().filter(cb => cb.checked);
|
||||
},
|
||||
|
||||
_checkedUnverifiedBoxes() {
|
||||
return this._checkedBoxes().filter(cb => cb.dataset.isVerified === '0');
|
||||
},
|
||||
|
||||
_setValidationError(message) {
|
||||
const errEl = document.getElementById('validationError');
|
||||
const errMsg = document.getElementById('validationErrorMessage');
|
||||
if (errEl && errMsg) {
|
||||
errMsg.textContent = message;
|
||||
errEl.classList.remove('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
_clearValidationError() {
|
||||
const errEl = document.getElementById('validationError');
|
||||
if (errEl) errEl.classList.add('hidden');
|
||||
},
|
||||
|
||||
toggleAll() {
|
||||
const selectAll = this.$el.querySelector('#selectAllUserList');
|
||||
this._checkboxes().forEach(cb => { cb.checked = selectAll?.checked ?? false; });
|
||||
this.onCheckboxChange();
|
||||
},
|
||||
|
||||
onCheckboxChange() {
|
||||
const selectAll = this.$el.querySelector('#selectAllUserList');
|
||||
const boxes = this._checkboxes();
|
||||
const checked = this._checkedBoxes();
|
||||
this.allChecked = boxes.length > 0 && checked.length === boxes.length;
|
||||
|
||||
if (selectAll) {
|
||||
selectAll.checked = this.allChecked;
|
||||
selectAll.indeterminate = checked.length > 0 && checked.length < boxes.length;
|
||||
}
|
||||
},
|
||||
|
||||
submitBulkAction(e) {
|
||||
e.preventDefault();
|
||||
this._clearValidationError();
|
||||
|
||||
const form = this.$el.querySelector('#bulkUserActionForm');
|
||||
const action = this.$el.querySelector('#bulkUserAction')?.value;
|
||||
const checked = this._checkedBoxes();
|
||||
const checkedCount = checked.length;
|
||||
|
||||
if (!action) { this._setValidationError('Please select an action.'); return; }
|
||||
if (checkedCount === 0) { this._setValidationError('Please select at least one non-admin active user.'); return; }
|
||||
|
||||
const actions = {
|
||||
delete: {
|
||||
title: 'Soft Delete Users',
|
||||
actionText: 'soft-delete',
|
||||
type: 'danger',
|
||||
confirmText: 'Soft Delete',
|
||||
},
|
||||
verify: {
|
||||
title: 'Verify Users',
|
||||
actionText: 'mark as verified',
|
||||
type: 'success',
|
||||
confirmText: 'Verify',
|
||||
},
|
||||
resend_verification: {
|
||||
title: 'Resend Verification Emails',
|
||||
actionText: 'send account verification emails to',
|
||||
type: 'warning',
|
||||
confirmText: 'Send Emails',
|
||||
},
|
||||
};
|
||||
|
||||
const config = actions[action];
|
||||
if (!config) { this._setValidationError('Please select a valid action.'); return; }
|
||||
|
||||
let targetCount = checkedCount;
|
||||
let message = 'Are you sure you want to ' + config.actionText + ' ' + targetCount + ' user(s)?';
|
||||
|
||||
if (action === 'resend_verification') {
|
||||
const unverifiedCount = this._checkedUnverifiedBoxes().length;
|
||||
const skippedCount = checkedCount - unverifiedCount;
|
||||
|
||||
if (unverifiedCount === 0) {
|
||||
this._setValidationError('Please select at least one not verified user to resend verification email.');
|
||||
return;
|
||||
}
|
||||
|
||||
targetCount = unverifiedCount;
|
||||
message = 'Are you sure you want to send account verification emails to ' + targetCount + ' not verified user(s)?';
|
||||
|
||||
if (skippedCount > 0) {
|
||||
message += ' ' + skippedCount + ' already verified user(s) will be skipped.';
|
||||
}
|
||||
}
|
||||
|
||||
showConfirm({
|
||||
title: config.title,
|
||||
message: message,
|
||||
type: config.type,
|
||||
confirmText: config.confirmText,
|
||||
onConfirm: function() { if (form) form.submit(); },
|
||||
});
|
||||
},
|
||||
}));
|
||||
@@ -51,7 +51,7 @@ const lazyComponentMap = {
|
||||
'adminGroups': () => import('./components/admin/groups.js'),
|
||||
'adminFeatures': () => import('./components/admin/features.js'),
|
||||
'adminUserEdit': () => import('./components/admin/features.js'), // same file
|
||||
'adminUserList': () => import('./components/admin/features.js'), // same file
|
||||
'adminUserList': () => import('./components/admin/user-list.js'),
|
||||
'adminDeletedUsers': () => import('./components/admin/features.js'), // same file
|
||||
'adminInvitations': () => import('./components/admin/features.js'), // same file
|
||||
'adminRegexForm': () => import('./components/admin/features.js'), // same file
|
||||
|
||||
@@ -3,20 +3,16 @@
|
||||
@section('content')
|
||||
<div class="space-y-6" x-data="adminUserList">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-semibold text-gray-800 dark:text-gray-200">
|
||||
<i class="fas fa-users mr-2"></i>{{ $title }}
|
||||
</h1>
|
||||
<x-admin.page-header :title="$title" icon="fas fa-users">
|
||||
<x-slot:actions>
|
||||
<a href="{{ url('admin/user-edit?action=add') }}" class="px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-lg hover:bg-blue-700 dark:hover:bg-blue-800">
|
||||
<i class="fas fa-plus mr-2"></i>Add New User
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot:actions>
|
||||
</x-admin.page-header>
|
||||
|
||||
<!-- Search Filters -->
|
||||
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
|
||||
<x-admin.search-bar>
|
||||
<form method="get" action="{{ url('admin/user-list') }}">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
@@ -95,34 +91,9 @@
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Success/Error Messages -->
|
||||
@if(request()->has('deleted') && request()->input('deleted') == 1)
|
||||
<div class="mx-6 mt-4 p-4 bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg">
|
||||
<p class="text-green-800 dark:text-green-200">
|
||||
<i class="fas fa-check-circle mr-2"></i>
|
||||
User "{{ request()->input('username') }}" has been deleted successfully.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('success'))
|
||||
<div class="mx-6 mt-4 p-4 bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg">
|
||||
<p class="text-green-800 dark:text-green-200">
|
||||
<i class="fas fa-check-circle mr-2"></i>{{ session('success') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="mx-6 mt-4 p-4 bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-lg">
|
||||
<p class="text-red-800 dark:text-red-200">
|
||||
<i class="fas fa-exclamation-circle mr-2"></i>{{ session('error') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</x-admin.search-bar>
|
||||
|
||||
<!-- Validation Messages -->
|
||||
@if($errors->any())
|
||||
<div class="mx-6 mt-4 p-4 bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-lg">
|
||||
<p class="text-red-800 dark:text-red-200">
|
||||
@@ -489,4 +460,3 @@
|
||||
@endsection
|
||||
|
||||
{{-- Scripts moved to resources/js/csp-safe.js --}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user