From 1576dece02ca716c265ea306b4cf9e7a524b68a0 Mon Sep 17 00:00:00 2001 From: DariusIII Date: Wed, 29 Oct 2025 15:49:59 +0100 Subject: [PATCH] Use better confirmation modals --- MIGRATION_GUIDE.md | 273 ---------- PER_USER_DARK_MODE.md | 153 ------ .../Admin/DeletedUsersController.php | 11 +- app/Models/User.php | 2 +- resources/js/csp-safe.js | 505 ++++++++++++------ resources/views/admin/users/deleted.blade.php | 37 +- resources/views/admin/users/index.blade.php | 64 ++- resources/views/layouts/admin.blade.php | 3 + resources/views/layouts/main.blade.php | 3 + .../partials/confirmation-modal.blade.php | 38 ++ 10 files changed, 451 insertions(+), 638 deletions(-) delete mode 100644 MIGRATION_GUIDE.md delete mode 100644 PER_USER_DARK_MODE.md create mode 100644 resources/views/partials/confirmation-modal.blade.php diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md deleted file mode 100644 index 117ed6ae9..000000000 --- a/MIGRATION_GUIDE.md +++ /dev/null @@ -1,273 +0,0 @@ -# Smarty 4 to Laravel Blade + TailwindCSS Migration Guide - -## Overview -This document provides a comprehensive guide for migrating the NNTmux application from Smarty 4 templates to Laravel Blade with TailwindCSS. - -## Changes Made - -### 1. New Blade Layouts Created -- `resources/views/layouts/main.blade.php` - Main application layout -- `resources/views/layouts/admin.blade.php` - Admin panel layout -- `resources/views/layouts/app.blade.php` - Existing Laravel auth layout (retained) - -### 2. New Blade Partials Created -- `resources/views/partials/header-menu.blade.php` - Top navigation menu -- `resources/views/partials/sidebar.blade.php` - Left sidebar navigation -- `resources/views/partials/footer.blade.php` - Footer content -- `resources/views/partials/admin-menu.blade.php` - Admin sidebar menu - -### 3. New Blade Views Created -- `resources/views/browse/index.blade.php` - Browse releases page -- `resources/views/details/index.blade.php` - Release details page -- `resources/views/home/index.blade.php` - Home page template - -### 4. Controllers Updated -The following controllers have been migrated from Smarty to Blade: -- `BasePageController.php` - Base controller (removed Smarty, added Blade support) -- `BrowseController.php` - Browse functionality -- `DetailsController.php` - Release details - -## Migration Pattern - -### Before (Smarty): -```php -public function index(Request $request) -{ - $this->setPreferences(); - - // Business logic... - - $this->smarty->assign('data', $data); - $this->smarty->assign('meta_title', 'Page Title'); - - $content = $this->smarty->fetch('template.tpl'); - $this->smarty->assign(compact('content', 'meta_title')); - $this->pagerender(); -} -``` - -### After (Blade): -```php -public function index(Request $request) -{ - $this->setPreferences(); - - // Business logic... - - $this->viewData = array_merge($this->viewData, [ - 'data' => $data, - 'meta_title' => 'Page Title', - ]); - - return view('module.index', $this->viewData); -} -``` - -## Template Syntax Migration - -### Variables -**Smarty:** -```smarty -{$variable} -{$user.name} -``` - -**Blade:** -```blade -{{ $variable }} -{{ $user->name }} -``` - -### Conditionals -**Smarty:** -```smarty -{if $condition} - Content -{elseif $other} - Other -{else} - Default -{/if} -``` - -**Blade:** -```blade -@if($condition) - Content -@elseif($other) - Other -@else - Default -@endif -``` - -### Loops -**Smarty:** -```smarty -{foreach $items as $item} - {$item.name} -{/foreach} -``` - -**Blade:** -```blade -@foreach($items as $item) - {{ $item->name }} -@endforeach -``` - -### URLs and Routes -**Smarty:** -```smarty -{{url('/browse/All')}} -{{route('series')}} -``` - -**Blade:** -```blade -{{ url('/browse/All') }} -{{ route('series') }} -``` - -### Authentication -**Smarty:** -```smarty -{if Auth::check()} - Logged in content -{/if} -``` - -**Blade:** -```blade -@auth - Logged in content -@endauth - -@guest - Guest content -@endguest -``` - -### Including Partials -**Smarty:** -```smarty -{include file='sidebar.tpl'} -``` - -**Blade:** -```blade -@include('partials.sidebar') -``` - -## TailwindCSS Integration - -### Styling Philosophy -- Replace Bootstrap classes with TailwindCSS utility classes -- Use responsive prefixes (sm:, md:, lg:, xl:) -- Utilize Tailwind's color palette and spacing system - -### Common Class Conversions - -| Bootstrap | TailwindCSS | -|-----------|-------------| -| `container` | `container mx-auto px-4` | -| `row` | `flex flex-wrap` or `grid` | -| `col-md-6` | `md:w-1/2` or `md:col-span-6` | -| `btn btn-primary` | `px-4 py-2 bg-blue-600 text-white rounded` | -| `text-center` | `text-center` | -| `mb-3` | `mb-3` (Tailwind uses same spacing scale) | -| `d-flex` | `flex` | -| `justify-content-between` | `justify-between` | -| `align-items-center` | `items-center` | - -## Remaining Work - -### Controllers to Update -All remaining controllers in `app/Http/Controllers/` need to be migrated: -- SearchController.php -- MovieController.php -- SeriesController.php -- ProfileController.php -- CartController.php -- Admin/* controllers -- And many more... - -### Templates to Create -Convert all `.tpl` files from `resources/views/themes/` to `.blade.php`: -- Admin templates -- Search templates -- Movie/TV templates -- Profile templates -- Forum templates -- etc. - -### Steps for Each Controller Migration - -1. **Update the controller method:** - - Remove `$this->smarty->assign()` calls - - Replace with `$this->viewData = array_merge()` - - Change `$this->pagerender()` to `return view('view.name', $this->viewData)` - -2. **Create corresponding Blade view:** - - Copy relevant HTML from `.tpl` file - - Convert Smarty syntax to Blade syntax - - Replace Bootstrap classes with TailwindCSS - - Ensure proper use of `@extends` and `@section` - -3. **Test thoroughly:** - - Verify all data displays correctly - - Check responsive design - - Test user interactions - - Validate forms and AJAX calls - -## Configuration Changes - -### Remove Smarty Package (After Migration Complete) -Once all templates are migrated, you can remove the Smarty dependency: - -```bash -composer remove smarty/smarty ytake/laravel-smarty -``` - -Remove from `config/app.php`: -- Smarty service provider entries - -Delete: -- `config/ytake-laravel-smarty.php` - -## Benefits of This Migration - -1. **Better Laravel Integration** - Blade is Laravel's native templating engine -2. **Performance** - Blade compiles to plain PHP and is cached -3. **Modern UI** - TailwindCSS provides utility-first CSS framework -4. **Maintainability** - Standard Laravel conventions -5. **Developer Experience** - Better IDE support and debugging -6. **Security** - Automatic XSS protection with Blade's `{{ }}` syntax - -## Testing Checklist - -- [ ] Authentication works (login/logout) -- [ ] Browse pages display correctly -- [ ] Release details show all information -- [ ] Search functionality works -- [ ] Admin panel is accessible -- [ ] Forms submit properly -- [ ] AJAX requests function -- [ ] Responsive design on mobile -- [ ] All routes resolve correctly -- [ ] No JavaScript errors in console - -## Notes - -- The `BasePageController` now uses `$viewData` array instead of Smarty assignments -- All views should extend `layouts.main` or `layouts.admin` -- Use `@stack` and `@push` for page-specific scripts/styles -- TailwindCSS config is in `tailwind.config.js` -- Vite is configured for asset compilation - -## Support - -For questions or issues during migration, refer to: -- [Laravel Blade Documentation](https://laravel.com/docs/blade) -- [TailwindCSS Documentation](https://tailwindcss.com/docs) - diff --git a/PER_USER_DARK_MODE.md b/PER_USER_DARK_MODE.md deleted file mode 100644 index 65a445536..000000000 --- a/PER_USER_DARK_MODE.md +++ /dev/null @@ -1,153 +0,0 @@ -# Per-User Dark Mode Implementation - -## Overview -The dark mode/light mode switch is now **per-user** instead of global. Each authenticated user's theme preference is stored in the database and persists across different browsers and devices. - -## What Changed - -### 1. Database Migration -- **File**: `database/migrations/2025_10_16_101145_add_dark_mode_to_users_table.php` -- Added `dark_mode` boolean column to the `users` table -- Default value: `false` (light mode) -- Migration has been applied successfully - -### 2. User Model -- The `dark_mode` field is now part of the User model -- Users can have their own individual theme preferences - -### 3. ProfileController -- **File**: `app/Http/Controllers/ProfileController.php` -- Added `updateTheme()` method to handle theme preference updates -- Accepts POST requests with `dark_mode` boolean value -- Validates input and saves to database -- Returns JSON response with success status - -### 4. Routes -- **File**: `routes/web.php` -- Added new route: `POST /profile/update-theme` -- Route name: `profile.update-theme` -- Protected by `isVerified` middleware (authenticated users only) - -### 5. Main Layout (User Interface) -- **File**: `resources/views/layouts/main.blade.php` -- **Initialization**: Theme is loaded from user's database preference for authenticated users -- **Toggle Button**: Sends AJAX request to backend when clicked -- **Fallback**: Non-authenticated users still use localStorage - -### 6. Admin Layout -- **File**: `resources/views/layouts/admin.blade.php` -- Same behavior as main layout -- Theme preference is synchronized across both admin and user interfaces - -- Informative descriptions for each theme option - -**Method 2: Profile Edit Page (Permanent Setting)** -1. Navigate to Profile → Edit Profile -2. In the "Theme Preference" section, select either: - - **Light Mode**: Bright and clean interface (sun icon) - - **Dark Mode**: Easy on the eyes, especially at night (moon icon) -3. The theme changes instantly as you select (preview) -4. Click "Save Changes" to permanently save the preference -5. Theme preference applies across all devices and browsers - -**Synchronized Across:** -- Different browsers -- Different devices -- Admin and user interfaces -- All sessions -1. When page loads, the user's `dark_mode` preference from database is applied immediately -2. When user clicks the theme toggle button: - - The DOM is updated instantly (visual feedback) - - An AJAX POST request is sent to `/profile/update-theme` - - The preference is saved to the database -3. Theme preference follows the user across: - - Different browsers - - Different devices - - Admin and user interfaces - -### For Non-Authenticated Users (Guests) -- Theme preference is stored in browser's localStorage -- Works the same as before (browser-specific) - -## Benefits - -1. **Consistency**: User's theme preference is consistent across all devices -2. **Persistence**: Theme preference survives browser cache clearing -3. **User-Specific**: Each user can have their own preference -4. **Seamless**: No page reload required when toggling theme -5. **Backward Compatible**: Guest users still have dark mode functionality - -## API Endpoint - -### Update Theme Preference -``` -POST /profile/update-theme -Content-Type: application/json -X-CSRF-TOKEN: {token} - -Request Body: -{ - "dark_mode": true // or false -} - -Response (Success): -{ - "success": true, - "dark_mode": true -} - -Response (Error): -{ - "success": false, - "message": "Error message" -} -### Quick Toggle Method -``` - -## Database Schema - - -### Profile Edit Page Method -1. **Login as a user** -2. **Navigate to** Profile → Edit Profile -3. **Select a theme** in the "Theme Preference" section -4. **Observe instant preview** - theme changes immediately -5. **Click "Save Changes"** to persist the setting -6. **Refresh the page** - theme should remain -7. **Login on a different device** - same theme should apply - -### Verify in Database -```sql -SELECT username, dark_mode FROM users WHERE id = {your_user_id}; -``` - -## Testing - -To test the implementation: - -1. **Login as a user** -2. **Toggle dark mode** using the button in bottom-left corner -3. **Refresh the page** - theme should persist -4. **Login on a different browser** - same theme should apply -5. **Check database**: - ```sql - SELECT username, dark_mode FROM users WHERE id = {your_user_id}; - ``` - -## Rollback - -If you need to rollback this feature: - -```bash -php artisan migrate:rollback --step=1 -``` - -This will remove the `dark_mode` column from the users table. You'll also need to revert the layout files to use only localStorage. - -## Notes - -- The feature is fully implemented and ready to use -- Routes have been cached successfully -- No breaking changes to existing functionality -- Guest users retain their localStorage-based theme switching - diff --git a/app/Http/Controllers/Admin/DeletedUsersController.php b/app/Http/Controllers/Admin/DeletedUsersController.php index 12d8cec54..19cb0a380 100644 --- a/app/Http/Controllers/Admin/DeletedUsersController.php +++ b/app/Http/Controllers/Admin/DeletedUsersController.php @@ -108,13 +108,18 @@ class DeletedUsersController extends BasePageController $action = $request->input('action'); $userIds = $request->input('user_ids', []); - if (! in_array($action, ['restore', 'delete'], true) || empty($userIds) || ! is_array($userIds)) { - return redirect()->route('admin.deleted.users.index')->with('error', 'Invalid bulk action request.'); + // Better validation with specific error messages + if (empty($action) || ! in_array($action, ['restore', 'delete'], true)) { + return redirect()->route('admin.deleted.users.index')->with('error', 'Please select a valid action.'); + } + + if (! is_array($userIds)) { + return redirect()->route('admin.deleted.users.index')->with('error', 'Invalid user selection format.'); } $userIds = array_filter(array_map('intval', $userIds)); if (empty($userIds)) { - return redirect()->route('admin.deleted.users.index')->with('error', 'No valid users selected.'); + return redirect()->route('admin.deleted.users.index')->with('error', 'Please select at least one user.'); } if ($action === 'restore') { diff --git a/app/Models/User.php b/app/Models/User.php index 545cb441e..13df14fbb 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -256,7 +256,7 @@ class User extends Authenticatable public static function getCount(?string $role = null, ?string $username = '', ?string $host = '', ?string $email = ''): int { - $res = self::query()->where('email', '<>', 'sharing@nZEDb.com'); + $res = self::query()->withTrashed()->where('email', '<>', 'sharing@nZEDb.com'); if (! empty($role)) { $res->where('roles_id', $role); diff --git a/resources/js/csp-safe.js b/resources/js/csp-safe.js index 0f311a1e7..c3cc314be 100644 --- a/resources/js/csp-safe.js +++ b/resources/js/csp-safe.js @@ -74,21 +74,49 @@ function initEventDelegation() { } } - // Handle confirm delete + // Handle confirm delete - using styled modal if (e.target.hasAttribute('data-confirm-delete') || e.target.closest('[data-confirm-delete]')) { - const confirmed = confirm('Are you sure you want to delete this item?'); - if (!confirmed) { - e.preventDefault(); - } + e.preventDefault(); + e.stopPropagation(); + + const element = e.target.hasAttribute('data-confirm-delete') ? e.target : e.target.closest('[data-confirm-delete]'); + const form = element.closest('form'); + + showConfirm({ + message: 'Are you sure you want to delete this item?', + type: 'danger', + confirmText: 'Delete', + onConfirm: function() { + if (form) { + form.submit(); + } else if (element.href) { + window.location.href = element.href; + } + } + }); } - // Handle confirm action + // Handle confirm action - using styled modal if (e.target.hasAttribute('data-confirm') || e.target.closest('[data-confirm]')) { - const message = e.target.getAttribute('data-confirm') || e.target.closest('[data-confirm]').getAttribute('data-confirm'); - const confirmed = confirm(message); - if (!confirmed) { - e.preventDefault(); - } + e.preventDefault(); + e.stopPropagation(); + + const element = e.target.hasAttribute('data-confirm') ? e.target : e.target.closest('[data-confirm]'); + const message = element.getAttribute('data-confirm'); + const form = element.closest('form'); + + showConfirm({ + message: message, + type: 'danger', + confirmText: 'Delete', + onConfirm: function() { + if (form) { + form.submit(); + } else if (element.href) { + window.location.href = element.href; + } + } + }); } // Handle download NZB buttons @@ -207,9 +235,142 @@ function initToastNotifications() { toast.remove(); }, 300); }, 5000); + + // Add close button listener + const closeBtn = toast.querySelector('.toast-close'); + if (closeBtn) { + closeBtn.addEventListener('click', function() { + toast.classList.add('removing'); + setTimeout(function() { + toast.remove(); + }, 300); + }); + } }; } +// Styled Confirmation Modal (replaces native confirm dialogs) +let confirmationCallback = null; + +window.showConfirm = function(options) { + // Options: { message, title, details, confirmText, cancelText, type, onConfirm } + const modal = document.getElementById('confirmationModal'); + if (!modal) return Promise.reject('Modal not found'); + + const defaults = { + title: 'Confirm Action', + message: 'Are you sure you want to proceed?', + details: '', + confirmText: 'Confirm', + cancelText: 'Cancel', + type: 'info', // 'info', 'warning', 'danger', 'success' + onConfirm: null + }; + + const config = { ...defaults, ...options }; + + // Set modal content + document.getElementById('confirmationModalTitleText').textContent = config.title; + document.getElementById('confirmationModalMessage').textContent = config.message; + document.getElementById('confirmationModalConfirmText').textContent = config.confirmText; + document.getElementById('confirmationModalCancelText').textContent = config.cancelText; + + // Set icon based on type + const icon = document.getElementById('confirmationModalIcon'); + const confirmBtn = document.getElementById('confirmationModalConfirmBtn'); + + // Remove all existing classes from icon safely (works with SVG elements) + while (icon.classList.length > 0) { + icon.classList.remove(icon.classList.item(0)); + } + icon.classList.add('fas', 'mr-2'); + + // Remove all existing classes from button + while (confirmBtn.classList.length > 0) { + confirmBtn.classList.remove(confirmBtn.classList.item(0)); + } + confirmBtn.classList.add('px-4', 'py-2', 'text-white', 'rounded-lg', 'transition', 'font-medium'); + + if (config.type === 'danger') { + icon.classList.add('fa-exclamation-triangle', 'text-red-600', 'dark:text-red-400'); + confirmBtn.classList.add('bg-red-600', 'dark:bg-red-700', 'hover:bg-red-700', 'dark:hover:bg-red-800'); + } else if (config.type === 'warning') { + icon.classList.add('fa-exclamation-circle', 'text-yellow-600', 'dark:text-yellow-400'); + confirmBtn.classList.add('bg-yellow-600', 'dark:bg-yellow-700', 'hover:bg-yellow-700', 'dark:hover:bg-yellow-800'); + } else if (config.type === 'success') { + icon.classList.add('fa-check-circle', 'text-green-600', 'dark:text-green-400'); + confirmBtn.classList.add('bg-green-600', 'dark:bg-green-700', 'hover:bg-green-700', 'dark:hover:bg-green-800'); + } else { // info + icon.classList.add('fa-info-circle', 'text-blue-600', 'dark:text-blue-400'); + confirmBtn.classList.add('bg-blue-600', 'dark:bg-blue-700', 'hover:bg-blue-700', 'dark:hover:bg-blue-800'); + } + + // Handle details + const detailsDiv = document.getElementById('confirmationModalDetails'); + const detailsText = document.getElementById('confirmationModalDetailsText'); + if (config.details) { + detailsText.textContent = config.details; + detailsDiv.classList.remove('hidden'); + } else { + detailsDiv.classList.add('hidden'); + } + + // Store callback + confirmationCallback = config.onConfirm; + + // Show modal + modal.classList.remove('hidden'); + modal.classList.add('flex'); + + // Return promise for async/await usage + return new Promise((resolve, reject) => { + confirmationCallback = function(confirmed) { + if (confirmed && config.onConfirm) { + config.onConfirm(); + } + resolve(confirmed); + }; + }); +}; + +window.closeConfirmationModal = function() { + const modal = document.getElementById('confirmationModal'); + if (modal) { + modal.classList.add('hidden'); + modal.classList.remove('flex'); + } + if (confirmationCallback) { + confirmationCallback(false); + confirmationCallback = null; + } +}; + +window.confirmConfirmationModal = function() { + const modal = document.getElementById('confirmationModal'); + if (modal) { + modal.classList.add('hidden'); + modal.classList.remove('flex'); + } + if (confirmationCallback) { + confirmationCallback(true); + confirmationCallback = null; + } +}; + +// Enhanced confirm function (backward compatible but with styled modal) +window.confirmStyled = function(message, title = 'Confirm', type = 'info') { + return new Promise((resolve) => { + showConfirm({ + message: message, + title: title, + type: type, + onConfirm: () => resolve(true) + }).then(result => { + if (!result) resolve(false); + }); + }); +}; + // NFO Modal function initNfoModal() { window.openNfoModal = function(guid) { @@ -989,63 +1150,6 @@ function initMediainfoAndFilelist() { // Cart and Multi-select functionality function initCartFunctionality() { - // Global showConfirmation function for cart page - window.showConfirmation = function(message, onConfirm) { - const modal = document.createElement('div'); - modal.className = 'confirmation-modal'; - modal.innerHTML = ` -
-
-
- -
-

