mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 00:01:21 +00:00
Use better confirmation modals
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') {
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+338
-167
@@ -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 = `
|
||||
<div class="confirmation-modal-content">
|
||||
<div class="confirmation-modal-header">
|
||||
<div class="confirmation-modal-icon">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<h3 class="confirmation-modal-title">Confirm Deletion</h3>
|
||||
</div>
|
||||
<div class="confirmation-modal-body">
|
||||
${message}
|
||||
</div>
|
||||
<div class="confirmation-modal-footer">
|
||||
<button class="btn-cancel">Cancel</button>
|
||||
<button class="btn-confirm">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 <strong>${selected.length}</strong> 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 <strong>${releaseName}</strong> 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 <strong>${selected.length}</strong> 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
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
</select>
|
||||
<button type="submit"
|
||||
class="px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-md hover:bg-blue-700"
|
||||
onclick="return confirmBulkAction()">
|
||||
onclick="confirmBulkAction(event)">
|
||||
<i class="fa fa-check mr-2"></i>Apply
|
||||
</button>
|
||||
</div>
|
||||
@@ -202,24 +202,18 @@
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex gap-2">
|
||||
<form action="{{ url('admin/deleted-users/restore/' . $user->id) }}" method="POST" class="inline-form">
|
||||
@csrf
|
||||
<button type="submit"
|
||||
class="text-green-600 hover:text-green-900 bg-transparent border-0 p-0 cursor-pointer"
|
||||
title="Restore User"
|
||||
data-confirm="Are you sure you want to restore user '{{ $user->username }}'?">
|
||||
<i class="fa fa-undo"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ url('admin/deleted-users/permanent-delete/' . $user->id) }}" method="POST" class="inline-form">
|
||||
@csrf
|
||||
<button type="submit"
|
||||
class="text-red-600 hover:text-red-900 bg-transparent border-0 p-0 cursor-pointer"
|
||||
title="Permanently Delete"
|
||||
data-confirm="Are you sure you want to PERMANENTLY delete user '{{ $user->username }}'? This action cannot be undone!">
|
||||
<i class="fa fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button"
|
||||
class="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 bg-transparent border-0 p-0 cursor-pointer"
|
||||
title="Restore User"
|
||||
onclick="restoreUser({{ $user->id }}, '{{ addslashes($user->username) }}')">
|
||||
<i class="fa fa-undo"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300 bg-transparent border-0 p-0 cursor-pointer"
|
||||
title="Permanently Delete"
|
||||
onclick="permanentDeleteUser({{ $user->id }}, '{{ addslashes($user->username) }}')">
|
||||
<i class="fa fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -243,6 +237,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Scripts moved to resources/js/csp-safe.js --}}
|
||||
<!-- Hidden form for individual actions -->
|
||||
<form id="individualActionForm" method="POST" style="display: none;">
|
||||
@csrf
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -106,12 +106,12 @@
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">ID</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Username</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Email</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Role</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Role Expiry</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Host</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Country</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Verified</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Bad User</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Created</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
@@ -124,6 +124,21 @@
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $user->username }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{{ $user->email }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@if($user->deleted_at)
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200" title="Soft Deleted on {{ \Carbon\Carbon::parse($user->deleted_at)->format('M j, Y g:i A') }}">
|
||||
<i class="fa fa-trash mr-1"></i>Deleted
|
||||
</span>
|
||||
@elseif(isset($user->apiaccess) && $user->apiaccess == 0)
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200" title="API Access Disabled">
|
||||
<i class="fa fa-ban mr-1"></i>Disabled
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
|
||||
<i class="fa fa-check-circle mr-1"></i>Active
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">
|
||||
{{ $user->roles->first()->name ?? 'N/A' }}
|
||||
@@ -235,29 +250,36 @@
|
||||
</div>
|
||||
|
||||
<!-- Verify User Confirmation Modal -->
|
||||
<div id="verifyUserModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800">
|
||||
<div class="mt-3 text-center">
|
||||
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 dark:bg-green-900">
|
||||
<i class="fa fa-check-circle text-green-600 dark:text-green-400 text-2xl"></i>
|
||||
</div>
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100 mt-4">Verify User</h3>
|
||||
<div class="mt-2 px-7 py-3">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Are you sure you want to manually verify this user? This will mark their email as verified.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-3 px-4 py-3">
|
||||
<button onclick="hideVerifyModal()"
|
||||
class="flex-1 px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-base font-medium rounded-md shadow-sm hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-300">
|
||||
Cancel
|
||||
</button>
|
||||
<button onclick="submitVerifyForm()"
|
||||
class="flex-1 px-4 py-2 bg-green-600 dark:bg-green-700 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-700 dark:hover:bg-green-800 focus:outline-none focus:ring-2 focus:ring-green-500">
|
||||
Verify
|
||||
<div id="verifyUserModal" class="fixed inset-0 bg-gray-900 dark:bg-black bg-opacity-50 dark:bg-opacity-70 hidden items-center justify-center z-50 transition-opacity">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4 transform transition-all">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<i class="fa fa-check-circle text-green-600 dark:text-green-400 mr-2"></i>
|
||||
Verify User
|
||||
</h3>
|
||||
<button type="button" onclick="hideVerifyModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
Are you sure you want to manually verify this user? This will mark their email as verified.
|
||||
</p>
|
||||
</div>
|
||||
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-3">
|
||||
<button type="button"
|
||||
onclick="hideVerifyModal()"
|
||||
class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition font-medium">
|
||||
<i class="fas fa-times mr-2"></i>Cancel
|
||||
</button>
|
||||
<button type="button"
|
||||
onclick="submitVerifyForm()"
|
||||
class="px-4 py-2 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700 dark:hover:bg-green-800 transition font-medium">
|
||||
<i class="fa fa-check mr-2"></i>Verify
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
<!-- Toast notifications will be dynamically inserted here -->
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Modal -->
|
||||
@include('partials.confirmation-modal')
|
||||
|
||||
<!-- Scripts -->
|
||||
<!-- Toast Notifications (must load before other scripts) -->
|
||||
@include('partials.toast-notifications')
|
||||
|
||||
@@ -144,6 +144,9 @@
|
||||
<!-- Toast notifications will be dynamically inserted here -->
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Modal -->
|
||||
@include('partials.confirmation-modal')
|
||||
|
||||
<!-- Scripts -->
|
||||
<!-- Toast Notifications (must load before other scripts) -->
|
||||
@include('partials.toast-notifications')
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<!-- Reusable Confirmation Modal -->
|
||||
<div id="confirmationModal" class="fixed inset-0 bg-gray-900 dark:bg-black bg-opacity-50 dark:bg-opacity-70 hidden items-center justify-center z-50 transition-opacity">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4 transform transition-all">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center" id="confirmationModalTitle">
|
||||
<i class="fas fa-exclamation-circle text-blue-600 dark:text-blue-400 mr-2" id="confirmationModalIcon"></i>
|
||||
<span id="confirmationModalTitleText">Confirm Action</span>
|
||||
</h3>
|
||||
<button type="button" onclick="closeConfirmationModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<p class="text-gray-700 dark:text-gray-300" id="confirmationModalMessage">
|
||||
Are you sure you want to proceed?
|
||||
</p>
|
||||
<div id="confirmationModalDetails" class="bg-gray-50 dark:bg-gray-900 rounded-lg p-3 mt-3 hidden">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400" id="confirmationModalDetailsText"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-3">
|
||||
<button type="button"
|
||||
onclick="closeConfirmationModal()"
|
||||
class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition font-medium">
|
||||
<i class="fas fa-times mr-2"></i><span id="confirmationModalCancelText">Cancel</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
onclick="confirmConfirmationModal()"
|
||||
id="confirmationModalConfirmBtn"
|
||||
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 transition font-medium">
|
||||
<i class="fas fa-check mr-2"></i><span id="confirmationModalConfirmText">Confirm</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user