Switch PHP views to Twig and add 2FA/UI enhancements

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.
This commit is contained in:
Hosteroid
2026-03-03 18:21:32 +02:00
parent cd4e3e6bcc
commit 4818172bc6
73 changed files with 9948 additions and 10686 deletions

View File

@@ -1,562 +0,0 @@
<?php
$title = 'TLD Import Logs';
$pageTitle = 'TLD Import Logs';
$pageDescription = 'History of TLD registry import operations';
$pageIcon = 'fas fa-history';
ob_start();
?>
<!-- Header with Actions -->
<div class="mb-4 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Import Logs</h1>
<p class="text-gray-600 mt-1">History of TLD registry import operations</p>
</div>
<div class="flex gap-2">
<a href="/tld-registry" class="inline-flex items-center px-4 py-2.5 border border-gray-300 text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors font-medium">
<i class="fas fa-arrow-left mr-2"></i>
Back to Registry
</a>
</div>
</div>
<!-- Statistics Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<!-- Total Imports Card -->
<div class="bg-white rounded-lg border border-gray-200 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Total Imports</p>
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $importStats['total_imports'] ?? 0 ?></p>
</div>
<div class="w-12 h-12 bg-blue-50 rounded-lg flex items-center justify-center">
<i class="fas fa-download text-blue-600 text-lg"></i>
</div>
</div>
</div>
<!-- Successful Imports Card -->
<div class="bg-white rounded-lg border border-gray-200 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Successful</p>
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $importStats['successful_imports'] ?? 0 ?></p>
</div>
<div class="w-12 h-12 bg-green-50 rounded-lg flex items-center justify-center">
<i class="fas fa-check-circle text-green-600 text-lg"></i>
</div>
</div>
</div>
<!-- Failed Imports Card -->
<div class="bg-white rounded-lg border border-gray-200 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Failed</p>
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $importStats['failed_imports'] ?? 0 ?></p>
</div>
<div class="w-12 h-12 bg-red-50 rounded-lg flex items-center justify-center">
<i class="fas fa-times-circle text-red-600 text-lg"></i>
</div>
</div>
</div>
<!-- Last Import Card -->
<div class="bg-white rounded-lg border border-gray-200 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Last Import</p>
<p class="text-sm font-semibold text-gray-900 mt-1">
<?php if (!empty($importStats['last_import'])): ?>
<?= date('M j, H:i', strtotime($importStats['last_import'])) ?>
<?php else: ?>
Never
<?php endif; ?>
</p>
</div>
<div class="w-12 h-12 bg-indigo-50 rounded-lg flex items-center justify-center">
<i class="fas fa-clock text-indigo-600 text-lg"></i>
</div>
</div>
</div>
</div>
<!-- Import Logs Table -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<?php if (!empty($imports)): ?>
<!-- Table View (Desktop) -->
<div class="hidden lg:block overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Import Type</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Results</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Publication Date</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Started</th>
<th class="px-6 py-3 text-right text-xs font-semibold text-gray-600 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<?php foreach ($imports as $import): ?>
<tr class="hover:bg-gray-50 transition-colors duration-150"
data-import-id="<?= $import['id'] ?>"
data-import-data="<?= htmlspecialchars(json_encode($import)) ?>">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<?php
$typeIcons = [
'tld_list' => 'fa-list',
'rdap' => 'fa-database',
'whois' => 'fa-server',
'complete_workflow' => 'fa-tasks',
'check_updates' => 'fa-sync-alt',
'manual' => 'fa-hand-pointer'
];
$typeLabels = [
'tld_list' => 'TLD List',
'rdap' => 'RDAP Servers',
'whois' => 'WHOIS Data',
'complete_workflow' => 'Complete Workflow',
'check_updates' => 'Update Check',
'manual' => 'Manual Import'
];
$typeDescriptions = [
'tld_list' => 'IANA TLD list import',
'rdap' => 'RDAP server bootstrap data',
'whois' => 'WHOIS server & registry URLs',
'complete_workflow' => 'Full import (TLD List → RDAP → WHOIS)',
'check_updates' => 'IANA update verification',
'manual' => 'Manual data import'
];
$icon = $typeIcons[$import['import_type']] ?? 'fa-file-import';
$label = $typeLabels[$import['import_type']] ?? ucfirst($import['import_type']);
$description = $typeDescriptions[$import['import_type']] ?? 'Import operation';
?>
<div class="flex-shrink-0 h-10 w-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center">
<i class="fas <?= $icon ?> text-primary"></i>
</div>
<div class="ml-4">
<div class="text-sm font-semibold text-gray-900"><?= $label ?></div>
<div class="text-sm text-gray-500"><?= $description ?></div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<?php
$statusClass = '';
$statusIcon = '';
$statusText = '';
if ($import['status'] === 'completed') {
$statusClass = 'bg-green-100 text-green-700 border-green-200';
$statusIcon = 'fa-check-circle';
$statusText = 'Completed';
} elseif ($import['status'] === 'failed') {
$statusClass = 'bg-red-100 text-red-700 border-red-200';
$statusIcon = 'fa-times-circle';
$statusText = 'Failed';
} else {
$statusClass = 'bg-yellow-100 text-yellow-700 border-yellow-200';
$statusIcon = 'fa-clock';
$statusText = 'In Progress';
}
?>
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border <?= $statusClass ?>">
<i class="fas <?= $statusIcon ?> mr-1"></i>
<?= $statusText ?>
</span>
</td>
<td class="px-6 py-4">
<div class="text-sm text-gray-900">
<div class="flex items-center space-x-4">
<span class="flex items-center">
<i class="fas fa-globe text-gray-400 mr-1"></i>
<?= $import['total_tlds'] ?> total
</span>
<span class="flex items-center text-green-600">
<i class="fas fa-plus mr-1"></i>
<?= $import['new_tlds'] ?> new
</span>
<span class="flex items-center text-blue-600">
<i class="fas fa-sync mr-1"></i>
<?= $import['updated_tlds'] ?> updated
</span>
<?php if ($import['failed_tlds'] > 0): ?>
<span class="flex items-center text-red-600">
<i class="fas fa-exclamation-triangle mr-1"></i>
<?= $import['failed_tlds'] ?> failed
</span>
<?php endif; ?>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<?php if ($import['iana_publication_date']): ?>
<div class="flex items-center">
<i class="far fa-calendar mr-2"></i>
<?php
$date = $import['iana_publication_date'];
// Try to parse the date, if it fails, display as-is
$parsedDate = strtotime($date);
if ($parsedDate && $parsedDate > 0) {
echo date('M j, Y', $parsedDate);
} else {
echo htmlspecialchars($date);
}
?>
</div>
<?php else: ?>
<span class="text-gray-400">N/A</span>
<?php endif; ?>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div class="flex items-center">
<i class="far fa-clock mr-2"></i>
<?= date('M j, H:i', strtotime($import['started_at'])) ?>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button onclick="showImportDetails(<?= $import['id'] ?>)" class="text-primary hover:text-primary-dark">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Card View (Mobile) -->
<div class="lg:hidden divide-y divide-gray-200">
<?php foreach ($imports as $import): ?>
<div class="p-6 hover:bg-gray-50 transition-colors duration-150"
data-import-id="<?= $import['id'] ?>"
data-import-data="<?= htmlspecialchars(json_encode($import)) ?>">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<?php
$typeIcons = [
'tld_list' => 'fa-list',
'rdap' => 'fa-database',
'whois' => 'fa-server',
'complete_workflow' => 'fa-tasks',
'check_updates' => 'fa-sync-alt',
'manual' => 'fa-hand-pointer'
];
$typeLabels = [
'tld_list' => 'TLD List',
'rdap' => 'RDAP Servers',
'whois' => 'WHOIS Data',
'complete_workflow' => 'Complete Workflow',
'check_updates' => 'Update Check',
'manual' => 'Manual Import'
];
$icon = $typeIcons[$import['import_type']] ?? 'fa-file-import';
$label = $typeLabels[$import['import_type']] ?? ucfirst($import['import_type']);
?>
<div class="w-10 h-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center mr-3">
<i class="fas <?= $icon ?> text-primary"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900"><?= $label ?></h3>
<p class="text-sm text-gray-500"><?= date('M j, Y H:i', strtotime($import['started_at'])) ?></p>
</div>
</div>
<?php
$statusClass = '';
$statusIcon = '';
$statusText = '';
if ($import['status'] === 'completed') {
$statusClass = 'bg-green-100 text-green-700';
$statusIcon = 'fa-check-circle';
$statusText = 'Completed';
} elseif ($import['status'] === 'failed') {
$statusClass = 'bg-red-100 text-red-700';
$statusIcon = 'fa-times-circle';
$statusText = 'Failed';
} else {
$statusClass = 'bg-yellow-100 text-yellow-700';
$statusIcon = 'fa-clock';
$statusText = 'In Progress';
}
?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold <?= $statusClass ?>">
<i class="fas <?= $statusIcon ?> mr-1"></i>
<?= $statusText ?>
</span>
</div>
<div class="space-y-2 text-sm">
<div class="flex items-center justify-between">
<span class="text-gray-600">Total TLDs:</span>
<span class="font-semibold"><?= $import['total_tlds'] ?></span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600">New:</span>
<span class="font-semibold text-green-600"><?= $import['new_tlds'] ?></span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600">Updated:</span>
<span class="font-semibold text-blue-600"><?= $import['updated_tlds'] ?></span>
</div>
<?php if ($import['failed_tlds'] > 0): ?>
<div class="flex items-center justify-between">
<span class="text-gray-600">Failed:</span>
<span class="font-semibold text-red-600"><?= $import['failed_tlds'] ?></span>
</div>
<?php endif; ?>
</div>
<div class="flex space-x-2 mt-3">
<button onclick="showImportDetails(<?= $import['id'] ?>)" class="flex-1 px-3 py-1.5 bg-blue-50 text-blue-600 rounded text-center text-sm hover:bg-blue-100 transition-colors">
<i class="fas fa-eye mr-1"></i> Details
</button>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Pagination -->
<?php if ($pagination['total_pages'] > 1): ?>
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
<?php if ($pagination['current_page'] > 1): ?>
<a href="?page=<?= $pagination['current_page'] - 1 ?>"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Previous
</a>
<?php endif; ?>
<?php if ($pagination['current_page'] < $pagination['total_pages']): ?>
<a href="?page=<?= $pagination['current_page'] + 1 ?>"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Next
</a>
<?php endif; ?>
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
Showing <span class="font-medium"><?= $pagination['showing_from'] ?></span> to
<span class="font-medium"><?= $pagination['showing_to'] ?></span> of
<span class="font-medium"><?= $pagination['total'] ?></span> results
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<?php for ($i = 1; $i <= $pagination['total_pages']; $i++): ?>
<a href="?page=<?= $i ?>"
class="relative inline-flex items-center px-4 py-2 border text-sm font-medium <?= $i === $pagination['current_page'] ? 'z-10 bg-primary border-primary text-white' : 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50' ?> <?= $i === 1 ? 'rounded-l-md' : '' ?> <?= $i === $pagination['total_pages'] ? 'rounded-r-md' : '' ?>">
<?= $i ?>
</a>
<?php endfor; ?>
</nav>
</div>
</div>
</div>
<?php endif; ?>
<?php else: ?>
<div class="text-center py-12 px-6">
<div class="mb-4">
<i class="fas fa-history text-gray-300 text-6xl"></i>
</div>
<h3 class="text-lg font-semibold text-gray-700 mb-1">No Import Logs</h3>
<p class="text-sm text-gray-500 mb-4">No TLD imports have been performed yet.</p>
<a href="/tld-registry" 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-arrow-left mr-2"></i>
Back to Registry
</a>
</div>
<?php endif; ?>
</div>
<!-- Import Details Modal -->
<div id="importDetailsModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
<div class="mt-3">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Import Details</h3>
<button onclick="closeImportDetails()" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div id="importDetailsContent" class="text-sm text-gray-600">
<!-- Content will be loaded here -->
</div>
</div>
</div>
</div>
<script>
function showImportDetails(importId) {
// Find the import data from the current page
const importData = findImportData(importId);
if (!importData) {
document.getElementById('importDetailsContent').innerHTML = `
<div class="text-center text-gray-500">
<p>Import details not found</p>
</div>
`;
document.getElementById('importDetailsModal').classList.remove('hidden');
return;
}
// Type labels mapping
const typeLabels = {
'tld_list': 'TLD List',
'rdap': 'RDAP Servers',
'whois': 'WHOIS Data',
'complete_workflow': 'Complete Workflow',
'check_updates': 'Update Check',
'manual': 'Manual Import'
};
const typeDescriptions = {
'tld_list': 'IANA TLD list import',
'rdap': 'RDAP server bootstrap data',
'whois': 'WHOIS server & registry URLs',
'complete_workflow': 'Full import (TLD List → RDAP → WHOIS)',
'check_updates': 'IANA update verification',
'manual': 'Manual data import'
};
const typeLabel = typeLabels[importData.import_type] || importData.import_type;
const typeDescription = typeDescriptions[importData.import_type] || 'Import operation';
// Calculate duration if we have both start and completion times
let duration = 'Unknown';
if (importData.started_at && importData.completed_at) {
const start = new Date(importData.started_at);
const end = new Date(importData.completed_at);
const diffMs = end - start;
const minutes = Math.floor(diffMs / 60000);
const seconds = Math.floor((diffMs % 60000) / 1000);
// If duration is very short (< 5 seconds), it might be manually completed
// Try to estimate from the log if it's a complete workflow
if (diffMs < 5000 && importData.import_type === 'complete_workflow') {
// Estimate: ~1 second per TLD for complete workflow
const estimatedSeconds = Math.round((importData.total_tlds || 0) * 1.1);
const estMinutes = Math.floor(estimatedSeconds / 60);
const estSeconds = estimatedSeconds % 60;
duration = `~${estMinutes} minutes ${estSeconds} seconds (estimated)`;
} else if (minutes === 0 && seconds === 0) {
duration = 'Less than 1 second';
} else {
duration = `${minutes} minutes ${seconds} seconds`;
}
}
// Determine status color
let statusClass = 'bg-gray-100 text-gray-800';
let statusText = 'Unknown';
if (importData.status === 'completed') {
statusClass = 'bg-green-100 text-green-800';
statusText = 'Completed';
} else if (importData.status === 'failed') {
statusClass = 'bg-red-100 text-red-800';
statusText = 'Failed';
} else if (importData.status === 'running') {
statusClass = 'bg-yellow-100 text-yellow-800';
statusText = 'Running';
}
document.getElementById('importDetailsContent').innerHTML = `
<div class="space-y-3">
<div class="flex justify-between">
<span class="font-medium">Import ID:</span>
<span>${importData.id}</span>
</div>
<div class="flex justify-between">
<span class="font-medium">Type:</span>
<span>${typeLabel}</span>
</div>
<div class="flex justify-between">
<span class="font-medium">Description:</span>
<span class="text-gray-600">${typeDescription}</span>
</div>
<div class="flex justify-between">
<span class="font-medium">Status:</span>
<span class="px-2 py-1 rounded text-xs ${statusClass}">${statusText}</span>
</div>
<div class="flex justify-between">
<span class="font-medium">Duration:</span>
<span>${duration}</span>
</div>
<div class="flex justify-between">
<span class="font-medium">Started:</span>
<span>${new Date(importData.started_at).toLocaleString()}</span>
</div>
${importData.completed_at ? `
<div class="flex justify-between">
<span class="font-medium">Completed:</span>
<span>${new Date(importData.completed_at).toLocaleString()}</span>
</div>
` : ''}
${importData.iana_publication_date ? `
<div class="flex justify-between">
<span class="font-medium">IANA Publication:</span>
<span>${importData.iana_publication_date}</span>
</div>
` : ''}
<div class="mt-4">
<h4 class="font-medium mb-2">Import Results:</h4>
<div class="bg-gray-100 p-3 rounded text-xs font-mono space-y-1">
<div>Total TLDs: ${importData.total_tlds || 0}</div>
<div>New TLDs: ${importData.new_tlds || 0}</div>
<div>Updated TLDs: ${importData.updated_tlds || 0}</div>
<div>Failed TLDs: ${importData.failed_tlds || 0}</div>
${importData.error_message ? `
<div class="text-red-600 mt-2">
<strong>Error:</strong> ${importData.error_message}
</div>
` : ''}
</div>
</div>
</div>
`;
document.getElementById('importDetailsModal').classList.remove('hidden');
}
function findImportData(importId) {
// Look for import data in the current page
const importRows = document.querySelectorAll('tr[data-import-id]');
for (let row of importRows) {
if (row.getAttribute('data-import-id') == importId) {
return JSON.parse(row.getAttribute('data-import-data'));
}
}
// Fallback: look for data in mobile view
const importCards = document.querySelectorAll('[data-import-id]');
for (let card of importCards) {
if (card.getAttribute('data-import-id') == importId) {
return JSON.parse(card.getAttribute('data-import-data'));
}
}
return null;
}
function closeImportDetails() {
document.getElementById('importDetailsModal').classList.add('hidden');
}
// Close modal when clicking outside
document.getElementById('importDetailsModal').addEventListener('click', function(e) {
if (e.target === this) {
closeImportDetails();
}
});
</script>
<?php
$content = ob_get_clean();
include __DIR__ . '/../layout/base.php';
?>

View File