Confirm Deletion

-
-
- ${message} -
- -
- `; - - document.body.appendChild(modal); - - const cancelBtn = modal.querySelector('.btn-cancel'); - const confirmBtn = modal.querySelector('.btn-confirm'); - - function closeModal() { - modal.style.animation = 'fadeOut 0.2s ease-out'; - setTimeout(() => modal.remove(), 200); - } - - cancelBtn.addEventListener('click', closeModal); - modal.addEventListener('click', function(e) { - if (e.target === modal) { - closeModal(); - } - }); - - confirmBtn.addEventListener('click', function() { - closeModal(); - onConfirm(); - }); - - // Focus on cancel button by default - setTimeout(() => cancelBtn.focus(), 100); - - // ESC key to close - const escHandler = function(e) { - if (e.key === 'Escape') { - closeModal(); - document.removeEventListener('keydown', escHandler); - } - }; - document.addEventListener('keydown', escHandler); - }; - // Cart page specific functionality const checkAll = document.getElementById('check-all'); const checkboxes = document.querySelectorAll('.cart-checkbox'); @@ -1121,9 +1225,12 @@ function initCartFunctionality() { return; } - showConfirmation( - `Are you sure you want to delete ${selected.length} item(s) from your cart?`, - function() { + showConfirm({ + title: 'Delete from Cart', + message: `Are you sure you want to delete ${selected.length} item${selected.length > 1 ? 's' : ''} from your cart?`, + type: 'danger', + confirmText: 'Delete', + onConfirm: function() { // Delete via AJAX fetch('/cart/delete/' + selected.join(','), { method: 'POST', @@ -1155,7 +1262,7 @@ function initCartFunctionality() { } }); } - ); + }); }); }); @@ -1168,9 +1275,12 @@ function initCartFunctionality() { const releaseName = this.getAttribute('data-release-name'); const deleteUrl = this.getAttribute('data-delete-url'); - showConfirmation( - `Are you sure you want to remove ${releaseName} from your cart?`, - function() { + showConfirm({ + title: 'Remove from Cart', + message: `Are you sure you want to remove "${releaseName}" from your cart?`, + type: 'warning', + confirmText: 'Remove', + onConfirm: function() { if (typeof showToast === 'function') { showToast('Removing item from cart...', 'info'); } @@ -1179,7 +1289,7 @@ function initCartFunctionality() { window.location.href = deleteUrl; }, 500); } - ); + }); }); }); } @@ -1288,10 +1398,15 @@ function initCartFunctionality() { return; } - const confirmMessage = `Are you sure you want to delete ${selected.length} release${selected.length > 1 ? 's' : ''}? This action cannot be undone.`; + const confirmMessage = `Are you sure you want to delete ${selected.length} release${selected.length > 1 ? 's' : ''}? This action cannot be undone.`; - // Use the custom confirmation modal - showConfirmation(confirmMessage, function() { + // Use styled confirmation modal + showConfirm({ + title: 'Delete Releases', + message: confirmMessage, + type: 'danger', + confirmText: 'Delete', + onConfirm: function() { console.log('User confirmed deletion'); const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; @@ -1385,7 +1500,8 @@ function initCartFunctionality() { setTimeout(() => window.location.reload(), 1500); } }); - }); + } + }); }); } else { console.log('Delete button not found on page'); @@ -3691,97 +3807,110 @@ function ajax_backfill_status(id, status) { } function ajax_group_reset(id) { - if (!confirm('Are you sure you want to reset this group?')) { - return; - } + showConfirm({ + title: 'Reset Group', + message: 'Are you sure you want to reset this group?', + type: 'warning', + confirmText: 'Reset', + onConfirm: function() { + const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl; + const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken; - const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl; - const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken; + if (!ajaxUrl || !csrfToken) return; - if (!ajaxUrl || !csrfToken) return; - - fetch(ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': csrfToken - }, - body: JSON.stringify({ - action: 'reset_group', - group_id: id - }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - location.reload(); + fetch(ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ + action: 'reset_group', + group_id: id + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + location.reload(); + } + }) + .catch(error => console.error('Error resetting group:', error)); } - }) - .catch(error => console.error('Error resetting group:', error)); + }); } function confirmGroupDelete(id) { - if (!confirm('Are you sure you want to delete this group?')) { - return; - } + showConfirm({ + title: 'Delete Group', + message: 'Are you sure you want to delete this group?', + type: 'danger', + confirmText: 'Delete', + onConfirm: function() { + const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl; + const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken; - const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl; - const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken; + if (!ajaxUrl || !csrfToken) return; - if (!ajaxUrl || !csrfToken) return; - - fetch(ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': csrfToken - }, - body: JSON.stringify({ - action: 'delete_group', - group_id: id - }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - const row = document.getElementById('grouprow-' + id); - if (row) { - row.classList.add('fade-out'); - setTimeout(() => row.remove(), 300); - } + fetch(ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ + action: 'delete_group', + group_id: id + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + const row = document.getElementById('grouprow-' + id); + if (row) { + row.classList.add('fade-out'); + setTimeout(() => row.remove(), 300); + } + } + }) + .catch(error => console.error('Error deleting group:', error)); } - }) - .catch(error => console.error('Error deleting group:', error)); + }); } function confirmGroupPurge(id) { - if (!confirm('Are you sure you want to purge this group? This will delete all releases and binaries!')) { - return; - } + showConfirm({ + title: 'Purge Group', + message: 'Are you sure you want to purge this group?', + details: 'This will delete all releases and binaries!', + type: 'danger', + confirmText: 'Purge', + onConfirm: function() { + const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl; + const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken; - const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl; - const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken; + if (!ajaxUrl || !csrfToken) return; - if (!ajaxUrl || !csrfToken) return; - - fetch(ajaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': csrfToken - }, - body: JSON.stringify({ - action: 'purge_group', - group_id: id - }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - location.reload(); + fetch(ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ + action: 'purge_group', + group_id: id + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + location.reload(); + } + }) + .catch(error => console.error('Error purging group:', error)); } - }) - .catch(error => console.error('Error purging group:', error)); + }); } function ajax_group_reset_all() { @@ -3886,6 +4015,7 @@ window.showVerifyModal = function(event, form) { const modal = document.getElementById('verifyUserModal'); if (modal) { modal.classList.remove('hidden'); + modal.classList.add('flex'); } }; @@ -3893,6 +4023,7 @@ window.hideVerifyModal = function() { const modal = document.getElementById('verifyUserModal'); if (modal) { modal.classList.add('hidden'); + modal.classList.remove('flex'); } window.currentVerifyForm = null; }; @@ -4139,7 +4270,9 @@ function initAdminDeletedUsers() { } // Bulk action confirmation - window.confirmBulkAction = function() { + window.confirmBulkAction = function(event) { + event?.preventDefault(); + const action = document.getElementById('bulkAction')?.value; const checkedBoxes = document.querySelectorAll('.user-checkbox:checked'); const validationError = document.getElementById('validationError'); @@ -4168,21 +4301,59 @@ function initAdminDeletedUsers() { const count = checkedBoxes.length; const actionText = action === 'restore' ? 'restore' : 'permanently delete'; + const type = action === 'restore' ? 'success' : 'danger'; + const title = action === 'restore' ? 'Restore Users' : 'Delete Users'; - return confirm(`Are you sure you want to ${actionText} ${count} user${count > 1 ? 's' : ''}?`); - }; - - // Individual action confirmations - document.querySelectorAll('.inline-form button[data-confirm]').forEach(button => { - button.addEventListener('click', function(e) { - const message = this.getAttribute('data-confirm'); - if (!confirm(message)) { - e.preventDefault(); + showConfirm({ + title: title, + message: `Are you sure you want to ${actionText} ${count} user${count > 1 ? 's' : ''}?`, + type: type, + confirmText: action === 'restore' ? 'Restore' : 'Delete', + onConfirm: function() { + document.getElementById('bulkActionForm')?.submit(); } }); - }); + + return false; + }; } +// Individual user restore/delete actions for deleted users page +window.restoreUser = function(userId, username) { + showConfirm({ + title: 'Restore User', + message: `Are you sure you want to restore user '${username}'?`, + type: 'success', + confirmText: 'Restore', + onConfirm: function() { + const form = document.getElementById('individualActionForm'); + if (form) { + const baseUrl = window.location.origin; + form.action = `${baseUrl}/admin/deleted-users/restore/${userId}`; + form.submit(); + } + } + }); +}; + +window.permanentDeleteUser = function(userId, username) { + showConfirm({ + title: 'Permanently Delete User', + message: `Are you sure you want to PERMANENTLY delete user '${username}'?`, + details: 'This action cannot be undone!', + type: 'danger', + confirmText: 'Delete Permanently', + onConfirm: function() { + const form = document.getElementById('individualActionForm'); + if (form) { + const baseUrl = window.location.origin; + form.action = `${baseUrl}/admin/deleted-users/permanent-delete/${userId}`; + form.submit(); + } + } + }); +}; + // My Movies page functionality function initMyMovies() { // Confirm before removing movies diff --git a/resources/views/admin/users/deleted.blade.php b/resources/views/admin/users/deleted.blade.php index 2d6d8f07a..77a8cdeb4 100644 --- a/resources/views/admin/users/deleted.blade.php +++ b/resources/views/admin/users/deleted.blade.php @@ -128,7 +128,7 @@ @@ -202,24 +202,18 @@
-
- @csrf - -
-
- @csrf - -
+ +
@@ -243,6 +237,9 @@ -{{-- Scripts moved to resources/js/csp-safe.js --}} + + @endsection diff --git a/resources/views/admin/users/index.blade.php b/resources/views/admin/users/index.blade.php index d780471c5..2317dec7a 100644 --- a/resources/views/admin/users/index.blade.php +++ b/resources/views/admin/users/index.blade.php @@ -106,12 +106,12 @@ ID Username Email + Status Role Role Expiry Host Country Verified - Bad User Created Actions @@ -124,6 +124,21 @@
{{ $user->username }}
{{ $user->email }} + + @if($user->deleted_at) + + Deleted + + @elseif(isset($user->apiaccess) && $user->apiaccess == 0) + + Disabled + + @else + + Active + + @endif + {{ $user->roles->first()->name ?? 'N/A' }} @@ -235,29 +250,36 @@ - diff --git a/resources/views/layouts/admin.blade.php b/resources/views/layouts/admin.blade.php index 1f1b0fe0e..ff9cc61ff 100644 --- a/resources/views/layouts/admin.blade.php +++ b/resources/views/layouts/admin.blade.php @@ -84,6 +84,9 @@ + + @include('partials.confirmation-modal') + @include('partials.toast-notifications') diff --git a/resources/views/layouts/main.blade.php b/resources/views/layouts/main.blade.php index a9bb8d77b..3cb074745 100644 --- a/resources/views/layouts/main.blade.php +++ b/resources/views/layouts/main.blade.php @@ -144,6 +144,9 @@ + + @include('partials.confirmation-modal') + @include('partials.toast-notifications') diff --git a/resources/views/partials/confirmation-modal.blade.php b/resources/views/partials/confirmation-modal.blade.php new file mode 100644 index 000000000..143a953a0 --- /dev/null +++ b/resources/views/partials/confirmation-modal.blade.php @@ -0,0 +1,38 @@ + + +