Initial Commit
This commit is contained in:
156
app/Views/auth/login.php
Normal file
156
app/Views/auth/login.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - Domain Monitor</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#4A90E2',
|
||||
dark: '#357ABD',
|
||||
light: '#6BA3E8',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen flex items-center justify-center p-4">
|
||||
<div class="max-w-md w-full">
|
||||
<!-- Login Card -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
|
||||
<!-- Logo and Title -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-flex items-center justify-center w-14 h-14 bg-primary rounded-lg mb-4">
|
||||
<i class="fas fa-globe text-white text-2xl"></i>
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900 mb-1">Welcome Back</h1>
|
||||
<p class="text-sm text-gray-500">Sign in to access your account</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Alert -->
|
||||
<?php if (isset($_SESSION['error'])): ?>
|
||||
<div class="mb-6 bg-red-50 border border-red-200 p-3 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-exclamation-circle text-red-500 mr-2"></i>
|
||||
<span class="text-sm text-red-700"><?= htmlspecialchars($_SESSION['error']) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php unset($_SESSION['error']); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form method="POST" action="/login" class="space-y-5">
|
||||
<!-- Username Field -->
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Username
|
||||
</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i class="fas fa-user text-gray-400 text-sm"></i>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
required
|
||||
autofocus
|
||||
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
placeholder="Enter your username">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Password
|
||||
</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i class="fas fa-lock text-gray-400 text-sm"></i>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
class="w-full pl-10 pr-10 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
placeholder="Enter your password">
|
||||
<button
|
||||
type="button"
|
||||
onclick="togglePassword()"
|
||||
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none">
|
||||
<i class="fas fa-eye text-sm" id="toggleIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="remember"
|
||||
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
|
||||
<span class="ml-2 text-sm text-gray-600">Remember me</span>
|
||||
</label>
|
||||
<a href="#" class="text-sm text-primary hover:text-primary-dark">
|
||||
Forgot password?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-primary hover:bg-primary-dark text-white py-2.5 rounded-lg font-medium transition-colors duration-200 flex items-center justify-center text-sm">
|
||||
<i class="fas fa-sign-in-alt mr-2"></i>
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="text-center mt-6">
|
||||
<p class="text-gray-500 text-xs">
|
||||
© <?= date('Y') ?> Domain Monitor. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePassword() {
|
||||
const passwordInput = document.getElementById('password');
|
||||
const toggleIcon = document.getElementById('toggleIcon');
|
||||
|
||||
if (passwordInput.type === 'password') {
|
||||
passwordInput.type = 'text';
|
||||
toggleIcon.classList.remove('fa-eye');
|
||||
toggleIcon.classList.add('fa-eye-slash');
|
||||
} else {
|
||||
passwordInput.type = 'password';
|
||||
toggleIcon.classList.remove('fa-eye-slash');
|
||||
toggleIcon.classList.add('fa-eye');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
232
app/Views/dashboard/index.php
Normal file
232
app/Views/dashboard/index.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
$title = 'Dashboard';
|
||||
$pageTitle = 'Dashboard Overview';
|
||||
$pageDescription = 'Monitor your domains and expiration dates';
|
||||
$pageIcon = 'fas fa-chart-line';
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<!-- Total Domains 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 Domains</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $stats['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 Domains 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"><?= $stats['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>
|
||||
|
||||
<!-- Expiring Soon 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">Expiring Soon</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $stats['expiring_soon'] ?? 0 ?></p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-orange-50 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-exclamation-triangle text-orange-600 text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inactive Domains 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">Inactive</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $stats['inactive'] ?? 0 ?></p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-gray-50 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-times-circle text-gray-600 text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<!-- Recent Domains -->
|
||||
<div class="lg:col-span-2 bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-clock text-gray-400 mr-2 text-sm"></i>
|
||||
Recent Domains
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<?php if (!empty($recentDomains)): ?>
|
||||
<div class="space-y-3">
|
||||
<?php foreach ($recentDomains as $domain): ?>
|
||||
<div class="flex items-center justify-between p-3 border border-gray-100 rounded-lg hover:border-gray-300 hover:shadow-sm transition-all duration-200">
|
||||
<div class="flex items-center space-x-3 flex-1 min-w-0">
|
||||
<div class="w-10 h-10 bg-gray-50 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-globe text-gray-400"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-medium text-gray-900 truncate"><?= htmlspecialchars($domain['domain_name']) ?></h3>
|
||||
<div class="flex items-center space-x-3 text-xs text-gray-500 mt-1">
|
||||
<span class="flex items-center">
|
||||
<i class="far fa-calendar mr-1"></i>
|
||||
<?php if ($domain['expiration_date']): ?>
|
||||
<?= date('M d, Y', strtotime($domain['expiration_date'])) ?>
|
||||
<?php else: ?>
|
||||
Not set
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php if ($domain['registrar']): ?>
|
||||
<span class="flex items-center truncate">
|
||||
<i class="fas fa-building mr-1"></i>
|
||||
<?= htmlspecialchars($domain['registrar']) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 flex-shrink-0">
|
||||
<?php
|
||||
$statusClass = $domain['status'] === 'active' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700';
|
||||
?>
|
||||
<span class="px-2 py-1 rounded text-xs font-medium <?= $statusClass ?>">
|
||||
<?= ucfirst($domain['status']) ?>
|
||||
</span>
|
||||
<a href="/domains/<?= $domain['id'] ?>" class="text-gray-400 hover:text-primary">
|
||||
<i class="fas fa-chevron-right text-sm"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t border-gray-100 text-center">
|
||||
<a href="/domains" class="text-sm text-primary hover:text-primary-dark font-medium inline-flex items-center">
|
||||
View All Domains
|
||||
<i class="fas fa-arrow-right ml-2 text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="text-center py-10">
|
||||
<i class="fas fa-globe text-gray-300 text-5xl mb-3"></i>
|
||||
<p class="text-gray-500">No domains added yet</p>
|
||||
<a href="/domains/create" class="mt-3 inline-flex items-center px-5 py-2 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors duration-200">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Add Your First Domain
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar: Quick Actions & Stats -->
|
||||
<div class="space-y-4">
|
||||
<!-- Quick Actions -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-5 py-3 border-b border-gray-200">
|
||||
<h2 class="text-sm font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-bolt text-gray-400 mr-2 text-xs"></i>
|
||||
Quick Actions
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-4 space-y-2">
|
||||
<a href="/domains/create" class="flex items-center p-3 border border-gray-200 hover:border-primary hover:bg-blue-50 rounded-lg transition-all duration-200 group">
|
||||
<div class="w-9 h-9 bg-blue-50 group-hover:bg-primary rounded-lg flex items-center justify-center group-hover:text-white text-blue-600 transition-colors duration-200">
|
||||
<i class="fas fa-plus text-sm"></i>
|
||||
</div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-700 group-hover:text-primary">Add New Domain</span>
|
||||
</a>
|
||||
<a href="/groups/create" 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">
|
||||
<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-bell text-sm"></i>
|
||||
</div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-700 group-hover:text-green-700">Create Group</span>
|
||||
</a>
|
||||
<a href="/debug/whois" class="flex items-center p-3 border border-gray-200 hover:border-purple-500 hover:bg-purple-50 rounded-lg transition-all duration-200 group">
|
||||
<div class="w-9 h-9 bg-purple-50 group-hover:bg-purple-500 rounded-lg flex items-center justify-center group-hover:text-white text-purple-600 transition-colors duration-200">
|
||||
<i class="fas fa-search text-sm"></i>
|
||||
</div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-700 group-hover:text-purple-700">WHOIS Lookup</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Status -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-5 py-3 border-b border-gray-200">
|
||||
<h2 class="text-sm font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-server text-gray-400 mr-2 text-xs"></i>
|
||||
System Status
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-4 space-y-3">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">Database</span>
|
||||
<span class="flex items-center text-green-600 font-medium">
|
||||
<i class="fas fa-circle text-xs mr-1.5"></i>
|
||||
Online
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">WHOIS Service</span>
|
||||
<span class="flex items-center text-green-600 font-medium">
|
||||
<i class="fas fa-circle text-xs mr-1.5"></i>
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">Notifications</span>
|
||||
<span class="flex items-center text-green-600 font-medium">
|
||||
<i class="fas fa-circle text-xs mr-1.5"></i>
|
||||
Enabled
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expiring This Month -->
|
||||
<?php if (!empty($expiringThisMonth)): ?>
|
||||
<div class="bg-white rounded-lg border-l-4 border-orange-500 border-t border-r border-b border-gray-200 overflow-hidden">
|
||||
<div class="bg-orange-50 px-5 py-3 border-b border-orange-100">
|
||||
<h2 class="text-sm font-semibold text-orange-900 flex items-center">
|
||||
<i class="fas fa-exclamation-triangle mr-2 text-xs"></i>
|
||||
Expiring This Month
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-4 space-y-2.5">
|
||||
<?php foreach ($expiringThisMonth as $domain): ?>
|
||||
<div class="flex items-center justify-between p-2 hover:bg-gray-50 rounded transition-colors duration-150">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 truncate"><?= htmlspecialchars($domain['domain_name']) ?></p>
|
||||
<p class="text-xs text-gray-500"><?= date('M d, Y', strtotime($domain['expiration_date'])) ?></p>
|
||||
</div>
|
||||
<a href="/domains/<?= $domain['id'] ?>" class="ml-2 text-gray-400 hover:text-primary flex-shrink-0">
|
||||
<i class="fas fa-chevron-right text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
318
app/Views/debug/whois.php
Normal file
318
app/Views/debug/whois.php
Normal file
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
$title = 'WHOIS Debug Tool';
|
||||
$pageTitle = 'WHOIS Debug Tool';
|
||||
$pageDescription = 'Test and debug WHOIS data extraction';
|
||||
$pageIcon = 'fas fa-search';
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<?php if (empty($domain)): ?>
|
||||
<!-- Search Form -->
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<form method="GET" action="/debug/whois" class="space-y-4">
|
||||
<div>
|
||||
<label for="domain" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Domain Name
|
||||
</label>
|
||||
<input type="text"
|
||||
id="domain"
|
||||
name="domain"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
placeholder="Enter domain (e.g., google.com)"
|
||||
required
|
||||
autofocus>
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
Enter a domain name without http:// or www.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="w-full inline-flex items-center justify-center px-5 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg font-medium transition-colors text-sm">
|
||||
<i class="fas fa-search mr-2"></i>
|
||||
Check WHOIS
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Info Card -->
|
||||
<div class="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-info-circle text-blue-500 text-lg"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-1">What is this tool?</h3>
|
||||
<p class="text-xs text-gray-600 leading-relaxed">
|
||||
This debug tool shows you the raw WHOIS data for any domain and how our system parses it.
|
||||
Use it to troubleshoot issues with domain information extraction.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- Back Button & Copy Report -->
|
||||
<div class="mb-4 flex justify-between items-center">
|
||||
<a href="/debug/whois" 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">
|
||||
<i class="fas fa-arrow-left mr-2"></i>
|
||||
Check Another Domain
|
||||
</a>
|
||||
<button onclick="copyDebugReport()" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors font-medium">
|
||||
<i class="fas fa-copy mr-2"></i>
|
||||
Copy Debug Report
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Domain Info Card -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-5 mb-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Domain</p>
|
||||
<p class="text-sm font-semibold text-gray-900 mt-1"><?= htmlspecialchars($domain) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">WHOIS Server</p>
|
||||
<p class="text-sm font-semibold text-gray-900 mt-1"><?= htmlspecialchars($server) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">TLD</p>
|
||||
<p class="text-sm font-semibold text-gray-900 mt-1"><?= htmlspecialchars($tld) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<!-- Parsed Data -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-5 py-3 border-b border-gray-200 bg-green-50">
|
||||
<h2 class="text-sm font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-check-circle text-green-600 mr-2 text-sm"></i>
|
||||
Extracted Data (What We Save)
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-xs font-medium text-gray-600">Domain</span>
|
||||
<span class="text-xs text-gray-900 font-mono"><?= htmlspecialchars($info['domain'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-xs font-medium text-gray-600">Registrar</span>
|
||||
<span class="text-xs text-gray-900 font-mono"><?= htmlspecialchars($info['registrar'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-xs font-medium text-gray-600">Expiration Date</span>
|
||||
<span class="text-xs text-gray-900 font-mono"><?= htmlspecialchars($info['expiration_date'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-xs font-medium text-gray-600">Creation Date</span>
|
||||
<span class="text-xs text-gray-900 font-mono"><?= htmlspecialchars($info['creation_date'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-xs font-medium text-gray-600">Updated Date</span>
|
||||
<span class="text-xs text-gray-900 font-mono"><?= htmlspecialchars($info['updated_date'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-xs font-medium text-gray-600">Registrar URL</span>
|
||||
<span class="text-xs text-gray-900 font-mono"><?= htmlspecialchars($info['registrar_url'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-xs font-medium text-gray-600">Abuse Email</span>
|
||||
<span class="text-xs text-gray-900 font-mono"><?= htmlspecialchars($info['abuse_email'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<span class="text-xs font-medium text-gray-600 block mb-2">Nameservers</span>
|
||||
<div class="space-y-1">
|
||||
<?php if (!empty($info['nameservers'])): ?>
|
||||
<?php foreach ($info['nameservers'] as $ns): ?>
|
||||
<div class="text-xs text-gray-900 font-mono bg-gray-50 px-2 py-1 rounded"><?= htmlspecialchars($ns) ?></div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<span class="text-xs text-gray-400">N/A</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key-Value Pairs -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-5 py-3 border-b border-gray-200 bg-blue-50">
|
||||
<h2 class="text-sm font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-table text-blue-600 mr-2 text-sm"></i>
|
||||
All Key-Value Pairs
|
||||
</h2>
|
||||
</div>
|
||||
<div class="overflow-y-auto" style="max-height: 500px;">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50 sticky top-0">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-600 uppercase">Key</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-600 uppercase">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-100">
|
||||
<?php foreach ($parsedData as $item): ?>
|
||||
<?php if (!empty($item['value'])): ?>
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-2 text-xs font-medium text-gray-700"><?= htmlspecialchars($item['key']) ?></td>
|
||||
<td class="px-4 py-2 text-xs text-gray-900 font-mono"><?= htmlspecialchars($item['value']) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Raw Response -->
|
||||
<div class="mt-4 bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-5 py-3 border-b border-gray-200 bg-gray-50">
|
||||
<h2 class="text-sm font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-file-code text-gray-600 mr-2 text-sm"></i>
|
||||
Raw WHOIS Response
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<pre class="text-xs font-mono bg-gray-50 p-4 rounded border border-gray-200 overflow-x-auto"><?= htmlspecialchars($response) ?></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden data for JS -->
|
||||
<script id="debug-data" type="application/json">
|
||||
{
|
||||
"domain": <?= json_encode($domain) ?>,
|
||||
"tld": <?= json_encode($tld) ?>,
|
||||
"server": <?= json_encode($server) ?>,
|
||||
"extractedData": <?= json_encode($info) ?>,
|
||||
"rawResponse": <?= json_encode($response) ?>,
|
||||
"parsedKeyValuePairs": <?= json_encode($parsedData) ?>
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function copyDebugReport() {
|
||||
const data = JSON.parse(document.getElementById('debug-data').textContent);
|
||||
|
||||
let report = `=== WHOIS DEBUG REPORT ===\n\n`;
|
||||
report += `Domain: ${data.domain}\n`;
|
||||
report += `TLD: ${data.tld}\n`;
|
||||
report += `WHOIS Server: ${data.server}\n`;
|
||||
report += `Date: ${new Date().toISOString()}\n\n`;
|
||||
|
||||
report += `--- EXTRACTED DATA (What We Save) ---\n`;
|
||||
report += `Domain: ${data.extractedData.domain || 'N/A'}\n`;
|
||||
report += `Registrar: ${data.extractedData.registrar || 'N/A'}\n`;
|
||||
report += `Registrar URL: ${data.extractedData.registrar_url || 'N/A'}\n`;
|
||||
report += `Expiration Date: ${data.extractedData.expiration_date || 'N/A'}\n`;
|
||||
report += `Creation Date: ${data.extractedData.creation_date || 'N/A'}\n`;
|
||||
report += `Updated Date: ${data.extractedData.updated_date || 'N/A'}\n`;
|
||||
report += `Abuse Email: ${data.extractedData.abuse_email || 'N/A'}\n`;
|
||||
report += `Nameservers: ${data.extractedData.nameservers && data.extractedData.nameservers.length > 0 ? data.extractedData.nameservers.join(', ') : 'N/A'}\n`;
|
||||
report += `Status: ${data.extractedData.status && data.extractedData.status.length > 0 ? data.extractedData.status.join(', ') : 'N/A'}\n\n`;
|
||||
|
||||
report += `--- ALL KEY-VALUE PAIRS ---\n`;
|
||||
if (data.parsedKeyValuePairs && data.parsedKeyValuePairs.length > 0) {
|
||||
data.parsedKeyValuePairs.forEach(item => {
|
||||
if (item.value) {
|
||||
report += `${item.key}: ${item.value}\n`;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
report += 'No key-value pairs found\n';
|
||||
}
|
||||
|
||||
report += `\n--- RAW WHOIS RESPONSE ---\n`;
|
||||
report += data.rawResponse;
|
||||
report += `\n\n=== END OF REPORT ===`;
|
||||
|
||||
// Copy to clipboard with fallback
|
||||
copyToClipboard(report);
|
||||
}
|
||||
|
||||
// Robust clipboard copy function with fallback
|
||||
function copyToClipboard(text) {
|
||||
// Try modern clipboard API first
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showCopySuccess();
|
||||
}).catch(err => {
|
||||
console.error('Modern clipboard API failed:', err);
|
||||
// Fallback to legacy method
|
||||
fallbackCopyTextToClipboard(text);
|
||||
});
|
||||
} else {
|
||||
// Use fallback for non-HTTPS or older browsers
|
||||
fallbackCopyTextToClipboard(text);
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopyTextToClipboard(text) {
|
||||
// Create a temporary textarea
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
||||
// Make it invisible but accessible
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.top = '0';
|
||||
textArea.style.left = '0';
|
||||
textArea.style.width = '2em';
|
||||
textArea.style.height = '2em';
|
||||
textArea.style.padding = '0';
|
||||
textArea.style.border = 'none';
|
||||
textArea.style.outline = 'none';
|
||||
textArea.style.boxShadow = 'none';
|
||||
textArea.style.background = 'transparent';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
showCopySuccess();
|
||||
} else {
|
||||
showCopyError();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fallback copy failed:', err);
|
||||
showCopyError();
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
function showCopySuccess() {
|
||||
const btn = event.target.closest('button');
|
||||
if (!btn) return;
|
||||
|
||||
const originalHTML = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>Copied!';
|
||||
btn.classList.remove('bg-blue-600', 'hover:bg-blue-700');
|
||||
btn.classList.add('bg-green-600', 'hover:bg-green-700');
|
||||
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = originalHTML;
|
||||
btn.classList.remove('bg-green-600', 'hover:bg-green-700');
|
||||
btn.classList.add('bg-blue-600', 'hover:bg-blue-700');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function showCopyError() {
|
||||
alert('Failed to copy to clipboard.\n\nYour browser may not support this feature, or the site needs HTTPS.\n\nPlease manually select and copy the text from the Raw WHOIS Response section below.');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
|
||||
128
app/Views/domains/bulk-add.php
Normal file
128
app/Views/domains/bulk-add.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
$title = 'Bulk Add Domains';
|
||||
$pageTitle = 'Bulk Add Domains';
|
||||
$pageDescription = 'Add multiple domains at once';
|
||||
$pageIcon = 'fas fa-layer-group';
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<!-- Main Form -->
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-layer-group text-gray-400 mr-2 text-sm"></i>
|
||||
Bulk Add Domains
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<form method="POST" action="/domains/bulk-add" class="space-y-5">
|
||||
<!-- Domains Textarea -->
|
||||
<div>
|
||||
<label for="domains" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Domain Names *
|
||||
</label>
|
||||
<textarea
|
||||
id="domains"
|
||||
name="domains"
|
||||
rows="10"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm font-mono"
|
||||
placeholder="example.com google.com github.com ..."
|
||||
required
|
||||
autofocus></textarea>
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
Enter one domain per line. Domains without http:// or www.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Notification Group -->
|
||||
<div>
|
||||
<label for="notification_group_id" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Notification Group (Optional)
|
||||
</label>
|
||||
<select id="notification_group_id"
|
||||
name="notification_group_id"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm">
|
||||
<option value="">-- No Group (No notifications) --</option>
|
||||
<?php foreach ($groups as $group): ?>
|
||||
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
Assign all domains to this notification group
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-3 pt-3">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center justify-center px-5 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg font-medium transition-colors text-sm">
|
||||
<i class="fas fa-plus-circle mr-2"></i>
|
||||
Add All Domains
|
||||
</button>
|
||||
<a href="/domains"
|
||||
class="inline-flex items-center justify-center px-5 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
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Cards -->
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- How it works -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-info-circle text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-1">How It Works</h3>
|
||||
<p class="text-xs text-gray-600 leading-relaxed">
|
||||
Paste multiple domain names, one per line. The system will fetch WHOIS information
|
||||
for each domain automatically. This may take a few moments depending on how many domains you're adding.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Important notes -->
|
||||
<div class="bg-orange-50 border border-orange-200 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-10 h-10 bg-orange-500 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-exclamation-triangle text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-1">Important Notes</h3>
|
||||
<ul class="text-xs text-gray-600 space-y-1">
|
||||
<li class="flex items-start">
|
||||
<i class="fas fa-circle text-orange-500 mt-1 mr-2" style="font-size: 6px;"></i>
|
||||
<span>Duplicate domains will be skipped</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<i class="fas fa-circle text-orange-500 mt-1 mr-2" style="font-size: 6px;"></i>
|
||||
<span>Invalid domains will be reported</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<i class="fas fa-circle text-orange-500 mt-1 mr-2" style="font-size: 6px;"></i>
|
||||
<span>Large batches may take several minutes</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
|
||||
130
app/Views/domains/create.php
Normal file
130
app/Views/domains/create.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
$title = 'Add New Domain';
|
||||
$pageTitle = 'Add New Domain';
|
||||
$pageDescription = 'Start monitoring a new domain';
|
||||
$pageIcon = 'fas fa-plus-circle';
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<!-- Main Form -->
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-globe text-gray-400 mr-2 text-sm"></i>
|
||||
Domain Information
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<form method="POST" action="/domains/store" class="space-y-5">
|
||||
<!-- Domain Name -->
|
||||
<div>
|
||||
<label for="domain_name" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Domain Name *
|
||||
</label>
|
||||
<input type="text"
|
||||
id="domain_name"
|
||||
name="domain_name"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
placeholder="example.com"
|
||||
required
|
||||
autofocus>
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
Enter the domain name without http:// or https://
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Notification Group -->
|
||||
<div>
|
||||
<label for="notification_group_id" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Notification Group
|
||||
</label>
|
||||
<select id="notification_group_id"
|
||||
name="notification_group_id"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm">
|
||||
<option value="">-- No Group (No notifications) --</option>
|
||||
<?php foreach ($groups as $group): ?>
|
||||
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
Optional: Assign to a notification group to receive expiry alerts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-3 pt-3">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center justify-center px-5 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg font-medium transition-colors text-sm">
|
||||
<i class="fas fa-plus-circle mr-2"></i>
|
||||
Add Domain
|
||||
</button>
|
||||
<a href="/domains"
|
||||
class="inline-flex items-center justify-center px-5 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
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Cards -->
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- How it works -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-info-circle text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-1">How It Works</h3>
|
||||
<p class="text-xs text-gray-600 leading-relaxed">
|
||||
When you add a domain, we automatically fetch its WHOIS information including
|
||||
expiration date, registrar, nameservers, and other important details. This may take a few seconds.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- What we track -->
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-check-circle text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-1">What We Track</h3>
|
||||
<ul class="text-xs text-gray-600 space-y-1">
|
||||
<li class="flex items-center">
|
||||
<i class="fas fa-circle text-green-500" style="font-size: 6px;"></i>
|
||||
<span class="ml-2">Domain expiration date</span>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<i class="fas fa-circle text-green-500" style="font-size: 6px;"></i>
|
||||
<span class="ml-2">Registrar information</span>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<i class="fas fa-circle text-green-500" style="font-size: 6px;"></i>
|
||||
<span class="ml-2">Nameservers</span>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<i class="fas fa-circle text-green-500" style="font-size: 6px;"></i>
|
||||
<span class="ml-2">Domain status</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
120
app/Views/domains/edit.php
Normal file
120
app/Views/domains/edit.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
$title = 'Edit Domain';
|
||||
$pageTitle = 'Edit Domain';
|
||||
$pageDescription = htmlspecialchars($domain['domain_name']);
|
||||
$pageIcon = 'fas fa-edit';
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<!-- Main Form -->
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-cog text-gray-400 mr-2 text-sm"></i>
|
||||
Domain Settings
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/update" class="space-y-5">
|
||||
|
||||
<!-- Domain Name (Read-only) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Domain Name
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg bg-gray-50 text-gray-600 cursor-not-allowed text-sm"
|
||||
value="<?= htmlspecialchars($domain['domain_name']) ?>"
|
||||
disabled>
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<i class="fas fa-lock text-gray-400 text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
Domain name cannot be changed after creation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Notification Group -->
|
||||
<div>
|
||||
<label for="notification_group_id" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Notification Group
|
||||
</label>
|
||||
<select id="notification_group_id"
|
||||
name="notification_group_id"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm">
|
||||
<option value="">-- No Group (No notifications) --</option>
|
||||
<?php foreach ($groups as $group): ?>
|
||||
<option value="<?= $group['id'] ?>"
|
||||
<?= $domain['notification_group_id'] == $group['id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($group['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
Change the notification group or remove it to stop receiving alerts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Active Monitoring -->
|
||||
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="checkbox"
|
||||
name="is_active"
|
||||
<?= $domain['is_active'] ? 'checked' : '' ?>
|
||||
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary cursor-pointer">
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-gray-900">Enable Active Monitoring</span>
|
||||
<p class="text-xs text-gray-600 mt-0.5">When enabled, this domain will be checked regularly and notifications will be sent</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-3 pt-3">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center justify-center px-5 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>
|
||||
Update Domain
|
||||
</button>
|
||||
<a href="/domains/<?= $domain['id'] ?>"
|
||||
class="inline-flex items-center justify-center px-5 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
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<a href="/domains/<?= $domain['id'] ?>"
|
||||
class="flex items-center justify-center p-3 bg-white border border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-colors group">
|
||||
<i class="fas fa-eye text-blue-600 mr-2 text-sm"></i>
|
||||
<span class="text-sm font-medium text-gray-700">View Details</span>
|
||||
</a>
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/refresh" class="m-0">
|
||||
<button type="submit"
|
||||
class="w-full flex items-center justify-center p-3 bg-white border border-gray-200 rounded-lg hover:border-green-300 hover:bg-green-50 transition-colors group">
|
||||
<i class="fas fa-sync-alt text-green-600 mr-2 text-sm"></i>
|
||||
<span class="text-sm font-medium text-gray-700">Refresh WHOIS</span>
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/delete" onsubmit="return confirm('Delete this domain permanently?')" class="m-0">
|
||||
<button type="submit"
|
||||
class="w-full flex items-center justify-center p-3 bg-white border border-gray-200 rounded-lg hover:border-red-300 hover:bg-red-50 transition-colors group">
|
||||
<i class="fas fa-trash text-red-600 mr-2 text-sm"></i>
|
||||
<span class="text-sm font-medium text-gray-700">Delete Domain</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
688
app/Views/domains/index.php
Normal file
688
app/Views/domains/index.php
Normal file
@@ -0,0 +1,688 @@
|
||||
<?php
|
||||
$title = 'Domains';
|
||||
$pageTitle = 'Domain Management';
|
||||
$pageDescription = 'Monitor and manage your domain portfolio';
|
||||
$pageIcon = 'fas fa-globe';
|
||||
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 '/domains?' . 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' => '', 'status' => '', 'group' => '', 'sort' => 'domain_name', 'order' => 'asc'];
|
||||
?>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="mb-4 flex flex-wrap gap-2 justify-between items-center">
|
||||
<div class="flex gap-2">
|
||||
<!-- Bulk Actions Toolbar (Hidden by default, shown when domains are selected) -->
|
||||
<div id="bulk-actions" class="hidden items-center gap-2">
|
||||
<span id="selected-count" class="text-sm font-medium text-gray-700"></span>
|
||||
|
||||
<button type="button" onclick="bulkRefresh()" class="inline-flex items-center px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 transition-colors font-medium">
|
||||
<i class="fas fa-sync-alt mr-2"></i>
|
||||
Refresh
|
||||
</button>
|
||||
|
||||
<div class="relative inline-block">
|
||||
<button type="button" onclick="toggleAssignGroupDropdown()" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors font-medium">
|
||||
<i class="fas fa-bell mr-2"></i>
|
||||
Assign Group
|
||||
<i class="fas fa-chevron-down ml-2 text-xs"></i>
|
||||
</button>
|
||||
<div id="assign-group-dropdown" class="hidden absolute left-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 z-10">
|
||||
<form method="POST" action="/domains/bulk-assign-group" id="bulk-assign-form">
|
||||
<input type="hidden" name="domain_ids" id="bulk-assign-ids">
|
||||
<div class="p-3">
|
||||
<select name="group_id" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<option value="">-- No Group --</option>
|
||||
<?php foreach ($groups as $group): ?>
|
||||
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 p-2 flex gap-2">
|
||||
<button type="submit" class="flex-1 px-3 py-1.5 bg-primary text-white text-xs rounded hover:bg-primary-dark">
|
||||
Assign
|
||||
</button>
|
||||
<button type="button" onclick="toggleAssignGroupDropdown()" class="flex-1 px-3 py-1.5 bg-gray-200 text-gray-700 text-xs rounded hover:bg-gray-300">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" onclick="bulkDelete()" class="inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium">
|
||||
<i class="fas fa-trash mr-2"></i>
|
||||
Delete
|
||||
</button>
|
||||
|
||||
<button type="button" onclick="clearSelection()" 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">
|
||||
<i class="fas fa-times mr-2"></i>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<?php if (!empty($domains)): ?>
|
||||
<form method="POST" action="/domains/bulk-refresh" id="refresh-all-form">
|
||||
<?php foreach ($domains as $domain): ?>
|
||||
<input type="hidden" name="domain_ids[]" value="<?= $domain['id'] ?>">
|
||||
<?php endforeach; ?>
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 transition-colors font-medium" title="Refresh all domains on this page">
|
||||
<i class="fas fa-sync-alt mr-2"></i>
|
||||
Refresh Page (<?= count($domains) ?>)
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<a href="/domains/bulk-add" class="inline-flex items-center px-4 py-2 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700 transition-colors font-medium">
|
||||
<i class="fas fa-layer-group mr-2"></i>
|
||||
Bulk Add
|
||||
</a>
|
||||
<a href="/domains/create" class="inline-flex items-center px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Add Domain
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters & Search -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-5 mb-4">
|
||||
<form method="GET" action="/domains" id="filter-form">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1.5">Search</label>
|
||||
<div class="relative">
|
||||
<input type="text" name="search" id="domainSearch" value="<?= htmlspecialchars($currentFilters['search']) ?>" placeholder="Search domains..." 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>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1.5">Status</label>
|
||||
<select name="status" id="statusFilter" 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 Statuses</option>
|
||||
<option value="active" <?= $currentFilters['status'] === 'active' ? 'selected' : '' ?>>Active</option>
|
||||
<option value="expiring_soon" <?= $currentFilters['status'] === 'expiring_soon' ? 'selected' : '' ?>>Expiring Soon</option>
|
||||
<option value="inactive" <?= $currentFilters['status'] === 'inactive' ? 'selected' : '' ?>>Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1.5">Group</label>
|
||||
<select name="group" id="groupFilter" 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 Groups</option>
|
||||
<?php foreach ($groups as $group): ?>
|
||||
<option value="<?= $group['id'] ?>" <?= $currentFilters['group'] == $group['id'] ? 'selected' : '' ?>><?= htmlspecialchars($group['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button type="submit" class="w-full 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 Filters
|
||||
</button>
|
||||
</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> domain(s)
|
||||
</div>
|
||||
|
||||
<form method="GET" action="/domains" class="flex items-center gap-2">
|
||||
<!-- Preserve current filters -->
|
||||
<input type="hidden" name="search" value="<?= htmlspecialchars($currentFilters['search']) ?>">
|
||||
<input type="hidden" name="status" value="<?= htmlspecialchars($currentFilters['status']) ?>">
|
||||
<input type="hidden" name="group" value="<?= htmlspecialchars($currentFilters['group']) ?>">
|
||||
<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>
|
||||
|
||||
<!-- Domains List -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<?php if (!empty($domains)): ?>
|
||||
<!-- 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-4 py-3 text-left w-12">
|
||||
<input type="checkbox" id="select-all" onclick="toggleSelectAll(this)" class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary cursor-pointer">
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
<a href="<?= sortUrl('domain_name', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
|
||||
Domain <?= sortIcon('domain_name', $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('registrar', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
|
||||
Registrar <?= sortIcon('registrar', $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('expiration_date', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
|
||||
Expiration <?= sortIcon('expiration_date', $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('status', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
|
||||
Status <?= sortIcon('status', $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('group_name', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
|
||||
Group <?= sortIcon('group_name', $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('last_checked', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
|
||||
Last Checked <?= sortIcon('last_checked', $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 ($domains as $domain): ?>
|
||||
<?php
|
||||
// Calculate days until expiry and determine status color
|
||||
$daysLeft = !empty($domain['expiration_date']) ? floor((strtotime($domain['expiration_date']) - time()) / 86400) : null;
|
||||
$expiryClass = '';
|
||||
if ($daysLeft !== null) {
|
||||
if ($daysLeft < 0) {
|
||||
$expiryClass = 'text-red-600 font-semibold';
|
||||
} elseif ($daysLeft <= 30) {
|
||||
$expiryClass = 'text-orange-600 font-semibold';
|
||||
} elseif ($daysLeft <= 90) {
|
||||
$expiryClass = 'text-yellow-600';
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate domain status if it's empty or error (for backward compatibility)
|
||||
$domainStatus = $domain['status'];
|
||||
if (empty($domainStatus) || $domainStatus === 'error') {
|
||||
$whoisData = json_decode($domain['whois_data'] ?? '{}', true);
|
||||
$statusArray = $whoisData['status'] ?? [];
|
||||
$isAvailable = false;
|
||||
foreach ($statusArray as $status) {
|
||||
if (stripos($status, 'AVAILABLE') !== false || stripos($status, 'FREE') !== false) {
|
||||
$isAvailable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isAvailable) {
|
||||
$domainStatus = 'available';
|
||||
} elseif ($daysLeft !== null) {
|
||||
if ($daysLeft < 0) {
|
||||
$domainStatus = 'expired';
|
||||
} elseif ($daysLeft <= 30) {
|
||||
$domainStatus = 'expiring_soon';
|
||||
} else {
|
||||
$domainStatus = 'active';
|
||||
}
|
||||
} else {
|
||||
$domainStatus = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
// Status badge color
|
||||
if ($domainStatus === 'available') {
|
||||
$statusClass = 'bg-blue-100 text-blue-700 border-blue-200';
|
||||
$statusText = 'Available';
|
||||
$statusIcon = 'fa-info-circle';
|
||||
} elseif ($daysLeft !== null && $daysLeft <= 30 && $daysLeft >= 0) {
|
||||
$statusClass = 'bg-orange-100 text-orange-700 border-orange-200';
|
||||
$statusText = 'Expiring Soon';
|
||||
$statusIcon = 'fa-exclamation-triangle';
|
||||
} elseif ($domainStatus === 'active') {
|
||||
$statusClass = 'bg-green-100 text-green-700 border-green-200';
|
||||
$statusText = 'Active';
|
||||
$statusIcon = 'fa-check-circle';
|
||||
} elseif ($domainStatus === 'expired') {
|
||||
$statusClass = 'bg-red-100 text-red-700 border-red-200';
|
||||
$statusText = 'Expired';
|
||||
$statusIcon = 'fa-times-circle';
|
||||
} elseif ($domainStatus === 'error') {
|
||||
$statusClass = 'bg-gray-100 text-gray-700 border-gray-200';
|
||||
$statusText = 'Error';
|
||||
$statusIcon = 'fa-exclamation-circle';
|
||||
} else {
|
||||
$statusClass = 'bg-gray-100 text-gray-700 border-gray-200';
|
||||
$statusText = ucfirst($domainStatus);
|
||||
$statusIcon = 'fa-times-circle';
|
||||
}
|
||||
?>
|
||||
<tr class="hover:bg-gray-50 transition-colors duration-150 domain-row">
|
||||
<td class="px-4 py-4">
|
||||
<input type="checkbox" class="domain-checkbox w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary cursor-pointer" value="<?= $domain['id'] ?>">
|
||||
</td>
|
||||
<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">
|
||||
<a href="/domains/<?= $domain['id'] ?>" class="text-sm font-semibold text-gray-900 hover:text-primary"><?= htmlspecialchars($domain['domain_name']) ?></a>
|
||||
<?php if (!empty($domain['nameservers'])): ?>
|
||||
<div class="text-xs text-gray-500">NS: <?= htmlspecialchars(explode(',', $domain['nameservers'])[0]) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<?php if (!empty($domain['registrar'])): ?>
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-building text-gray-400 mr-2"></i>
|
||||
<span class="text-sm text-gray-900"><?= htmlspecialchars($domain['registrar']) ?></span>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-sm text-gray-400">Unknown</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<?php if (!empty($domain['expiration_date'])): ?>
|
||||
<div class="text-sm">
|
||||
<div class="font-medium text-gray-900"><?= date('M d, Y', strtotime($domain['expiration_date'])) ?></div>
|
||||
<div class="text-xs <?= $expiryClass ?>">
|
||||
<?= $daysLeft ?> days
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-sm text-gray-400">Not set</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 <?= $statusClass ?>">
|
||||
<i class="fas <?= $statusIcon ?> mr-1"></i>
|
||||
<?= $statusText ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<?php if (!empty($domain['group_name'])): ?>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<i class="fas fa-bell mr-1"></i>
|
||||
<?= htmlspecialchars($domain['group_name']) ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="text-gray-400">No Group</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<?php if (!empty($domain['last_checked'])): ?>
|
||||
<div class="flex items-center">
|
||||
<i class="far fa-clock mr-2"></i>
|
||||
<?= date('M d, H:i', strtotime($domain['last_checked'])) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-gray-400">Never</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
<a href="/domains/<?= $domain['id'] ?>" class="text-blue-600 hover:text-blue-800" title="View">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/refresh" class="inline">
|
||||
<button type="submit" class="text-green-600 hover:text-green-800" title="Refresh WHOIS">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
<a href="/domains/<?= $domain['id'] ?>/edit" class="text-yellow-600 hover:text-yellow-800" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/delete" class="inline" onsubmit="return confirm('Delete this domain?')">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Card View (Mobile) - Simplified for brevity -->
|
||||
<div class="lg:hidden divide-y divide-gray-200">
|
||||
<?php foreach ($domains as $domain): ?>
|
||||
<div class="p-6 hover:bg-gray-50 transition-colors duration-150">
|
||||
<div class="flex items-center mb-3">
|
||||
<input type="checkbox" class="domain-checkbox-mobile w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary cursor-pointer mr-3" value="<?= $domain['id'] ?>">
|
||||
<a href="/domains/<?= $domain['id'] ?>" class="text-lg font-semibold text-gray-900 hover:text-primary"><?= htmlspecialchars($domain['domain_name']) ?></a>
|
||||
</div>
|
||||
<!-- Add mobile view content here if needed -->
|
||||
</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 Domains Yet</h3>
|
||||
<p class="text-sm text-gray-500 mb-4">Start monitoring your domains by adding your first one</p>
|
||||
<a href="/domains/create" class="inline-flex items-center px-5 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
<span>Add Your First Domain</span>
|
||||
</a>
|
||||
</div>
|
||||
<?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 '/domains?' . 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; ?>
|
||||
|
||||
<script>
|
||||
// Multi-select functionality
|
||||
function toggleSelectAll(checkbox) {
|
||||
// Only select checkboxes that are currently visible
|
||||
const desktopCheckboxes = document.querySelectorAll('.domain-checkbox');
|
||||
const mobileCheckboxes = document.querySelectorAll('.domain-checkbox-mobile');
|
||||
|
||||
// Check if desktop view is visible (lg:block class)
|
||||
const desktopTable = document.querySelector('.hidden.lg\\:block');
|
||||
const isDesktopVisible = desktopTable && !desktopTable.classList.contains('hidden');
|
||||
|
||||
if (isDesktopVisible) {
|
||||
// Desktop view is visible, select desktop checkboxes
|
||||
desktopCheckboxes.forEach(cb => cb.checked = checkbox.checked);
|
||||
} else {
|
||||
// Mobile view is visible, select mobile checkboxes
|
||||
mobileCheckboxes.forEach(cb => cb.checked = checkbox.checked);
|
||||
}
|
||||
|
||||
updateBulkActions();
|
||||
}
|
||||
|
||||
function updateBulkActions() {
|
||||
// Only count checkboxes that are currently visible
|
||||
const desktopCheckboxes = document.querySelectorAll('.domain-checkbox:checked');
|
||||
const mobileCheckboxes = document.querySelectorAll('.domain-checkbox-mobile:checked');
|
||||
|
||||
// Check if desktop view is visible
|
||||
const desktopTable = document.querySelector('.hidden.lg\\:block');
|
||||
const isDesktopVisible = desktopTable && !desktopTable.classList.contains('hidden');
|
||||
|
||||
const checkboxes = isDesktopVisible ? desktopCheckboxes : mobileCheckboxes;
|
||||
const bulkActions = document.getElementById('bulk-actions');
|
||||
const selectedCount = document.getElementById('selected-count');
|
||||
|
||||
if (checkboxes.length > 0) {
|
||||
bulkActions.classList.remove('hidden');
|
||||
bulkActions.classList.add('flex');
|
||||
selectedCount.textContent = `${checkboxes.length} selected`;
|
||||
} else {
|
||||
bulkActions.classList.add('hidden');
|
||||
bulkActions.classList.remove('flex');
|
||||
}
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
const desktopCheckboxes = document.querySelectorAll('.domain-checkbox');
|
||||
const mobileCheckboxes = document.querySelectorAll('.domain-checkbox-mobile');
|
||||
|
||||
// Check if desktop view is visible
|
||||
const desktopTable = document.querySelector('.hidden.lg\\:block');
|
||||
const isDesktopVisible = desktopTable && !desktopTable.classList.contains('hidden');
|
||||
|
||||
if (isDesktopVisible) {
|
||||
desktopCheckboxes.forEach(cb => cb.checked = false);
|
||||
} else {
|
||||
mobileCheckboxes.forEach(cb => cb.checked = false);
|
||||
}
|
||||
|
||||
document.getElementById('select-all').checked = false;
|
||||
updateBulkActions();
|
||||
}
|
||||
|
||||
function getSelectedIds() {
|
||||
// Only get IDs from currently visible checkboxes
|
||||
const desktopCheckboxes = document.querySelectorAll('.domain-checkbox:checked');
|
||||
const mobileCheckboxes = document.querySelectorAll('.domain-checkbox-mobile:checked');
|
||||
|
||||
// Check if desktop view is visible
|
||||
const desktopTable = document.querySelector('.hidden.lg\\:block');
|
||||
const isDesktopVisible = desktopTable && !desktopTable.classList.contains('hidden');
|
||||
|
||||
const checkboxes = isDesktopVisible ? desktopCheckboxes : mobileCheckboxes;
|
||||
return Array.from(checkboxes).map(cb => cb.value);
|
||||
}
|
||||
|
||||
function bulkRefresh() {
|
||||
const ids = getSelectedIds();
|
||||
if (ids.length === 0) return;
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/domains/bulk-refresh';
|
||||
|
||||
ids.forEach(id => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'domain_ids[]';
|
||||
input.value = id;
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function bulkDelete() {
|
||||
const ids = getSelectedIds();
|
||||
if (ids.length === 0) return;
|
||||
|
||||
if (!confirm(`Delete ${ids.length} domain(s)? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/domains/bulk-delete';
|
||||
|
||||
ids.forEach(id => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'domain_ids[]';
|
||||
input.value = id;
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function toggleAssignGroupDropdown() {
|
||||
const dropdown = document.getElementById('assign-group-dropdown');
|
||||
dropdown.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
// Update bulk assign form with selected IDs
|
||||
document.getElementById('bulk-assign-form')?.addEventListener('submit', function(e) {
|
||||
const ids = getSelectedIds();
|
||||
const container = this;
|
||||
|
||||
// Clear existing hidden inputs
|
||||
container.querySelectorAll('input[name="domain_ids[]"]').forEach(el => el.remove());
|
||||
|
||||
// Add selected IDs
|
||||
ids.forEach(id => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'domain_ids[]';
|
||||
input.value = id;
|
||||
container.appendChild(input);
|
||||
});
|
||||
});
|
||||
|
||||
// Listen to checkbox changes
|
||||
document.querySelectorAll('.domain-checkbox, .domain-checkbox-mobile').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
// Update the select-all checkbox state
|
||||
const desktopCheckboxes = document.querySelectorAll('.domain-checkbox');
|
||||
const mobileCheckboxes = document.querySelectorAll('.domain-checkbox-mobile');
|
||||
|
||||
// Check if desktop view is visible
|
||||
const desktopTable = document.querySelector('.hidden.lg\\:block');
|
||||
const isDesktopVisible = desktopTable && !desktopTable.classList.contains('hidden');
|
||||
|
||||
const checkboxes = isDesktopVisible ? desktopCheckboxes : mobileCheckboxes;
|
||||
const checkedBoxes = isDesktopVisible ?
|
||||
document.querySelectorAll('.domain-checkbox:checked') :
|
||||
document.querySelectorAll('.domain-checkbox-mobile:checked');
|
||||
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
if (checkedBoxes.length === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
} else if (checkedBoxes.length === checkboxes.length) {
|
||||
selectAllCheckbox.checked = true;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
} else {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = true;
|
||||
}
|
||||
|
||||
updateBulkActions();
|
||||
});
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
const dropdown = document.getElementById('assign-group-dropdown');
|
||||
const button = event.target.closest('button[onclick="toggleAssignGroupDropdown()"]');
|
||||
|
||||
if (!button && !dropdown.contains(event.target)) {
|
||||
dropdown?.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle window resize to sync checkboxes when switching between desktop/mobile views
|
||||
window.addEventListener('resize', function() {
|
||||
// Small delay to allow CSS classes to update
|
||||
setTimeout(updateBulkActions, 100);
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
461
app/Views/domains/view.php
Normal file
461
app/Views/domains/view.php
Normal file
@@ -0,0 +1,461 @@
|
||||
<?php
|
||||
$title = 'Domain Details';
|
||||
$pageTitle = htmlspecialchars($domain['domain_name']);
|
||||
$pageDescription = 'Domain information and monitoring status';
|
||||
$pageIcon = 'fas fa-globe';
|
||||
$whoisData = json_decode($domain['whois_data'] ?? '{}', true);
|
||||
$daysLeft = !empty($domain['expiration_date']) ? floor((strtotime($domain['expiration_date']) - time()) / 86400) : null;
|
||||
|
||||
// Recalculate domain status if it's empty or error (for backward compatibility)
|
||||
$domainStatus = $domain['status'];
|
||||
if (empty($domainStatus) || $domainStatus === 'error') {
|
||||
// Check WHOIS data for AVAILABLE status
|
||||
$statusArray = $whoisData['status'] ?? [];
|
||||
$isAvailable = false;
|
||||
foreach ($statusArray as $status) {
|
||||
if (stripos($status, 'AVAILABLE') !== false || stripos($status, 'FREE') !== false) {
|
||||
$isAvailable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isAvailable) {
|
||||
$domainStatus = 'available';
|
||||
} elseif ($daysLeft !== null) {
|
||||
if ($daysLeft < 0) {
|
||||
$domainStatus = 'expired';
|
||||
} elseif ($daysLeft <= 30) {
|
||||
$domainStatus = 'expiring_soon';
|
||||
} else {
|
||||
$domainStatus = 'active';
|
||||
}
|
||||
} else {
|
||||
$domainStatus = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
// Determine expiry color
|
||||
$expiryColor = 'green';
|
||||
if ($daysLeft !== null) {
|
||||
if ($daysLeft < 0) $expiryColor = 'red';
|
||||
elseif ($daysLeft <= 30) $expiryColor = 'orange';
|
||||
elseif ($daysLeft <= 90) $expiryColor = 'yellow';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<!-- Top Action Bar -->
|
||||
<div class="mb-3 flex flex-wrap gap-2 justify-between items-center">
|
||||
<div class="flex gap-2">
|
||||
<?php
|
||||
// Determine domain status badge
|
||||
if ($domainStatus === 'available') {
|
||||
$statusClass = 'bg-blue-100 text-blue-700 border-blue-200';
|
||||
$statusText = 'Available (Not Registered)';
|
||||
$statusIcon = 'fa-info-circle';
|
||||
} elseif ($domainStatus === 'expired') {
|
||||
$statusClass = 'bg-red-100 text-red-700 border-red-200';
|
||||
$statusText = 'Expired';
|
||||
$statusIcon = 'fa-times-circle';
|
||||
} elseif ($domainStatus === 'expiring_soon' || ($daysLeft !== null && $daysLeft <= 30 && $daysLeft >= 0)) {
|
||||
$statusClass = 'bg-orange-100 text-orange-700 border-orange-200';
|
||||
$statusText = 'Expiring Soon';
|
||||
$statusIcon = 'fa-exclamation-triangle';
|
||||
} elseif ($domainStatus === 'active') {
|
||||
$statusClass = 'bg-green-100 text-green-700 border-green-200';
|
||||
$statusText = 'Active';
|
||||
$statusIcon = 'fa-check-circle';
|
||||
} elseif ($domainStatus === 'error') {
|
||||
$statusClass = 'bg-gray-100 text-gray-700 border-gray-200';
|
||||
$statusText = 'Error';
|
||||
$statusIcon = 'fa-exclamation-circle';
|
||||
} else {
|
||||
$statusClass = 'bg-gray-100 text-gray-700 border-gray-200';
|
||||
$statusText = ucfirst($domainStatus);
|
||||
$statusIcon = 'fa-question-circle';
|
||||
}
|
||||
?>
|
||||
<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold <?= $statusClass ?>">
|
||||
<i class="fas <?= $statusIcon ?> mr-1.5"></i>
|
||||
<?= $statusText ?>
|
||||
</span>
|
||||
<?php if ($domainStatus !== 'available'): ?>
|
||||
<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold bg-<?= $expiryColor ?>-100 text-<?= $expiryColor ?>-800 border border-<?= $expiryColor ?>-200">
|
||||
<i class="fas fa-calendar-alt mr-1.5"></i>
|
||||
<?= $daysLeft !== null ? $daysLeft . ' days left' : 'No expiry date' ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold bg-purple-100 text-purple-800 border border-purple-200">
|
||||
<i class="fas fa-<?= $domain['is_active'] ? 'check-circle' : 'pause-circle' ?> mr-1.5"></i>
|
||||
<?= $domain['is_active'] ? 'Monitoring Active' : 'Monitoring Paused' ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/refresh" class="inline">
|
||||
<button type="submit" 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]">
|
||||
<i class="fas fa-sync-alt mr-1.5"></i>
|
||||
Refresh
|
||||
</button>
|
||||
</form>
|
||||
<a href="/domains/<?= $domain['id'] ?>/edit" class="inline-flex items-center justify-center px-3 py-2 bg-blue-600 text-white text-xs rounded-lg hover:bg-blue-700 transition-colors font-medium min-w-[80px] h-[32px]">
|
||||
<i class="fas fa-edit mr-1.5"></i>
|
||||
Edit
|
||||
</a>
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/delete" onsubmit="return confirm('Delete this domain?')" class="inline">
|
||||
<button type="submit" class="inline-flex items-center justify-center px-3 py-2 bg-red-600 text-white text-xs rounded-lg hover:bg-red-700 transition-colors font-medium min-w-[80px] h-[32px]">
|
||||
<i class="fas fa-trash mr-1.5"></i>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
<a href="/domains" 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">
|
||||
|
||||
<!-- Registration Details -->
|
||||
<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-building text-gray-400 mr-2" style="font-size: 10px;"></i>
|
||||
Registration Details
|
||||
</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">Registrar</label>
|
||||
<p class="text-gray-900 font-semibold"><?= htmlspecialchars($domain['registrar'] ?? 'Unknown') ?></p>
|
||||
</div>
|
||||
<?php if (!empty($domain['registrar_url'])): ?>
|
||||
<div>
|
||||
<label class="text-gray-500 font-medium block mb-0.5">Registrar URL</label>
|
||||
<a href="<?= htmlspecialchars($domain['registrar_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
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($domain['abuse_email'])): ?>
|
||||
<div>
|
||||
<label class="text-gray-500 font-medium block mb-0.5">Abuse Contact</label>
|
||||
<a href="mailto:<?= htmlspecialchars($domain['abuse_email']) ?>" class="text-blue-600 hover:text-blue-800">
|
||||
<?= htmlspecialchars($domain['abuse_email']) ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($whoisData['whois_server'])): ?>
|
||||
<div>
|
||||
<label class="text-gray-500 font-medium block mb-0.5">WHOIS Server</label>
|
||||
<p class="text-gray-900 font-mono"><?= htmlspecialchars($whoisData['whois_server']) ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($whoisData['owner'])): ?>
|
||||
<div class="col-span-2">
|
||||
<label class="text-gray-500 font-medium block mb-0.5">Owner</label>
|
||||
<p class="text-gray-900"><?= htmlspecialchars($whoisData['owner']) ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Important Dates -->
|
||||
<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-calendar text-gray-400 mr-2" style="font-size: 10px;"></i>
|
||||
Important Dates
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="space-y-2">
|
||||
<?php if (!empty($domain['expiration_date'])): ?>
|
||||
<div class="flex items-center justify-between p-2 bg-<?= $expiryColor ?>-50 rounded border border-<?= $expiryColor ?>-200">
|
||||
<div class="flex items-center">
|
||||
<div class="w-7 h-7 bg-<?= $expiryColor ?>-500 rounded flex items-center justify-center mr-2">
|
||||
<i class="fas fa-exclamation-triangle text-white text-xs"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-600 font-medium">Expiration</p>
|
||||
<p class="text-xs font-semibold text-gray-900"><?= date('M j, Y', strtotime($domain['expiration_date'])) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="px-2 py-1 bg-<?= $expiryColor ?>-100 text-<?= $expiryColor ?>-800 rounded text-xs font-bold">
|
||||
<?= $daysLeft ?> days
|
||||
</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($domain['updated_date'])): ?>
|
||||
<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-clock 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', strtotime($domain['updated_date'])) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($whoisData['creation_date'])): ?>
|
||||
<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-calendar-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', strtotime($whoisData['creation_date'])) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="flex items-center p-2 bg-purple-50 rounded border border-purple-200">
|
||||
<div class="w-7 h-7 bg-purple-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 Checked</p>
|
||||
<p class="text-xs font-semibold text-gray-900"><?= date('M j, Y H:i', strtotime($domain['last_checked'])) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nameservers -->
|
||||
<?php if (!empty($whoisData['nameservers'])): ?>
|
||||
<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-server text-gray-400 mr-2" style="font-size: 10px;"></i>
|
||||
Nameservers (<?= count($whoisData['nameservers']) ?>)
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="space-y-1.5">
|
||||
<?php foreach ($whoisData['nameservers'] as $index => $ns): ?>
|
||||
<div class="flex items-center p-2 bg-gray-50 rounded hover:bg-gray-100 transition-colors">
|
||||
<div class="w-6 h-6 bg-teal-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($ns) ?></p>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Domain Status -->
|
||||
<?php if (!empty($whoisData['status']) && is_array($whoisData['status'])): ?>
|
||||
<?php
|
||||
// Pre-filter to count only valid statuses
|
||||
$validStatuses = [];
|
||||
foreach ($whoisData['status'] as $status) {
|
||||
$cleanStatus = trim($status);
|
||||
|
||||
// Skip if it's just a URL or starts with http/https or //
|
||||
if (empty($cleanStatus) ||
|
||||
strpos($cleanStatus, 'http') === 0 ||
|
||||
strpos($cleanStatus, '//') === 0 ||
|
||||
strpos($cleanStatus, 'www.') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep the full status text, don't split by spaces
|
||||
// Skip if after cleaning it's empty or just a URL
|
||||
if (empty($cleanStatus) || strpos($cleanStatus, 'http') === 0 || strpos($cleanStatus, '//') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$validStatuses[] = $cleanStatus;
|
||||
}
|
||||
?>
|
||||
<?php if (!empty($validStatuses)): ?>
|
||||
<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>
|
||||
Domain Status (<?= count($validStatuses) ?>)
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<?php foreach ($validStatuses as $cleanStatus): ?>
|
||||
<?php
|
||||
// Convert to readable format
|
||||
$readableStatus = $cleanStatus;
|
||||
|
||||
// Convert camelCase to readable format (for cases like "clientTransferProhibited")
|
||||
$readableStatus = preg_replace('/([a-z])([A-Z])/', '$1 $2', $readableStatus);
|
||||
|
||||
// Convert underscores to spaces and capitalize words
|
||||
$readableStatus = str_replace('_', ' ', $readableStatus);
|
||||
$readableStatus = ucwords(strtolower($readableStatus));
|
||||
?>
|
||||
<span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs font-medium" title="<?= htmlspecialchars($cleanStatus) ?>">
|
||||
<?= htmlspecialchars($readableStatus) ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN -->
|
||||
<div class="space-y-3">
|
||||
|
||||
<!-- Notification Group -->
|
||||
<?php if (!empty($domain['group_name'])): ?>
|
||||
<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-bell text-gray-400 mr-2" style="font-size: 10px;"></i>
|
||||
Notification Group
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center mr-3">
|
||||
<i class="fas fa-users text-green-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-sm text-gray-900"><?= htmlspecialchars($domain['group_name']) ?></p>
|
||||
<?php if (!empty($domain['channels'])): ?>
|
||||
<?php
|
||||
$activeChannels = array_filter($domain['channels'], fn($ch) => $ch['is_active']);
|
||||
?>
|
||||
<p class="text-xs text-gray-600">
|
||||
<?= count($activeChannels) ?> / <?= count($domain['channels']) ?> channels active
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($domain['channels'])): ?>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<?php foreach ($domain['channels'] as $channel): ?>
|
||||
<div class="flex items-center p-2 rounded <?= $channel['is_active'] ? 'bg-green-50 border border-green-200' : 'bg-gray-50 border border-gray-200' ?>">
|
||||
<i class="fas fa-<?= $channel['is_active'] ? 'check-circle text-green-600' : 'times-circle text-gray-400' ?> mr-2 text-xs"></i>
|
||||
<span class="text-xs font-medium text-gray-700"><?= ucfirst($channel['channel_type']) ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="bg-orange-50 rounded-lg border border-orange-200 p-4">
|
||||
<div class="flex items-start mb-2">
|
||||
<i class="fas fa-exclamation-triangle text-orange-500 mr-2 mt-0.5"></i>
|
||||
<div>
|
||||
<h3 class="text-xs font-semibold text-gray-900">No Group Assigned</h3>
|
||||
<p class="text-xs text-gray-600 mt-0.5">Won't receive notifications</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/domains/<?= $domain['id'] ?>/edit" class="block w-full text-center px-3 py-1.5 bg-orange-500 text-white text-xs rounded-lg hover:bg-orange-600 transition-colors font-medium">
|
||||
<i class="fas fa-plus mr-1"></i>
|
||||
Assign Group
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Notification 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>
|
||||
Notification History (<?= count($logs) ?>)
|
||||
</h3>
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<?php if (empty($logs)): ?>
|
||||
<div class="p-8 text-center">
|
||||
<i class="fas fa-bell-slash text-gray-300 text-3xl mb-2"></i>
|
||||
<p class="text-xs text-gray-500">No notifications sent yet</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 text-xs">
|
||||
<thead class="bg-gray-50 sticky top-0">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-600 uppercase">Channel</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-600 uppercase">Status</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-600 uppercase">Date</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-600 uppercase">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<?php foreach ($logs as $log): ?>
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-3 py-2 whitespace-nowrap">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<?= ucfirst($log['channel_type']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">
|
||||
<?php $statusClass = $log['status'] === 'sent' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'; ?>
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium <?= $statusClass ?>">
|
||||
<?= ucfirst($log['status']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap text-gray-600"><?= date('M j, H:i', strtotime($log['sent_at'])) ?></td>
|
||||
<td class="px-3 py-2 text-gray-700 max-w-xs truncate" title="<?= htmlspecialchars($log['message']) ?>">
|
||||
<?= htmlspecialchars($log['message']) ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Raw WHOIS Data (Collapsible) -->
|
||||
<?php if (!empty($domain['whois_data'])): ?>
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<button onclick="toggleWhoisData()" 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 WHOIS Data
|
||||
</span>
|
||||
<i class="fas fa-chevron-down text-gray-400 text-xs transition-transform" id="whois-chevron"></i>
|
||||
</h3>
|
||||
</button>
|
||||
<div id="whois-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($whoisData, JSON_PRETTY_PRINT)) ?></pre>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleWhoisData() {
|
||||
const dataDiv = document.getElementById('whois-data');
|
||||
const chevron = document.getElementById('whois-chevron');
|
||||
dataDiv.classList.toggle('hidden');
|
||||
chevron.classList.toggle('rotate-180');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
85
app/Views/errors/404.php
Normal file
85
app/Views/errors/404.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>404 - Page Not Found</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#4A90E2',
|
||||
dark: '#357ABD',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 min-h-screen flex items-center justify-center p-6">
|
||||
<div class="max-w-2xl w-full">
|
||||
<div class="bg-white rounded-2xl shadow-2xl p-12 text-center">
|
||||
<!-- 404 Icon -->
|
||||
<div class="mb-8">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-500 text-8xl mb-4 animate-pulse"></i>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<h1 class="text-9xl font-bold text-gray-800 mb-4">404</h1>
|
||||
<h2 class="text-3xl font-bold text-gray-700 mb-4">Page Not Found</h2>
|
||||
<p class="text-gray-600 text-lg mb-8 leading-relaxed">
|
||||
Oops! The page you're looking for doesn't exist. It might have been moved or deleted.
|
||||
</p>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="/" class="inline-flex items-center justify-center px-8 py-4 bg-primary text-white rounded-lg hover:bg-primary-dark transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||
<i class="fas fa-home mr-2"></i>
|
||||
Go to Dashboard
|
||||
</a>
|
||||
<button onclick="history.back()" class="inline-flex items-center justify-center px-8 py-4 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-all duration-200 shadow-md hover:shadow-lg">
|
||||
<i class="fas fa-arrow-left mr-2"></i>
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Helpful Links -->
|
||||
<div class="mt-12 pt-8 border-t border-gray-200">
|
||||
<p class="text-sm text-gray-500 mb-4">Quick Links:</p>
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
<a href="/domains" class="text-primary hover:text-primary-dark transition-colors duration-150">
|
||||
<i class="fas fa-globe mr-1"></i>
|
||||
Domains
|
||||
</a>
|
||||
<a href="/groups" class="text-primary hover:text-primary-dark transition-colors duration-150">
|
||||
<i class="fas fa-bell mr-1"></i>
|
||||
Notification Groups
|
||||
</a>
|
||||
<a href="/debug/whois" class="text-primary hover:text-primary-dark transition-colors duration-150">
|
||||
<i class="fas fa-search mr-1"></i>
|
||||
WHOIS Lookup
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="text-center mt-8">
|
||||
<p class="text-gray-600">
|
||||
<i class="fas fa-globe text-primary"></i>
|
||||
<span class="ml-2">Domain Monitor © <?= date('Y') ?></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
102
app/Views/groups/create.php
Normal file
102
app/Views/groups/create.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
$title = 'Create Notification Group';
|
||||
$pageTitle = 'Create Notification Group';
|
||||
$pageDescription = 'Set up a new notification group for your domains';
|
||||
$pageIcon = 'fas fa-plus-circle';
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<!-- Main Form -->
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-bell text-gray-400 mr-2 text-sm"></i>
|
||||
Group Information
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<form method="POST" action="/groups/store" class="space-y-5">
|
||||
<!-- Group Name -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Group Name *
|
||||
</label>
|
||||
<input type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
placeholder="e.g., Production Alerts, Team Notifications"
|
||||
required
|
||||
autofocus>
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
Choose a descriptive name for this notification group
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Description (Optional)
|
||||
</label>
|
||||
<textarea id="description"
|
||||
name="description"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
rows="4"
|
||||
placeholder="Add details about this notification group, its purpose, or who should be notified..."></textarea>
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
Optional: Add notes to help identify this group's purpose
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-3 pt-3">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center justify-center px-5 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg font-medium transition-colors text-sm">
|
||||
<i class="fas fa-plus-circle mr-2"></i>
|
||||
Create Group
|
||||
</button>
|
||||
<a href="/groups"
|
||||
class="inline-flex items-center justify-center px-5 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
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Section -->
|
||||
<div class="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-info-circle text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-1">Next Steps</h3>
|
||||
<ul class="text-xs text-gray-600 space-y-1">
|
||||
<li class="flex items-center">
|
||||
<i class="fas fa-circle text-blue-500" style="font-size: 6px;"></i>
|
||||
<span class="ml-2">After creating the group, you'll be able to add notification channels (Email, Telegram, Discord, Slack)</span>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<i class="fas fa-circle text-blue-500" style="font-size: 6px;"></i>
|
||||
<span class="ml-2">Configure each channel with the necessary credentials and settings</span>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<i class="fas fa-circle text-blue-500" style="font-size: 6px;"></i>
|
||||
<span class="ml-2">Assign domains to this group to start receiving notifications</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
304
app/Views/groups/edit.php
Normal file
304
app/Views/groups/edit.php
Normal file
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
$title = 'Edit Notification Group';
|
||||
$pageTitle = 'Edit Notification Group';
|
||||
$pageDescription = htmlspecialchars($group['name']);
|
||||
$pageIcon = 'fas fa-edit';
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div class="max-w-7xl mx-auto space-y-4">
|
||||
<!-- Group Details Form -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-info-circle text-gray-400 mr-2 text-sm"></i>
|
||||
Group Details
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<form method="POST" action="/groups/update" class="space-y-5">
|
||||
<input type="hidden" name="id" value="<?= $group['id'] ?>">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<!-- Group Name -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Group Name *
|
||||
</label>
|
||||
<input type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
value="<?= htmlspecialchars($group['name']) ?>"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Description (Optional)
|
||||
</label>
|
||||
<textarea id="description"
|
||||
name="description"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
rows="3"><?= htmlspecialchars($group['description'] ?? '') ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-5 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>
|
||||
Update Group
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification Channels -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-plug text-gray-400 mr-2 text-sm"></i>
|
||||
Notification Channels
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<?php if (empty($group['channels'])): ?>
|
||||
<div class="text-center py-10">
|
||||
<i class="fas fa-plug text-gray-300 text-5xl mb-3"></i>
|
||||
<p class="text-gray-500">No channels configured yet</p>
|
||||
<p class="text-sm text-gray-400 mt-1">Add your first channel below to start receiving notifications</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||
<?php foreach ($group['channels'] as $channel):
|
||||
$config = json_decode($channel['channel_config'], true);
|
||||
$icons = ['email' => 'fa-envelope', 'telegram' => 'fa-telegram', 'discord' => 'fa-discord', 'slack' => 'fa-slack'];
|
||||
$colors = ['email' => 'blue', 'telegram' => 'blue', 'discord' => 'indigo', 'slack' => 'purple'];
|
||||
$icon = $icons[$channel['channel_type']] ?? 'fa-bell';
|
||||
$color = $colors[$channel['channel_type']] ?? 'gray';
|
||||
?>
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-6 hover:shadow-md transition-shadow duration-200">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="w-12 h-12 bg-<?= $color ?>-100 rounded-lg flex items-center justify-center">
|
||||
<i class="fab <?= $icon ?> text-<?= $color ?>-600 text-xl"></i>
|
||||
</div>
|
||||
<span class="px-3 py-1 rounded-full text-xs font-semibold <?= $channel['is_active'] ? 'bg-green-100 text-green-800' : 'bg-gray-200 text-gray-600' ?>">
|
||||
<?= $channel['is_active'] ? 'Active' : 'Disabled' ?>
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-800 mb-2"><?= ucfirst($channel['channel_type']) ?></h3>
|
||||
<p class="text-sm text-gray-600 mb-4 truncate">
|
||||
<?php
|
||||
if ($channel['channel_type'] === 'email') {
|
||||
echo htmlspecialchars($config['email'] ?? 'No email');
|
||||
} elseif ($channel['channel_type'] === 'telegram') {
|
||||
echo "Chat: " . htmlspecialchars($config['chat_id'] ?? 'N/A');
|
||||
} else {
|
||||
echo "Webhook configured";
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<a href="/channels/toggle?id=<?= $channel['id'] ?>&group_id=<?= $group['id'] ?>"
|
||||
class="flex-1 px-3 py-2 bg-yellow-50 text-yellow-700 rounded text-center text-sm hover:bg-yellow-100 transition-colors duration-150">
|
||||
<i class="fas fa-<?= $channel['is_active'] ? 'pause' : 'play' ?> mr-1"></i>
|
||||
<?= $channel['is_active'] ? 'Disable' : 'Enable' ?>
|
||||
</a>
|
||||
<a href="/channels/delete?id=<?= $channel['id'] ?>&group_id=<?= $group['id'] ?>"
|
||||
class="flex-1 px-3 py-2 bg-red-50 text-red-700 rounded text-center text-sm hover:bg-red-100 transition-colors duration-150"
|
||||
onclick="return confirm('Delete this channel?')">
|
||||
<i class="fas fa-trash mr-1"></i>
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Add Channel Form -->
|
||||
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<i class="fas fa-plus-circle text-gray-400 mr-2 text-sm"></i>
|
||||
Add New Channel
|
||||
</h3>
|
||||
|
||||
<form method="POST" action="/channels/add" id="channelForm" class="space-y-5">
|
||||
<input type="hidden" name="group_id" value="<?= $group['id'] ?>">
|
||||
|
||||
<!-- Channel Type -->
|
||||
<div>
|
||||
<label for="channel_type" class="block text-sm font-medium text-gray-700 mb-1.5">Channel Type</label>
|
||||
<select id="channel_type"
|
||||
name="channel_type"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
onchange="toggleChannelFields()">
|
||||
<option value="">-- Select Channel Type --</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="slack">Slack</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Email Fields -->
|
||||
<div id="email_fields" class="hidden space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Email Address
|
||||
</label>
|
||||
<input type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
placeholder="user@example.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Telegram Fields -->
|
||||
<div id="telegram_fields" class="hidden space-y-4">
|
||||
<div>
|
||||
<label for="bot_token" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Bot Token
|
||||
</label>
|
||||
<input type="text"
|
||||
id="bot_token"
|
||||
name="bot_token"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11">
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
Get from @BotFather on Telegram
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="chat_id" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Chat ID
|
||||
</label>
|
||||
<input type="text"
|
||||
id="chat_id"
|
||||
name="chat_id"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
placeholder="123456789">
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
Use @userinfobot to get your chat ID
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Discord Fields -->
|
||||
<div id="discord_fields" class="hidden space-y-4">
|
||||
<div>
|
||||
<label for="discord_webhook" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Webhook URL
|
||||
</label>
|
||||
<input type="url"
|
||||
id="discord_webhook"
|
||||
name="webhook_url"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
placeholder="https://discord.com/api/webhooks/...">
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
Create in Discord Server Settings → Integrations → Webhooks
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slack Fields -->
|
||||
<div id="slack_fields" class="hidden space-y-4">
|
||||
<div>
|
||||
<label for="slack_webhook" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Webhook URL
|
||||
</label>
|
||||
<input type="url"
|
||||
id="slack_webhook"
|
||||
name="webhook_url"
|
||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||
placeholder="https://hooks.slack.com/services/...">
|
||||
<p class="mt-1.5 text-xs text-gray-500">
|
||||
Create in Slack App Settings → Incoming Webhooks
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-5 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg font-medium transition-colors text-sm">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Add Channel
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assigned Domains -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-globe text-gray-400 mr-2 text-sm"></i>
|
||||
Assigned Domains (<?= count($group['domains']) ?>)
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<?php if (empty($group['domains'])): ?>
|
||||
<div class="text-center py-10">
|
||||
<i class="fas fa-globe text-gray-300 text-5xl mb-3"></i>
|
||||
<p class="text-gray-500">No domains assigned to this group yet</p>
|
||||
<a href="/domains/create" class="mt-3 inline-flex items-center px-5 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Add a Domain
|
||||
</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<?php foreach ($group['domains'] as $domain): ?>
|
||||
<a href="/domains/<?= $domain['id'] ?>" class="block bg-gray-50 border border-gray-200 rounded-lg p-6 hover:shadow-md hover:border-primary transition-all duration-200">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="w-12 h-12 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-globe text-primary text-xl"></i>
|
||||
</div>
|
||||
<?php
|
||||
$statusClass = $domain['status'] === 'active' ? 'bg-green-100 text-green-800' : 'bg-gray-200 text-gray-600';
|
||||
?>
|
||||
<span class="px-3 py-1 rounded-full text-xs font-semibold <?= $statusClass ?>">
|
||||
<?= ucfirst($domain['status']) ?>
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-800 mb-2 truncate"><?= htmlspecialchars($domain['domain_name']) ?></h3>
|
||||
<p class="text-sm text-gray-600 flex items-center">
|
||||
<i class="far fa-calendar mr-2"></i>
|
||||
Expires: <?= date('M j, Y', strtotime($domain['expiration_date'])) ?>
|
||||
</p>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleChannelFields() {
|
||||
const channelType = document.getElementById('channel_type').value;
|
||||
|
||||
// Hide all fields
|
||||
document.getElementById('email_fields').classList.add('hidden');
|
||||
document.getElementById('telegram_fields').classList.add('hidden');
|
||||
document.getElementById('discord_fields').classList.add('hidden');
|
||||
document.getElementById('slack_fields').classList.add('hidden');
|
||||
|
||||
// Show selected field
|
||||
if (channelType) {
|
||||
document.getElementById(channelType + '_fields').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
156
app/Views/groups/index.php
Normal file
156
app/Views/groups/index.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
$title = 'Notification Groups';
|
||||
$pageTitle = 'Notification Groups';
|
||||
$pageDescription = 'Manage notification channels and assignments';
|
||||
$pageIcon = 'fas fa-bell';
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="mb-4 flex justify-end">
|
||||
<a href="/groups/create" class="inline-flex items-center px-4 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Create New Group
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Info Card -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-info-circle text-blue-500 text-lg"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-1">About Notification Groups</h3>
|
||||
<p class="text-xs text-gray-600 leading-relaxed">
|
||||
Notification groups allow you to organize your notification channels. You can create multiple channels
|
||||
(Email, Telegram, Discord, Slack) within each group, then assign domains to the group. When a domain
|
||||
is about to expire, all active channels in its group will receive notifications.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Groups List -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<?php if (!empty($groups)): ?>
|
||||
<!-- Table View (Desktop) -->
|
||||
<div class="hidden md: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-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Group Name</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Description</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Channels</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Domains</th>
|
||||
<th class="px-6 py-4 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 ($groups as $group): ?>
|
||||
<tr class="hover:bg-gray-50 transition-colors duration-150">
|
||||
<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-bell text-primary"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-semibold text-gray-900"><?= htmlspecialchars($group['name']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-700 max-w-xs truncate">
|
||||
<?= htmlspecialchars($group['description'] ?? 'No description') ?>
|
||||
</div>
|
||||
</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 bg-blue-100 text-blue-800">
|
||||
<i class="fas fa-plug mr-1"></i>
|
||||
<?= $group['channel_count'] ?> channel<?= $group['channel_count'] != 1 ? 's' : '' ?>
|
||||
</span>
|
||||
</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 bg-green-100 text-green-800">
|
||||
<i class="fas fa-globe mr-1"></i>
|
||||
<?= $group['domain_count'] ?> domain<?= $group['domain_count'] != 1 ? 's' : '' ?>
|
||||
</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="/groups/edit?id=<?= $group['id'] ?>" class="text-blue-600 hover:text-blue-800" title="Manage">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
<a href="/groups/delete?id=<?= $group['id'] ?>"
|
||||
class="text-red-600 hover:text-red-800"
|
||||
title="Delete"
|
||||
onclick="return confirm('Are you sure? Domains will be unassigned from this group.')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Card View (Mobile) -->
|
||||
<div class="md:hidden divide-y divide-gray-200">
|
||||
<?php foreach ($groups as $group): ?>
|
||||
<div class="p-6 hover:bg-gray-50 transition-colors duration-150">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<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-bell text-primary"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="font-semibold text-gray-900"><?= htmlspecialchars($group['name']) ?></h3>
|
||||
<p class="text-sm text-gray-500"><?= htmlspecialchars($group['description'] ?? 'No description') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3 mb-3">
|
||||
<span class="px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<i class="fas fa-plug mr-1"></i>
|
||||
<?= $group['channel_count'] ?> channels
|
||||
</span>
|
||||
<span class="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
<i class="fas fa-globe mr-1"></i>
|
||||
<?= $group['domain_count'] ?> domains
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2">
|
||||
<a href="/groups/edit?id=<?= $group['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-cog mr-1"></i> Manage
|
||||
</a>
|
||||
<a href="/groups/delete?id=<?= $group['id'] ?>"
|
||||
class="flex-1 px-3 py-1.5 bg-red-50 text-red-600 rounded text-center text-sm hover:bg-red-100 transition-colors"
|
||||
onclick="return confirm('Are you sure? Domains will be unassigned from this group.')">
|
||||
<i class="fas fa-trash mr-1"></i> Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="text-center py-12 px-6">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-bell-slash text-gray-300 text-6xl"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 mb-1">No Notification Groups</h3>
|
||||
<p class="text-sm text-gray-500 mb-4">Create your first notification group to start receiving alerts</p>
|
||||
<a href="/groups/create" class="inline-flex items-center px-5 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Create Your First Group
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
310
app/Views/layout/base.php
Normal file
310
app/Views/layout/base.php
Normal file
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
/**
|
||||
* Base Layout Template
|
||||
* Contains: HTML structure, meta tags, CSS/JS includes, global stats
|
||||
*/
|
||||
|
||||
// Fetch global stats for sidebar (available on all pages)
|
||||
if (!isset($globalStats)) {
|
||||
try {
|
||||
$pdo = \Core\Database::getConnection();
|
||||
|
||||
// Get total domains
|
||||
$totalStmt = $pdo->query("SELECT COUNT(*) as count FROM domains");
|
||||
$totalResult = $totalStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
$total = $totalResult['count'] ?? 0;
|
||||
|
||||
// Get active domains
|
||||
$activeStmt = $pdo->query("SELECT COUNT(*) as count FROM domains WHERE is_active = 1");
|
||||
$activeResult = $activeStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
$active = $activeResult['count'] ?? 0;
|
||||
|
||||
// Get expiring soon (within 30 days)
|
||||
$expiringSoonStmt = $pdo->query("SELECT COUNT(*) as count FROM domains WHERE expiration_date IS NOT NULL AND expiration_date <= DATE_ADD(NOW(), INTERVAL 30 DAY) AND expiration_date >= NOW()");
|
||||
$expiringSoonResult = $expiringSoonStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
$expiringSoon = $expiringSoonResult['count'] ?? 0;
|
||||
|
||||
$globalStats = [
|
||||
'total' => $total,
|
||||
'active' => $active,
|
||||
'expiring_soon' => $expiringSoon
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$globalStats = [
|
||||
'total' => 0,
|
||||
'active' => 0,
|
||||
'expiring_soon' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="description" content="Domain Monitor - Track and monitor your domain expiration dates">
|
||||
<meta name="author" content="Domain Monitor">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<!-- Title -->
|
||||
<title><?= $title ?? 'Domain Monitor' ?> - <?= $_ENV['APP_NAME'] ?? 'Domain Monitor' ?></title>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Flag Icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icons/7.5.0/css/flag-icons.min.css" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- Tailwind Configuration -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#4A90E2',
|
||||
dark: '#357ABD',
|
||||
light: '#6BA3E8',
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: '#1F2937',
|
||||
light: '#374151',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
|
||||
<!-- Custom Page Styles (optional) -->
|
||||
<?php if (isset($customStyles)): ?>
|
||||
<style><?= $customStyles ?></style>
|
||||
<?php endif; ?>
|
||||
|
||||
<style>
|
||||
/* Sidebar full height */
|
||||
.sidebar {
|
||||
height: 100vh;
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dropdown animation */
|
||||
.dropdown-menu {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.dropdown-menu.show {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Active sidebar link */
|
||||
.sidebar-link.active {
|
||||
background: #374151;
|
||||
border-left: 4px solid #4A90E2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
|
||||
<?php include __DIR__ . '/top-nav.php'; ?>
|
||||
|
||||
<?php include __DIR__ . '/sidebar.php'; ?>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="md:ml-64 pt-16 min-h-screen bg-gray-50">
|
||||
<div class="p-6">
|
||||
<!-- Flash Messages -->
|
||||
<?php include __DIR__ . '/messages.php'; ?>
|
||||
|
||||
<!-- Page Content -->
|
||||
<?php if (isset($content)): ?>
|
||||
<?= $content ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Global Scripts -->
|
||||
<script>
|
||||
// Toggle sidebar on mobile
|
||||
function toggleSidebar() {
|
||||
document.getElementById('sidebar').classList.toggle('open');
|
||||
}
|
||||
|
||||
// Toggle user dropdown
|
||||
function toggleDropdown() {
|
||||
document.getElementById('userDropdown').classList.toggle('show');
|
||||
}
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
const dropdown = document.getElementById('userDropdown');
|
||||
const isClickInside = event.target.closest('[onclick="toggleDropdown()"]') || event.target.closest('#userDropdown');
|
||||
|
||||
if (!isClickInside && dropdown && dropdown.classList.contains('show')) {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Live Search Functionality
|
||||
let searchTimeout;
|
||||
const searchInput = document.getElementById('globalSearchInput');
|
||||
const searchDropdown = document.getElementById('searchDropdown');
|
||||
const searchResults = document.getElementById('searchResults');
|
||||
const searchLoading = document.getElementById('searchLoading');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
const query = e.target.value.trim();
|
||||
|
||||
clearTimeout(searchTimeout);
|
||||
|
||||
if (query.length < 2) {
|
||||
searchDropdown.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading
|
||||
searchDropdown.classList.remove('hidden');
|
||||
searchLoading.classList.remove('hidden');
|
||||
searchResults.innerHTML = '';
|
||||
|
||||
// Debounce search
|
||||
searchTimeout = setTimeout(() => {
|
||||
fetch(`/api/search/suggest?q=${encodeURIComponent(query)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
searchLoading.classList.add('hidden');
|
||||
renderSearchResults(data);
|
||||
})
|
||||
.catch(error => {
|
||||
searchLoading.classList.add('hidden');
|
||||
searchResults.innerHTML = '<div class="p-4 text-red-600 text-sm">Error loading results</div>';
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
const searchForm = document.getElementById('globalSearchForm');
|
||||
if (searchForm) {
|
||||
searchForm.addEventListener('submit', function(e) {
|
||||
searchDropdown.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
if (searchDropdown && !searchDropdown.contains(event.target) && event.target !== searchInput) {
|
||||
searchDropdown.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderSearchResults(data) {
|
||||
let html = '';
|
||||
|
||||
if (data.domains && data.domains.length > 0) {
|
||||
html += '<div class="p-2">';
|
||||
html += '<p class="px-3 py-2 text-xs font-semibold text-gray-500 uppercase">Your Domains</p>';
|
||||
|
||||
data.domains.forEach(domain => {
|
||||
const statusColors = {
|
||||
'red': 'text-red-600',
|
||||
'orange': 'text-orange-600',
|
||||
'yellow': 'text-yellow-600',
|
||||
'green': 'text-green-600',
|
||||
'gray': 'text-gray-400'
|
||||
};
|
||||
const colorClass = statusColors[domain.status_color] || 'text-gray-600';
|
||||
|
||||
html += `
|
||||
<a href="/domains/${domain.id}" class="block px-3 py-2 hover:bg-gray-50 rounded-lg transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-semibold text-gray-900 truncate">${escapeHtml(domain.domain_name)}</p>
|
||||
<p class="text-xs text-gray-500">${escapeHtml(domain.registrar || 'Unknown registrar')}</p>
|
||||
</div>
|
||||
${domain.days_left !== null ? `
|
||||
<div class="ml-3 text-right">
|
||||
<p class="text-xs font-semibold ${colorClass}">${domain.days_left} days</p>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Show WHOIS lookup option if no results and looks like a domain
|
||||
if (data.domains.length === 0 && data.isDomainLike) {
|
||||
html += `
|
||||
<div class="p-4 border-t border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Domain not in portfolio</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Perform WHOIS lookup for ${escapeHtml(data.query)}</p>
|
||||
</div>
|
||||
<button onclick="window.location.href='/search?q=${encodeURIComponent(data.query)}'" class="px-3 py-1.5 bg-primary text-white text-xs rounded-lg hover:bg-primary-dark">
|
||||
Lookup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (data.domains.length === 0) {
|
||||
html += '<div class="p-4 text-center text-sm text-gray-500">No results found</div>';
|
||||
}
|
||||
|
||||
// Add "View all results" link if there are results
|
||||
if (data.domains.length > 0) {
|
||||
html += `
|
||||
<div class="border-t border-gray-200 p-2">
|
||||
<a href="/search?q=${encodeURIComponent(data.query)}" class="block px-3 py-2 text-center text-sm font-medium text-primary hover:bg-gray-50 rounded-lg">
|
||||
View all results →
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
searchResults.innerHTML = html;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Custom Page Scripts (optional) -->
|
||||
<?php if (isset($customScripts)): ?>
|
||||
<script><?= $customScripts ?></script>
|
||||
<?php endif; ?>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
119
app/Views/layout/messages.php
Normal file
119
app/Views/layout/messages.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<!-- Toast Notifications Container -->
|
||||
<div id="toast-container" class="fixed bottom-4 right-4 z-50 space-y-3 max-w-sm">
|
||||
|
||||
<!-- Success Toast -->
|
||||
<?php if (isset($_SESSION['success'])): ?>
|
||||
<div class="toast bg-white border-l-4 border-green-500 rounded-lg shadow-lg p-4 flex items-start animate-slide-in">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-check text-green-600 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">Success</p>
|
||||
<p class="text-sm text-gray-600 mt-0.5"><?= htmlspecialchars($_SESSION['success']) ?></p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php unset($_SESSION['success']); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Error Toast -->
|
||||
<?php if (isset($_SESSION['error'])): ?>
|
||||
<div class="toast bg-white border-l-4 border-red-500 rounded-lg shadow-lg p-4 flex items-start animate-slide-in">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-times text-red-600 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">Error</p>
|
||||
<p class="text-sm text-gray-600 mt-0.5"><?= htmlspecialchars($_SESSION['error']) ?></p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php unset($_SESSION['error']); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Warning Toast -->
|
||||
<?php if (isset($_SESSION['warning'])): ?>
|
||||
<div class="toast bg-white border-l-4 border-orange-500 rounded-lg shadow-lg p-4 flex items-start animate-slide-in">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-exclamation-triangle text-orange-600 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">Warning</p>
|
||||
<p class="text-sm text-gray-600 mt-0.5"><?= htmlspecialchars($_SESSION['warning']) ?></p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php unset($_SESSION['warning']); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Info Toast -->
|
||||
<?php if (isset($_SESSION['info'])): ?>
|
||||
<div class="toast bg-white border-l-4 border-blue-500 rounded-lg shadow-lg p-4 flex items-start animate-slide-in">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-info text-blue-600 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">Info</p>
|
||||
<p class="text-sm text-gray-600 mt-0.5"><?= htmlspecialchars($_SESSION['info']) ?></p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php unset($_SESSION['info']); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Toast Auto-Dismiss Script -->
|
||||
<script>
|
||||
// Auto-dismiss toasts after 5 seconds
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const toasts = document.querySelectorAll('.toast');
|
||||
|
||||
toasts.forEach(toast => {
|
||||
// Add fade-out animation after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
|
||||
// Remove from DOM after animation
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-in {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
102
app/Views/layout/sidebar.php
Normal file
102
app/Views/layout/sidebar.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside id="sidebar" class="sidebar fixed left-0 top-0 w-64 bg-gray-900 text-white z-30">
|
||||
<div class="h-full overflow-y-auto flex flex-col">
|
||||
|
||||
<!-- Logo Section -->
|
||||
<div class="h-16 px-5 border-b border-gray-800 flex items-center">
|
||||
<div class="flex items-center">
|
||||
<div class="w-9 h-9 bg-primary rounded-lg flex items-center justify-center mr-3">
|
||||
<i class="fas fa-globe text-white text-sm"></i>
|
||||
</div>
|
||||
<h1 class="text-sm font-semibold text-white"><?= $_ENV['APP_NAME'] ?? 'Domain Monitor' ?></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<nav class="px-4 py-3">
|
||||
<div class="space-y-0.5">
|
||||
<a href="/" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= $_SERVER['REQUEST_URI'] === '/' ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-chart-line text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">Dashboard</span>
|
||||
</a>
|
||||
|
||||
<a href="/domains" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/domains') !== false ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-globe text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">Domains</span>
|
||||
</a>
|
||||
|
||||
<a href="/groups" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/groups') !== false ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-bell text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">Notification Groups</span>
|
||||
</a>
|
||||
|
||||
<a href="/tld-registry" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/tld-registry') !== false ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-database text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">TLD Registry</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Tools Section -->
|
||||
<div class="mt-4 pt-3 border-t border-gray-800">
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider px-3 mb-1">Tools</p>
|
||||
<div class="space-y-0.5">
|
||||
<a href="/debug/whois" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150">
|
||||
<i class="fas fa-search text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">WHOIS Lookup</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Quick Stats Cards - Pinned to Bottom -->
|
||||
<div class="mt-auto px-4 pb-3 border-t border-gray-800 pt-3">
|
||||
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wider px-3 mb-2">Quick Stats</div>
|
||||
<div class="space-y-1.5">
|
||||
<div class="bg-gray-800 rounded-md p-2.5 border border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-7 h-7 bg-blue-500/20 rounded flex items-center justify-center mr-2.5">
|
||||
<i class="fas fa-globe text-blue-400 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-gray-400 text-xs">Total</span>
|
||||
</div>
|
||||
<span class="text-white font-semibold text-sm"><?= $globalStats['total'] ?? 0 ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800 rounded-md p-2.5 border border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-7 h-7 bg-orange-500/20 rounded flex items-center justify-center mr-2.5">
|
||||
<i class="fas fa-exclamation-triangle text-orange-400 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-gray-400 text-xs">Expiring</span>
|
||||
</div>
|
||||
<span class="text-orange-400 font-semibold text-sm"><?= $globalStats['expiring_soon'] ?? 0 ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800 rounded-md p-2.5 border border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-7 h-7 bg-green-500/20 rounded flex items-center justify-center mr-2.5">
|
||||
<i class="fas fa-check-circle text-green-400 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-gray-400 text-xs">Active</span>
|
||||
</div>
|
||||
<span class="text-green-400 font-semibold text-sm"><?= $globalStats['active'] ?? 0 ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-4 py-3 border-t border-gray-800">
|
||||
<div class="text-center">
|
||||
<p class="text-xs text-gray-500">© <?= date('Y') ?> Domain Monitor</p>
|
||||
<p class="text-xs text-gray-600 mt-0.5">v1.0.0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
133
app/Views/layout/top-nav.php
Normal file
133
app/Views/layout/top-nav.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<!-- Top Navigation Bar -->
|
||||
<nav class="bg-white border-b border-gray-200 fixed top-0 left-0 md:left-64 right-0 z-20">
|
||||
<div class="px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Left: Menu button and Page Header -->
|
||||
<div class="flex items-center min-w-0">
|
||||
<button onclick="toggleSidebar()" class="text-gray-500 hover:text-gray-700 focus:outline-none focus:text-gray-700 md:hidden mr-4">
|
||||
<i class="fas fa-bars text-xl"></i>
|
||||
</button>
|
||||
|
||||
<!-- Page Title & Description -->
|
||||
<div class="hidden md:block">
|
||||
<h2 class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<?php if (isset($pageIcon)): ?>
|
||||
<i class="<?= $pageIcon ?> text-primary mr-2"></i>
|
||||
<?php endif; ?>
|
||||
<?= $pageTitle ?? $title ?? 'Dashboard' ?>
|
||||
</h2>
|
||||
<?php if (isset($pageDescription)): ?>
|
||||
<p class="text-sm text-gray-600 mt-0.5"><?= $pageDescription ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center: Search Bar -->
|
||||
<div class="flex-1 max-w-2xl mx-8">
|
||||
<form action="/search" method="GET" class="relative hidden md:block" id="globalSearchForm">
|
||||
<input type="text"
|
||||
name="q"
|
||||
placeholder="Search domains or lookup WHOIS..."
|
||||
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent text-sm"
|
||||
id="globalSearchInput"
|
||||
autocomplete="off">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
|
||||
|
||||
<!-- Search Results Dropdown -->
|
||||
<div id="searchDropdown" class="hidden absolute top-full left-0 right-0 mt-2 bg-white rounded-lg shadow-xl border border-gray-200 max-h-96 overflow-y-auto z-50">
|
||||
<!-- Loading state -->
|
||||
<div id="searchLoading" class="hidden p-4 text-center">
|
||||
<i class="fas fa-spinner fa-spin text-primary"></i>
|
||||
<p class="text-sm text-gray-600 mt-2">Searching...</p>
|
||||
</div>
|
||||
|
||||
<!-- Results will be inserted here -->
|
||||
<div id="searchResults"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Right: Actions & User -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Quick Add Domain -->
|
||||
<a href="/domains/create" title="Add Domain" class="flex items-center justify-center w-9 h-9 bg-primary hover:bg-primary-dark text-white rounded-lg transition-colors duration-150">
|
||||
<i class="fas fa-plus"></i>
|
||||
</a>
|
||||
|
||||
<!-- Notifications -->
|
||||
<button title="Notifications" class="relative flex items-center justify-center w-9 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors duration-150">
|
||||
<i class="fas fa-bell"></i>
|
||||
<?php if (($globalStats['expiring_soon'] ?? 0) > 0): ?>
|
||||
<span class="absolute top-1 right-1 flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
|
||||
<!-- Settings -->
|
||||
<button title="Settings" class="flex items-center justify-center w-9 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors duration-150">
|
||||
<i class="fas fa-cog"></i>
|
||||
</button>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="hidden md:block h-8 w-px bg-gray-300"></div>
|
||||
|
||||
<!-- User Dropdown -->
|
||||
<div class="relative">
|
||||
<button onclick="toggleDropdown()" class="flex items-center space-x-3 p-2 hover:bg-gray-100 rounded-lg transition-colors duration-150 focus:outline-none">
|
||||
<div class="w-9 h-9 rounded-full bg-primary flex items-center justify-center text-white font-semibold">
|
||||
<?= strtoupper(substr($_SESSION['username'] ?? 'A', 0, 1)) ?>
|
||||
</div>
|
||||
<div class="hidden lg:block text-left">
|
||||
<p class="text-sm font-medium text-gray-700"><?= htmlspecialchars($_SESSION['username'] ?? 'User') ?></p>
|
||||
<p class="text-xs text-gray-500">Administrator</p>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down text-gray-400 text-xs hidden md:block"></i>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<div id="userDropdown" class="dropdown-menu absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg py-2 border border-gray-200">
|
||||
<div class="px-4 py-3 border-b border-gray-200">
|
||||
<p class="text-sm font-medium text-gray-900"><?= htmlspecialchars($_SESSION['full_name'] ?? $_SESSION['username'] ?? 'User') ?></p>
|
||||
<p class="text-xs text-gray-500 mt-1"><?= htmlspecialchars($_SESSION['email'] ?? 'admin@example.com') ?></p>
|
||||
<span class="inline-block mt-2 px-2 py-1 bg-green-100 text-green-800 text-xs font-semibold rounded">
|
||||
<i class="fas fa-circle text-xs mr-1"></i>Online
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
|
||||
<i class="fas fa-user-circle w-5 text-gray-400 mr-3"></i>
|
||||
My Profile
|
||||
</a>
|
||||
|
||||
<a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
|
||||
<i class="fas fa-cog w-5 text-gray-400 mr-3"></i>
|
||||
Account Settings
|
||||
</a>
|
||||
|
||||
<a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
|
||||
<i class="fas fa-bell w-5 text-gray-400 mr-3"></i>
|
||||
Notifications
|
||||
</a>
|
||||
|
||||
<div class="border-t border-gray-200 my-1"></div>
|
||||
|
||||
<a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
|
||||
<i class="fas fa-question-circle w-5 text-gray-400 mr-3"></i>
|
||||
Help & Support
|
||||
</a>
|
||||
|
||||
<div class="border-t border-gray-200 my-1"></div>
|
||||
|
||||
<a href="/logout" class="flex items-center px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors duration-150">
|
||||
<i class="fas fa-sign-out-alt w-5 mr-3"></i>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
304
app/Views/search/results.php
Normal file
304
app/Views/search/results.php
Normal file
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
$title = 'Search Results';
|
||||
$pageTitle = 'Search Results';
|
||||
$pageDescription = 'Results for "' . htmlspecialchars($query) . '"';
|
||||
$pageIcon = 'fas fa-search';
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<!-- Search Query Display -->
|
||||
<div class="mb-4 bg-white rounded-lg border border-gray-200 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Searching for:</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900"><?= htmlspecialchars($query) ?></h3>
|
||||
</div>
|
||||
<form action="/search" method="GET" class="flex gap-2">
|
||||
<input type="text"
|
||||
name="q"
|
||||
value="<?= htmlspecialchars($query) ?>"
|
||||
placeholder="Search again..."
|
||||
class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm"
|
||||
autofocus>
|
||||
<button type="submit" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark text-sm font-medium">
|
||||
<i class="fas fa-search mr-2"></i>
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Info & Per Page Selector -->
|
||||
<?php if (!empty($existingDomains) && $pagination['total'] > 0): ?>
|
||||
<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> result(s)
|
||||
</div>
|
||||
|
||||
<form method="GET" action="/search" class="flex items-center gap-2">
|
||||
<input type="hidden" name="q" value="<?= htmlspecialchars($query) ?>">
|
||||
|
||||
<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>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($existingDomains)): ?>
|
||||
<!-- Existing Domains Found -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden mb-4">
|
||||
<div class="px-6 py-4 border-b border-gray-200 bg-green-50">
|
||||
<h2 class="text-lg font-semibold text-green-900 flex items-center">
|
||||
<i class="fas fa-check-circle text-green-600 mr-2"></i>
|
||||
Found <?= $pagination['total'] ?> Matching Domain(s) in Your Portfolio
|
||||
</h2>
|
||||
</div>
|
||||
<div class="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">Domain</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase">Registrar</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase">Expiration</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase">Status</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-semibold text-gray-600 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<?php foreach ($existingDomains as $domain): ?>
|
||||
<?php
|
||||
$daysLeft = !empty($domain['expiration_date']) ? floor((strtotime($domain['expiration_date']) - time()) / 86400) : null;
|
||||
$expiryClass = '';
|
||||
if ($daysLeft !== null) {
|
||||
if ($daysLeft < 0) $expiryClass = 'text-red-600 font-semibold';
|
||||
elseif ($daysLeft <= 30) $expiryClass = 'text-orange-600 font-semibold';
|
||||
elseif ($daysLeft <= 90) $expiryClass = 'text-yellow-600';
|
||||
}
|
||||
?>
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4">
|
||||
<a href="/domains/<?= $domain['id'] ?>" class="text-sm font-semibold text-primary hover:text-primary-dark">
|
||||
<?= htmlspecialchars($domain['domain_name']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-900">
|
||||
<?= htmlspecialchars($domain['registrar'] ?? 'Unknown') ?>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<?php if (!empty($domain['expiration_date'])): ?>
|
||||
<div class="text-sm">
|
||||
<div class="font-medium text-gray-900"><?= date('M d, Y', strtotime($domain['expiration_date'])) ?></div>
|
||||
<div class="text-xs <?= $expiryClass ?>">
|
||||
<?= $daysLeft ?> days
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-sm text-gray-400">Not set</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<?php
|
||||
$statusClass = $domain['status'] === 'active'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-gray-100 text-gray-800';
|
||||
?>
|
||||
<span class="px-3 py-1 rounded-full text-xs font-semibold <?= $statusClass ?>">
|
||||
<?= ucfirst($domain['status']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<a href="/domains/<?= $domain['id'] ?>" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
|
||||
View Details →
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</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, $query, $perPage) {
|
||||
return '/search?q=' . urlencode($query) . '&page=' . $page . '&per_page=' . $perPage;
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- First Page -->
|
||||
<?php if ($currentPage > 1): ?>
|
||||
<a href="<?= paginationUrl(1, $query, $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, $query, $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, $query, $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, $query, $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, $query, $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, $query, $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, $query, $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; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isDomainLike && $pagination['total'] == 0): ?>
|
||||
<!-- WHOIS Lookup Results -->
|
||||
<?php if ($whoisData): ?>
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden mb-4">
|
||||
<div class="px-6 py-4 border-b border-gray-200 bg-blue-50">
|
||||
<h2 class="text-lg font-semibold text-blue-900 flex items-center">
|
||||
<i class="fas fa-search text-blue-600 mr-2"></i>
|
||||
WHOIS Lookup Results
|
||||
</h2>
|
||||
<p class="text-sm text-blue-700 mt-1">Domain not found in your portfolio - showing WHOIS information</p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-600 mb-1">Domain</label>
|
||||
<p class="text-lg font-semibold text-gray-900"><?= htmlspecialchars($whoisData['domain']) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-600 mb-1">Registrar</label>
|
||||
<p class="text-lg text-gray-900"><?= htmlspecialchars($whoisData['registrar']) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-600 mb-1">Expiration Date</label>
|
||||
<p class="text-lg text-gray-900">
|
||||
<?= $whoisData['expiration_date'] ? date('M d, Y', strtotime($whoisData['expiration_date'])) : 'N/A' ?>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-600 mb-1">Creation Date</label>
|
||||
<p class="text-lg text-gray-900">
|
||||
<?= $whoisData['creation_date'] ? date('M d, Y', strtotime($whoisData['creation_date'])) : 'N/A' ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php if (!empty($whoisData['nameservers'])): ?>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-semibold text-gray-600 mb-2">Nameservers</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<?php foreach ($whoisData['nameservers'] as $ns): ?>
|
||||
<span class="px-3 py-1 bg-gray-100 text-gray-700 rounded text-sm font-mono"><?= htmlspecialchars($ns) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Add Domain Button -->
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<form method="POST" action="/domains/store" class="flex items-center justify-between">
|
||||
<input type="hidden" name="domain_name" value="<?= htmlspecialchars($whoisData['domain']) ?>">
|
||||
<p class="text-sm text-gray-600">Want to monitor this domain?</p>
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Add to Portfolio
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php elseif ($whoisError): ?>
|
||||
<!-- WHOIS Error -->
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-6">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-exclamation-circle text-red-500 text-xl mr-3 mt-0.5"></i>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-red-900">WHOIS Lookup Failed</h3>
|
||||
<p class="text-sm text-red-700 mt-1"><?= htmlspecialchars($whoisError) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($pagination['total'] == 0 && !$isDomainLike): ?>
|
||||
<!-- No Results -->
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-info-circle text-yellow-500 text-xl mr-3 mt-0.5"></i>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-yellow-900">No Results Found</h3>
|
||||
<p class="text-sm text-yellow-700 mt-1">
|
||||
No domains match your search. Try a different search term or enter a domain name to perform a WHOIS lookup.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
|
||||
562
app/Views/tld-registry/import-logs.php
Normal file
562
app/Views/tld-registry/import-logs.php
Normal file
@@ -0,0 +1,562 @@
|
||||
<?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"><?= $stats['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"><?= $stats['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"><?= $stats['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($stats['last_import'])): ?>
|
||||
<?= date('M j, H:i', strtotime($stats['last_import'])) ?>
|
||||
<?php else: ?>
|
||||
Never
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-purple-50 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-clock text-purple-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';
|
||||
?>
|
||||
302
app/Views/tld-registry/import-progress.php
Normal file
302
app/Views/tld-registry/import-progress.php
Normal file
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
$title = $title ?? 'Import Progress';
|
||||
$pageTitle = $title;
|
||||
$pageDescription = 'Progressive data import with real-time progress';
|
||||
$pageIcon = 'fas fa-tasks';
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- 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 $descriptions[$import_type] ?? '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">
|
||||
<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">
|
||||
<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">
|
||||
<i class="fas fa-clock mr-2"></i>
|
||||
<span id="status-text">Starting...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between text-sm text-gray-600 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 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) -->
|
||||
<div id="step-progress" class="mb-4" style="display: none;">
|
||||
<div class="text-sm text-gray-600 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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="bg-gray-50 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>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Total</p>
|
||||
<p id="total-count" class="text-xl font-semibold text-gray-900">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 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>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Processed</p>
|
||||
<p id="processed-count" class="text-xl font-semibold text-gray-900">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 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>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Failed</p>
|
||||
<p id="failed-count" class="text-xl font-semibold text-gray-900">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 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>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Remaining</p>
|
||||
<p id="remaining-count" class="text-xl font-semibold text-gray-900">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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let logId = <?= json_encode($log_id) ?>;
|
||||
let importType = <?= json_encode($import_type) ?>;
|
||||
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';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function updateProgress(data) {
|
||||
const total = data.total || 0;
|
||||
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';
|
||||
statusText.innerHTML = '<i class="fas fa-check mr-2"></i>Complete';
|
||||
isComplete = true;
|
||||
addLogMessage('Import completed successfully!', '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';
|
||||
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';
|
||||
statusText.innerHTML = '<i class="fas fa-exclamation-triangle mr-2"></i>Error';
|
||||
addLogMessage(data.message || 'An error occurred', 'error');
|
||||
isComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
function checkProgress() {
|
||||
if (isComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/tld-registry/api/import-progress?log_id=${logId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
addLogMessage('Error: ' + data.error, 'error');
|
||||
isComplete = true;
|
||||
return;
|
||||
}
|
||||
|
||||
updateProgress(data);
|
||||
|
||||
if (data.status !== 'complete' && data.status !== 'error') {
|
||||
setTimeout(checkProgress, 2000); // Check again in 2 seconds
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
addLogMessage('Network error: ' + error.message, 'error');
|
||||
isComplete = true;
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
} 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';
|
||||
} 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';
|
||||
}
|
||||
}
|
||||
|
||||
// Start checking progress
|
||||
checkProgress();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
578
app/Views/tld-registry/index.php
Normal file
578
app/Views/tld-registry/index.php
Normal file
@@ -0,0 +1,578 @@
|
||||
<?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 -->
|
||||
<div class="mb-4">
|
||||
<div class="flex flex-wrap gap-2 justify-between items-center">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<form method="POST" action="/tld-registry/start-progressive-import" class="inline">
|
||||
<input type="hidden" name="import_type" value="complete_workflow">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors font-medium" title="Complete TLD import workflow: TLD List → RDAP → WHOIS → Registry URLs">
|
||||
<i class="fas fa-rocket mr-2"></i>
|
||||
Import TLDs
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/tld-registry/start-progressive-import" class="inline">
|
||||
<input type="hidden" name="import_type" value="check_updates">
|
||||
<button type="submit" <?= $stats['total'] == 0 ? 'disabled' : '' ?> class="inline-flex items-center px-4 py-2.5 <?= $stats['total'] == 0 ? 'bg-gray-400 cursor-not-allowed' : 'bg-purple-600 hover:bg-purple-700' ?> text-white text-sm rounded-lg transition-colors font-medium" title="<?= $stats['total'] == 0 ? 'Import TLDs first' : 'Check for IANA updates' ?>">
|
||||
<i class="fas fa-sync-alt mr-2"></i>
|
||||
Check Updates
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<a href="/tld-registry/import-logs" 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-history mr-2"></i>
|
||||
Import Logs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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"><?= $stats['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"><?= $stats['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"><?= $stats['with_rdap'] ?? 0 ?></p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-purple-50 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-database text-purple-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"><?= $stats['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>
|
||||
|
||||
<!-- Bulk Actions -->
|
||||
<?php if (!empty($tlds)): ?>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-4 mb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm text-gray-600">Bulk Actions:</span>
|
||||
<form method="POST" action="/tld-registry/bulk-delete" id="bulk-delete-form" class="inline">
|
||||
<button type="button" onclick="confirmBulkDelete()" class="inline-flex items-center px-3 py-2 border border-red-300 text-red-700 text-sm rounded-lg hover:bg-red-50 transition-colors font-medium">
|
||||
<i class="fas fa-trash mr-2"></i>
|
||||
Delete Selected
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
<span id="selected-count">0</span> selected
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- TLD Registry Table -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<?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>
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</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="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> View
|
||||
</a>
|
||||
<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>
|
||||
</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">
|
||||
<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; ?>
|
||||
|
||||
<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;
|
||||
document.getElementById('selected-count').textContent = count;
|
||||
|
||||
// Update select all checkbox state
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
const allCheckboxes = document.querySelectorAll('.tld-checkbox');
|
||||
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 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();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
258
app/Views/tld-registry/view.php
Normal file
258
app/Views/tld-registry/view.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?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">
|
||||
<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>
|
||||
<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 -->
|
||||
<?php if ($tld['rdap_servers']): ?>
|
||||
<?php
|
||||
$rdapServers = json_decode($tld['rdap_servers'], true);
|
||||
if (is_array($rdapServers) && !empty($rdapServers)):
|
||||
?>
|
||||
<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-database text-gray-400 mr-2" style="font-size: 10px;"></i>
|
||||
RDAP Servers (<?= count($rdapServers) ?>)
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<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-purple-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>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- WHOIS Server -->
|
||||
<?php if ($tld['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">
|
||||
<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>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</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-purple-50 rounded border border-purple-200">
|
||||
<div class="w-7 h-7 bg-purple-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">
|
||||
<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 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>
|
||||
|
||||
<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');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
Reference in New Issue
Block a user