Migrate many view templates from raw PHP to Twig and modernize UI/UX for 2FA and settings. Controllers updated to provide avatar data and two-factor info (ProfileController, UserController) and SettingsController now includes timezone lists, notification preset selection, cron path, cached update state and rollback availability. ErrorHandler now attempts to render error pages via a new Core\TwigService with a safe fallback to raw PHP views. TwoFactorService generation silences deprecated warnings during QR code creation. Numerous .php view files were removed and replaced with .twig equivalents (2fa setup/verify/backup-codes and many auth, dashboard, domains, errors, layout, users, tags, tld-registry, etc.), and core/TwigService was added. These changes move the app toward a Twig-based templating system, improve 2FA flows, surface avatar images in lists/profiles, and make error rendering more robust.
982 lines
52 KiB
Twig
982 lines
52 KiB
Twig
{% extends 'layout/base.twig' %}
|
|
|
|
{% set title = 'Domains' %}
|
|
{% set pageTitle = 'Domain Management' %}
|
|
{% set pageDescription = 'Monitor and manage your domain portfolio' %}
|
|
{% set pageIcon = 'fas fa-globe' %}
|
|
|
|
{% block content %}
|
|
{# Action Buttons #}
|
|
<div class="mb-4 flex gap-2 justify-end">
|
|
{# Export Dropdown #}
|
|
<div class="relative" id="domainExportDropdownWrapper">
|
|
<button onclick="document.getElementById('domainExportMenu').classList.toggle('hidden')" class="inline-flex items-center px-4 py-2 bg-emerald-600 text-white text-sm rounded-lg hover:bg-emerald-700 transition-colors font-medium">
|
|
<i class="fas fa-download mr-2"></i>
|
|
Export
|
|
<i class="fas fa-chevron-down ml-2 text-xs"></i>
|
|
</button>
|
|
<div id="domainExportMenu" class="hidden absolute right-0 mt-1 w-44 bg-white dark:bg-slate-800 rounded-lg shadow-lg border border-gray-200 dark:border-slate-700 z-30 overflow-hidden">
|
|
<a href="/domains/export?format=csv" class="flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-slate-300 hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors">
|
|
<i class="fas fa-file-csv text-green-600 mr-2.5"></i>
|
|
Export as CSV
|
|
</a>
|
|
<a href="/domains/export?format=json" class="flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-slate-300 hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors border-t border-gray-100 dark:border-slate-700">
|
|
<i class="fas fa-file-code text-blue-600 mr-2.5"></i>
|
|
Export as JSON
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<a href="/domains/bulk-add" class="inline-flex items-center px-4 py-2 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
|
|
<i class="fas fa-layer-group mr-2"></i>
|
|
Bulk Add
|
|
</a>
|
|
<a href="/domains/create" class="inline-flex items-center px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
|
|
<i class="fas fa-plus mr-2"></i>
|
|
Add Domain
|
|
</a>
|
|
</div>
|
|
|
|
{# Filters & Search #}
|
|
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 mb-4">
|
|
<form method="GET" action="/domains" id="filter-form">
|
|
<div class="grid grid-cols-1 md:grid-cols-5 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300 mb-1.5">Search</label>
|
|
<div class="relative">
|
|
<input type="text" name="search" id="domainSearch" value="{{ filters.search }}" placeholder="Search domains..." class="w-full pl-9 pr-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm bg-white dark:bg-slate-900 text-gray-900 dark:text-white">
|
|
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-slate-500 text-xs"></i>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300 mb-1.5">Status</label>
|
|
<select name="status" id="statusFilter" class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm bg-white dark:bg-slate-900 text-gray-900 dark:text-white">
|
|
<option value="">All Statuses</option>
|
|
<option value="active" {{ filters.status == 'active' ? 'selected' : '' }}>Active</option>
|
|
<option value="expiring_soon" {{ filters.status == 'expiring_soon' ? 'selected' : '' }}>Expiring Soon</option>
|
|
<option value="expired" {{ filters.status == 'expired' ? 'selected' : '' }}>Expired</option>
|
|
<option value="available" {{ filters.status == 'available' ? 'selected' : '' }}>Available</option>
|
|
<option value="redemption_period" {{ filters.status == 'redemption_period' ? 'selected' : '' }}>Redemption Period</option>
|
|
<option value="pending_delete" {{ filters.status == 'pending_delete' ? 'selected' : '' }}>Pending Delete</option>
|
|
<option value="error" {{ filters.status == 'error' ? 'selected' : '' }}>Error</option>
|
|
<option value="inactive" {{ filters.status == 'inactive' ? 'selected' : '' }}>Inactive</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300 mb-1.5">Tags</label>
|
|
<select name="tag" id="tagFilter" class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm bg-white dark:bg-slate-900 text-gray-900 dark:text-white">
|
|
<option value="">All Tags</option>
|
|
{% set tagIcons = {
|
|
'production': '🟢',
|
|
'staging': '🟡',
|
|
'development': '🔵',
|
|
'client': '🟣',
|
|
'personal': '🟠',
|
|
'archived': '⚪'
|
|
} %}
|
|
{% for tagOption in allTags %}
|
|
{% set icon = tagIcons[tagOption]|default('🏷️') %}
|
|
<option value="{{ tagOption }}" {{ filters.tag|default('') == tagOption ? 'selected' : '' }}>
|
|
{{ icon }} {{ tagOption|capitalize }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300 mb-1.5">Group</label>
|
|
<select name="group" id="groupFilter" class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm bg-white dark:bg-slate-900 text-gray-900 dark:text-white">
|
|
<option value="">All Groups</option>
|
|
{% for group in groups %}
|
|
<option value="{{ group.id }}" {{ filters.group == group.id ? 'selected' : '' }}>{{ group.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="flex items-end space-x-2">
|
|
<button type="submit" class="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
|
|
<i class="fas fa-filter mr-2"></i>
|
|
Apply
|
|
</button>
|
|
<a href="/domains" class="px-4 py-2 border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-slate-300 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-sm font-medium">
|
|
<i class="fas fa-times"></i>
|
|
Clear
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" name="sort" value="{{ filters.sort }}">
|
|
<input type="hidden" name="order" value="{{ filters.order }}">
|
|
</form>
|
|
</div>
|
|
|
|
{# Pagination Info & Per Page Selector #}
|
|
<div class="mb-4 flex justify-between items-center">
|
|
<div class="text-sm text-gray-600 dark:text-slate-400">
|
|
Showing <span class="font-semibold text-gray-900 dark:text-white">{{ pagination.showing_from }}</span> to
|
|
<span class="font-semibold text-gray-900 dark:text-white">{{ pagination.showing_to }}</span> of
|
|
<span class="font-semibold text-gray-900 dark:text-white">{{ pagination.total }}</span> domain(s)
|
|
</div>
|
|
|
|
<form method="GET" action="/domains" class="flex items-center gap-2">
|
|
<input type="hidden" name="search" value="{{ filters.search }}">
|
|
<input type="hidden" name="status" value="{{ filters.status }}">
|
|
<input type="hidden" name="group" value="{{ filters.group }}">
|
|
<input type="hidden" name="sort" value="{{ filters.sort }}">
|
|
<input type="hidden" name="order" value="{{ filters.order }}">
|
|
|
|
<label for="per_page" class="text-sm text-gray-600 dark:text-slate-400">Show:</label>
|
|
<select name="per_page" id="per_page" onchange="this.form.submit()" class="px-3 py-1.5 border border-gray-300 dark:border-slate-600 rounded-lg text-sm focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-slate-900 text-gray-900 dark:text-white">
|
|
<option value="10" {{ pagination.per_page == 10 ? 'selected' : '' }}>10</option>
|
|
<option value="25" {{ pagination.per_page == 25 ? 'selected' : '' }}>25</option>
|
|
<option value="50" {{ pagination.per_page == 50 ? 'selected' : '' }}>50</option>
|
|
<option value="100" {{ pagination.per_page == 100 ? 'selected' : '' }}>100</option>
|
|
</select>
|
|
</form>
|
|
</div>
|
|
|
|
{# Domains List #}
|
|
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
|
|
{# Bulk Actions Bar #}
|
|
<div id="bulk-actions" class="hidden px-6 py-3 bg-blue-50 dark:bg-blue-500/10 border-b border-blue-200 dark:border-blue-500/30 flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<span id="selected-count" class="text-sm font-medium text-gray-700 dark:text-slate-300"></span>
|
|
<div class="flex items-center gap-3 flex-wrap">
|
|
<div class="flex items-center gap-2">
|
|
<button type="button" onclick="bulkRefresh()" class="inline-flex items-center px-4 py-1.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm font-medium">
|
|
<i class="fas fa-sync-alt mr-1"></i> Refresh Selected
|
|
</button>
|
|
{% if auth.isAdmin %}
|
|
<button type="button" onclick="bulkTransfer()" class="inline-flex items-center px-4 py-1.5 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
|
|
<i class="fas fa-exchange-alt mr-1"></i> Transfer Selected
|
|
</button>
|
|
{% endif %}
|
|
<div class="relative inline-block">
|
|
<button type="button" onclick="toggleAssignTagsDropdown()" class="inline-flex items-center px-4 py-1.5 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium">
|
|
<i class="fas fa-tags mr-1"></i> Manage Tags
|
|
<i class="fas fa-chevron-down ml-2 text-xs"></i>
|
|
</button>
|
|
<div id="assign-tags-dropdown" class="hidden absolute left-0 mt-2 w-72 bg-white dark:bg-slate-800 rounded-lg shadow-lg border border-gray-200 dark:border-slate-700 z-10">
|
|
<div class="p-3">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300">Tag Management</label>
|
|
<a href="/tags" class="text-xs text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
|
<i class="fas fa-cog mr-1"></i>
|
|
Manage Tags
|
|
</a>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300 mb-2">Add Tags to Selected Domains</label>
|
|
<div class="flex flex-wrap gap-1.5 mb-3">
|
|
{% for tag in availableTags %}
|
|
<button type="button" onclick="bulkAssignExistingTag({{ tag.id }}, '{{ tag.name|e('js') }}')"
|
|
class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium border {{ tag.color }} hover:opacity-80">
|
|
<i class="fas fa-plus mr-1" style="font-size: 8px;"></i>
|
|
{{ tag.name }}
|
|
</button>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="border-t border-gray-200 dark:border-slate-700 pt-2">
|
|
<button type="button" onclick="openTagSelector()" class="w-full px-3 py-1.5 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 text-xs rounded hover:bg-blue-200 dark:hover:bg-blue-500/30 font-medium">
|
|
<i class="fas fa-plus mr-1"></i>
|
|
Add Custom Tag
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="border-t border-gray-200 dark:border-slate-700 pt-3">
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300 mb-2">Remove Tags from Selected Domains</label>
|
|
<div class="space-y-2">
|
|
<button type="button" onclick="bulkRemoveAllTags()" class="w-full px-3 py-1.5 bg-gray-100 dark:bg-slate-700 text-gray-700 dark:text-slate-300 text-xs rounded hover:bg-gray-200 dark:hover:bg-slate-600 font-medium">
|
|
<i class="fas fa-times mr-1"></i>
|
|
Remove All Tags
|
|
</button>
|
|
<button type="button" onclick="openTagRemovalSelector()" class="w-full px-3 py-1.5 bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400 text-xs rounded hover:bg-red-200 dark:hover:bg-red-500/30 font-medium">
|
|
<i class="fas fa-minus mr-1"></i>
|
|
Remove Specific Tag
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="border-t border-gray-200 dark:border-slate-700 p-2">
|
|
<button type="button" onclick="toggleAssignTagsDropdown()" class="w-full px-3 py-1.5 bg-gray-200 dark:bg-slate-600 text-gray-700 dark:text-slate-300 text-xs rounded hover:bg-gray-300 dark:hover:bg-slate-500">
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="relative inline-block">
|
|
<button type="button" onclick="toggleAssignGroupDropdown()" class="inline-flex items-center px-4 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium">
|
|
<i class="fas fa-bell mr-1"></i> Assign Group
|
|
<i class="fas fa-chevron-down ml-2 text-xs"></i>
|
|
</button>
|
|
<div id="assign-group-dropdown" class="hidden absolute left-0 mt-2 w-64 bg-white dark:bg-slate-800 rounded-lg shadow-lg border border-gray-200 dark:border-slate-700 z-10">
|
|
<form method="POST" action="/domains/bulk-assign-group" id="bulk-assign-form">
|
|
{{ csrf_field() }}
|
|
<div class="p-3">
|
|
<select name="group_id" class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-900 text-gray-900 dark:text-white">
|
|
<option value="">-- No Group --</option>
|
|
{% for group in groups %}
|
|
<option value="{{ group.id }}">{{ group.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="border-t border-gray-200 dark:border-slate-700 p-2 flex gap-2">
|
|
<button type="submit" class="flex-1 px-3 py-1.5 bg-primary text-white text-xs rounded hover:bg-primary-dark">
|
|
Assign
|
|
</button>
|
|
<button type="button" onclick="toggleAssignGroupDropdown()" class="flex-1 px-3 py-1.5 bg-gray-200 dark:bg-slate-600 text-gray-700 dark:text-slate-300 text-xs rounded hover:bg-gray-300 dark:hover:bg-slate-500">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<button type="button" onclick="bulkDelete()" class="inline-flex items-center px-4 py-1.5 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm font-medium">
|
|
<i class="fas fa-trash mr-1"></i> Delete Selected
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button type="button" onclick="clearSelection()" class="inline-flex items-center text-sm font-medium text-gray-600 dark:text-slate-400 hover:text-gray-900 dark:hover:text-white hover:bg-blue-100 dark:hover:bg-blue-500/20 px-3 py-1.5 rounded-lg transition-colors">
|
|
<i class="fas fa-times mr-1.5"></i> Clear Selection
|
|
</button>
|
|
</div>
|
|
{% if domains is not empty %}
|
|
{# Table View (Desktop) #}
|
|
<div class="hidden lg:block overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-slate-700">
|
|
<thead class="bg-gray-50 dark:bg-slate-900">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left">
|
|
<input type="checkbox" id="select-all" onchange="toggleSelectAll(this)" class="rounded border-gray-300 dark:border-slate-600 text-primary focus:ring-primary">
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">
|
|
<a href="{{ sort_url('domain_name', filters.sort, filters.order, filters) }}" class="hover:text-primary flex items-center">
|
|
Domain {{ sort_icon('domain_name', filters.sort, filters.order)|raw }}
|
|
</a>
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">
|
|
<a href="{{ sort_url('registrar', filters.sort, filters.order, filters) }}" class="hover:text-primary flex items-center">
|
|
Registrar {{ sort_icon('registrar', filters.sort, filters.order)|raw }}
|
|
</a>
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">
|
|
<a href="{{ sort_url('expiration_date', filters.sort, filters.order, filters) }}" class="hover:text-primary flex items-center">
|
|
Expiration {{ sort_icon('expiration_date', filters.sort, filters.order)|raw }}
|
|
</a>
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">
|
|
<a href="{{ sort_url('status', filters.sort, filters.order, filters) }}" class="hover:text-primary flex items-center">
|
|
Status {{ sort_icon('status', filters.sort, filters.order)|raw }}
|
|
</a>
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">
|
|
<a href="{{ sort_url('group_name', filters.sort, filters.order, filters) }}" class="hover:text-primary flex items-center">
|
|
Group {{ sort_icon('group_name', filters.sort, filters.order)|raw }}
|
|
</a>
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">
|
|
<a href="{{ sort_url('last_checked', filters.sort, filters.order, filters) }}" class="hover:text-primary flex items-center">
|
|
Last Checked {{ sort_icon('last_checked', filters.sort, filters.order)|raw }}
|
|
</a>
|
|
</th>
|
|
<th class="px-6 py-3 text-right text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-slate-700">
|
|
{% for domain in domains %}
|
|
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-150 domain-row">
|
|
<td class="px-6 py-4">
|
|
<input type="checkbox" class="domain-checkbox rounded border-gray-300 dark:border-slate-600 text-primary focus:ring-primary" value="{{ domain.id }}" onchange="updateBulkActions()">
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 h-10 w-10 bg-primary bg-opacity-10 dark:bg-opacity-20 rounded-lg flex items-center justify-center">
|
|
<i class="fas fa-globe text-primary"></i>
|
|
</div>
|
|
<div class="ml-4">
|
|
<a href="/domains/{{ domain.id }}" class="text-sm font-semibold text-gray-900 dark:text-white hover:text-primary">{{ domain.domain_name }}</a>
|
|
<div class="flex items-center gap-1.5 mt-1">
|
|
{% set domainTags = domain.tags ? domain.tags|split(',') : [] %}
|
|
{% set tagColors = domain.tag_colors ? domain.tag_colors|split('|') : [] %}
|
|
{% for tag in domainTags %}
|
|
{% set colorClass = tagColors[loop.index0]|default('bg-gray-100 text-gray-700 border-gray-200 dark:bg-slate-700 dark:text-slate-300 dark:border-slate-600') %}
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border {{ colorClass }}">
|
|
<i class="fas fa-tag mr-1" style="font-size: 9px;"></i>
|
|
{{ tag|trim|capitalize }}
|
|
</span>
|
|
{% endfor %}
|
|
{% if domain.nameservers and domainTags is empty %}
|
|
<span class="text-xs text-gray-500 dark:text-slate-400">NS: {{ domain.nameservers|split(',')[0] }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
{% if domain.registrar %}
|
|
<div class="flex items-center">
|
|
<i class="fas fa-building text-gray-400 dark:text-slate-500 mr-2"></i>
|
|
<span class="text-sm text-gray-900 dark:text-white">{{ domain.registrar }}</span>
|
|
</div>
|
|
{% else %}
|
|
<span class="text-sm text-gray-400 dark:text-slate-500">Unknown</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
{% if domain.expiration_date %}
|
|
<div class="text-sm">
|
|
<div class="font-medium text-gray-900 dark:text-white flex items-center">
|
|
{{ domain.expiration_date|date('M d, Y') }}
|
|
{% if domain.isManualExpiration %}
|
|
<span class="ml-1 inline-flex items-center px-1 py-0.5 rounded text-xs font-medium bg-amber-100 dark:bg-amber-500/10 text-amber-800 dark:text-amber-400" title="Manual expiration date">
|
|
<i class="fas fa-edit" style="font-size: 8px;"></i>
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="text-xs {{ domain.expiryClass }}">
|
|
{{ domain.daysLeft }} days
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<span class="text-sm text-gray-400 dark:text-slate-500">Not set</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border {{ domain.statusClass }}">
|
|
<i class="fas {{ domain.statusIcon }} mr-1"></i>
|
|
{{ domain.statusText }}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
|
{% if domain.group_name %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-500/20 text-blue-800 dark:text-blue-400">
|
|
<i class="fas fa-bell mr-1"></i>
|
|
{{ domain.group_name }}
|
|
</span>
|
|
{% else %}
|
|
<span class="text-gray-400 dark:text-slate-500">No Group</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-slate-400">
|
|
{% if domain.last_checked %}
|
|
<div class="flex items-center">
|
|
<i class="far fa-clock mr-2"></i>
|
|
{{ domain.last_checked|date('M d, H:i') }}
|
|
</div>
|
|
{% else %}
|
|
<span class="text-gray-400 dark:text-slate-500">Never</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<div class="flex items-center justify-end space-x-2">
|
|
<a href="/domains/{{ domain.id }}" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" title="View">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
<form method="POST" action="/domains/{{ domain.id }}/refresh" class="inline">
|
|
{{ csrf_field() }}
|
|
<button type="submit" class="text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300" title="Refresh WHOIS">
|
|
<i class="fas fa-sync-alt"></i>
|
|
</button>
|
|
</form>
|
|
<a href="/domains/{{ domain.id }}/edit?from=/domains" class="text-yellow-600 hover:text-yellow-800 dark:text-yellow-400 dark:hover:text-yellow-300" title="Edit">
|
|
<i class="fas fa-edit"></i>
|
|
</a>
|
|
<form method="POST" action="/domains/{{ domain.id }}/delete" class="inline" onsubmit="return confirm('Delete this domain?')">
|
|
{{ csrf_field() }}
|
|
<button type="submit" class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300" title="Delete">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{# Card View (Mobile) #}
|
|
<div class="lg:hidden divide-y divide-gray-200 dark:divide-slate-700">
|
|
{% for domain in domains %}
|
|
<div class="p-6 hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-150">
|
|
<div class="flex items-center mb-3">
|
|
<input type="checkbox" class="domain-checkbox-mobile rounded border-gray-300 dark:border-slate-600 text-primary focus:ring-primary mr-3" value="{{ domain.id }}" onchange="updateBulkActions()">
|
|
<a href="/domains/{{ domain.id }}" class="text-lg font-semibold text-gray-900 dark:text-white hover:text-primary">{{ domain.domain_name }}</a>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-12 px-6">
|
|
<div class="mb-4">
|
|
<i class="fas fa-globe text-gray-300 dark:text-slate-600 text-6xl"></i>
|
|
</div>
|
|
<h3 class="text-lg font-semibold text-gray-700 dark:text-slate-300 mb-1">No Domains Yet</h3>
|
|
<p class="text-sm text-gray-500 dark:text-slate-400 mb-4">Start monitoring your domains by adding your first one</p>
|
|
<a href="/domains/create" class="inline-flex items-center px-5 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
|
|
<i class="fas fa-plus mr-2"></i>
|
|
<span>Add Your First Domain</span>
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# Pagination Controls #}
|
|
{% if pagination.total_pages > 1 %}
|
|
{% set currentPage = pagination.current_page %}
|
|
{% set totalPages = pagination.total_pages %}
|
|
{% set range = 2 %}
|
|
{% set start = max(1, currentPage - range) %}
|
|
{% set end = min(totalPages, currentPage + range) %}
|
|
<div class="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
{# Page Info #}
|
|
<div class="text-sm text-gray-600 dark:text-slate-400">
|
|
Page <span class="font-semibold text-gray-900 dark:text-white">{{ currentPage }}</span> of
|
|
<span class="font-semibold text-gray-900 dark:text-white">{{ totalPages }}</span>
|
|
</div>
|
|
|
|
{# Pagination Buttons #}
|
|
<div class="flex items-center gap-1">
|
|
{# First Page #}
|
|
{% if currentPage > 1 %}
|
|
<a href="{{ pagination_url(1, filters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300">
|
|
<i class="fas fa-angle-double-left"></i>
|
|
</a>
|
|
{% endif %}
|
|
|
|
{# Previous Page #}
|
|
{% if currentPage > 1 %}
|
|
<a href="{{ pagination_url(currentPage - 1, filters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300">
|
|
<i class="fas fa-angle-left"></i> Previous
|
|
</a>
|
|
{% endif %}
|
|
|
|
{# First page + ellipsis if needed #}
|
|
{% if start > 1 %}
|
|
<a href="{{ pagination_url(1, filters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300">1</a>
|
|
{% if start > 2 %}
|
|
<span class="px-2 text-gray-500 dark:text-slate-400">...</span>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{# Page Numbers #}
|
|
{% for i in start..end %}
|
|
{% if i == currentPage %}
|
|
<span class="px-3 py-2 text-sm bg-primary text-white rounded-lg font-semibold">{{ i }}</span>
|
|
{% else %}
|
|
<a href="{{ pagination_url(i, filters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300">{{ i }}</a>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{# Last page + ellipsis if needed #}
|
|
{% if end < totalPages %}
|
|
{% if end < totalPages - 1 %}
|
|
<span class="px-2 text-gray-500 dark:text-slate-400">...</span>
|
|
{% endif %}
|
|
<a href="{{ pagination_url(totalPages, filters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300">{{ totalPages }}</a>
|
|
{% endif %}
|
|
|
|
{# Next Page #}
|
|
{% if currentPage < totalPages %}
|
|
<a href="{{ pagination_url(currentPage + 1, filters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300">
|
|
Next <i class="fas fa-angle-right"></i>
|
|
</a>
|
|
{% endif %}
|
|
|
|
{# Last Page #}
|
|
{% if currentPage < totalPages %}
|
|
<a href="{{ pagination_url(totalPages, filters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300">
|
|
<i class="fas fa-angle-double-right"></i>
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function toggleSelectAll(checkbox) {
|
|
const checkboxes = document.querySelectorAll('.domain-checkbox, .domain-checkbox-mobile');
|
|
checkboxes.forEach(cb => {
|
|
cb.checked = checkbox.checked;
|
|
});
|
|
updateBulkActions();
|
|
}
|
|
|
|
function updateBulkActions() {
|
|
const checkboxes = document.querySelectorAll('.domain-checkbox:checked, .domain-checkbox-mobile:checked');
|
|
const bulkActions = document.getElementById('bulk-actions');
|
|
const selectedCount = document.getElementById('selected-count');
|
|
const selectAllCheckbox = document.getElementById('select-all');
|
|
|
|
const uniqueIds = new Set(Array.from(checkboxes).map(cb => cb.value));
|
|
const count = uniqueIds.size;
|
|
|
|
if (count > 0) {
|
|
bulkActions.classList.remove('hidden');
|
|
selectedCount.textContent = count + ' domain(s) selected';
|
|
} else {
|
|
bulkActions.classList.add('hidden');
|
|
}
|
|
|
|
const allCheckboxes = document.querySelectorAll('.domain-checkbox');
|
|
const checkedDesktopBoxes = document.querySelectorAll('.domain-checkbox:checked');
|
|
if (selectAllCheckbox) {
|
|
selectAllCheckbox.checked = allCheckboxes.length > 0 && checkedDesktopBoxes.length === allCheckboxes.length;
|
|
selectAllCheckbox.indeterminate = checkedDesktopBoxes.length > 0 && checkedDesktopBoxes.length < allCheckboxes.length;
|
|
}
|
|
}
|
|
|
|
function clearSelection() {
|
|
const checkboxes = document.querySelectorAll('.domain-checkbox, .domain-checkbox-mobile');
|
|
checkboxes.forEach(cb => {
|
|
cb.checked = false;
|
|
});
|
|
document.getElementById('select-all').checked = false;
|
|
updateBulkActions();
|
|
}
|
|
|
|
function getSelectedIds() {
|
|
const checkboxes = document.querySelectorAll('.domain-checkbox:checked, .domain-checkbox-mobile:checked');
|
|
const ids = Array.from(checkboxes).map(cb => cb.value);
|
|
return [...new Set(ids)];
|
|
}
|
|
|
|
function bulkRefresh() {
|
|
const ids = getSelectedIds();
|
|
if (ids.length === 0) return;
|
|
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = '/domains/bulk-refresh';
|
|
|
|
const csrfInput = document.createElement('input');
|
|
csrfInput.type = 'hidden';
|
|
csrfInput.name = 'csrf_token';
|
|
csrfInput.value = '{{ csrf_token() }}';
|
|
form.appendChild(csrfInput);
|
|
|
|
ids.forEach(id => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'domain_ids[]';
|
|
input.value = id;
|
|
form.appendChild(input);
|
|
});
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
|
|
function bulkDelete() {
|
|
const ids = getSelectedIds();
|
|
if (ids.length === 0) return;
|
|
|
|
if (!confirm(`Delete ${ids.length} domain(s)? This action cannot be undone.`)) {
|
|
return;
|
|
}
|
|
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = '/domains/bulk-delete';
|
|
|
|
const csrfInput = document.createElement('input');
|
|
csrfInput.type = 'hidden';
|
|
csrfInput.name = 'csrf_token';
|
|
csrfInput.value = '{{ csrf_token() }}';
|
|
form.appendChild(csrfInput);
|
|
|
|
ids.forEach(id => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'domain_ids[]';
|
|
input.value = id;
|
|
form.appendChild(input);
|
|
});
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
|
|
function toggleAssignTagsDropdown() {
|
|
const dropdown = document.getElementById('assign-tags-dropdown');
|
|
dropdown.classList.toggle('hidden');
|
|
}
|
|
|
|
function toggleAssignGroupDropdown() {
|
|
const dropdown = document.getElementById('assign-group-dropdown');
|
|
dropdown.classList.toggle('hidden');
|
|
}
|
|
|
|
function bulkAddTag(tagName) {
|
|
const ids = getSelectedIds();
|
|
if (ids.length === 0) {
|
|
alert('Please select at least one domain');
|
|
return;
|
|
}
|
|
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = '/domains/bulk-add-tags';
|
|
|
|
const csrfInput = document.createElement('input');
|
|
csrfInput.type = 'hidden';
|
|
csrfInput.name = 'csrf_token';
|
|
csrfInput.value = '{{ csrf_token() }}';
|
|
form.appendChild(csrfInput);
|
|
|
|
const tagInput = document.createElement('input');
|
|
tagInput.type = 'hidden';
|
|
tagInput.name = 'tag';
|
|
tagInput.value = tagName;
|
|
form.appendChild(tagInput);
|
|
|
|
ids.forEach(id => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'domain_ids[]';
|
|
input.value = id;
|
|
form.appendChild(input);
|
|
});
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
|
|
function bulkRemoveAllTags() {
|
|
const ids = getSelectedIds();
|
|
if (ids.length === 0) {
|
|
alert('Please select at least one domain');
|
|
return;
|
|
}
|
|
|
|
if (!confirm(`Remove all tags from ${ids.length} domain(s)?`)) {
|
|
return;
|
|
}
|
|
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = '/domains/bulk-remove-tags';
|
|
|
|
const csrfInput = document.createElement('input');
|
|
csrfInput.type = 'hidden';
|
|
csrfInput.name = 'csrf_token';
|
|
csrfInput.value = '{{ csrf_token() }}';
|
|
form.appendChild(csrfInput);
|
|
|
|
ids.forEach(id => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'domain_ids[]';
|
|
input.value = id;
|
|
form.appendChild(input);
|
|
});
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
|
|
function bulkTransfer() {
|
|
const ids = getSelectedIds();
|
|
if (ids.length === 0) {
|
|
alert('Please select at least one domain');
|
|
return;
|
|
}
|
|
|
|
const users = {{ users|default([])|json_encode|raw }};
|
|
if (users.length === 0) {
|
|
alert('No users available for transfer');
|
|
return;
|
|
}
|
|
|
|
let userOptions = users.map(user =>
|
|
`<option value="${user.id}">${user.username} (${user.full_name || user.email})</option>`
|
|
).join('');
|
|
|
|
const modal = document.createElement('div');
|
|
modal.className = 'fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50';
|
|
modal.innerHTML = `
|
|
<div class="relative top-20 mx-auto p-5 border border-gray-200 dark:border-slate-700 w-96 shadow-lg rounded-md bg-white dark:bg-slate-800">
|
|
<div class="mt-3">
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Transfer ${ids.length} Domain(s)</h3>
|
|
<p class="text-sm text-gray-500 dark:text-slate-400 mb-4">Select the user to transfer the selected domains to:</p>
|
|
|
|
<form method="POST" action="/domains/bulk-transfer">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
${ids.map(id => `<input type="hidden" name="domain_ids[]" value="${id}">`).join('')}
|
|
|
|
<div class="mb-4">
|
|
<label for="target_user_id" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">Transfer to User:</label>
|
|
<select name="target_user_id" id="target_user_id" required class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-slate-900 text-gray-900 dark:text-white">
|
|
<option value="">Select a user...</option>
|
|
${userOptions}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button" onclick="this.closest('.fixed').remove()" class="px-4 py-2 bg-gray-300 dark:bg-slate-600 text-gray-700 dark:text-slate-300 rounded-md hover:bg-gray-400 dark:hover:bg-slate-500">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary-dark">
|
|
Transfer Domains
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
}
|
|
|
|
document.getElementById('bulk-assign-form')?.addEventListener('submit', function(e) {
|
|
const ids = getSelectedIds();
|
|
const container = this;
|
|
|
|
container.querySelectorAll('input[name="domain_ids[]"]').forEach(el => el.remove());
|
|
|
|
ids.forEach(id => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'domain_ids[]';
|
|
input.value = id;
|
|
container.appendChild(input);
|
|
});
|
|
});
|
|
|
|
document.addEventListener('click', function(event) {
|
|
const groupDropdown = document.getElementById('assign-group-dropdown');
|
|
const tagsDropdown = document.getElementById('assign-tags-dropdown');
|
|
const groupButton = event.target.closest('button[onclick="toggleAssignGroupDropdown()"]');
|
|
const tagsButton = event.target.closest('button[onclick="toggleAssignTagsDropdown()"]');
|
|
|
|
if (!groupButton && !groupDropdown.contains(event.target)) {
|
|
groupDropdown?.classList.add('hidden');
|
|
}
|
|
|
|
if (!tagsButton && !tagsDropdown.contains(event.target)) {
|
|
tagsDropdown?.classList.add('hidden');
|
|
}
|
|
});
|
|
|
|
function bulkAssignExistingTag(tagId, tagName) {
|
|
const ids = getSelectedIds();
|
|
if (ids.length === 0) {
|
|
alert('Please select at least one domain');
|
|
return;
|
|
}
|
|
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = '/domains/bulk-assign-existing-tag';
|
|
|
|
const csrfInput = document.createElement('input');
|
|
csrfInput.type = 'hidden';
|
|
csrfInput.name = 'csrf_token';
|
|
csrfInput.value = '{{ csrf_token() }}';
|
|
form.appendChild(csrfInput);
|
|
|
|
const tagInput = document.createElement('input');
|
|
tagInput.type = 'hidden';
|
|
tagInput.name = 'tag_id';
|
|
tagInput.value = tagId;
|
|
form.appendChild(tagInput);
|
|
|
|
ids.forEach(id => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'domain_ids[]';
|
|
input.value = id;
|
|
form.appendChild(input);
|
|
});
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
|
|
function openTagSelector() {
|
|
const ids = getSelectedIds();
|
|
if (ids.length === 0) {
|
|
alert('Please select at least one domain');
|
|
return;
|
|
}
|
|
|
|
const modal = document.createElement('div');
|
|
modal.className = 'fixed inset-0 bg-gray-600 bg-opacity-50 z-50';
|
|
modal.innerHTML = `
|
|
<div class="flex items-center justify-center min-h-screen p-4">
|
|
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-xl max-w-md w-full">
|
|
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700">
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Add Custom Tag</h3>
|
|
</div>
|
|
<div class="px-6 py-4">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1">Tag Name</label>
|
|
<input type="text" id="custom-tag-name" class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-slate-900 text-gray-900 dark:text-white" placeholder="Enter tag name">
|
|
<p class="text-xs text-gray-500 dark:text-slate-400 mt-1">Use only letters, numbers, and hyphens</p>
|
|
</div>
|
|
</div>
|
|
<div class="px-6 py-4 bg-gray-50 dark:bg-slate-900 flex justify-end space-x-3 rounded-b-lg">
|
|
<button type="button" onclick="closeTagSelector()" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-slate-300 bg-white dark:bg-slate-700 border border-gray-300 dark:border-slate-600 rounded-md hover:bg-gray-50 dark:hover:bg-slate-600">
|
|
Cancel
|
|
</button>
|
|
<button type="button" onclick="submitCustomTag()" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700">
|
|
Add Tag
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
document.getElementById('custom-tag-name').focus();
|
|
}
|
|
|
|
function closeTagSelector() {
|
|
const modal = document.querySelector('.fixed.inset-0.bg-gray-600');
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
}
|
|
|
|
function submitCustomTag() {
|
|
const tagName = document.getElementById('custom-tag-name').value.trim();
|
|
if (!tagName) {
|
|
alert('Please enter a tag name');
|
|
return;
|
|
}
|
|
|
|
if (!/^[a-z0-9-]+$/.test(tagName)) {
|
|
alert('Invalid tag name format (use only letters, numbers, and hyphens)');
|
|
return;
|
|
}
|
|
|
|
bulkAddTag(tagName);
|
|
closeTagSelector();
|
|
}
|
|
|
|
function openTagRemovalSelector() {
|
|
const ids = getSelectedIds();
|
|
if (ids.length === 0) {
|
|
alert('Please select at least one domain');
|
|
return;
|
|
}
|
|
|
|
const modal = document.createElement('div');
|
|
modal.className = 'fixed inset-0 bg-gray-600 bg-opacity-50 z-50';
|
|
modal.innerHTML = `
|
|
<div class="flex items-center justify-center min-h-screen p-4">
|
|
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-xl max-w-md w-full">
|
|
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700">
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Remove Specific Tag</h3>
|
|
</div>
|
|
<div class="px-6 py-4">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1">Select Tag to Remove</label>
|
|
<select id="tag-to-remove" class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-slate-900 text-gray-900 dark:text-white">
|
|
<option value="">Loading tags...</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="px-6 py-4 bg-gray-50 dark:bg-slate-900 flex justify-end space-x-3 rounded-b-lg">
|
|
<button type="button" onclick="closeTagRemovalSelector()" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-slate-300 bg-white dark:bg-slate-700 border border-gray-300 dark:border-slate-600 rounded-md hover:bg-gray-50 dark:hover:bg-slate-600">
|
|
Cancel
|
|
</button>
|
|
<button type="button" onclick="submitTagRemoval()" class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700">
|
|
Remove Tag
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
const select = document.getElementById('tag-to-remove');
|
|
select.innerHTML = '<option value="">Loading tags...</option>';
|
|
|
|
fetch('/domains/get-tags-for-domains', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
domain_ids: ids,
|
|
csrf_token: '{{ csrf_token() }}'
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
select.innerHTML = '<option value="">Select a tag to remove</option>';
|
|
if (data.tags && data.tags.length > 0) {
|
|
data.tags.forEach(tag => {
|
|
const option = document.createElement('option');
|
|
option.value = tag.id;
|
|
option.textContent = tag.name;
|
|
select.appendChild(option);
|
|
});
|
|
} else {
|
|
select.innerHTML = '<option value="">No tags found on selected domains</option>';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading tags:', error);
|
|
select.innerHTML = '<option value="">Error loading tags</option>';
|
|
});
|
|
}
|
|
|
|
function closeTagRemovalSelector() {
|
|
const modal = document.querySelector('.fixed.inset-0.bg-gray-600');
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
}
|
|
|
|
function submitTagRemoval() {
|
|
const tagId = document.getElementById('tag-to-remove').value;
|
|
if (!tagId) {
|
|
alert('Please select a tag to remove');
|
|
return;
|
|
}
|
|
|
|
const ids = getSelectedIds();
|
|
if (ids.length === 0) {
|
|
alert('Please select at least one domain');
|
|
return;
|
|
}
|
|
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = '/domains/bulk-remove-specific-tag';
|
|
|
|
const csrfInput = document.createElement('input');
|
|
csrfInput.type = 'hidden';
|
|
csrfInput.name = 'csrf_token';
|
|
csrfInput.value = '{{ csrf_token() }}';
|
|
form.appendChild(csrfInput);
|
|
|
|
const tagInput = document.createElement('input');
|
|
tagInput.type = 'hidden';
|
|
tagInput.name = 'tag_id';
|
|
tagInput.value = tagId;
|
|
form.appendChild(tagInput);
|
|
|
|
ids.forEach(id => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'domain_ids[]';
|
|
input.value = id;
|
|
form.appendChild(input);
|
|
});
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
const wrapper = document.getElementById('domainExportDropdownWrapper');
|
|
if (wrapper && !wrapper.contains(e.target)) {
|
|
document.getElementById('domainExportMenu').classList.add('hidden');
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|