@@ -0,0 +1,506 @@
{% extends 'layout/base.twig' %}
{% set title = 'TLD Import Logs' %}
{% set pageTitle = 'TLD Import Logs' %}
{% set pageDescription = 'History of TLD registry import operations' %}
{% set pageIcon = 'fas fa-history' %}
{% block content %}
{# Header with Actions #}
<div class="mb-4 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Import Logs</h1>
<p class="text-gray-600 dark:text-slate-400 mt-1">History of TLD registry import operations</p>
</div>
<div class="flex gap-2">
<a href="/tld-registry" class="inline-flex items-center px-4 py-2.5 border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-slate-300 text-sm rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors font-medium">
<i class="fas fa-arrow-left mr-2"></i>
Back to Registry
</a>
</div>
</div>
{# Statistics Cards #}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{# Total Imports Card #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wide">Total Imports</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ importStats.total_imports|default(0) }}</p>
</div>
<div class="w-12 h-12 bg-blue-50 dark:bg-blue-500/10 rounded-lg flex items-center justify-center">
<i class="fas fa-download text-blue-600 dark:text-blue-400 text-lg"></i>
</div>
</div>
</div>
{# Successful Imports Card #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wide">Successful</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ importStats.successful_imports|default(0) }}</p>
</div>
<div class="w-12 h-12 bg-green-50 dark:bg-green-500/10 rounded-lg flex items-center justify-center">
<i class="fas fa-check-circle text-green-600 dark:text-green-400 text-lg"></i>
</div>
</div>
</div>
{# Failed Imports Card #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wide">Failed</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ importStats.failed_imports|default(0) }}</p>
</div>
<div class="w-12 h-12 bg-red-50 dark:bg-red-500/10 rounded-lg flex items-center justify-center">
<i class="fas fa-times-circle text-red-600 dark:text-red-400 text-lg"></i>
</div>
</div>
</div>
{# Last Import Card #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wide">Last Import</p>
<p class="text-sm font-semibold text-gray-900 dark:text-white mt-1">
{% if importStats.last_import is not empty %}
{{ importStats.last_import|date('M j, H:i') }}
{% else %}
Never
{% endif %}
</p>
</div>
<div class="w-12 h-12 bg-indigo-50 dark:bg-indigo-500/10 rounded-lg flex items-center justify-center">
<i class="fas fa-clock text-indigo-600 dark:text-indigo-400 text-lg"></i>
</div>
</div>
</div>
</div>
{# Import Logs Table #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
{% if imports 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 text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Import Type</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Results</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Publication Date</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Started</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">
{% set typeIcons = {
tld_list: 'fa-list',
rdap: 'fa-database',
whois: 'fa-server',
complete_workflow: 'fa-tasks',
check_updates: 'fa-sync-alt',
manual: 'fa-hand-pointer'
} %}
{% set typeLabels = {
tld_list: 'TLD List',
rdap: 'RDAP Servers',
whois: 'WHOIS Data',
complete_workflow: 'Complete Workflow',
check_updates: 'Update Check',
manual: 'Manual Import'
} %}
{% set typeDescriptions = {
tld_list: 'IANA TLD list import',
rdap: 'RDAP server bootstrap data',
whois: 'WHOIS server & registry URLs',
complete_workflow: 'Full import (TLD List → RDAP → WHOIS)',
check_updates: 'IANA update verification',
manual: 'Manual data import'
} %}
{% for import in imports %}
{% set icon = typeIcons[import.import_type]|default('fa-file-import') %}
{% set label = typeLabels[import.import_type]|default(import.import_type|capitalize) %}
{% set description = typeDescriptions[import.import_type]|default('Import operation') %}
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-150"
data-import-id="{{ import.id }}"
data-import-data="{{ import|json_encode|e('html_attr') }}">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center">
<i class="fas {{ icon }} text-primary"></i>
</div>
<div class="ml-4">
<div class="text-sm font-semibold text-gray-900 dark:text-white">{{ label }}</div>
<div class="text-sm text-gray-500 dark:text-slate-400">{{ description }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{% if import.status == 'completed' %}
{% set statusClass = 'bg-green-100 dark:bg-green-500/10 text-green-700 dark:text-green-400 border-green-200 dark:border-green-500/30' %}
{% set statusIcon = 'fa-check-circle' %}
{% set statusText = 'Completed' %}
{% elseif import.status == 'failed' %}
{% set statusClass = 'bg-red-100 dark:bg-red-500/10 text-red-700 dark:text-red-400 border-red-200 dark:border-red-500/30' %}
{% set statusIcon = 'fa-times-circle' %}
{% set statusText = 'Failed' %}
{% else %}
{% set statusClass = 'bg-yellow-100 dark:bg-yellow-500/10 text-yellow-700 dark:text-yellow-400 border-yellow-200 dark:border-yellow-500/30' %}
{% set statusIcon = 'fa-clock' %}
{% set statusText = 'In Progress' %}
{% endif %}
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border {{ statusClass }}">
<i class="fas {{ statusIcon }} mr-1"></i>
{{ statusText }}
</span>
</td>
<td class="px-6 py-4">
<div class="text-sm text-gray-900 dark:text-white">
<div class="flex items-center space-x-4">
<span class="flex items-center">
<i class="fas fa-globe text-gray-400 dark:text-slate-500 mr-1"></i>
{{ import.total_tlds }} total
</span>
<span class="flex items-center text-green-600 dark:text-green-400">
<i class="fas fa-plus mr-1"></i>
{{ import.new_tlds }} new
</span>
<span class="flex items-center text-blue-600 dark:text-blue-400">
<i class="fas fa-sync mr-1"></i>
{{ import.updated_tlds }} updated
</span>
{% if import.failed_tlds > 0 %}
<span class="flex items-center text-red-600 dark:text-red-400">
<i class="fas fa-exclamation-triangle mr-1"></i>
{{ import.failed_tlds }} failed
</span>
{% endif %}
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-slate-400">
{% if import.iana_publication_date %}
<div class="flex items-center">
<i class="far fa-calendar mr-2"></i>
{{ import.iana_publication_date|date('M j, Y') }}
</div>
{% else %}
<span class="text-gray-400 dark:text-slate-500">N/A</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-slate-400">
<div class="flex items-center">
<i class="far fa-clock mr-2"></i>
{{ import.started_at|date('M j, H:i') }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button onclick="showImportDetails({{ import.id }})" class="text-primary hover:text-primary-dark">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# Card View (Mobile) #}
<div class="lg:hidden divide-y divide-gray-200 dark:divide-slate-700">
{% for import in imports %}
{% set icon = typeIcons[import.import_type]|default('fa-file-import') %}
{% set label = typeLabels[import.import_type]|default(import.import_type|capitalize) %}
<div class="p-6 hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-150"
data-import-id="{{ import.id }}"
data-import-data="{{ import|json_encode|e('html_attr') }}">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<div class="w-10 h-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center mr-3">
<i class="fas {{ icon }} text-primary"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-white">{{ label }}</h3>
<p class="text-sm text-gray-500 dark:text-slate-400">{{ import.started_at|date('M j, Y H:i') }}</p>
</div>
</div>
{% if import.status == 'completed' %}
{% set mobileStatusClass = 'bg-green-100 dark:bg-green-500/10 text-green-700 dark:text-green-400' %}
{% set mobileStatusIcon = 'fa-check-circle' %}
{% set mobileStatusText = 'Completed' %}
{% elseif import.status == 'failed' %}
{% set mobileStatusClass = 'bg-red-100 dark:bg-red-500/10 text-red-700 dark:text-red-400' %}
{% set mobileStatusIcon = 'fa-times-circle' %}
{% set mobileStatusText = 'Failed' %}
{% else %}
{% set mobileStatusClass = 'bg-yellow-100 dark:bg-yellow-500/10 text-yellow-700 dark:text-yellow-400' %}
{% set mobileStatusIcon = 'fa-clock' %}
{% set mobileStatusText = 'In Progress' %}
{% endif %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold {{ mobileStatusClass }}">
<i class="fas {{ mobileStatusIcon }} mr-1"></i>
{{ mobileStatusText }}
</span>
</div>
<div class="space-y-2 text-sm">
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-slate-400">Total TLDs:</span>
<span class="font-semibold text-gray-900 dark:text-white">{{ import.total_tlds }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-slate-400">New:</span>
<span class="font-semibold text-green-600 dark:text-green-400">{{ import.new_tlds }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-slate-400">Updated:</span>
<span class="font-semibold text-blue-600 dark:text-blue-400">{{ import.updated_tlds }}</span>
</div>
{% if import.failed_tlds > 0 %}
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-slate-400">Failed:</span>
<span class="font-semibold text-red-600 dark:text-red-400">{{ import.failed_tlds }}</span>
</div>
{% endif %}
</div>
<div class="flex space-x-2 mt-3">
<button onclick="showImportDetails({{ import.id }})" class="flex-1 px-3 py-1.5 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded text-center text-sm hover:bg-blue-100 dark:hover:bg-blue-500/20 transition-colors">
<i class="fas fa-eye mr-1"></i> Details
</button>
</div>
</div>
{% endfor %}
</div>
{# Pagination #}
{% if pagination.total_pages > 1 %}
<div class="bg-white dark:bg-slate-800 px-4 py-3 flex items-center justify-between border-t border-gray-200 dark:border-slate-700 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
{% if pagination.current_page > 1 %}
<a href="?page={{ pagination.current_page - 1 }}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-slate-600 text-sm font-medium rounded-md text-gray-700 dark:text-slate-300 bg-white dark:bg-slate-800 hover:bg-gray-50 dark:hover:bg-slate-700">
Previous
</a>
{% endif %}
{% if pagination.current_page < pagination.total_pages %}
<a href="?page={{ pagination.current_page + 1 }}"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-slate-600 text-sm font-medium rounded-md text-gray-700 dark:text-slate-300 bg-white dark:bg-slate-800 hover:bg-gray-50 dark:hover:bg-slate-700">
Next
</a>
{% endif %}
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700 dark:text-slate-300">
Showing <span class="font-medium">{{ pagination.showing_from }}</span> to
<span class="font-medium">{{ pagination.showing_to }}</span> of
<span class="font-medium">{{ pagination.total }}</span> results
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
{% for i in 1..pagination.total_pages %}
<a href="?page={{ i }}"
class="relative inline-flex items-center px-4 py-2 border text-sm font-medium {{ i == pagination.current_page ? 'z-10 bg-primary border-primary text-white' : 'bg-white dark:bg-slate-800 border-gray-300 dark:border-slate-600 text-gray-500 dark:text-slate-400 hover:bg-gray-50 dark:hover:bg-slate-700' }} {{ i == 1 ? 'rounded-l-md' : '' }} {{ i == pagination.total_pages ? 'rounded-r-md' : '' }}">
{{ i }}
</a>
{% endfor %}
</nav>
</div>
</div>
</div>
{% endif %}
{% else %}
<div class="text-center py-12 px-6">
<div class="mb-4">
<i class="fas fa-history 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 Import Logs</h3>
<p class="text-sm text-gray-500 dark:text-slate-400 mb-4">No TLD imports have been performed yet.</p>
<a href="/tld-registry" 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-arrow-left mr-2"></i>
Back to Registry
</a>
</div>
{% endif %}
</div>
{# Import Details Modal #}
<div id="importDetailsModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
<div class="relative top-20 mx-auto p-5 border border-gray-200 dark:border-slate-700 w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white dark:bg-slate-800">
<div class="mt-3">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Import Details</h3>
<button onclick="closeImportDetails()" class="text-gray-400 dark:text-slate-500 hover:text-gray-600 dark:hover:text-slate-300">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div id="importDetailsContent" class="text-sm text-gray-600 dark:text-slate-400">
</div>
</div>
</div>
</div>
<script>
function showImportDetails(importId) {
const importData = findImportData(importId);
if (!importData) {
document.getElementById('importDetailsContent').innerHTML = `
<div class="text-center text-gray-500 dark:text-slate-400">
<p>Import details not found</p>
</div>
`;
document.getElementById('importDetailsModal').classList.remove('hidden');
return;
}
const typeLabels = {
'tld_list': 'TLD List',
'rdap': 'RDAP Servers',
'whois': 'WHOIS Data',
'complete_workflow': 'Complete Workflow',
'check_updates': 'Update Check',
'manual': 'Manual Import'
};
const typeDescriptions = {
'tld_list': 'IANA TLD list import',
'rdap': 'RDAP server bootstrap data',
'whois': 'WHOIS server & registry URLs',
'complete_workflow': 'Full import (TLD List → RDAP → WHOIS)',
'check_updates': 'IANA update verification',
'manual': 'Manual data import'
};
const typeLabel = typeLabels[importData.import_type] || importData.import_type;
const typeDescription = typeDescriptions[importData.import_type] || 'Import operation';
let duration = 'Unknown';
if (importData.started_at && importData.completed_at) {
const start = new Date(importData.started_at);
const end = new Date(importData.completed_at);
const diffMs = end - start;
const minutes = Math.floor(diffMs / 60000);
const seconds = Math.floor((diffMs % 60000) / 1000);
if (diffMs < 5000 && importData.import_type === 'complete_workflow') {
const estimatedSeconds = Math.round((importData.total_tlds || 0) * 1.1);
const estMinutes = Math.floor(estimatedSeconds / 60);
const estSeconds = estimatedSeconds % 60;
duration = `~${estMinutes} minutes ${estSeconds} seconds (estimated)`;
} else if (minutes === 0 && seconds === 0) {
duration = 'Less than 1 second';
} else {
duration = `${minutes} minutes ${seconds} seconds`;
}
}
let statusClass = 'bg-gray-100 dark:bg-slate-700 text-gray-800 dark:text-slate-200';
let statusText = 'Unknown';
if (importData.status === 'completed') {
statusClass = 'bg-green-100 dark:bg-green-500/10 text-green-800 dark:text-green-400';
statusText = 'Completed';
} else if (importData.status === 'failed') {
statusClass = 'bg-red-100 dark:bg-red-500/10 text-red-800 dark:text-red-400';
statusText = 'Failed';
} else if (importData.status === 'running') {
statusClass = 'bg-yellow-100 dark:bg-yellow-500/10 text-yellow-800 dark:text-yellow-400';
statusText = 'Running';
}
document.getElementById('importDetailsContent').innerHTML = `
<div class="space-y-3">
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Import ID:</span>
<span class="text-gray-700 dark:text-slate-300">${importData.id}</span>
</div>
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Type:</span>
<span class="text-gray-700 dark:text-slate-300">${typeLabel}</span>
</div>
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Description:</span>
<span class="text-gray-600 dark:text-slate-400">${typeDescription}</span>
</div>
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Status:</span>
<span class="px-2 py-1 rounded text-xs ${statusClass}">${statusText}</span>
</div>
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Duration:</span>
<span class="text-gray-700 dark:text-slate-300">${duration}</span>
</div>
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Started:</span>
<span class="text-gray-700 dark:text-slate-300">${new Date(importData.started_at).toLocaleString()}</span>
</div>
${importData.completed_at ? `
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Completed:</span>
<span class="text-gray-700 dark:text-slate-300">${new Date(importData.completed_at).toLocaleString()}</span>
</div>
` : ''}
${importData.iana_publication_date ? `
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">IANA Publication:</span>
<span class="text-gray-700 dark:text-slate-300">${importData.iana_publication_date}</span>
</div>
` : ''}
<div class="mt-4">
<h4 class="font-medium text-gray-900 dark:text-white mb-2">Import Results:</h4>
<div class="bg-gray-100 dark:bg-slate-900 p-3 rounded text-xs font-mono space-y-1 text-gray-800 dark:text-slate-300">
<div>Total TLDs: ${importData.total_tlds || 0}</div>
<div>New TLDs: ${importData.new_tlds || 0}</div>
<div>Updated TLDs: ${importData.updated_tlds || 0}</div>
<div>Failed TLDs: ${importData.failed_tlds || 0}</div>
${importData.error_message ? `
<div class="text-red-600 dark:text-red-400 mt-2">
<strong>Error:</strong> ${importData.error_message}
</div>
` : ''}
</div>
</div>
</div>
`;
document.getElementById('importDetailsModal').classList.remove('hidden');
}
function findImportData(importId) {
const importRows = document.querySelectorAll('tr[data-import-id]');
for (let row of importRows) {
if (row.getAttribute('data-import-id') == importId) {
return JSON.parse(row.getAttribute('data-import-data'));
}
}
const importCards = document.querySelectorAll('[data-import-id]');
for (let card of importCards) {
if (card.getAttribute('data-import-id') == importId) {
return JSON.parse(card.getAttribute('data-import-data'));
}
}
return null;
}
function closeImportDetails() {
document.getElementById('importDetailsModal').classList.add('hidden');
}
document.getElementById('importDetailsModal').addEventListener('click', function(e) {
if (e.target === this) {
closeImportDetails();
}
});
</script>
{% endblock %}

View File

@@ -1,135 +1,134 @@
<?php
$title = $title ?? 'Import Progress';
$pageTitle = $title;
$pageDescription = 'Progressive data import with real-time progress';
$pageIcon = 'fas fa-tasks';
ob_start();
?>
{% extends 'layout/base.twig' %}
{% set title = title|default('Import Progress') %}
{% set pageTitle = title %}
{% set pageDescription = 'Progressive data import with real-time progress' %}
{% set pageIcon = 'fas fa-tasks' %}
{% block content %}
<div class="max-w-4xl mx-auto">
<!-- Header -->
{# Header #}
<div class="mb-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900"><?= htmlspecialchars($title) ?></h1>
<p class="text-gray-600 mt-1">
<?php
$descriptions = [
'tld_list' => 'Importing complete TLD list from IANA',
'rdap' => 'Importing RDAP servers for existing TLDs',
'whois' => 'Importing WHOIS & Registry data via RDAP API (with HTML fallback)',
'check_updates' => 'Checking for IANA updates',
'complete_workflow' => 'Complete TLD import workflow (TLD List → RDAP → WHOIS & Registry Data)'
];
echo htmlspecialchars($descriptions[$import_type] ?? 'Processing import');
?>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ title }}</h1>
<p class="text-gray-600 dark:text-slate-400 mt-1">
{% set descriptions = {
tld_list: 'Importing complete TLD list from IANA',
rdap: 'Importing RDAP servers for existing TLDs',
whois: 'Importing WHOIS & Registry data via RDAP API (with HTML fallback)',
check_updates: 'Checking for IANA updates',
complete_workflow: 'Complete TLD import workflow (TLD List → RDAP → WHOIS & Registry Data)'
} %}
{{ descriptions[import_type]|default('Processing import') }}
</p>
</div>
<a href="/tld-registry" class="inline-flex items-center px-4 py-2 border border-gray-300 text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors font-medium">
<a href="/tld-registry" class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-slate-300 text-sm rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors font-medium">
<i class="fas fa-arrow-left mr-2"></i>
Back to TLD Registry
</a>
</div>
</div>
<!-- Progress Card -->
<div class="bg-white rounded-lg border border-gray-200 p-6 mb-6">
{# Progress Card #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900">Import Status</h2>
<div id="status-badge" class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Import Status</h2>
<div id="status-badge" class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-yellow-100 dark:bg-yellow-500/10 text-yellow-800 dark:text-yellow-400">
<i class="fas fa-clock mr-2"></i>
<span id="status-text">Starting...</span>
</div>
</div>
<!-- Progress Bar -->
{# Progress Bar #}
<div class="mb-4">
<div class="flex justify-between text-sm text-gray-600 mb-2">
<div class="flex justify-between text-sm text-gray-600 dark:text-slate-400 mb-2">
<span id="progress-text">0 of 0 items processed</span>
<span id="percentage-text">0%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-3">
<div class="w-full bg-gray-200 dark:bg-slate-700 rounded-full h-3">
<div id="progress-bar" class="bg-blue-600 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
<!-- Step Progress (for complete workflow) -->
{# Step Progress (for complete workflow) #}
<div id="step-progress" class="mb-4" style="display: none;">
<div class="text-sm text-gray-600 mb-2">Workflow Steps:</div>
<div class="text-sm text-gray-600 dark:text-slate-400 mb-2">Workflow Steps:</div>
<div class="grid grid-cols-3 gap-2">
<div class="step-item bg-gray-100 rounded-lg p-2 text-center">
<div class="text-xs font-medium text-gray-600">Step 1</div>
<div class="text-xs text-gray-500">TLD List</div>
<div id="step-1-status" class="text-xs text-gray-400">Pending</div>
<div class="step-item bg-gray-100 dark:bg-slate-700 rounded-lg p-2 text-center">
<div class="text-xs font-medium text-gray-600 dark:text-slate-400">Step 1</div>
<div class="text-xs text-gray-500 dark:text-slate-400">TLD List</div>
<div id="step-1-status" class="text-xs text-gray-400 dark:text-slate-500">Pending</div>
</div>
<div class="step-item bg-gray-100 rounded-lg p-2 text-center">
<div class="text-xs font-medium text-gray-600">Step 2</div>
<div class="text-xs text-gray-500">RDAP</div>
<div id="step-2-status" class="text-xs text-gray-400">Pending</div>
<div class="step-item bg-gray-100 dark:bg-slate-700 rounded-lg p-2 text-center">
<div class="text-xs font-medium text-gray-600 dark:text-slate-400">Step 2</div>
<div class="text-xs text-gray-500 dark:text-slate-400">RDAP</div>
<div id="step-2-status" class="text-xs text-gray-400 dark:text-slate-500">Pending</div>
</div>
<div class="step-item bg-gray-100 rounded-lg p-2 text-center">
<div class="text-xs font-medium text-gray-600">Step 3</div>
<div class="text-xs text-gray-500">WHOIS & Registry</div>
<div id="step-3-status" class="text-xs text-gray-400">Pending</div>
<div class="step-item bg-gray-100 dark:bg-slate-700 rounded-lg p-2 text-center">
<div class="text-xs font-medium text-gray-600 dark:text-slate-400">Step 3</div>
<div class="text-xs text-gray-500 dark:text-slate-400">WHOIS & Registry</div>
<div id="step-3-status" class="text-xs text-gray-400 dark:text-slate-500">Pending</div>
</div>
</div>
</div>
<!-- Statistics -->
{# Statistics #}
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="bg-gray-50 rounded-lg p-4">
<div class="bg-gray-50 dark:bg-slate-900 rounded-lg p-4">
<div class="flex items-center">
<div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-list text-blue-600"></i>
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-500/10 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-list text-blue-600 dark:text-blue-400"></i>
</div>
<div>
<p class="text-sm text-gray-500">Total</p>
<p id="total-count" class="text-xl font-semibold text-gray-900">0</p>
<p class="text-sm text-gray-500 dark:text-slate-400">Total</p>
<p id="total-count" class="text-xl font-semibold text-gray-900 dark:text-white">0</p>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<div class="bg-gray-50 dark:bg-slate-900 rounded-lg p-4">
<div class="flex items-center">
<div class="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-check text-green-600"></i>
<div class="w-10 h-10 bg-green-100 dark:bg-green-500/10 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-check text-green-600 dark:text-green-400"></i>
</div>
<div>
<p class="text-sm text-gray-500">Processed</p>
<p id="processed-count" class="text-xl font-semibold text-gray-900">0</p>
<p class="text-sm text-gray-500 dark:text-slate-400">Processed</p>
<p id="processed-count" class="text-xl font-semibold text-gray-900 dark:text-white">0</p>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<div class="bg-gray-50 dark:bg-slate-900 rounded-lg p-4">
<div class="flex items-center">
<div class="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-times text-red-600"></i>
<div class="w-10 h-10 bg-red-100 dark:bg-red-500/10 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-times text-red-600 dark:text-red-400"></i>
</div>
<div>
<p class="text-sm text-gray-500">Failed</p>
<p id="failed-count" class="text-xl font-semibold text-gray-900">0</p>
<p class="text-sm text-gray-500 dark:text-slate-400">Failed</p>
<p id="failed-count" class="text-xl font-semibold text-gray-900 dark:text-white">0</p>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<div class="bg-gray-50 dark:bg-slate-900 rounded-lg p-4">
<div class="flex items-center">
<div class="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-hourglass-half text-orange-600"></i>
<div class="w-10 h-10 bg-orange-100 dark:bg-orange-500/10 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-hourglass-half text-orange-600 dark:text-orange-400"></i>
</div>
<div>
<p class="text-sm text-gray-500">Remaining</p>
<p id="remaining-count" class="text-xl font-semibold text-gray-900">0</p>
<p class="text-sm text-gray-500 dark:text-slate-400">Remaining</p>
<p id="remaining-count" class="text-xl font-semibold text-gray-900 dark:text-white">0</p>
</div>
</div>
</div>
</div>
</div>
<!-- Log Output -->
<div class="bg-white rounded-lg border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Import Log</h3>
{# Log Output #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Import Log</h3>
<div id="log-output" class="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm h-64 overflow-y-auto">
<div class="text-gray-500">Initializing import process...</div>
</div>
@@ -137,13 +136,12 @@ ob_start();
</div>
<script>
let logId = <?= json_encode($log_id) ?>;
let importType = <?= json_encode($import_type) ?>;
let logId = {{ log_id|json_encode|raw }};
let importType = {{ import_type|json_encode|raw }};
let isComplete = false;
let totalProcessed = 0;
let totalFailed = 0;
// Show step progress for complete workflow
if (importType === 'complete_workflow') {
document.getElementById('step-progress').style.display = 'block';
}
@@ -152,11 +150,11 @@ function addLogMessage(message, type = 'info') {
const logOutput = document.getElementById('log-output');
const timestamp = new Date().toLocaleTimeString();
const colorClass = type === 'error' ? 'text-red-400' : type === 'success' ? 'text-green-400' : 'text-blue-400';
const logEntry = document.createElement('div');
logEntry.className = colorClass;
logEntry.innerHTML = `[${timestamp}] ${message}`;
logOutput.appendChild(logEntry);
logOutput.scrollTop = logOutput.scrollHeight;
}
@@ -166,51 +164,45 @@ function updateProgress(data) {
const processed = data.processed || 0;
const failed = data.failed || 0;
const remaining = data.remaining || 0;
// Update counts (use absolute values, not cumulative)
document.getElementById('total-count').textContent = total;
document.getElementById('processed-count').textContent = processed;
document.getElementById('failed-count').textContent = failed;
document.getElementById('remaining-count').textContent = remaining;
// Update progress bar
const totalToProcess = processed + remaining;
const percentage = totalToProcess > 0 ? Math.round((processed / totalToProcess) * 100) : 0;
document.getElementById('progress-bar').style.width = percentage + '%';
document.getElementById('progress-text').textContent = `${processed} of ${totalToProcess} items processed`;
document.getElementById('percentage-text').textContent = percentage + '%';
// Update step progress for complete workflow
if (importType === 'complete_workflow' && data.message) {
updateStepProgress(data.message, processed, total);
}
// Update status
const statusBadge = document.getElementById('status-badge');
const statusText = document.getElementById('status-text');
if (data.status === 'complete') {
statusBadge.className = 'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800';
statusBadge.className = 'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 dark:bg-green-500/10 text-green-800 dark:text-green-400';
statusText.innerHTML = '<i class="fas fa-check mr-2"></i>Complete';
isComplete = true;
// Show the actual completion message from API
const completionMessage = data.message || 'Import completed successfully!';
addLogMessage(completionMessage, 'success');
// Mark all steps as completed for complete workflow
if (importType === 'complete_workflow') {
for (let i = 1; i <= 3; i++) {
updateStepStatus(i, 'completed');
}
}
} else if (data.status === 'in_progress') {
statusBadge.className = 'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800';
statusBadge.className = 'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 dark:bg-blue-500/10 text-blue-800 dark:text-blue-400';
statusText.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>In Progress';
addLogMessage(data.message || 'Processing batch...', 'info');
} else if (data.status === 'error') {
statusBadge.className = 'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800';
statusBadge.className = 'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 dark:bg-red-500/10 text-red-800 dark:text-red-400';
statusText.innerHTML = '<i class="fas fa-exclamation-triangle mr-2"></i>Error';
addLogMessage(data.message || 'An error occurred', 'error');
isComplete = true;
@@ -221,10 +213,9 @@ function checkProgress() {
if (isComplete) {
return;
}
fetch(`/tld-registry/api/import-progress?log_id=${logId}`)
.then(response => {
// Check if response is actually JSON
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
return response.text().then(text => {
@@ -242,17 +233,17 @@ function checkProgress() {
isComplete = true;
return;
}
updateProgress(data);
if (data.status !== 'complete' && data.status !== 'error') {
setTimeout(checkProgress, 2000); // Check again in 2 seconds
setTimeout(checkProgress, 2000);
}
})
.catch(error => {
if (error.message.includes('Gateway Timeout') || error.message.includes('timeout')) {
addLogMessage('Gateway timeout detected. Retrying in 5 seconds...', 'warning');
setTimeout(checkProgress, 5000); // Retry after 5 seconds
setTimeout(checkProgress, 5000);
} else {
addLogMessage('Network error: ' + error.message, 'error');
isComplete = true;
@@ -261,33 +252,26 @@ function checkProgress() {
}
function updateStepProgress(message, currentStep, totalSteps) {
// Extract step number from message (handle both /3 and /4 formats)
const stepMatch = message.match(/Step (\d+)\/(\d+)/);
if (stepMatch) {
const stepNumber = parseInt(stepMatch[1]);
const totalSteps = parseInt(stepMatch[2]);
// Check if this step is completed
const isCompleted = message.toLowerCase().includes('completed');
if (isCompleted) {
// Mark all steps up to and including this one as completed
for (let i = 1; i <= stepNumber; i++) {
updateStepStatus(i, 'completed');
}
// Mark next step as in progress if not the last step
if (stepNumber < totalSteps) {
updateStepStatus(stepNumber + 1, 'in_progress');
}
} else {
// Step is in progress
// Mark previous steps as completed
for (let i = 1; i < stepNumber; i++) {
updateStepStatus(i, 'completed');
}
// Mark current step as in progress
updateStepStatus(stepNumber, 'in_progress');
}
}
@@ -296,27 +280,23 @@ function updateStepProgress(message, currentStep, totalSteps) {
function updateStepStatus(stepNumber, status) {
const stepElement = document.getElementById(`step-${stepNumber}-status`);
const stepItem = stepElement.closest('.step-item');
if (status === 'completed') {
stepElement.textContent = 'Completed';
stepElement.className = 'text-xs text-green-600';
stepItem.className = 'step-item bg-green-100 rounded-lg p-2 text-center';
stepElement.className = 'text-xs text-green-600 dark:text-green-400';
stepItem.className = 'step-item bg-green-100 dark:bg-green-500/10 rounded-lg p-2 text-center';
} else if (status === 'in_progress') {
stepElement.textContent = 'In Progress';
stepElement.className = 'text-xs text-blue-600';
stepItem.className = 'step-item bg-blue-100 rounded-lg p-2 text-center';
stepElement.className = 'text-xs text-blue-600 dark:text-blue-400';
stepItem.className = 'step-item bg-blue-100 dark:bg-blue-500/10 rounded-lg p-2 text-center';
} else if (status === 'failed') {
stepElement.textContent = 'Failed';
stepElement.className = 'text-xs text-red-600';
stepItem.className = 'step-item bg-red-100 rounded-lg p-2 text-center';
stepElement.className = 'text-xs text-red-600 dark:text-red-400';
stepItem.className = 'step-item bg-red-100 dark:bg-red-500/10 rounded-lg p-2 text-center';
}
}
// Start checking progress
checkProgress();
</script>
<?php
$content = ob_get_clean();
include __DIR__ . '/../layout/base.php';
?>
{% endblock %}

View File

@@ -1,894 +0,0 @@
<?php
$title = 'TLD Registry';
$pageTitle = 'TLD Registry';
$pageDescription = 'Manage Top-Level Domain registry information';
$pageIcon = 'fas fa-database';
ob_start();
// Helper function to generate sort URL
function sortUrl($column, $currentSort, $currentOrder) {
$newOrder = ($currentSort === $column && $currentOrder === 'asc') ? 'desc' : 'asc';
$params = $_GET;
$params['sort'] = $column;
$params['order'] = $newOrder;
return '/tld-registry?' . http_build_query($params);
}
// Helper function for sort icon
function sortIcon($column, $currentSort, $currentOrder) {
if ($currentSort !== $column) {
return '<i class="fas fa-sort text-gray-400 ml-1 text-xs"></i>';
}
$icon = $currentOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down';
return '<i class="fas ' . $icon . ' text-primary ml-1 text-xs"></i>';
}
// Get current filters
$currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'];
?>
<!-- Action Buttons -->
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<div class="mb-4 flex flex-wrap gap-2 justify-end">
<!-- IANA Dropdown -->
<div class="relative" id="ianaDropdownWrapper">
<button onclick="document.getElementById('ianaDropdownMenu').classList.toggle('hidden')" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm rounded-lg hover:bg-indigo-700 transition-colors font-medium">
<i class="fas fa-globe mr-2"></i>
IANA
<i class="fas fa-chevron-down ml-2 text-xs"></i>
</button>
<div id="ianaDropdownMenu" class="hidden absolute right-0 mt-1 w-56 bg-white rounded-lg shadow-lg border border-gray-200 z-30 overflow-hidden">
<form method="POST" action="/tld-registry/start-progressive-import">
<?= csrf_field() ?>
<input type="hidden" name="import_type" value="complete_workflow">
<button type="submit" class="w-full flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors" title="Complete TLD import workflow: TLD List → RDAP → WHOIS → Registry URLs">
<i class="fas fa-rocket text-indigo-600 mr-2.5"></i>
Import TLDs from IANA
</button>
</form>
<form method="POST" action="/tld-registry/start-progressive-import">
<?= csrf_field() ?>
<input type="hidden" name="import_type" value="check_updates">
<button type="submit" <?= $tldStats['total'] == 0 ? 'disabled' : '' ?> class="w-full flex items-center px-4 py-2.5 text-sm <?= $tldStats['total'] == 0 ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-50' ?> transition-colors border-t border-gray-100" title="<?= $tldStats['total'] == 0 ? 'Import TLDs first' : 'Check for IANA updates' ?>">
<i class="fas fa-sync-alt text-blue-600 mr-2.5"></i>
Check for Updates
</button>
</form>
<a href="/tld-registry/import-logs" class="flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors border-t border-gray-100">
<i class="fas fa-history text-gray-500 mr-2.5"></i>
IANA Import Logs
</a>
</div>
</div>
<!-- Export Dropdown -->
<div class="relative" id="tldExportDropdownWrapper">
<button onclick="document.getElementById('tldExportDropdownMenu').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="tldExportDropdownMenu" class="hidden absolute right-0 mt-1 w-44 bg-white rounded-lg shadow-lg border border-gray-200 z-30 overflow-hidden">
<a href="/tld-registry/export?format=csv" class="flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors">
<i class="fas fa-file-csv text-green-600 mr-2.5"></i>
Export as CSV
</a>
<a href="/tld-registry/export?format=json" class="flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors border-t border-gray-100">
<i class="fas fa-file-code text-blue-600 mr-2.5"></i>
Export as JSON
</a>
</div>
</div>
<!-- Import Button -->
<button onclick="document.getElementById('tldImportModal').classList.remove('hidden')" 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-upload mr-2"></i>
Import
</button>
<!-- Create Button -->
<button onclick="openCreateTldModal()" 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-plus mr-2"></i>
Create TLD
</button>
</div>
<?php else: ?>
<div class="mb-4 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div class="flex items-center">
<i class="fas fa-info-circle text-yellow-600 mr-2"></i>
<p class="text-sm text-yellow-800">
View-only mode. Contact admin to import or modify TLD data.
</p>
</div>
</div>
<?php endif; ?>
<!-- Statistics Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<!-- Total TLDs Card -->
<div class="bg-white rounded-lg border border-gray-200 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Total TLDs</p>
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $tldStats['total'] ?? 0 ?></p>
</div>
<div class="w-12 h-12 bg-blue-50 rounded-lg flex items-center justify-center">
<i class="fas fa-globe text-blue-600 text-lg"></i>
</div>
</div>
</div>
<!-- Active TLDs Card -->
<div class="bg-white rounded-lg border border-gray-200 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Active</p>
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $tldStats['active'] ?? 0 ?></p>
</div>
<div class="w-12 h-12 bg-green-50 rounded-lg flex items-center justify-center">
<i class="fas fa-check-circle text-green-600 text-lg"></i>
</div>
</div>
</div>
<!-- With RDAP Card -->
<div class="bg-white rounded-lg border border-gray-200 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">With RDAP</p>
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $tldStats['with_rdap'] ?? 0 ?></p>
</div>
<div class="w-12 h-12 bg-indigo-50 rounded-lg flex items-center justify-center">
<i class="fas fa-database text-indigo-600 text-lg"></i>
</div>
</div>
</div>
<!-- With WHOIS Card -->
<div class="bg-white rounded-lg border border-gray-200 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">With WHOIS</p>
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $tldStats['with_whois'] ?? 0 ?></p>
</div>
<div class="w-12 h-12 bg-orange-50 rounded-lg flex items-center justify-center">
<i class="fas fa-server text-orange-600 text-lg"></i>
</div>
</div>
</div>
</div>
<!-- Search and Filters -->
<div class="bg-white rounded-lg border border-gray-200 p-5 mb-4">
<form method="GET" action="/tld-registry" id="filter-form">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Search -->
<div>
<label class="block text-xs font-medium text-gray-700 mb-1.5">Search TLDs</label>
<div class="relative">
<input type="text" name="search" id="tldSearch" value="<?= htmlspecialchars($currentFilters['search']) ?>" placeholder="Search TLDs..." class="w-full pl-9 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm">
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs"></i>
</div>
</div>
<!-- Status Filter -->
<div>
<label class="block text-xs font-medium text-gray-700 mb-1.5">Status</label>
<select name="status" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm">
<option value="">All Status</option>
<option value="active" <?= ($_GET['status'] ?? '') === 'active' ? 'selected' : '' ?>>Active</option>
<option value="inactive" <?= ($_GET['status'] ?? '') === 'inactive' ? 'selected' : '' ?>>Inactive</option>
</select>
</div>
<!-- Data Type Filter -->
<div>
<label class="block text-xs font-medium text-gray-700 mb-1.5">Data Type</label>
<select name="data_type" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm">
<option value="">All Types</option>
<option value="with_rdap" <?= ($_GET['data_type'] ?? '') === 'with_rdap' ? 'selected' : '' ?>>With RDAP</option>
<option value="with_whois" <?= ($_GET['data_type'] ?? '') === 'with_whois' ? 'selected' : '' ?>>With WHOIS</option>
<option value="with_registry" <?= ($_GET['data_type'] ?? '') === 'with_registry' ? 'selected' : '' ?>>With Registry URL</option>
<option value="missing_data" <?= ($_GET['data_type'] ?? '') === 'missing_data' ? 'selected' : '' ?>>Missing Data</option>
</select>
</div>
<!-- Actions -->
<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="/tld-registry" class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium">
<i class="fas fa-times mr-2"></i>
Clear
</a>
</div>
</div>
<input type="hidden" name="sort" value="<?= htmlspecialchars($currentFilters['sort']) ?>">
<input type="hidden" name="order" value="<?= htmlspecialchars($currentFilters['order']) ?>">
</form>
</div>
<!-- Pagination Info & Per Page Selector -->
<div class="mb-4 flex justify-between items-center">
<div class="text-sm text-gray-600">
Showing <span class="font-semibold text-gray-900"><?= $pagination['showing_from'] ?></span> to
<span class="font-semibold text-gray-900"><?= $pagination['showing_to'] ?></span> of
<span class="font-semibold text-gray-900"><?= $pagination['total'] ?></span> TLD(s)
</div>
<form method="GET" action="/tld-registry" class="flex items-center gap-2">
<!-- Preserve current filters -->
<input type="hidden" name="search" value="<?= htmlspecialchars($currentFilters['search']) ?>">
<input type="hidden" name="sort" value="<?= htmlspecialchars($currentFilters['sort']) ?>">
<input type="hidden" name="order" value="<?= htmlspecialchars($currentFilters['order']) ?>">
<label for="per_page" class="text-sm text-gray-600">Show:</label>
<select name="per_page" id="per_page" onchange="this.form.submit()" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary focus:border-primary">
<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>
<!-- TLD Registry Table -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<!-- Bulk Actions Bar (shown when TLDs are selected) -->
<div id="bulk-actions" class="hidden px-6 py-3 bg-blue-50 border-b border-blue-200 flex items-center justify-between">
<div class="flex items-center gap-4">
<span id="selected-count" class="text-sm font-medium text-gray-700"></span>
<div class="flex items-center gap-3 flex-wrap">
<div class="flex items-center gap-2">
<form method="POST" action="/tld-registry/bulk-delete" id="bulk-delete-form" class="inline">
<?= csrf_field() ?>
<button type="button" onclick="confirmBulkDelete()" 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>
</form>
</div>
</div>
</div>
<button type="button" onclick="clearSelection()" class="inline-flex items-center text-sm font-medium text-gray-600 hover:text-gray-900 hover:bg-blue-100 px-3 py-1.5 rounded-lg transition-colors">
<i class="fas fa-times mr-1.5"></i> Clear Selection
</button>
</div>
<?php endif; ?>
<?php if (!empty($tlds)): ?>
<!-- Table View (Desktop) -->
<div class="hidden lg:block overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<input type="checkbox" id="select-all" class="rounded border-gray-300 text-primary focus:ring-primary" onchange="toggleAllCheckboxes(this)">
</th>
<?php endif; ?>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<a href="<?= sortUrl('tld', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
TLD <?= sortIcon('tld', $currentFilters['sort'], $currentFilters['order']) ?>
</a>
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<a href="<?= sortUrl('rdap_servers', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
RDAP Servers <?= sortIcon('rdap_servers', $currentFilters['sort'], $currentFilters['order']) ?>
</a>
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<a href="<?= sortUrl('whois_server', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
WHOIS Server <?= sortIcon('whois_server', $currentFilters['sort'], $currentFilters['order']) ?>
</a>
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<a href="<?= sortUrl('updated_at', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
Last Updated <?= sortIcon('updated_at', $currentFilters['sort'], $currentFilters['order']) ?>
</a>
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<a href="<?= sortUrl('is_active', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
Status <?= sortIcon('is_active', $currentFilters['sort'], $currentFilters['order']) ?>
</a>
</th>
<th class="px-6 py-3 text-right text-xs font-semibold text-gray-600 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<?php foreach ($tlds as $tld): ?>
<tr class="hover:bg-gray-50 transition-colors duration-150">
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<td class="px-6 py-4 whitespace-nowrap">
<input type="checkbox" name="tld_ids[]" value="<?= $tld['id'] ?>" class="tld-checkbox rounded border-gray-300 text-primary focus:ring-primary">
</td>
<?php endif; ?>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center">
<i class="fas fa-globe text-primary"></i>
</div>
<div class="ml-4">
<div class="text-sm font-semibold text-gray-900"><?= htmlspecialchars($tld['tld']) ?></div>
<?php if ($tld['registry_url']): ?>
<div class="text-sm text-gray-500">
<a href="<?= htmlspecialchars($tld['registry_url']) ?>" target="_blank" class="text-primary hover:text-primary-dark">
<i class="fas fa-external-link-alt mr-1"></i>
Registry
</a>
</div>
<?php endif; ?>
</div>
</div>
</td>
<td class="px-6 py-4">
<?php if ($tld['rdap_servers']): ?>
<?php
$rdapServers = json_decode($tld['rdap_servers'], true);
if (is_array($rdapServers) && !empty($rdapServers)):
?>
<div class="text-sm text-gray-900">
<?php foreach (array_slice($rdapServers, 0, 2) as $server): ?>
<div class="font-mono text-xs bg-gray-50 px-2 py-1 rounded mb-1"><?= htmlspecialchars($server) ?></div>
<?php endforeach; ?>
<?php if (count($rdapServers) > 2): ?>
<div class="text-xs text-gray-500">+<?= count($rdapServers) - 2 ?> more</div>
<?php endif; ?>
</div>
<?php else: ?>
<span class="text-sm text-gray-400">None</span>
<?php endif; ?>
<?php else: ?>
<span class="text-sm text-gray-400">None</span>
<?php endif; ?>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<?php if ($tld['whois_server']): ?>
<div class="text-sm font-mono text-gray-900 bg-gray-50 px-2 py-1 rounded"><?= htmlspecialchars($tld['whois_server']) ?></div>
<?php else: ?>
<span class="text-sm text-gray-400">None</span>
<?php endif; ?>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<?php if ($tld['updated_at']): ?>
<div class="flex items-center">
<i class="far fa-clock mr-2"></i>
<?= date('M d, H:i', strtotime($tld['updated_at'])) ?>
</div>
<?php else: ?>
<span class="text-gray-400">Never</span>
<?php 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 <?= $tld['is_active'] ? 'bg-green-100 text-green-700 border-green-200' : 'bg-gray-100 text-gray-700 border-gray-200' ?>">
<i class="fas <?= $tld['is_active'] ? 'fa-check-circle' : 'fa-pause-circle' ?> mr-1"></i>
<?= $tld['is_active'] ? 'Active' : 'Inactive' ?>
</span>
</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="/tld-registry/<?= $tld['id'] ?>" class="text-blue-600 hover:text-blue-800" title="View">
<i class="fas fa-eye"></i>
</a>
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<a href="/tld-registry/<?= $tld['id'] ?>/refresh" class="text-green-600 hover:text-green-800" title="Refresh" onclick="return confirm('Refresh TLD data from IANA?')">
<i class="fas fa-sync-alt"></i>
</a>
<a href="/tld-registry/<?= $tld['id'] ?>/toggle-active" class="text-orange-600 hover:text-orange-800" title="Toggle Status" onclick="return confirm('Toggle TLD status?')">
<i class="fas fa-power-off"></i>
</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Card View (Mobile) -->
<div class="lg:hidden divide-y divide-gray-200">
<?php foreach ($tlds as $tld): ?>
<div class="p-6 hover:bg-gray-50 transition-colors duration-150">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<div class="w-10 h-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-globe text-primary"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900"><?= htmlspecialchars($tld['tld']) ?></h3>
<?php if ($tld['registry_url']): ?>
<a href="<?= htmlspecialchars($tld['registry_url']) ?>" target="_blank" class="text-xs text-primary hover:text-primary-dark">
<i class="fas fa-external-link-alt mr-1"></i>
Registry
</a>
<?php endif; ?>
</div>
</div>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold <?= $tld['is_active'] ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700' ?>">
<?= $tld['is_active'] ? 'Active' : 'Inactive' ?>
</span>
</div>
<div class="space-y-2 text-sm">
<?php if ($tld['rdap_servers']): ?>
<?php
$rdapServers = json_decode($tld['rdap_servers'], true);
if (is_array($rdapServers) && !empty($rdapServers)):
?>
<div class="flex items-start">
<i class="fas fa-database text-gray-400 mr-2 w-4 mt-0.5"></i>
<div class="flex-1">
<div class="font-mono text-xs bg-gray-50 px-2 py-1 rounded mb-1"><?= htmlspecialchars($rdapServers[0]) ?></div>
<?php if (count($rdapServers) > 1): ?>
<div class="text-xs text-gray-500">+<?= count($rdapServers) - 1 ?> more RDAP server(s)</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if ($tld['whois_server']): ?>
<div class="flex items-center">
<i class="fas fa-server text-gray-400 mr-2 w-4"></i>
<span class="font-mono text-xs bg-gray-50 px-2 py-1 rounded"><?= htmlspecialchars($tld['whois_server']) ?></span>
</div>
<?php endif; ?>
<div class="flex items-center">
<i class="far fa-clock text-gray-400 mr-2 w-4"></i>
<span class="text-gray-500"><?= $tld['updated_at'] ? date('M d, H:i', strtotime($tld['updated_at'])) : 'Never updated' ?></span>
</div>
</div>
<div class="flex space-x-2 mt-3">
<a href="/tld-registry/<?= $tld['id'] ?>" class="<?= (isset($_SESSION['role']) && $_SESSION['role'] === 'admin') ? 'flex-1' : 'w-full' ?> px-3 py-1.5 bg-blue-50 text-blue-600 rounded text-center text-sm hover:bg-blue-100 transition-colors">
<i class="fas fa-eye mr-1"></i> View
</a>
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<a href="/tld-registry/<?= $tld['id'] ?>/refresh" class="flex-1 px-3 py-1.5 bg-green-50 text-green-600 rounded text-center text-sm hover:bg-green-100 transition-colors" onclick="return confirm('Refresh TLD data?')">
<i class="fas fa-sync-alt mr-1"></i> Refresh
</a>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="text-center py-12 px-6">
<div class="mb-4">
<i class="fas fa-globe text-gray-300 text-6xl"></i>
</div>
<h3 class="text-lg font-semibold text-gray-700 mb-1">No TLDs Found</h3>
<p class="text-sm text-gray-500 mb-4">
<?php if (!empty($currentFilters['search'])): ?>
No TLDs match your search criteria.
<?php else: ?>
Start by importing the TLD list from IANA.
<?php endif; ?>
</p>
<?php if (empty($currentFilters['search'])): ?>
<form method="POST" action="/tld-registry/start-progressive-import" class="inline">
<?= csrf_field() ?>
<input type="hidden" name="import_type" value="complete_workflow">
<button type="submit" class="inline-flex items-center px-5 py-2.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors font-medium">
<i class="fas fa-rocket mr-2"></i>
Import TLDs
</button>
</form>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<!-- Pagination Controls -->
<?php if ($pagination['total_pages'] > 1): ?>
<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">
Page <span class="font-semibold text-gray-900"><?= $pagination['current_page'] ?></span> of
<span class="font-semibold text-gray-900"><?= $pagination['total_pages'] ?></span>
</div>
<!-- Pagination Buttons -->
<div class="flex items-center gap-1">
<?php
$currentPage = $pagination['current_page'];
$totalPages = $pagination['total_pages'];
// Helper function to build pagination URL
function paginationUrl($page, $filters, $perPage) {
$params = $filters;
$params['page'] = $page;
$params['per_page'] = $perPage;
return '/tld-registry?' . http_build_query($params);
}
?>
<!-- First Page -->
<?php if ($currentPage > 1): ?>
<a href="<?= paginationUrl(1, $currentFilters, $pagination['per_page']) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<i class="fas fa-angle-double-left"></i>
</a>
<?php endif; ?>
<!-- Previous Page -->
<?php if ($currentPage > 1): ?>
<a href="<?= paginationUrl($currentPage - 1, $currentFilters, $pagination['per_page']) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<i class="fas fa-angle-left"></i> Previous
</a>
<?php endif; ?>
<!-- Page Numbers -->
<?php
$range = 2; // Show 2 pages on each side of current page
$start = max(1, $currentPage - $range);
$end = min($totalPages, $currentPage + $range);
// Show first page + ellipsis if needed
if ($start > 1) {
echo '<a href="' . paginationUrl(1, $currentFilters, $pagination['per_page']) . '" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">1</a>';
if ($start > 2) {
echo '<span class="px-2 text-gray-500">...</span>';
}
}
// Page numbers
for ($i = $start; $i <= $end; $i++) {
if ($i == $currentPage) {
echo '<span class="px-3 py-2 text-sm bg-primary text-white rounded-lg font-semibold">' . $i . '</span>';
} else {
echo '<a href="' . paginationUrl($i, $currentFilters, $pagination['per_page']) . '" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">' . $i . '</a>';
}
}
// Show last page + ellipsis if needed
if ($end < $totalPages) {
if ($end < $totalPages - 1) {
echo '<span class="px-2 text-gray-500">...</span>';
}
echo '<a href="' . paginationUrl($totalPages, $currentFilters, $pagination['per_page']) . '" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">' . $totalPages . '</a>';
}
?>
<!-- Next Page -->
<?php if ($currentPage < $totalPages): ?>
<a href="<?= paginationUrl($currentPage + 1, $currentFilters, $pagination['per_page']) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
Next <i class="fas fa-angle-right"></i>
</a>
<?php endif; ?>
<!-- Last Page -->
<?php if ($currentPage < $totalPages): ?>
<a href="<?= paginationUrl($totalPages, $currentFilters, $pagination['per_page']) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<i class="fas fa-angle-double-right"></i>
</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<!-- Create TLD Modal -->
<div id="createTldModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
<form method="POST" action="/tld-registry/create">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Create New TLD</h3>
</div>
<div class="px-6 py-4 space-y-4">
<div>
<label for="create_tld" class="block text-sm font-medium text-gray-700 mb-1">TLD Name</label>
<input type="text" id="create_tld" name="tld" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g., .com, .xyz, .co.uk">
<p class="text-xs text-gray-500 mt-1">The dot prefix will be added automatically. Multi-level TLDs supported (e.g., co.uk, com.au)</p>
</div>
<div>
<label for="create_whois_server" class="block text-sm font-medium text-gray-700 mb-1">WHOIS Server (Optional)</label>
<input type="text" id="create_whois_server" name="whois_server"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g., whois.verisign-grs.com">
</div>
<div>
<label for="create_rdap_servers" class="block text-sm font-medium text-gray-700 mb-1">RDAP Servers (Optional)</label>
<textarea id="create_rdap_servers" name="rdap_servers" rows="2"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g., https://rdap.verisign.com/com/v1/"></textarea>
<p class="text-xs text-gray-500 mt-1">One URL per line or comma-separated</p>
</div>
<div>
<label for="create_registry_url" class="block text-sm font-medium text-gray-700 mb-1">Registry URL (Optional)</label>
<input type="url" id="create_registry_url" name="registry_url"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g., https://www.verisign.com">
</div>
</div>
<div class="px-6 py-4 bg-gray-50 flex justify-end space-x-3">
<button type="button" onclick="closeCreateTldModal()"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Cancel
</button>
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700">
Create TLD
</button>
</div>
<input type="hidden" name="csrf_token" value="<?= csrf_token() ?>">
</form>
</div>
</div>
</div>
<!-- Import TLD Modal -->
<div id="tldImportModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md">
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900">
<i class="fas fa-upload text-primary mr-2"></i>Import TLDs
</h3>
<button onclick="document.getElementById('tldImportModal').classList.add('hidden')" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times"></i>
</button>
</div>
<form method="POST" action="/tld-registry/import" enctype="multipart/form-data" id="tldImportForm">
<?= csrf_field() ?>
<div class="p-6 space-y-4">
<!-- Drag & Drop Zone -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Select File</label>
<div id="tldDropzone" class="relative border-2 border-dashed border-gray-300 rounded-lg p-6 text-center cursor-pointer transition-all hover:border-primary hover:bg-gray-50">
<input type="file" name="import_file" accept=".csv,.json" required id="tldFileInput"
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
<div id="tldDropzoneContent">
<i class="fas fa-cloud-upload-alt text-3xl text-gray-400 mb-2"></i>
<p class="text-sm text-gray-600 font-medium">Drag & drop your file here</p>
<p class="text-xs text-gray-400 my-1">or</p>
<span class="inline-flex items-center px-3 py-1.5 bg-primary text-white text-xs rounded-lg font-medium">
<i class="fas fa-folder-open mr-1.5"></i>Browse Files
</span>
<p class="mt-2.5 text-xs text-gray-400">CSV, JSON &middot; Max <?= \App\Helpers\ViewHelper::getMaxUploadSize() ?></p>
</div>
<div id="tldDropzoneFile" class="hidden">
<i class="fas fa-file-alt text-2xl text-primary mb-1.5"></i>
<p class="text-sm font-medium text-gray-700" id="tldFileName"></p>
<p class="text-xs text-gray-400" id="tldFileSize"></p>
<button type="button" id="tldFileRemove" class="mt-1.5 text-xs text-red-500 hover:text-red-700 font-medium">
<i class="fas fa-trash-alt mr-1"></i>Remove
</button>
</div>
</div>
</div>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p class="text-xs text-gray-700 font-medium mb-1"><i class="fas fa-info-circle text-blue-500 mr-1"></i> Expected Format</p>
<p class="text-xs text-gray-600">CSV columns: <code class="bg-white px-1 rounded">tld, whois_server, rdap_servers, registry_url, is_active</code></p>
<p class="text-xs text-gray-600 mt-0.5">JSON: array of objects with same fields</p>
<p class="text-xs text-gray-500 mt-1">Existing TLDs will be updated. New TLDs will be created as active.</p>
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50 flex justify-end gap-2 rounded-b-lg">
<button type="button" onclick="document.getElementById('tldImportModal').classList.add('hidden')" class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors">
Cancel
</button>
<button type="submit" id="tldImportBtn" class="px-4 py-2 bg-primary text-white rounded-lg text-sm font-medium hover:bg-primary-dark transition-colors">
<i class="fas fa-upload mr-1.5"></i>Import TLDs
</button>
</div>
</form>
</div>
</div>
<script>
function toggleAllCheckboxes(selectAllCheckbox) {
const checkboxes = document.querySelectorAll('.tld-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
updateSelectedCount();
}
function updateSelectedCount() {
const checkboxes = document.querySelectorAll('.tld-checkbox:checked');
const count = checkboxes.length;
const bulkActions = document.getElementById('bulk-actions');
const selectedCount = document.getElementById('selected-count');
const selectAllCheckbox = document.getElementById('select-all');
if (count > 0) {
bulkActions.classList.remove('hidden');
selectedCount.textContent = count + ' TLD(s) selected';
} else {
bulkActions.classList.add('hidden');
}
// Update select all checkbox state
const allCheckboxes = document.querySelectorAll('.tld-checkbox');
if (selectAllCheckbox) {
if (count === 0) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = false;
} else if (count === allCheckboxes.length) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = true;
} else {
selectAllCheckbox.indeterminate = true;
}
}
}
function clearSelection() {
const checkboxes = document.querySelectorAll('.tld-checkbox');
checkboxes.forEach(cb => {
cb.checked = false;
});
const selectAllCheckbox = document.getElementById('select-all');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
}
updateSelectedCount();
}
function confirmBulkDelete() {
const checkboxes = document.querySelectorAll('.tld-checkbox:checked');
if (checkboxes.length === 0) {
alert('Please select TLDs to delete');
return;
}
if (confirm(`Are you sure you want to delete ${checkboxes.length} selected TLD(s)? This action cannot be undone.`)) {
// Add selected checkboxes to form
const form = document.getElementById('bulk-delete-form');
checkboxes.forEach(checkbox => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'tld_ids[]';
input.value = checkbox.value;
form.appendChild(input);
});
form.submit();
}
}
// Add event listeners to checkboxes
document.addEventListener('DOMContentLoaded', function() {
const checkboxes = document.querySelectorAll('.tld-checkbox');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', updateSelectedCount);
});
updateSelectedCount();
});
// Create TLD Modal
function openCreateTldModal() {
document.getElementById('createTldModal').classList.remove('hidden');
document.getElementById('create_tld').focus();
}
function closeCreateTldModal() {
document.getElementById('createTldModal').classList.add('hidden');
document.querySelector('#createTldModal form').reset();
}
document.getElementById('createTldModal').addEventListener('click', function(e) {
if (e.target === this) {
closeCreateTldModal();
}
});
// Import Modal
document.getElementById('tldImportModal').addEventListener('click', function(e) {
if (e.target === this) {
document.getElementById('tldImportModal').classList.add('hidden');
}
});
// Close dropdowns when clicking outside
document.addEventListener('click', function(e) {
const exportWrapper = document.getElementById('tldExportDropdownWrapper');
if (exportWrapper && !exportWrapper.contains(e.target)) {
document.getElementById('tldExportDropdownMenu').classList.add('hidden');
}
const ianaWrapper = document.getElementById('ianaDropdownWrapper');
if (ianaWrapper && !ianaWrapper.contains(e.target)) {
document.getElementById('ianaDropdownMenu').classList.add('hidden');
}
});
// Close modals on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeCreateTldModal();
document.getElementById('tldImportModal').classList.add('hidden');
}
});
// Import drag-and-drop & loading
(function() {
const dropzone = document.getElementById('tldDropzone');
const fileInput = document.getElementById('tldFileInput');
const content = document.getElementById('tldDropzoneContent');
const fileInfo = document.getElementById('tldDropzoneFile');
const fileName = document.getElementById('tldFileName');
const fileSize = document.getElementById('tldFileSize');
const removeBtn = document.getElementById('tldFileRemove');
const form = document.getElementById('tldImportForm');
const submitBtn = document.getElementById('tldImportBtn');
if (!dropzone) return;
function formatSize(bytes) {
if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB';
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB';
return bytes + ' B';
}
function showFile(file) {
fileName.textContent = file.name;
fileSize.textContent = formatSize(file.size);
content.classList.add('hidden');
fileInfo.classList.remove('hidden');
dropzone.classList.remove('border-gray-300');
dropzone.classList.add('border-primary', 'bg-primary/5');
}
function resetDropzone() {
fileInput.value = '';
content.classList.remove('hidden');
fileInfo.classList.add('hidden');
dropzone.classList.add('border-gray-300');
dropzone.classList.remove('border-primary', 'bg-primary/5');
}
fileInput.addEventListener('change', function() {
if (this.files.length) showFile(this.files[0]);
});
removeBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
resetDropzone();
});
['dragenter', 'dragover'].forEach(evt => {
dropzone.addEventListener(evt, function(e) {
e.preventDefault();
dropzone.classList.add('border-primary', 'bg-primary/5');
dropzone.classList.remove('border-gray-300');
});
});
['dragleave', 'drop'].forEach(evt => {
dropzone.addEventListener(evt, function(e) {
e.preventDefault();
if (!fileInput.files.length) {
dropzone.classList.remove('border-primary', 'bg-primary/5');
dropzone.classList.add('border-gray-300');
}
});
});
dropzone.addEventListener('drop', function(e) {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length) {
fileInput.files = files;
showFile(files[0]);
}
});
form.addEventListener('submit', function() {
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1.5"></i>Importing...';
});
})();
</script>
<?php
$content = ob_get_clean();
include __DIR__ . '/../layout/base.php';
?>

View File

@@ -0,0 +1,842 @@
{% extends 'layout/base.twig' %}
{% set title = 'TLD Registry' %}
{% set pageTitle = 'TLD Registry' %}
{% set pageDescription = 'Manage Top-Level Domain registry information' %}
{% set pageIcon = 'fas fa-database' %}
{% set currentFilters = filters|default({search: '', sort: 'tld', order: 'asc'}) %}
{% block content %}
{# Action Buttons #}
{% if session.role is defined and session.role == 'admin' %}
<div class="mb-4 flex flex-wrap gap-2 justify-end">
{# IANA Dropdown #}
<div class="relative" id="ianaDropdownWrapper">
<button onclick="document.getElementById('ianaDropdownMenu').classList.toggle('hidden')" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm rounded-lg hover:bg-indigo-700 transition-colors font-medium">
<i class="fas fa-globe mr-2"></i>
IANA
<i class="fas fa-chevron-down ml-2 text-xs"></i>
</button>
<div id="ianaDropdownMenu" class="hidden absolute right-0 mt-1 w-56 bg-white dark:bg-slate-800 rounded-lg shadow-lg border border-gray-200 dark:border-slate-700 z-30 overflow-hidden">
<form method="POST" action="/tld-registry/start-progressive-import">
{{ csrf_field() }}
<input type="hidden" name="import_type" value="complete_workflow">
<button type="submit" class="w-full 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" title="Complete TLD import workflow: TLD List → RDAP → WHOIS → Registry URLs">
<i class="fas fa-rocket text-indigo-600 mr-2.5"></i>
Import TLDs from IANA
</button>
</form>
<form method="POST" action="/tld-registry/start-progressive-import">
{{ csrf_field() }}
<input type="hidden" name="import_type" value="check_updates">
<button type="submit" {{ tldStats.total == 0 ? 'disabled' : '' }} class="w-full flex items-center px-4 py-2.5 text-sm {{ tldStats.total == 0 ? 'text-gray-400 dark:text-slate-500 cursor-not-allowed' : '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" title="{{ tldStats.total == 0 ? 'Import TLDs first' : 'Check for IANA updates' }}">
<i class="fas fa-sync-alt text-blue-600 mr-2.5"></i>
Check for Updates
</button>
</form>
<a href="/tld-registry/import-logs" 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-history text-gray-500 dark:text-slate-400 mr-2.5"></i>
IANA Import Logs
</a>
</div>
</div>
{# Export Dropdown #}
<div class="relative" id="tldExportDropdownWrapper">
<button onclick="document.getElementById('tldExportDropdownMenu').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="tldExportDropdownMenu" 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="/tld-registry/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="/tld-registry/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>
{# Import Button #}
<button onclick="document.getElementById('tldImportModal').classList.remove('hidden')" 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-upload mr-2"></i>
Import
</button>
{# Create Button #}
<button onclick="openCreateTldModal()" 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-plus mr-2"></i>
Create TLD
</button>
</div>
{% else %}
<div class="mb-4 bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-200 dark:border-yellow-500/30 rounded-lg p-4">
<div class="flex items-center">
<i class="fas fa-info-circle text-yellow-600 mr-2"></i>
<p class="text-sm text-yellow-800 dark:text-yellow-400">
View-only mode. Contact admin to import or modify TLD data.
</p>
</div>
</div>
{% endif %}
{# Statistics Cards #}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{# Total TLDs Card #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wide">Total TLDs</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ tldStats.total|default(0) }}</p>
</div>
<div class="w-12 h-12 bg-blue-50 dark:bg-blue-500/10 rounded-lg flex items-center justify-center">
<i class="fas fa-globe text-blue-600 dark:text-blue-400 text-lg"></i>
</div>
</div>
</div>
{# Active TLDs Card #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wide">Active</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ tldStats.active|default(0) }}</p>
</div>
<div class="w-12 h-12 bg-green-50 dark:bg-green-500/10 rounded-lg flex items-center justify-center">
<i class="fas fa-check-circle text-green-600 dark:text-green-400 text-lg"></i>
</div>
</div>
</div>
{# With RDAP Card #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wide">With RDAP</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ tldStats.with_rdap|default(0) }}</p>
</div>
<div class="w-12 h-12 bg-indigo-50 dark:bg-indigo-500/10 rounded-lg flex items-center justify-center">
<i class="fas fa-database text-indigo-600 dark:text-indigo-400 text-lg"></i>
</div>
</div>
</div>
{# With WHOIS Card #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wide">With WHOIS</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ tldStats.with_whois|default(0) }}</p>
</div>
<div class="w-12 h-12 bg-orange-50 dark:bg-orange-500/10 rounded-lg flex items-center justify-center">
<i class="fas fa-server text-orange-600 dark:text-orange-400 text-lg"></i>
</div>
</div>
</div>
</div>
{# Search and Filters #}
<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="/tld-registry" id="filter-form">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{# Search #}
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300 mb-1.5">Search TLDs</label>
<div class="relative">
<input type="text" name="search" id="tldSearch" value="{{ currentFilters.search }}" placeholder="Search TLDs..." 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>
{# Status Filter #}
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300 mb-1.5">Status</label>
<select name="status" 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 Status</option>
<option value="active" {{ currentFilters.status|default('') == 'active' ? 'selected' : '' }}>Active</option>
<option value="inactive" {{ currentFilters.status|default('') == 'inactive' ? 'selected' : '' }}>Inactive</option>
</select>
</div>
{# Data Type Filter #}
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300 mb-1.5">Data Type</label>
<select name="data_type" 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 Types</option>
<option value="with_rdap" {{ currentFilters.data_type|default('') == 'with_rdap' ? 'selected' : '' }}>With RDAP</option>
<option value="with_whois" {{ currentFilters.data_type|default('') == 'with_whois' ? 'selected' : '' }}>With WHOIS</option>
<option value="with_registry" {{ currentFilters.data_type|default('') == 'with_registry' ? 'selected' : '' }}>With Registry URL</option>
<option value="missing_data" {{ currentFilters.data_type|default('') == 'missing_data' ? 'selected' : '' }}>Missing Data</option>
</select>
</div>
{# Actions #}
<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="/tld-registry" 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 mr-2"></i>
Clear
</a>
</div>
</div>
<input type="hidden" name="sort" value="{{ currentFilters.sort }}">
<input type="hidden" name="order" value="{{ currentFilters.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> TLD(s)
</div>
<form method="GET" action="/tld-registry" class="flex items-center gap-2">
<input type="hidden" name="search" value="{{ currentFilters.search }}">
<input type="hidden" name="sort" value="{{ currentFilters.sort }}">
<input type="hidden" name="order" value="{{ currentFilters.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>
{# TLD Registry Table #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
{% if session.role is defined and session.role == 'admin' %}
{# 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">
<form method="POST" action="/tld-registry/bulk-delete" id="bulk-delete-form" class="inline">
{{ csrf_field() }}
<button type="button" onclick="confirmBulkDelete()" 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>
</form>
</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>
{% endif %}
{% if tlds 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>
{% if session.role is defined and session.role == 'admin' %}
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">
<input type="checkbox" id="select-all" class="rounded border-gray-300 dark:border-slate-600 text-primary focus:ring-primary" onchange="toggleAllCheckboxes(this)">
</th>
{% endif %}
<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('tld', currentFilters.sort, currentFilters.order, currentFilters) }}" class="hover:text-primary flex items-center">
TLD {{ sort_icon('tld', currentFilters.sort, currentFilters.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('rdap_servers', currentFilters.sort, currentFilters.order, currentFilters) }}" class="hover:text-primary flex items-center">
RDAP Servers {{ sort_icon('rdap_servers', currentFilters.sort, currentFilters.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('whois_server', currentFilters.sort, currentFilters.order, currentFilters) }}" class="hover:text-primary flex items-center">
WHOIS Server {{ sort_icon('whois_server', currentFilters.sort, currentFilters.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('updated_at', currentFilters.sort, currentFilters.order, currentFilters) }}" class="hover:text-primary flex items-center">
Last Updated {{ sort_icon('updated_at', currentFilters.sort, currentFilters.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('is_active', currentFilters.sort, currentFilters.order, currentFilters) }}" class="hover:text-primary flex items-center">
Status {{ sort_icon('is_active', currentFilters.sort, currentFilters.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 tld in tlds %}
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-150">
{% if session.role is defined and session.role == 'admin' %}
<td class="px-6 py-4 whitespace-nowrap">
<input type="checkbox" name="tld_ids[]" value="{{ tld.id }}" class="tld-checkbox rounded border-gray-300 dark:border-slate-600 text-primary focus:ring-primary">
</td>
{% endif %}
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center">
<i class="fas fa-globe text-primary"></i>
</div>
<div class="ml-4">
<div class="text-sm font-semibold text-gray-900 dark:text-white">{{ tld.tld }}</div>
{% if tld.registry_url %}
<div class="text-sm text-gray-500 dark:text-slate-400">
<a href="{{ tld.registry_url }}" target="_blank" class="text-primary hover:text-primary-dark">
<i class="fas fa-external-link-alt mr-1"></i>
Registry
</a>
</div>
{% endif %}
</div>
</div>
</td>
<td class="px-6 py-4">
{% if tld.rdap_servers %}
{% set rdapServers = tld.rdap_servers|from_json %}
{% if rdapServers is iterable and rdapServers is not empty %}
<div class="text-sm text-gray-900 dark:text-white">
{% for server in rdapServers|slice(0, 2) %}
<div class="font-mono text-xs bg-gray-50 dark:bg-slate-700 px-2 py-1 rounded mb-1 text-gray-900 dark:text-white">{{ server }}</div>
{% endfor %}
{% if rdapServers|length > 2 %}
<div class="text-xs text-gray-500 dark:text-slate-400">+{{ rdapServers|length - 2 }} more</div>
{% endif %}
</div>
{% else %}
<span class="text-sm text-gray-400 dark:text-slate-500">None</span>
{% endif %}
{% else %}
<span class="text-sm text-gray-400 dark:text-slate-500">None</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap">
{% if tld.whois_server %}
<div class="text-sm font-mono text-gray-900 dark:text-white bg-gray-50 dark:bg-slate-700 px-2 py-1 rounded">{{ tld.whois_server }}</div>
{% else %}
<span class="text-sm text-gray-400 dark:text-slate-500">None</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-slate-400">
{% if tld.updated_at %}
<div class="flex items-center">
<i class="far fa-clock mr-2"></i>
{{ tld.updated_at|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">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border {{ tld.is_active ? 'bg-green-100 dark:bg-green-500/10 text-green-700 dark:text-green-400 border-green-200 dark:border-green-500/30' : 'bg-gray-100 dark:bg-slate-700 text-gray-700 dark:text-slate-300 border-gray-200 dark:border-slate-600' }}">
<i class="fas {{ tld.is_active ? 'fa-check-circle' : 'fa-pause-circle' }} mr-1"></i>
{{ tld.is_active ? 'Active' : 'Inactive' }}
</span>
</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="/tld-registry/{{ tld.id }}" class="text-blue-600 hover:text-blue-800" title="View">
<i class="fas fa-eye"></i>
</a>
{% if session.role is defined and session.role == 'admin' %}
<a href="/tld-registry/{{ tld.id }}/refresh" class="text-green-600 hover:text-green-800" title="Refresh" onclick="return confirm('Refresh TLD data from IANA?')">
<i class="fas fa-sync-alt"></i>
</a>
<a href="/tld-registry/{{ tld.id }}/toggle-active" class="text-orange-600 hover:text-orange-800" title="Toggle Status" onclick="return confirm('Toggle TLD status?')">
<i class="fas fa-power-off"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# Card View (Mobile) #}
<div class="lg:hidden divide-y divide-gray-200 dark:divide-slate-700">
{% for tld in tlds %}
<div class="p-6 hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-150">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<div class="w-10 h-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-globe text-primary"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-white">{{ tld.tld }}</h3>
{% if tld.registry_url %}
<a href="{{ tld.registry_url }}" target="_blank" class="text-xs text-primary hover:text-primary-dark">
<i class="fas fa-external-link-alt mr-1"></i>
Registry
</a>
{% endif %}
</div>
</div>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold {{ tld.is_active ? 'bg-green-100 dark:bg-green-500/10 text-green-700 dark:text-green-400' : 'bg-gray-100 dark:bg-slate-700 text-gray-700 dark:text-slate-300' }}">
{{ tld.is_active ? 'Active' : 'Inactive' }}
</span>
</div>
<div class="space-y-2 text-sm">
{% if tld.rdap_servers %}
{% set rdapServers = tld.rdap_servers|from_json %}
{% if rdapServers is iterable and rdapServers is not empty %}
<div class="flex items-start">
<i class="fas fa-database text-gray-400 dark:text-slate-500 mr-2 w-4 mt-0.5"></i>
<div class="flex-1">
<div class="font-mono text-xs bg-gray-50 dark:bg-slate-700 px-2 py-1 rounded mb-1 text-gray-900 dark:text-white">{{ rdapServers[0] }}</div>
{% if rdapServers|length > 1 %}
<div class="text-xs text-gray-500 dark:text-slate-400">+{{ rdapServers|length - 1 }} more RDAP server(s)</div>
{% endif %}
</div>
</div>
{% endif %}
{% endif %}
{% if tld.whois_server %}
<div class="flex items-center">
<i class="fas fa-server text-gray-400 dark:text-slate-500 mr-2 w-4"></i>
<span class="font-mono text-xs bg-gray-50 dark:bg-slate-700 px-2 py-1 rounded text-gray-900 dark:text-white">{{ tld.whois_server }}</span>
</div>
{% endif %}
<div class="flex items-center">
<i class="far fa-clock text-gray-400 dark:text-slate-500 mr-2 w-4"></i>
<span class="text-gray-500 dark:text-slate-400">{{ tld.updated_at ? tld.updated_at|date('M d, H:i') : 'Never updated' }}</span>
</div>
</div>
<div class="flex space-x-2 mt-3">
<a href="/tld-registry/{{ tld.id }}" class="{{ (session.role is defined and session.role == 'admin') ? 'flex-1' : 'w-full' }} px-3 py-1.5 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded text-center text-sm hover:bg-blue-100 dark:hover:bg-blue-500/20 transition-colors">
<i class="fas fa-eye mr-1"></i> View
</a>
{% if session.role is defined and session.role == 'admin' %}
<a href="/tld-registry/{{ tld.id }}/refresh" class="flex-1 px-3 py-1.5 bg-green-50 dark:bg-green-500/10 text-green-600 dark:text-green-400 rounded text-center text-sm hover:bg-green-100 dark:hover:bg-green-500/20 transition-colors" onclick="return confirm('Refresh TLD data?')">
<i class="fas fa-sync-alt mr-1"></i> Refresh
</a>
{% endif %}
</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 TLDs Found</h3>
<p class="text-sm text-gray-500 dark:text-slate-400 mb-4">
{% if currentFilters.search is not empty %}
No TLDs match your search criteria.
{% else %}
Start by importing the TLD list from IANA.
{% endif %}
</p>
{% if currentFilters.search is empty %}
<form method="POST" action="/tld-registry/start-progressive-import" class="inline">
{{ csrf_field() }}
<input type="hidden" name="import_type" value="complete_workflow">
<button type="submit" class="inline-flex items-center px-5 py-2.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors font-medium">
<i class="fas fa-rocket mr-2"></i>
Import TLDs
</button>
</form>
{% endif %}
</div>
{% endif %}
</div>
{# Pagination Controls #}
{% if pagination.total_pages > 1 %}
<div class="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="text-sm text-gray-600 dark:text-slate-400">
Page <span class="font-semibold text-gray-900 dark:text-white">{{ pagination.current_page }}</span> of
<span class="font-semibold text-gray-900 dark:text-white">{{ pagination.total_pages }}</span>
</div>
<div class="flex items-center gap-1">
{% set currentPage = pagination.current_page %}
{% set totalPages = pagination.total_pages %}
{# First Page #}
{% if currentPage > 1 %}
<a href="{{ pagination_url(1, currentFilters, 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, currentFilters, 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 %}
{# Page Numbers #}
{% set range = 2 %}
{% set start = max(1, currentPage - range) %}
{% set end = min(totalPages, currentPage + range) %}
{% if start > 1 %}
<a href="{{ pagination_url(1, currentFilters, 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 %}
{% 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, currentFilters, 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 %}
{% 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, currentFilters, 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, currentFilters, 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, currentFilters, 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 %}
{# Create TLD Modal #}
<div id="createTldModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
<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">
<form method="POST" action="/tld-registry/create">
<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">Create New TLD</h3>
</div>
<div class="px-6 py-4 space-y-4">
<div>
<label for="create_tld" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1">TLD Name</label>
<input type="text" id="create_tld" name="tld" 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"
placeholder="e.g., .com, .xyz, .co.uk">
<p class="text-xs text-gray-500 dark:text-slate-400 mt-1">The dot prefix will be added automatically. Multi-level TLDs supported (e.g., co.uk, com.au)</p>
</div>
<div>
<label for="create_whois_server" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1">WHOIS Server (Optional)</label>
<input type="text" id="create_whois_server" name="whois_server"
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="e.g., whois.verisign-grs.com">
</div>
<div>
<label for="create_rdap_servers" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1">RDAP Servers (Optional)</label>
<textarea id="create_rdap_servers" name="rdap_servers" rows="2"
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="e.g., https://rdap.verisign.com/com/v1/"></textarea>
<p class="text-xs text-gray-500 dark:text-slate-400 mt-1">One URL per line or comma-separated</p>
</div>
<div>
<label for="create_registry_url" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1">Registry URL (Optional)</label>
<input type="url" id="create_registry_url" name="registry_url"
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="e.g., https://www.verisign.com">
</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="closeCreateTldModal()"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-gray-300 dark:border-slate-600 rounded-md hover:bg-gray-50 dark:hover:bg-slate-700">
Cancel
</button>
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700">
Create TLD
</button>
</div>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
</form>
</div>
</div>
</div>
{# Import TLD Modal #}
<div id="tldImportModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-md">
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
<i class="fas fa-upload text-primary mr-2"></i>Import TLDs
</h3>
<button onclick="document.getElementById('tldImportModal').classList.add('hidden')" class="text-gray-400 dark:text-slate-500 hover:text-gray-600 dark:hover:text-slate-300">
<i class="fas fa-times"></i>
</button>
</div>
<form method="POST" action="/tld-registry/import" enctype="multipart/form-data" id="tldImportForm">
{{ csrf_field() }}
<div class="p-6 space-y-4">
{# Drag & Drop Zone #}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">Select File</label>
<div id="tldDropzone" class="relative border-2 border-dashed border-gray-300 dark:border-slate-600 rounded-lg p-6 text-center cursor-pointer transition-all hover:border-primary hover:bg-gray-50 dark:hover:bg-slate-700">
<input type="file" name="import_file" accept=".csv,.json" required id="tldFileInput"
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
<div id="tldDropzoneContent">
<i class="fas fa-cloud-upload-alt text-3xl text-gray-400 dark:text-slate-500 mb-2"></i>
<p class="text-sm text-gray-600 dark:text-slate-400 font-medium">Drag & drop your file here</p>
<p class="text-xs text-gray-400 dark:text-slate-500 my-1">or</p>
<span class="inline-flex items-center px-3 py-1.5 bg-primary text-white text-xs rounded-lg font-medium">
<i class="fas fa-folder-open mr-1.5"></i>Browse Files
</span>
<p class="mt-2.5 text-xs text-gray-400 dark:text-slate-500">CSV, JSON &middot; Max {{ max_upload_size() }}</p>
</div>
<div id="tldDropzoneFile" class="hidden">
<i class="fas fa-file-alt text-2xl text-primary mb-1.5"></i>
<p class="text-sm font-medium text-gray-700 dark:text-slate-300" id="tldFileName"></p>
<p class="text-xs text-gray-400 dark:text-slate-500" id="tldFileSize"></p>
<button type="button" id="tldFileRemove" class="mt-1.5 text-xs text-red-500 hover:text-red-700 font-medium">
<i class="fas fa-trash-alt mr-1"></i>Remove
</button>
</div>
</div>
</div>
<div class="bg-blue-50 dark:bg-blue-500/10 border border-blue-200 dark:border-blue-500/30 rounded-lg p-3">
<p class="text-xs text-gray-700 dark:text-slate-300 font-medium mb-1"><i class="fas fa-info-circle text-blue-500 mr-1"></i> Expected Format</p>
<p class="text-xs text-gray-600 dark:text-slate-400">CSV columns: <code class="bg-white dark:bg-slate-800 px-1 rounded">tld, whois_server, rdap_servers, registry_url, is_active</code></p>
<p class="text-xs text-gray-600 dark:text-slate-400 mt-0.5">JSON: array of objects with same fields</p>
<p class="text-xs text-gray-500 dark:text-slate-400 mt-1">Existing TLDs will be updated. New TLDs will be created as active.</p>
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900 flex justify-end gap-2 rounded-b-lg">
<button type="button" onclick="document.getElementById('tldImportModal').classList.add('hidden')" class="px-4 py-2 border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-slate-300 rounded-lg text-sm font-medium hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors">
Cancel
</button>
<button type="submit" id="tldImportBtn" class="px-4 py-2 bg-primary text-white rounded-lg text-sm font-medium hover:bg-primary-dark transition-colors">
<i class="fas fa-upload mr-1.5"></i>Import TLDs
</button>
</div>
</form>
</div>
</div>
<script>
function toggleAllCheckboxes(selectAllCheckbox) {
const checkboxes = document.querySelectorAll('.tld-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
updateSelectedCount();
}
function updateSelectedCount() {
const checkboxes = document.querySelectorAll('.tld-checkbox:checked');
const count = checkboxes.length;
const bulkActions = document.getElementById('bulk-actions');
const selectedCount = document.getElementById('selected-count');
const selectAllCheckbox = document.getElementById('select-all');
if (count > 0) {
bulkActions.classList.remove('hidden');
selectedCount.textContent = count + ' TLD(s) selected';
} else {
bulkActions.classList.add('hidden');
}
const allCheckboxes = document.querySelectorAll('.tld-checkbox');
if (selectAllCheckbox) {
if (count === 0) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = false;
} else if (count === allCheckboxes.length) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = true;
} else {
selectAllCheckbox.indeterminate = true;
}
}
}
function clearSelection() {
const checkboxes = document.querySelectorAll('.tld-checkbox');
checkboxes.forEach(cb => {
cb.checked = false;
});
const selectAllCheckbox = document.getElementById('select-all');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
}
updateSelectedCount();
}
function confirmBulkDelete() {
const checkboxes = document.querySelectorAll('.tld-checkbox:checked');
if (checkboxes.length === 0) {
alert('Please select TLDs to delete');
return;
}
if (confirm(`Are you sure you want to delete ${checkboxes.length} selected TLD(s)? This action cannot be undone.`)) {
const form = document.getElementById('bulk-delete-form');
checkboxes.forEach(checkbox => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'tld_ids[]';
input.value = checkbox.value;
form.appendChild(input);
});
form.submit();
}
}
document.addEventListener('DOMContentLoaded', function() {
const checkboxes = document.querySelectorAll('.tld-checkbox');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', updateSelectedCount);
});
updateSelectedCount();
});
function openCreateTldModal() {
document.getElementById('createTldModal').classList.remove('hidden');
document.getElementById('create_tld').focus();
}
function closeCreateTldModal() {
document.getElementById('createTldModal').classList.add('hidden');
document.querySelector('#createTldModal form').reset();
}
document.getElementById('createTldModal').addEventListener('click', function(e) {
if (e.target === this) {
closeCreateTldModal();
}
});
document.getElementById('tldImportModal').addEventListener('click', function(e) {
if (e.target === this) {
document.getElementById('tldImportModal').classList.add('hidden');
}
});
document.addEventListener('click', function(e) {
const exportWrapper = document.getElementById('tldExportDropdownWrapper');
if (exportWrapper && !exportWrapper.contains(e.target)) {
document.getElementById('tldExportDropdownMenu').classList.add('hidden');
}
const ianaWrapper = document.getElementById('ianaDropdownWrapper');
if (ianaWrapper && !ianaWrapper.contains(e.target)) {
document.getElementById('ianaDropdownMenu').classList.add('hidden');
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeCreateTldModal();
document.getElementById('tldImportModal').classList.add('hidden');
}
});
(function() {
const dropzone = document.getElementById('tldDropzone');
const fileInput = document.getElementById('tldFileInput');
const content = document.getElementById('tldDropzoneContent');
const fileInfo = document.getElementById('tldDropzoneFile');
const fileName = document.getElementById('tldFileName');
const fileSize = document.getElementById('tldFileSize');
const removeBtn = document.getElementById('tldFileRemove');
const form = document.getElementById('tldImportForm');
const submitBtn = document.getElementById('tldImportBtn');
if (!dropzone) return;
function formatSize(bytes) {
if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB';
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB';
return bytes + ' B';
}
function showFile(file) {
fileName.textContent = file.name;
fileSize.textContent = formatSize(file.size);
content.classList.add('hidden');
fileInfo.classList.remove('hidden');
dropzone.classList.remove('border-gray-300');
dropzone.classList.add('border-primary', 'bg-primary/5');
}
function resetDropzone() {
fileInput.value = '';
content.classList.remove('hidden');
fileInfo.classList.add('hidden');
dropzone.classList.add('border-gray-300');
dropzone.classList.remove('border-primary', 'bg-primary/5');
}
fileInput.addEventListener('change', function() {
if (this.files.length) showFile(this.files[0]);
});
removeBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
resetDropzone();
});
['dragenter', 'dragover'].forEach(evt => {
dropzone.addEventListener(evt, function(e) {
e.preventDefault();
dropzone.classList.add('border-primary', 'bg-primary/5');
dropzone.classList.remove('border-gray-300');
});
});
['dragleave', 'drop'].forEach(evt => {
dropzone.addEventListener(evt, function(e) {
e.preventDefault();
if (!fileInput.files.length) {
dropzone.classList.remove('border-primary', 'bg-primary/5');
dropzone.classList.add('border-gray-300');
}
});
});
dropzone.addEventListener('drop', function(e) {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length) {
fileInput.files = files;
showFile(files[0]);
}
});
form.addEventListener('submit', function() {
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1.5"></i>Importing...';
});
})();
</script>
{% endblock %}

View File

@@ -1,427 +0,0 @@
<?php
$title = 'TLD Details';
$pageTitle = htmlspecialchars($tld['tld']);
$pageDescription = 'TLD registry information and server details';
$pageIcon = 'fas fa-globe';
ob_start();
?>
<!-- Top Action Bar -->
<div class="mb-3 flex flex-wrap gap-2 justify-between items-center">
<div class="flex gap-2">
<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold bg-primary text-white">
<i class="fas fa-globe mr-1.5"></i>
TLD Registry
</span>
<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold <?= $tld['is_active'] ? 'bg-green-100 text-green-700 border border-green-200' : 'bg-gray-100 text-gray-700 border border-gray-200' ?>">
<i class="fas <?= $tld['is_active'] ? 'fa-check-circle' : 'fa-pause-circle' ?> mr-1.5"></i>
<?= $tld['is_active'] ? 'Active' : 'Inactive' ?>
</span>
</div>
<div class="flex gap-2 items-center">
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<a href="/tld-registry/<?= $tld['id'] ?>/refresh" class="inline-flex items-center justify-center px-3 py-2 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 transition-colors font-medium min-w-[80px] h-[32px]" onclick="return confirm('Refresh TLD data from IANA?')">
<i class="fas fa-sync-alt mr-1.5"></i>
Refresh
</a>
<a href="/tld-registry/<?= $tld['id'] ?>/toggle-active" class="inline-flex items-center justify-center px-3 py-2 bg-orange-600 text-white text-xs rounded-lg hover:bg-orange-700 transition-colors font-medium min-w-[80px] h-[32px]" onclick="return confirm('Toggle TLD status?')">
<i class="fas fa-power-off mr-1.5"></i>
Toggle
</a>
<?php endif; ?>
<a href="/tld-registry" class="inline-flex items-center justify-center px-3 py-2 border border-gray-300 text-gray-700 text-xs rounded-lg hover:bg-gray-50 transition-colors font-medium min-w-[80px] h-[32px]">
<i class="fas fa-arrow-left mr-1.5"></i>
Back
</a>
</div>
</div>
<!-- Main 2-Column Layout -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
<!-- LEFT COLUMN -->
<div class="space-y-3">
<!-- TLD Information -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div class="px-4 py-2 border-b border-gray-200 bg-gray-50">
<h3 class="text-xs font-semibold text-gray-700 uppercase tracking-wider flex items-center">
<i class="fas fa-info-circle text-gray-400 mr-2" style="font-size: 10px;"></i>
TLD Information
</h3>
</div>
<div class="p-4">
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-xs">
<div>
<label class="text-gray-500 font-medium block mb-0.5">TLD</label>
<p class="text-gray-900 font-semibold"><?= htmlspecialchars($tld['tld']) ?></p>
</div>
<?php if ($tld['registry_url']): ?>
<div>
<label class="text-gray-500 font-medium block mb-0.5">Registry URL</label>
<a href="<?= htmlspecialchars($tld['registry_url']) ?>" target="_blank" class="text-blue-600 hover:text-blue-800 flex items-center">
<i class="fas fa-external-link-alt mr-1" style="font-size: 9px;"></i>
Visit Registry
</a>
</div>
<?php endif; ?>
<?php if ($tld['registration_date']): ?>
<div>
<label class="text-gray-500 font-medium block mb-0.5">Registration Date</label>
<p class="text-gray-900"><?= date('M j, Y', strtotime($tld['registration_date'])) ?></p>
</div>
<?php endif; ?>
<?php if ($tld['record_last_updated']): ?>
<div>
<label class="text-gray-500 font-medium block mb-0.5">Record Last Updated</label>
<p class="text-gray-900"><?= date('M j, Y', strtotime($tld['record_last_updated'])) ?></p>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- RDAP Servers -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div class="px-4 py-2 border-b border-gray-200 bg-gray-50 flex items-center justify-between">
<h3 class="text-xs font-semibold text-gray-700 uppercase tracking-wider flex items-center">
<i class="fas fa-database text-gray-400 mr-2" style="font-size: 10px;"></i>
RDAP Servers
<?php if ($tld['rdap_servers']): ?>
<?php
$rdapServers = json_decode($tld['rdap_servers'], true);
if (is_array($rdapServers) && !empty($rdapServers)):
?>
(<?= count($rdapServers) ?>)
<?php endif; ?>
<?php endif; ?>
</h3>
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<button onclick="openEditRdapModal()" class="inline-flex items-center px-2 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition-colors font-medium">
<i class="fas fa-edit mr-1" style="font-size: 9px;"></i>
Edit
</button>
<?php endif; ?>
</div>
<div class="p-4">
<?php if ($tld['rdap_servers']): ?>
<?php
$rdapServers = json_decode($tld['rdap_servers'], true);
if (is_array($rdapServers) && !empty($rdapServers)):
?>
<div class="space-y-1.5">
<?php foreach ($rdapServers as $index => $server): ?>
<div class="flex items-center p-2 bg-gray-50 rounded hover:bg-gray-100 transition-colors">
<div class="w-6 h-6 bg-indigo-500 rounded flex items-center justify-center text-white font-bold text-xs mr-2">
<?= $index + 1 ?>
</div>
<p class="font-mono text-xs text-gray-800"><?= htmlspecialchars($server) ?></p>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="text-xs text-gray-400 italic">No RDAP servers configured</p>
<?php endif; ?>
<?php else: ?>
<p class="text-xs text-gray-400 italic">No RDAP servers configured</p>
<?php endif; ?>
</div>
</div>
<!-- WHOIS Server -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div class="px-4 py-2 border-b border-gray-200 bg-gray-50 flex items-center justify-between">
<h3 class="text-xs font-semibold text-gray-700 uppercase tracking-wider flex items-center">
<i class="fas fa-server text-gray-400 mr-2" style="font-size: 10px;"></i>
WHOIS Server
</h3>
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<button onclick="openEditWhoisModal()" class="inline-flex items-center px-2 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition-colors font-medium">
<i class="fas fa-edit mr-1" style="font-size: 9px;"></i>
Edit
</button>
<?php endif; ?>
</div>
<div class="p-4">
<?php if ($tld['whois_server']): ?>
<div class="flex items-center p-2 bg-gray-50 rounded">
<div class="w-6 h-6 bg-orange-500 rounded flex items-center justify-center text-white font-bold text-xs mr-2">
<i class="fas fa-server"></i>
</div>
<p class="font-mono text-xs text-gray-800"><?= htmlspecialchars($tld['whois_server']) ?></p>
</div>
<?php else: ?>
<p class="text-xs text-gray-400 italic">No WHOIS server configured</p>
<?php endif; ?>
</div>
</div>
</div>
<!-- RIGHT COLUMN -->
<div class="space-y-3">
<!-- Import History -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div class="px-4 py-2 border-b border-gray-200 bg-gray-50">
<h3 class="text-xs font-semibold text-gray-700 uppercase tracking-wider flex items-center">
<i class="fas fa-history text-gray-400 mr-2" style="font-size: 10px;"></i>
Import History
</h3>
</div>
<div class="p-4">
<div class="space-y-2">
<div class="flex items-center p-2 bg-blue-50 rounded border border-blue-200">
<div class="w-7 h-7 bg-blue-500 rounded flex items-center justify-center mr-2">
<i class="fas fa-plus text-white text-xs"></i>
</div>
<div>
<p class="text-xs text-gray-600 font-medium">Created</p>
<p class="text-xs font-semibold text-gray-900"><?= date('M j, Y H:i', strtotime($tld['created_at'])) ?></p>
</div>
</div>
<?php if ($tld['updated_at']): ?>
<div class="flex items-center p-2 bg-green-50 rounded border border-green-200">
<div class="w-7 h-7 bg-green-500 rounded flex items-center justify-center mr-2">
<i class="fas fa-sync text-white text-xs"></i>
</div>
<div>
<p class="text-xs text-gray-600 font-medium">Last Updated</p>
<p class="text-xs font-semibold text-gray-900"><?= date('M j, Y H:i', strtotime($tld['updated_at'])) ?></p>
</div>
</div>
<?php endif; ?>
<?php if ($tld['iana_publication_date']): ?>
<div class="flex items-center p-2 bg-indigo-50 rounded border border-indigo-200">
<div class="w-7 h-7 bg-indigo-500 rounded flex items-center justify-center mr-2">
<i class="fas fa-calendar text-white text-xs"></i>
</div>
<div>
<p class="text-xs text-gray-600 font-medium">IANA Publication</p>
<p class="text-xs font-semibold text-gray-900"><?= date('M j, Y H:i', strtotime($tld['iana_publication_date'])) ?></p>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div class="px-4 py-2 border-b border-gray-200 bg-gray-50">
<h3 class="text-xs font-semibold text-gray-700 uppercase tracking-wider flex items-center">
<i class="fas fa-bolt text-gray-400 mr-2" style="font-size: 10px;"></i>
Quick Actions
</h3>
</div>
<div class="p-4 space-y-2">
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<a href="/tld-registry/<?= $tld['id'] ?>/refresh" class="flex items-center p-3 border border-gray-200 hover:border-green-500 hover:bg-green-50 rounded-lg transition-all duration-200 group" onclick="return confirm('Refresh TLD data from IANA?')">
<div class="w-9 h-9 bg-green-50 group-hover:bg-green-500 rounded-lg flex items-center justify-center group-hover:text-white text-green-600 transition-colors duration-200">
<i class="fas fa-sync-alt text-sm"></i>
</div>
<span class="ml-3 text-sm font-medium text-gray-700 group-hover:text-green-700">Refresh from IANA</span>
</a>
<a href="/tld-registry/<?= $tld['id'] ?>/toggle-active" class="flex items-center p-3 border border-gray-200 hover:border-orange-500 hover:bg-orange-50 rounded-lg transition-all duration-200 group" onclick="return confirm('Toggle TLD status?')">
<div class="w-9 h-9 bg-orange-50 group-hover:bg-orange-500 rounded-lg flex items-center justify-center group-hover:text-white text-orange-600 transition-colors duration-200">
<i class="fas fa-power-off text-sm"></i>
</div>
<span class="ml-3 text-sm font-medium text-gray-700 group-hover:text-orange-700">Toggle Status</span>
</a>
<?php endif; ?>
<?php if ($tld['registry_url']): ?>
<a href="<?= htmlspecialchars($tld['registry_url']) ?>" target="_blank" class="flex items-center p-3 border border-gray-200 hover:border-blue-500 hover:bg-blue-50 rounded-lg transition-all duration-200 group">
<div class="w-9 h-9 bg-blue-50 group-hover:bg-blue-500 rounded-lg flex items-center justify-center group-hover:text-white text-blue-600 transition-colors duration-200">
<i class="fas fa-external-link-alt text-sm"></i>
</div>
<span class="ml-3 text-sm font-medium text-gray-700 group-hover:text-blue-700">Visit Registry</span>
</a>
<?php endif; ?>
</div>
</div>
<!-- Raw Data (Collapsible) -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<button onclick="toggleRawData()" class="w-full px-4 py-2 border-b border-gray-200 bg-gray-50 text-left hover:bg-gray-100 transition-colors">
<h3 class="text-xs font-semibold text-gray-700 uppercase tracking-wider flex items-center justify-between">
<span class="flex items-center">
<i class="fas fa-code text-gray-400 mr-2" style="font-size: 10px;"></i>
Raw TLD Data
</span>
<i class="fas fa-chevron-down text-gray-400 text-xs transition-transform" id="raw-data-chevron"></i>
</h3>
</button>
<div id="raw-data" class="hidden p-4 bg-gray-900 max-h-64 overflow-y-auto">
<pre class="text-xs text-green-400 font-mono"><?= htmlspecialchars(json_encode([
'tld' => $tld['tld'],
'rdap_servers' => $tld['rdap_servers'] ? json_decode($tld['rdap_servers'], true) : null,
'whois_server' => $tld['whois_server'],
'registry_url' => $tld['registry_url'],
'registration_date' => $tld['registration_date'],
'record_last_updated' => $tld['record_last_updated'],
'iana_publication_date' => $tld['iana_publication_date'],
'is_active' => $tld['is_active'],
'created_at' => $tld['created_at'],
'updated_at' => $tld['updated_at']
], JSON_PRETTY_PRINT)) ?></pre>
</div>
</div>
</div>
</div>
<!-- Edit WHOIS Server Modal -->
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<div id="editWhoisModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 flex items-center">
<i class="fas fa-server text-orange-600 mr-2"></i>
Edit WHOIS Server
</h3>
<button onclick="closeEditWhoisModal()" class="text-gray-400 hover:text-gray-600 transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
<form method="POST" action="/tld-registry/<?= $tld['id'] ?>/update-whois-server" class="p-6">
<?= csrf_field() ?>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">
WHOIS Server
</label>
<input type="text"
name="whois_server"
id="whois_server_input"
value="<?= htmlspecialchars($tld['whois_server'] ?? '') ?>"
placeholder="whois.example.com"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm">
<p class="mt-1.5 text-xs text-gray-500">
Enter the WHOIS server hostname (e.g., whois.example.com). Leave empty to remove.
</p>
</div>
<div class="flex gap-3">
<button type="submit"
class="flex-1 inline-flex items-center justify-center px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors text-sm">
<i class="fas fa-save mr-2"></i>
Save Changes
</button>
<button type="button"
onclick="closeEditWhoisModal()"
class="flex-1 inline-flex items-center justify-center px-4 py-2.5 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors text-sm">
<i class="fas fa-times mr-2"></i>
Cancel
</button>
</div>
</form>
</div>
</div>
<!-- Edit RDAP Servers Modal -->
<div id="editRdapModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 flex items-center">
<i class="fas fa-database text-indigo-600 mr-2"></i>
Edit RDAP Servers
</h3>
<button onclick="closeEditRdapModal()" class="text-gray-400 hover:text-gray-600 transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
<form method="POST" action="/tld-registry/<?= $tld['id'] ?>/update-rdap-servers" class="p-6">
<?= csrf_field() ?>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">
RDAP Servers
</label>
<textarea name="rdap_servers"
id="rdap_servers_input"
rows="6"
placeholder="https://rdap.example.com/&#10;https://rdap2.example.com/"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm font-mono"><?php
if ($tld['rdap_servers']):
$rdapServers = json_decode($tld['rdap_servers'], true);
if (is_array($rdapServers) && !empty($rdapServers)):
echo htmlspecialchars(implode("\n", $rdapServers));
endif;
endif;
?></textarea>
<p class="mt-1.5 text-xs text-gray-500">
Enter RDAP server URLs (one per line or comma-separated). Must start with http:// or https://. Leave empty to remove all servers.
</p>
</div>
<div class="flex gap-3">
<button type="submit"
class="flex-1 inline-flex items-center justify-center px-4 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg font-medium transition-colors text-sm">
<i class="fas fa-save mr-2"></i>
Save Changes
</button>
<button type="button"
onclick="closeEditRdapModal()"
class="flex-1 inline-flex items-center justify-center px-4 py-2.5 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors text-sm">
<i class="fas fa-times mr-2"></i>
Cancel
</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
<script>
function toggleRawData() {
const dataDiv = document.getElementById('raw-data');
const chevron = document.getElementById('raw-data-chevron');
dataDiv.classList.toggle('hidden');
chevron.classList.toggle('rotate-180');
}
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
function openEditWhoisModal() {
document.getElementById('editWhoisModal').classList.remove('hidden');
document.getElementById('whois_server_input').focus();
}
function closeEditWhoisModal() {
document.getElementById('editWhoisModal').classList.add('hidden');
}
function openEditRdapModal() {
document.getElementById('editRdapModal').classList.remove('hidden');
document.getElementById('rdap_servers_input').focus();
}
function closeEditRdapModal() {
document.getElementById('editRdapModal').classList.add('hidden');
}
// Close modals on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeEditWhoisModal();
closeEditRdapModal();
}
});
// Close modals when clicking outside
document.getElementById('editWhoisModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeEditWhoisModal();
}
});
document.getElementById('editRdapModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeEditRdapModal();
}
});
<?php endif; ?>
</script>
<?php
$content = ob_get_clean();
include __DIR__ . '/../layout/base.php';
?>

View File

@@ -0,0 +1,420 @@
{% extends 'layout/base.twig' %}
{% set title = 'TLD Details' %}
{% set pageTitle = tld.tld %}
{% set pageDescription = 'TLD registry information and server details' %}
{% set pageIcon = 'fas fa-globe' %}
{% block content %}
{# Top Action Bar #}
<div class="mb-3 flex flex-wrap gap-2 justify-between items-center">
<div class="flex gap-2">
<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold bg-primary text-white">
<i class="fas fa-globe mr-1.5"></i>
TLD Registry
</span>
<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold {{ tld.is_active ? 'bg-green-100 dark:bg-green-500/10 text-green-700 dark:text-green-400 border border-green-200 dark:border-green-500/30' : 'bg-gray-100 dark:bg-slate-700 text-gray-700 dark:text-slate-300 border border-gray-200 dark:border-slate-600' }}">
<i class="fas {{ tld.is_active ? 'fa-check-circle' : 'fa-pause-circle' }} mr-1.5"></i>
{{ tld.is_active ? 'Active' : 'Inactive' }}
</span>
</div>
<div class="flex gap-2 items-center">
{% if session.role is defined and session.role == 'admin' %}
<a href="/tld-registry/{{ tld.id }}/refresh" class="inline-flex items-center justify-center px-3 py-2 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 transition-colors font-medium min-w-[80px] h-[32px]" onclick="return confirm('Refresh TLD data from IANA?')">
<i class="fas fa-sync-alt mr-1.5"></i>
Refresh
</a>
<a href="/tld-registry/{{ tld.id }}/toggle-active" class="inline-flex items-center justify-center px-3 py-2 bg-orange-600 text-white text-xs rounded-lg hover:bg-orange-700 transition-colors font-medium min-w-[80px] h-[32px]" onclick="return confirm('Toggle TLD status?')">
<i class="fas fa-power-off mr-1.5"></i>
Toggle
</a>
{% endif %}
<a href="/tld-registry" class="inline-flex items-center justify-center px-3 py-2 border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-slate-300 text-xs rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors font-medium min-w-[80px] h-[32px]">
<i class="fas fa-arrow-left mr-1.5"></i>
Back
</a>
</div>
</div>
{# Main 2-Column Layout #}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
{# LEFT COLUMN #}
<div class="space-y-3">
{# TLD Information #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
<div class="px-4 py-2 border-b border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900">
<h3 class="text-xs font-semibold text-gray-700 dark:text-slate-300 uppercase tracking-wider flex items-center">
<i class="fas fa-info-circle text-gray-400 dark:text-slate-500 mr-2" style="font-size: 10px;"></i>
TLD Information
</h3>
</div>
<div class="p-4">
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-xs">
<div>
<label class="text-gray-500 dark:text-slate-400 font-medium block mb-0.5">TLD</label>
<p class="text-gray-900 dark:text-white font-semibold">{{ tld.tld }}</p>
</div>
{% if tld.registry_url %}
<div>
<label class="text-gray-500 dark:text-slate-400 font-medium block mb-0.5">Registry URL</label>
<a href="{{ tld.registry_url }}" target="_blank" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 flex items-center">
<i class="fas fa-external-link-alt mr-1" style="font-size: 9px;"></i>
Visit Registry
</a>
</div>
{% endif %}
{% if tld.registration_date %}
<div>
<label class="text-gray-500 dark:text-slate-400 font-medium block mb-0.5">Registration Date</label>
<p class="text-gray-900 dark:text-white">{{ tld.registration_date|date('M j, Y') }}</p>
</div>
{% endif %}
{% if tld.record_last_updated %}
<div>
<label class="text-gray-500 dark:text-slate-400 font-medium block mb-0.5">Record Last Updated</label>
<p class="text-gray-900 dark:text-white">{{ tld.record_last_updated|date('M j, Y') }}</p>
</div>
{% endif %}
</div>
</div>
</div>
{# RDAP Servers #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
<div class="px-4 py-2 border-b border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900 flex items-center justify-between">
<h3 class="text-xs font-semibold text-gray-700 dark:text-slate-300 uppercase tracking-wider flex items-center">
<i class="fas fa-database text-gray-400 dark:text-slate-500 mr-2" style="font-size: 10px;"></i>
RDAP Servers
{% if tld.rdap_servers %}
{% set rdapServers = tld.rdap_servers|from_json %}
{% if rdapServers is iterable and rdapServers is not empty %}
({{ rdapServers|length }})
{% endif %}
{% endif %}
</h3>
{% if session.role is defined and session.role == 'admin' %}
<button onclick="openEditRdapModal()" class="inline-flex items-center px-2 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition-colors font-medium">
<i class="fas fa-edit mr-1" style="font-size: 9px;"></i>
Edit
</button>
{% endif %}
</div>
<div class="p-4">
{% if tld.rdap_servers %}
{% set rdapServers = tld.rdap_servers|from_json %}
{% if rdapServers is iterable and rdapServers is not empty %}
<div class="space-y-1.5">
{% for server in rdapServers %}
<div class="flex items-center p-2 bg-gray-50 dark:bg-slate-700 rounded hover:bg-gray-100 dark:hover:bg-slate-600 transition-colors">
<div class="w-6 h-6 bg-indigo-500 rounded flex items-center justify-center text-white font-bold text-xs mr-2">
{{ loop.index }}
</div>
<p class="font-mono text-xs text-gray-800 dark:text-slate-200">{{ server }}</p>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-xs text-gray-400 dark:text-slate-500 italic">No RDAP servers configured</p>
{% endif %}
{% else %}
<p class="text-xs text-gray-400 dark:text-slate-500 italic">No RDAP servers configured</p>
{% endif %}
</div>
</div>
{# WHOIS Server #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
<div class="px-4 py-2 border-b border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900 flex items-center justify-between">
<h3 class="text-xs font-semibold text-gray-700 dark:text-slate-300 uppercase tracking-wider flex items-center">
<i class="fas fa-server text-gray-400 dark:text-slate-500 mr-2" style="font-size: 10px;"></i>
WHOIS Server
</h3>
{% if session.role is defined and session.role == 'admin' %}
<button onclick="openEditWhoisModal()" class="inline-flex items-center px-2 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition-colors font-medium">
<i class="fas fa-edit mr-1" style="font-size: 9px;"></i>
Edit
</button>
{% endif %}
</div>
<div class="p-4">
{% if tld.whois_server %}
<div class="flex items-center p-2 bg-gray-50 dark:bg-slate-700 rounded">
<div class="w-6 h-6 bg-orange-500 rounded flex items-center justify-center text-white font-bold text-xs mr-2">
<i class="fas fa-server"></i>
</div>
<p class="font-mono text-xs text-gray-800 dark:text-slate-200">{{ tld.whois_server }}</p>
</div>
{% else %}
<p class="text-xs text-gray-400 dark:text-slate-500 italic">No WHOIS server configured</p>
{% endif %}
</div>
</div>
</div>
{# RIGHT COLUMN #}
<div class="space-y-3">
{# Import History #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
<div class="px-4 py-2 border-b border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900">
<h3 class="text-xs font-semibold text-gray-700 dark:text-slate-300 uppercase tracking-wider flex items-center">
<i class="fas fa-history text-gray-400 dark:text-slate-500 mr-2" style="font-size: 10px;"></i>
Import History
</h3>
</div>
<div class="p-4">
<div class="space-y-2">
<div class="flex items-center p-2 bg-blue-50 dark:bg-blue-500/10 rounded border border-blue-200 dark:border-blue-500/30">
<div class="w-7 h-7 bg-blue-500 rounded flex items-center justify-center mr-2">
<i class="fas fa-plus text-white text-xs"></i>
</div>
<div>
<p class="text-xs text-gray-600 dark:text-slate-400 font-medium">Created</p>
<p class="text-xs font-semibold text-gray-900 dark:text-white">{{ tld.created_at|date('M j, Y H:i') }}</p>
</div>
</div>
{% if tld.updated_at %}
<div class="flex items-center p-2 bg-green-50 dark:bg-green-500/10 rounded border border-green-200 dark:border-green-500/30">
<div class="w-7 h-7 bg-green-500 rounded flex items-center justify-center mr-2">
<i class="fas fa-sync text-white text-xs"></i>
</div>
<div>
<p class="text-xs text-gray-600 dark:text-slate-400 font-medium">Last Updated</p>
<p class="text-xs font-semibold text-gray-900 dark:text-white">{{ tld.updated_at|date('M j, Y H:i') }}</p>
</div>
</div>
{% endif %}
{% if tld.iana_publication_date %}
<div class="flex items-center p-2 bg-indigo-50 dark:bg-indigo-500/10 rounded border border-indigo-200 dark:border-indigo-500/30">
<div class="w-7 h-7 bg-indigo-500 rounded flex items-center justify-center mr-2">
<i class="fas fa-calendar text-white text-xs"></i>
</div>
<div>
<p class="text-xs text-gray-600 dark:text-slate-400 font-medium">IANA Publication</p>
<p class="text-xs font-semibold text-gray-900 dark:text-white">{{ tld.iana_publication_date|date('M j, Y H:i') }}</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{# Quick Actions #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
<div class="px-4 py-2 border-b border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900">
<h3 class="text-xs font-semibold text-gray-700 dark:text-slate-300 uppercase tracking-wider flex items-center">
<i class="fas fa-bolt text-gray-400 dark:text-slate-500 mr-2" style="font-size: 10px;"></i>
Quick Actions
</h3>
</div>
<div class="p-4 space-y-2">
{% if session.role is defined and session.role == 'admin' %}
<a href="/tld-registry/{{ tld.id }}/refresh" class="flex items-center p-3 border border-gray-200 dark:border-slate-700 hover:border-green-500 hover:bg-green-50 dark:hover:bg-green-500/10 rounded-lg transition-all duration-200 group" onclick="return confirm('Refresh TLD data from IANA?')">
<div class="w-9 h-9 bg-green-50 dark:bg-green-500/10 group-hover:bg-green-500 rounded-lg flex items-center justify-center group-hover:text-white text-green-600 dark:text-green-400 transition-colors duration-200">
<i class="fas fa-sync-alt text-sm"></i>
</div>
<span class="ml-3 text-sm font-medium text-gray-700 dark:text-slate-300 group-hover:text-green-700 dark:group-hover:text-green-400">Refresh from IANA</span>
</a>
<a href="/tld-registry/{{ tld.id }}/toggle-active" class="flex items-center p-3 border border-gray-200 dark:border-slate-700 hover:border-orange-500 hover:bg-orange-50 dark:hover:bg-orange-500/10 rounded-lg transition-all duration-200 group" onclick="return confirm('Toggle TLD status?')">
<div class="w-9 h-9 bg-orange-50 dark:bg-orange-500/10 group-hover:bg-orange-500 rounded-lg flex items-center justify-center group-hover:text-white text-orange-600 dark:text-orange-400 transition-colors duration-200">
<i class="fas fa-power-off text-sm"></i>
</div>
<span class="ml-3 text-sm font-medium text-gray-700 dark:text-slate-300 group-hover:text-orange-700 dark:group-hover:text-orange-400">Toggle Status</span>
</a>
{% endif %}
{% if tld.registry_url %}
<a href="{{ tld.registry_url }}" target="_blank" class="flex items-center p-3 border border-gray-200 dark:border-slate-700 hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-500/10 rounded-lg transition-all duration-200 group">
<div class="w-9 h-9 bg-blue-50 dark:bg-blue-500/10 group-hover:bg-blue-500 rounded-lg flex items-center justify-center group-hover:text-white text-blue-600 dark:text-blue-400 transition-colors duration-200">
<i class="fas fa-external-link-alt text-sm"></i>
</div>
<span class="ml-3 text-sm font-medium text-gray-700 dark:text-slate-300 group-hover:text-blue-700 dark:group-hover:text-blue-400">Visit Registry</span>
</a>
{% endif %}
</div>
</div>
{# Raw Data (Collapsible) #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
<button onclick="toggleRawData()" class="w-full px-4 py-2 border-b border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900 text-left hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors">
<h3 class="text-xs font-semibold text-gray-700 dark:text-slate-300 uppercase tracking-wider flex items-center justify-between">
<span class="flex items-center">
<i class="fas fa-code text-gray-400 dark:text-slate-500 mr-2" style="font-size: 10px;"></i>
Raw TLD Data
</span>
<i class="fas fa-chevron-down text-gray-400 dark:text-slate-500 text-xs transition-transform" id="raw-data-chevron"></i>
</h3>
</button>
<div id="raw-data" class="hidden p-4 bg-gray-900 max-h-64 overflow-y-auto">
{% set rawData = {
tld: tld.tld,
rdap_servers: tld.rdap_servers ? tld.rdap_servers|from_json : null,
whois_server: tld.whois_server,
registry_url: tld.registry_url,
registration_date: tld.registration_date,
record_last_updated: tld.record_last_updated,
iana_publication_date: tld.iana_publication_date,
is_active: tld.is_active,
created_at: tld.created_at,
updated_at: tld.updated_at
} %}
<pre class="text-xs text-green-400 font-mono">{{ rawData|json_encode(constant('JSON_PRETTY_PRINT'))|e }}</pre>
</div>
</div>
</div>
</div>
{# Edit WHOIS Server Modal #}
{% if session.role is defined and session.role == 'admin' %}
<div id="editWhoisModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
<i class="fas fa-server text-orange-600 mr-2"></i>
Edit WHOIS Server
</h3>
<button onclick="closeEditWhoisModal()" class="text-gray-400 dark:text-slate-500 hover:text-gray-600 dark:hover:text-slate-300 transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
<form method="POST" action="/tld-registry/{{ tld.id }}/update-whois-server" class="p-6">
{{ csrf_field() }}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
WHOIS Server
</label>
<input type="text"
name="whois_server"
id="whois_server_input"
value="{{ tld.whois_server|default('') }}"
placeholder="whois.example.com"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm bg-white dark:bg-slate-900 text-gray-900 dark:text-white">
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
Enter the WHOIS server hostname (e.g., whois.example.com). Leave empty to remove.
</p>
</div>
<div class="flex gap-3">
<button type="submit"
class="flex-1 inline-flex items-center justify-center px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors text-sm">
<i class="fas fa-save mr-2"></i>
Save Changes
</button>
<button type="button"
onclick="closeEditWhoisModal()"
class="flex-1 inline-flex items-center justify-center px-4 py-2.5 border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-slate-300 rounded-lg font-medium hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-sm">
<i class="fas fa-times mr-2"></i>
Cancel
</button>
</div>
</form>
</div>
</div>
{# Edit RDAP Servers Modal #}
<div id="editRdapModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
<i class="fas fa-database text-indigo-600 mr-2"></i>
Edit RDAP Servers
</h3>
<button onclick="closeEditRdapModal()" class="text-gray-400 dark:text-slate-500 hover:text-gray-600 dark:hover:text-slate-300 transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
<form method="POST" action="/tld-registry/{{ tld.id }}/update-rdap-servers" class="p-6">
{{ csrf_field() }}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
RDAP Servers
</label>
{% set rdapTextarea = '' %}
{% if tld.rdap_servers %}
{% set rdapServers = tld.rdap_servers|from_json %}
{% if rdapServers is iterable and rdapServers is not empty %}
{% set rdapTextarea = rdapServers|join("\n") %}
{% endif %}
{% endif %}
<textarea name="rdap_servers"
id="rdap_servers_input"
rows="6"
placeholder="https://rdap.example.com/&#10;https://rdap2.example.com/"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm font-mono bg-white dark:bg-slate-900 text-gray-900 dark:text-white">{{ rdapTextarea }}</textarea>
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
Enter RDAP server URLs (one per line or comma-separated). Must start with http:// or https://. Leave empty to remove all servers.
</p>
</div>
<div class="flex gap-3">
<button type="submit"
class="flex-1 inline-flex items-center justify-center px-4 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg font-medium transition-colors text-sm">
<i class="fas fa-save mr-2"></i>
Save Changes
</button>
<button type="button"
onclick="closeEditRdapModal()"
class="flex-1 inline-flex items-center justify-center px-4 py-2.5 border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-slate-300 rounded-lg font-medium hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-sm">
<i class="fas fa-times mr-2"></i>
Cancel
</button>
</div>
</form>
</div>
</div>
{% endif %}
<script>
function toggleRawData() {
const dataDiv = document.getElementById('raw-data');
const chevron = document.getElementById('raw-data-chevron');
dataDiv.classList.toggle('hidden');
chevron.classList.toggle('rotate-180');
}
{% if session.role is defined and session.role == 'admin' %}
function openEditWhoisModal() {
document.getElementById('editWhoisModal').classList.remove('hidden');
document.getElementById('whois_server_input').focus();
}
function closeEditWhoisModal() {
document.getElementById('editWhoisModal').classList.add('hidden');
}
function openEditRdapModal() {
document.getElementById('editRdapModal').classList.remove('hidden');
document.getElementById('rdap_servers_input').focus();
}
function closeEditRdapModal() {
document.getElementById('editRdapModal').classList.add('hidden');
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeEditWhoisModal();
closeEditRdapModal();
}
});
document.getElementById('editWhoisModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeEditWhoisModal();
}
});
document.getElementById('editRdapModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeEditRdapModal();
}
});
{% endif %}
</script>
{% endblock %}