Initial commit: DNS Things - Comprehensive DNS utility website

Features implemented:
- Modular JavaScript architecture (theme.js, dns-tools.js, whois.js, punycode.js, ip-tools.js, main.js)
- Responsive design with dark/light theme toggle
- DNS Lookup and Reverse DNS Lookup tools
- Whois Lookup functionality
- IDN Punycode Converter with full Unicode support
- Comprehensive IP Address Tools (validation, IPv4-to-IPv6 mapping, IPv6 compression/expansion)
- Dynamic tab descriptions that change based on active tool
- Mobile-responsive horizontal scrollable tabs
- Copy-to-clipboard functionality for all results
- Clean footer with dynamic year
- IPv4-mapped IPv6 address explanation with clear warnings

Technical improvements:
- Separated concerns with modular JS files
- Fixed browser compatibility issues with punycode library
- Implemented proper error handling and user feedback
- Added comprehensive input validation
- Optimized for mobile devices with touch-friendly UI
This commit is contained in:
dwindown
2025-08-01 23:13:52 +07:00
commit e5195ba1f1
9 changed files with 1824 additions and 0 deletions

231
assets/css/main.css Normal file
View File

@@ -0,0 +1,231 @@
/* DNS Things - Main Styles */
/* Custom Tailwind Configuration */
.tab-btn.active {
@apply bg-indigo-600 text-white shadow-md;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Custom scrollbar for dark mode */
.dark ::-webkit-scrollbar {
width: 8px;
}
.dark ::-webkit-scrollbar-track {
background: #374151;
}
.dark ::-webkit-scrollbar-thumb {
background: #6b7280;
border-radius: 4px;
}
.dark ::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
/* Hero section adjustments */
.hero-section {
min-height: 60vh;
}
/* DNS Tools section */
.dns-tools-section {
min-height: 70vh;
}
/* Tab navigation */
.tab-navigation {
min-width: 200px;
}
/* Results styling */
.dns-result-item {
transition: all 0.2s ease-in-out;
}
.dns-result-item:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.dark .dns-result-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
/* Copy button animations */
.copy-btn {
transition: all 0.2s ease-in-out;
}
.copy-btn:hover {
transform: scale(1.05);
}
/* Loading animations */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.loading-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.tab-navigation {
min-width: auto;
width: 100%;
}
.hero-section {
min-height: 50vh;
}
.dns-tools-section {
min-height: auto;
}
}
/* Mobile Tab Navigation - Horizontal Scrollable */
@media (max-width: 767px) {
/* Main container becomes vertical stack */
.flex.flex-col.md\:flex-row.gap-8 {
flex-direction: column !important;
gap: 1rem !important;
}
/* Tab container takes full width */
.w-full.md\:w-1\/3.lg\:w-1\/4 {
width: 100% !important;
margin-bottom: 1rem;
order: 1;
}
/* Content area takes full width */
.w-full.md\:w-2\/3.lg\:w-3\/4 {
width: 100% !important;
order: 2;
}
/* Tab buttons container becomes horizontal scrollable */
.flex.flex-col.space-y-2.p-2.bg-gray-50.dark\:bg-gray-700.rounded-lg {
flex-direction: row !important;
overflow-x: auto;
overflow-y: hidden;
padding: 0.75rem !important;
gap: 0.5rem !important;
-webkit-overflow-scrolling: touch;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
/* Hide scrollbar for webkit browsers */
.flex.flex-col.space-y-2.p-2.bg-gray-50.dark\:bg-gray-700.rounded-lg::-webkit-scrollbar {
display: none;
}
/* Tab buttons optimized for mobile */
.tab-btn {
flex-shrink: 0 !important;
white-space: nowrap !important;
min-width: auto !important;
padding: 0.5rem 1rem !important;
font-size: 0.875rem !important;
text-align: center !important;
margin: 0 !important;
}
/* Remove space-y utility on mobile */
.flex.flex-col.space-y-2.p-2.bg-gray-50.dark\:bg-gray-700.rounded-lg > * + * {
margin-top: 0 !important;
}
}
/* Focus states for accessibility */
.focus-ring:focus {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
/* Error and success states */
.error-state {
border-color: #ef4444;
background-color: #fef2f2;
}
.dark .error-state {
border-color: #dc2626;
background-color: #7f1d1d;
}
.success-state {
border-color: #10b981;
background-color: #f0fdf4;
}
.dark .success-state {
border-color: #059669;
background-color: #064e3b;
}
/* IP Tools specific styles */
.ip-validation-result {
max-width: 100%;
word-break: break-all;
}
/* Subnet calculator grid */
.subnet-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
/* Punycode converter styles */
.punycode-example {
font-family: 'Courier New', monospace;
font-size: 0.875rem;
}
/* Whois result styles */
.whois-badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 500;
}
.whois-badge.ipv4 {
background-color: #dbeafe;
color: #1e40af;
}
.dark .whois-badge.ipv4 {
background-color: #1e3a8a;
color: #93c5fd;
}
.whois-badge.ipv6 {
background-color: #e0e7ff;
color: #5b21b6;
}
.dark .whois-badge.ipv6 {
background-color: #581c87;
color: #c4b5fd;
}

BIN
assets/img/hero.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

262
assets/js/dns-tools.js Normal file
View File

@@ -0,0 +1,262 @@
// DNS Things - DNS Tools Module
// Initialize IP display functionality
async function initIPDisplay() {
const ipv4Element = document.getElementById('ipv4');
const ipv6Element = document.getElementById('ipv6');
if (!ipv4Element || !ipv6Element) return;
try {
// Fetch IPv4
const ipv4Response = await fetch('https://api.ipify.org?format=json');
const ipv4Data = await ipv4Response.json();
ipv4Element.textContent = ipv4Data.ip;
} catch (error) {
ipv4Element.textContent = 'Unable to fetch';
console.error('Error fetching IPv4:', error);
}
try {
// Fetch IPv6
const ipv6Response = await fetch('https://api64.ipify.org?format=json');
const ipv6Data = await ipv6Response.json();
ipv6Element.textContent = ipv6Data.ip;
} catch (error) {
ipv6Element.textContent = 'Not available';
console.error('Error fetching IPv6:', error);
}
}
// Initialize DNS lookup form
function initDNSForm() {
const form = document.getElementById('dns-form');
const input = document.getElementById('domain-input');
const results = document.getElementById('dns-results-container');
const loader = document.getElementById('dns-loader');
if (!form || !input || !results || !loader) return;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const domain = input.value.trim();
if (!domain) return;
// Show loader
loader.classList.remove('hidden');
results.innerHTML = '';
try {
// DNS record types to query
const recordTypes = ['A', 'AAAA', 'MX', 'TXT', 'NS', 'CNAME', 'SOA', 'PTR', 'SRV'];
// Create promises for all DNS queries
const queries = recordTypes.map(type =>
fetch(`https://dns.google/resolve?name=${encodeURIComponent(domain)}&type=${type}`)
.then(response => response.json())
.then(data => ({ type, data }))
.catch(error => ({ type, error }))
);
// Wait for all queries to complete
const responses = await Promise.all(queries);
// Group results by record type
const groupedResults = {};
responses.forEach(({ type, data, error }) => {
if (error) {
console.error(`Error fetching ${type} records:`, error);
return;
}
if (data.Answer && data.Answer.length > 0) {
groupedResults[type] = data.Answer.map(record => {
// Clean the record data
let cleanData = record.data;
if (typeof cleanData === 'string') {
cleanData = cleanData.replace(/^"(.*)"$/, '$1').replace(/\.$/, '');
}
return {
...record,
data: cleanData
};
});
}
});
// Display results
if (Object.keys(groupedResults).length === 0) {
results.innerHTML = `
<div class="p-4 bg-yellow-50 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300 rounded-lg">
<h3 class="font-medium mb-1">No DNS records found</h3>
<p class="text-sm">The domain "${domain}" does not have any DNS records or does not exist.</p>
</div>
`;
} else {
let resultsHTML = '<div class="space-y-4">';
Object.entries(groupedResults).forEach(([type, records]) => {
resultsHTML += `
<div class="dns-result-item p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 class="font-semibold text-gray-800 dark:text-white mb-3">${type} Records (${records.length})</h4>
<ul class="space-y-2">
`;
records.forEach(record => {
resultsHTML += `
<li class="flex justify-between items-center p-2 bg-gray-50 dark:bg-gray-700 rounded">
<span class="font-mono text-sm break-all">${record.data}</span>
<button onclick="copyToClipboard('${record.data}', this)" class="ml-2 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" title="Copy to clipboard">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</li>
`;
});
resultsHTML += '</ul></div>';
});
resultsHTML += '</div>';
results.innerHTML = resultsHTML;
}
} catch (error) {
console.error('DNS lookup error:', error);
results.innerHTML = `
<div class="p-4 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg">
<h3 class="font-medium mb-1">Error</h3>
<p class="text-sm">Failed to perform DNS lookup. Please try again.</p>
</div>
`;
} finally {
loader.classList.add('hidden');
}
});
// Auto-submit on paste
input.addEventListener('paste', () => {
setTimeout(() => {
if (input.value.trim()) {
form.dispatchEvent(new Event('submit'));
}
}, 100);
});
}
// Initialize reverse DNS lookup form
function initReverseDNSForm() {
const form = document.getElementById('reverse-dns-form');
const input = document.getElementById('ip-input');
const results = document.getElementById('reverse-dns-results');
const loader = document.getElementById('reverse-dns-loader');
if (!form || !input || !results || !loader) return;
// IPv6 reverse DNS helper
function ipv6ToReverseDNS(ipv6) {
// Expand IPv6 to full format
let expanded = ipv6;
if (ipv6.includes('::')) {
const parts = ipv6.split('::');
const left = parts[0] ? parts[0].split(':') : [];
const right = parts[1] ? parts[1].split(':') : [];
const middle = new Array(8 - left.length - right.length).fill('0000');
expanded = left.concat(middle).concat(right).join(':');
}
// Remove colons and pad each segment to 4 characters
const fullHex = expanded.split(':').map(segment =>
segment.padStart(4, '0')
).join('');
// Reverse and add dots
return fullHex.split('').reverse().join('.') + '.ip6.arpa';
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const ip = input.value.trim();
if (!ip) return;
// Show loader
loader.classList.remove('hidden');
results.innerHTML = '';
try {
let reverseDomain;
// Check if IPv4 or IPv6
if (ip.includes(':')) {
// IPv6
try {
reverseDomain = ipv6ToReverseDNS(ip);
} catch (error) {
throw new Error('Invalid IPv6 address format');
}
} else {
// IPv4
const parts = ip.split('.');
if (parts.length !== 4 || parts.some(part => isNaN(part) || part < 0 || part > 255)) {
throw new Error('Invalid IPv4 address format');
}
reverseDomain = parts.reverse().join('.') + '.in-addr.arpa';
}
// Perform reverse DNS lookup
const response = await fetch(`https://dns.google/resolve?name=${encodeURIComponent(reverseDomain)}&type=PTR`);
const data = await response.json();
if (data.Answer && data.Answer.length > 0) {
let resultsHTML = '<div class="space-y-2">';
data.Answer.forEach(record => {
const hostname = record.data.replace(/\.$/, '');
resultsHTML += `
<div class="flex justify-between items-center p-3 bg-green-50 dark:bg-green-900/30 rounded-lg">
<div>
<div class="font-medium text-green-800 dark:text-green-200">Hostname found:</div>
<div class="font-mono text-sm text-green-700 dark:text-green-300">${hostname}</div>
</div>
<button onclick="copyToClipboard('${hostname}', this)" class="p-2 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-800 rounded" title="Copy to clipboard">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
`;
});
resultsHTML += '</div>';
results.innerHTML = resultsHTML;
} else {
results.innerHTML = `
<div class="p-4 bg-yellow-50 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300 rounded-lg">
<h3 class="font-medium mb-1">No reverse DNS record found</h3>
<p class="text-sm">The IP address "${ip}" does not have a reverse DNS (PTR) record.</p>
</div>
`;
}
} catch (error) {
console.error('Reverse DNS lookup error:', error);
results.innerHTML = `
<div class="p-4 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg">
<h3 class="font-medium mb-1">Error</h3>
<p class="text-sm">${error.message || 'Failed to perform reverse DNS lookup. Please check the IP address format.'}</p>
</div>
`;
} finally {
loader.classList.add('hidden');
}
});
// Auto-submit on paste
input.addEventListener('paste', () => {
setTimeout(() => {
if (input.value.trim()) {
form.dispatchEvent(new Event('submit'));
}
}, 100);
});
}

437
assets/js/ip-tools.js Normal file
View File

@@ -0,0 +1,437 @@
// DNS Things - IP Tools Module
// IP Tools utility functions
function isValidIPv4(ip) {
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
return ipv4Regex.test(ip);
}
function isValidIPv6(ip) {
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$|^(?:[0-9a-fA-F]{1,4}:)*::(?:[0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$/;
return ipv6Regex.test(ip) || /^(?:[0-9a-fA-F]{0,4}:){0,7}[0-9a-fA-F]{0,4}$/.test(ip);
}
function ipv4ToIPv6(ipv4) {
const parts = ipv4.split('.');
const hex1 = (parseInt(parts[0]) * 256 + parseInt(parts[1])).toString(16).padStart(4, '0');
const hex2 = (parseInt(parts[2]) * 256 + parseInt(parts[3])).toString(16).padStart(4, '0');
return `::ffff:${hex1}:${hex2}`;
}
function compressIPv6(ipv6) {
// Expand first if compressed
let expanded = expandIPv6(ipv6);
// Then compress by removing leading zeros and consecutive zero groups
let compressed = expanded.replace(/\b0+([0-9a-fA-F]+)/g, '$1');
// Replace longest sequence of consecutive zero groups with ::
let parts = compressed.split(':');
let maxZeroStart = -1, maxZeroLength = 0;
let currentZeroStart = -1, currentZeroLength = 0;
for (let i = 0; i < parts.length; i++) {
if (parts[i] === '0' || parts[i] === '') {
if (currentZeroStart === -1) currentZeroStart = i;
currentZeroLength++;
} else {
if (currentZeroLength > maxZeroLength) {
maxZeroStart = currentZeroStart;
maxZeroLength = currentZeroLength;
}
currentZeroStart = -1;
currentZeroLength = 0;
}
}
if (currentZeroLength > maxZeroLength) {
maxZeroStart = currentZeroStart;
maxZeroLength = currentZeroLength;
}
if (maxZeroLength > 1) {
let before = parts.slice(0, maxZeroStart).join(':');
let after = parts.slice(maxZeroStart + maxZeroLength).join(':');
compressed = before + '::' + after;
compressed = compressed.replace(/^:/, '').replace(/:$/, '');
if (compressed === '') compressed = '::';
}
return compressed;
}
function expandIPv6(ipv6) {
if (ipv6.includes('::')) {
let parts = ipv6.split('::');
let left = parts[0] ? parts[0].split(':') : [];
let right = parts[1] ? parts[1].split(':') : [];
let middle = new Array(8 - left.length - right.length).fill('0000');
let expanded = left.concat(middle).concat(right);
return expanded.map(part => part.padStart(4, '0')).join(':');
}
return ipv6.split(':').map(part => part.padStart(4, '0')).join(':');
}
function ipv4ToBinary(ipv4) {
return ipv4.split('.').map(octet =>
parseInt(octet).toString(2).padStart(8, '0')
).join('.');
}
function ipv4ToDecimal(ipv4) {
const parts = ipv4.split('.');
return (parseInt(parts[0]) << 24) + (parseInt(parts[1]) << 16) + (parseInt(parts[2]) << 8) + parseInt(parts[3]);
}
function calculateSubnet(cidr) {
const [ip, prefixLength] = cidr.split('/');
const prefix = parseInt(prefixLength);
if (!isValidIPv4(ip) || prefix < 0 || prefix > 32) {
throw new Error('Invalid CIDR notation');
}
const ipParts = ip.split('.').map(x => parseInt(x));
const ipInt = (ipParts[0] << 24) + (ipParts[1] << 16) + (ipParts[2] << 8) + ipParts[3];
const subnetMask = (0xFFFFFFFF << (32 - prefix)) >>> 0;
const networkInt = (ipInt & subnetMask) >>> 0;
const broadcastInt = (networkInt | (0xFFFFFFFF >>> prefix)) >>> 0;
const networkIP = [
(networkInt >>> 24) & 0xFF,
(networkInt >>> 16) & 0xFF,
(networkInt >>> 8) & 0xFF,
networkInt & 0xFF
].join('.');
const broadcastIP = [
(broadcastInt >>> 24) & 0xFF,
(broadcastInt >>> 16) & 0xFF,
(broadcastInt >>> 8) & 0xFF,
broadcastInt & 0xFF
].join('.');
const subnetMaskIP = [
(subnetMask >>> 24) & 0xFF,
(subnetMask >>> 16) & 0xFF,
(subnetMask >>> 8) & 0xFF,
subnetMask & 0xFF
].join('.');
const totalHosts = Math.pow(2, 32 - prefix);
const usableHosts = totalHosts > 2 ? totalHosts - 2 : totalHosts;
return {
network: networkIP,
broadcast: broadcastIP,
subnetMask: subnetMaskIP,
totalHosts,
usableHosts,
prefix
};
}
// Initialize IP Tools
function initIPTools() {
// IP Validation
const ipValidateInput = document.getElementById('ip-validate-input');
const ipValidateBtn = document.getElementById('ip-validate-btn');
const ipValidateResult = document.getElementById('ip-validate-result');
const validateIP = () => {
const input = ipValidateInput.value.trim();
if (!input) {
ipValidateResult.innerHTML = '';
return;
}
const isIPv4 = isValidIPv4(input);
const isIPv6 = isValidIPv6(input);
if (isIPv4) {
ipValidateResult.innerHTML = `
<div class="p-4 bg-green-50 dark:bg-green-900/30 rounded-lg">
<div class="flex justify-between items-start">
<div class="flex-1">
<h5 class="font-medium text-green-800 dark:text-green-200 mb-2">✅ Valid IPv4 Address</h5>
<div class="space-y-1 text-sm">
<div><strong>Type:</strong> IPv4</div>
<div><strong>Address:</strong> ${input}</div>
<div><strong>Binary:</strong> ${ipv4ToBinary(input)}</div>
<div><strong>Decimal:</strong> ${ipv4ToDecimal(input)}</div>
</div>
</div>
<button onclick="copyToClipboard('${input}', this)" class="ml-2 p-2 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-800 rounded" title="Copy to clipboard">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
</div>
`;
} else if (isIPv6) {
const compressed = compressIPv6(input);
const expanded = expandIPv6(input);
ipValidateResult.innerHTML = `
<div class="p-4 bg-green-50 dark:bg-green-900/30 rounded-lg">
<div class="flex justify-between items-start">
<div class="flex-1">
<h5 class="font-medium text-green-800 dark:text-green-200 mb-2">✅ Valid IPv6 Address</h5>
<div class="space-y-1 text-sm">
<div><strong>Type:</strong> IPv6</div>
<div><strong>Compressed:</strong> ${compressed}</div>
<div><strong>Expanded:</strong> ${expanded}</div>
</div>
</div>
<button onclick="copyToClipboard('${compressed}', this)" class="ml-2 p-2 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-800 rounded" title="Copy to clipboard">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
</div>
`;
} else {
ipValidateResult.innerHTML = `
<div class="p-4 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg">
<h5 class="font-medium mb-1">❌ Invalid IP Address</h5>
<p class="text-sm">Please enter a valid IPv4 or IPv6 address.</p>
</div>
`;
}
};
if (ipValidateBtn && ipValidateInput) {
ipValidateBtn.addEventListener('click', validateIP);
ipValidateInput.addEventListener('input', () => {
clearTimeout(ipValidateInput.timeout);
ipValidateInput.timeout = setTimeout(validateIP, 500);
});
}
// IPv4 to IPv6 Mapping
const ipv4ToIPv6Input = document.getElementById('ipv4-to-ipv6-input');
const ipv4ToIPv6Btn = document.getElementById('ipv4-to-ipv6-btn');
const ipv4ToIPv6Result = document.getElementById('ipv4-to-ipv6-result');
const convertIPv4ToIPv6 = () => {
const input = ipv4ToIPv6Input.value.trim();
if (!input) {
ipv4ToIPv6Result.innerHTML = '';
return;
}
if (isValidIPv4(input)) {
const ipv6 = ipv4ToIPv6(input);
// Escape the IPv6 address for safe HTML insertion
const escapedIPv6 = ipv6.replace(/'/g, '&#39;');
ipv4ToIPv6Result.innerHTML = `
<div class="p-4 bg-green-50 dark:bg-green-900/30 rounded-lg">
<div class="flex justify-between items-start">
<div class="flex-1">
<h5 class="font-medium text-green-800 dark:text-green-200 mb-2">IPv6 Mapped Address:</h5>
<div class="font-mono text-sm break-all p-2 bg-white dark:bg-gray-800 rounded border">${escapedIPv6}</div>
<p class="text-xs text-green-600 dark:text-green-400 mt-2">✅ IPv4-mapped IPv6 address for ${input}</p>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2 p-2 bg-red-50 dark:bg-red-900/20 rounded border-l-2 border-red-400">
<p class="font-medium text-red-700 dark:text-red-300 mb-1">⚠️ Important:</p>
<p>This is NOT your actual IPv6 address! This is a special technical format that represents your IPv4 address (${input}) in IPv6 notation.</p>
<p class="mt-1">Your real IPv6 address is shown in the "Your IP Address" section above and is completely different.</p>
<p class="mt-1"><strong>Use case:</strong> IPv4-mapped IPv6 addresses are used for network programming and IPv4/IPv6 compatibility.</p>
</div>
</div>
<button onclick="copyToClipboard('${escapedIPv6}', this)" class="ml-2 p-2 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-800 rounded" title="Copy to clipboard">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
</div>
`;
} else {
ipv4ToIPv6Result.innerHTML = `
<div class="p-4 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg">
<h5 class="font-medium mb-1">Error:</h5>
<p class="text-sm">Please enter a valid IPv4 address.</p>
</div>
`;
}
};
if (ipv4ToIPv6Btn && ipv4ToIPv6Input) {
ipv4ToIPv6Btn.addEventListener('click', convertIPv4ToIPv6);
ipv4ToIPv6Input.addEventListener('input', () => {
clearTimeout(ipv4ToIPv6Input.timeout);
ipv4ToIPv6Input.timeout = setTimeout(convertIPv4ToIPv6, 500);
});
}
// IPv6 Compression/Expansion
const ipv6CompressInput = document.getElementById('ipv6-compress-input');
const ipv6CompressBtn = document.getElementById('ipv6-compress-btn');
const ipv6CompressResult = document.getElementById('ipv6-compress-result');
const processIPv6 = () => {
const input = ipv6CompressInput.value.trim();
if (!input) {
ipv6CompressResult.innerHTML = '';
return;
}
if (isValidIPv6(input)) {
const compressed = compressIPv6(input);
const expanded = expandIPv6(input);
ipv6CompressResult.innerHTML = `
<div class="p-4 bg-green-50 dark:bg-green-900/30 rounded-lg">
<div class="space-y-3">
<div>
<h5 class="font-medium text-green-800 dark:text-green-200 mb-2">Compressed Form:</h5>
<div class="flex justify-between items-center">
<div class="font-mono text-sm break-all p-2 bg-white dark:bg-gray-800 rounded border flex-1 mr-2">${compressed}</div>
<button onclick="copyToClipboard('${compressed}', this)" class="p-2 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-800 rounded" title="Copy compressed">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
</div>
<div>
<h5 class="font-medium text-green-800 dark:text-green-200 mb-2">Expanded Form:</h5>
<div class="flex justify-between items-center">
<div class="font-mono text-sm break-all p-2 bg-white dark:bg-gray-800 rounded border flex-1 mr-2">${expanded}</div>
<button onclick="copyToClipboard('${expanded}', this)" class="p-2 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-800 rounded" title="Copy expanded">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
`;
} else {
ipv6CompressResult.innerHTML = `
<div class="p-4 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg">
<h5 class="font-medium mb-1">Error:</h5>
<p class="text-sm">Please enter a valid IPv6 address.</p>
</div>
`;
}
};
if (ipv6CompressBtn && ipv6CompressInput) {
ipv6CompressBtn.addEventListener('click', processIPv6);
ipv6CompressInput.addEventListener('input', () => {
clearTimeout(ipv6CompressInput.timeout);
ipv6CompressInput.timeout = setTimeout(processIPv6, 500);
});
}
// IP to Binary/Decimal
const ipBinaryInput = document.getElementById('ip-binary-input');
const ipBinaryBtn = document.getElementById('ip-binary-btn');
const ipBinaryResult = document.getElementById('ip-binary-result');
const convertIPToBinary = () => {
const input = ipBinaryInput.value.trim();
if (!input) {
ipBinaryResult.innerHTML = '';
return;
}
if (isValidIPv4(input)) {
const binary = ipv4ToBinary(input);
const decimal = ipv4ToDecimal(input);
ipBinaryResult.innerHTML = `
<div class="p-4 bg-green-50 dark:bg-green-900/30 rounded-lg">
<div class="space-y-3">
<div>
<h5 class="font-medium text-green-800 dark:text-green-200 mb-2">Binary Representation:</h5>
<div class="flex justify-between items-center">
<div class="font-mono text-sm break-all p-2 bg-white dark:bg-gray-800 rounded border flex-1 mr-2">${binary}</div>
<button onclick="copyToClipboard('${binary}', this)" class="p-2 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-800 rounded" title="Copy binary">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
</div>
<div>
<h5 class="font-medium text-green-800 dark:text-green-200 mb-2">Decimal Representation:</h5>
<div class="flex justify-between items-center">
<div class="font-mono text-sm break-all p-2 bg-white dark:bg-gray-800 rounded border flex-1 mr-2">${decimal}</div>
<button onclick="copyToClipboard('${decimal}', this)" class="p-2 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-800 rounded" title="Copy decimal">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
`;
} else {
ipBinaryResult.innerHTML = `
<div class="p-4 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg">
<h5 class="font-medium mb-1">Error:</h5>
<p class="text-sm">Please enter a valid IPv4 address.</p>
</div>
`;
}
};
if (ipBinaryBtn && ipBinaryInput) {
ipBinaryBtn.addEventListener('click', convertIPToBinary);
ipBinaryInput.addEventListener('input', () => {
clearTimeout(ipBinaryInput.timeout);
ipBinaryInput.timeout = setTimeout(convertIPToBinary, 500);
});
}
// Subnet Calculator
const subnetInput = document.getElementById('subnet-input');
const subnetBtn = document.getElementById('subnet-btn');
const subnetResult = document.getElementById('subnet-result');
const calculateSubnetInfo = () => {
const input = subnetInput.value.trim();
if (!input) {
subnetResult.innerHTML = '';
return;
}
try {
const subnet = calculateSubnet(input);
subnetResult.innerHTML = `
<div class="p-4 bg-green-50 dark:bg-green-900/30 rounded-lg">
<h5 class="font-medium text-green-800 dark:text-green-200 mb-3">Subnet Information:</h5>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div class="space-y-2">
<div><strong>Network Address:</strong> ${subnet.network}</div>
<div><strong>Broadcast Address:</strong> ${subnet.broadcast}</div>
<div><strong>Subnet Mask:</strong> ${subnet.subnetMask}</div>
</div>
<div class="space-y-2">
<div><strong>Prefix Length:</strong> /${subnet.prefix}</div>
<div><strong>Total Hosts:</strong> ${subnet.totalHosts.toLocaleString()}</div>
<div><strong>Usable Hosts:</strong> ${subnet.usableHosts.toLocaleString()}</div>
</div>
</div>
</div>
`;
} catch (error) {
subnetResult.innerHTML = `
<div class="p-4 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg">
<h5 class="font-medium mb-1">Error:</h5>
<p class="text-sm">${error.message}</p>
</div>
`;
}
};
if (subnetBtn && subnetInput) {
subnetBtn.addEventListener('click', calculateSubnetInfo);
subnetInput.addEventListener('input', () => {
clearTimeout(subnetInput.timeout);
subnetInput.timeout = setTimeout(calculateSubnetInfo, 500);
});
}
}

33
assets/js/main.js Normal file
View File

@@ -0,0 +1,33 @@
// DNS Things - Main Application Initialization
// Initialize everything when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Initialize core UI components
initTheme();
initTabs();
initCopyButtons();
// Initialize DNS tools
initIPDisplay();
initDNSForm();
initReverseDNSForm();
// Initialize additional tools
initWhoisForm();
initPunycodeConverter();
initIPTools();
// Set dynamic year in footer
initFooter();
console.log('DNS Things application initialized successfully');
});
// Initialize footer with dynamic year
function initFooter() {
const yearElement = document.getElementById('current-year');
if (yearElement) {
const currentYear = new Date().getFullYear();
yearElement.textContent = currentYear;
}
}

176
assets/js/punycode.js Normal file
View File

@@ -0,0 +1,176 @@
// DNS Things - Punycode Module
// Real Punycode conversion functions using punycode.js library
function toPunycode(domain) {
try {
// Check if punycode library is available
if (typeof punycode === 'undefined') {
throw new Error('Punycode library not loaded');
}
// Split domain into parts and convert each part
const parts = domain.split('.');
const convertedParts = parts.map(part => {
// Check if part contains non-ASCII characters
if (/[^\x00-\x7F]/.test(part)) {
return punycode.toASCII(part);
}
return part;
});
return convertedParts.join('.');
} catch (error) {
// Fallback to URL API if punycode library fails
try {
const url = new URL(`http://${domain}`);
return url.hostname;
} catch {
throw new Error('Invalid domain format');
}
}
}
function fromPunycode(domain) {
try {
// Check if domain contains punycode
if (!domain.includes('xn--')) {
return domain; // No punycode to convert
}
// Check if punycode library is available
if (typeof punycode === 'undefined') {
throw new Error('Punycode library not loaded');
}
// Split domain into parts and convert each part
const parts = domain.split('.');
const convertedParts = parts.map(part => {
if (part.startsWith('xn--')) {
try {
return punycode.toUnicode(part);
} catch {
return part; // Return original if conversion fails
}
}
return part;
});
return convertedParts.join('.');
} catch (error) {
throw new Error('Invalid punycode format');
}
}
// Initialize IDN Punycode Converter
function initPunycodeConverter() {
const unicodeInput = document.getElementById('unicode-input');
const punycodeInput = document.getElementById('punycode-input');
const unicodeToPunycodeBtn = document.getElementById('unicode-to-punycode-btn');
const punycodeToUnicodeBtn = document.getElementById('punycode-to-unicode-btn');
const punycodeResult = document.getElementById('punycode-result');
const unicodeResult = document.getElementById('unicode-result');
if (!unicodeInput || !punycodeInput) return;
// Unicode to Punycode conversion
const convertUnicodeToPunycode = () => {
const input = unicodeInput.value.trim();
if (!input) {
punycodeResult.innerHTML = '';
return;
}
try {
const converted = toPunycode(input);
const isConverted = converted !== input;
punycodeResult.innerHTML = `
<div class="p-4 bg-green-50 dark:bg-green-900/30 rounded-lg">
<div class="flex justify-between items-start">
<div class="flex-1">
<h5 class="font-medium text-green-800 dark:text-green-200 mb-2">Punycode Result:</h5>
<div class="font-mono text-sm break-all p-2 bg-white dark:bg-gray-800 rounded border">${converted}</div>
${isConverted ? '<p class="text-xs text-green-600 dark:text-green-400 mt-2">✅ Conversion applied</p>' : '<p class="text-xs text-gray-500 dark:text-gray-400 mt-2"> No conversion needed (ASCII only)</p>'}
</div>
<button onclick="copyToClipboard('${converted}', this)" class="ml-2 p-2 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-800 rounded" title="Copy to clipboard">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
</div>
`;
} catch (error) {
punycodeResult.innerHTML = `
<div class="p-4 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg">
<h5 class="font-medium mb-1">Error:</h5>
<p class="text-sm">${error.message}</p>
</div>
`;
}
};
// Punycode to Unicode conversion
const convertPunycodeToUnicode = () => {
const input = punycodeInput.value.trim();
if (!input) {
unicodeResult.innerHTML = '';
return;
}
try {
const converted = fromPunycode(input);
const isConverted = converted !== input;
unicodeResult.innerHTML = `
<div class="p-4 bg-green-50 dark:bg-green-900/30 rounded-lg">
<div class="flex justify-between items-start">
<div class="flex-1">
<h5 class="font-medium text-green-800 dark:text-green-200 mb-2">Unicode Result:</h5>
<div class="font-mono text-sm break-all p-2 bg-white dark:bg-gray-800 rounded border">${converted}</div>
${isConverted ? '<p class="text-xs text-green-600 dark:text-green-400 mt-2">✅ Conversion applied</p>' : '<p class="text-xs text-gray-500 dark:text-gray-400 mt-2"> No conversion needed (no punycode found)</p>'}
</div>
<button onclick="copyToClipboard('${converted}', this)" class="ml-2 p-2 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-800 rounded" title="Copy to clipboard">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
</div>
`;
} catch (error) {
unicodeResult.innerHTML = `
<div class="p-4 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg">
<h5 class="font-medium mb-1">Error:</h5>
<p class="text-sm">${error.message}</p>
</div>
`;
}
};
// Event listeners
unicodeToPunycodeBtn.addEventListener('click', convertUnicodeToPunycode);
punycodeToUnicodeBtn.addEventListener('click', convertPunycodeToUnicode);
// Auto-convert on input (with debounce)
let unicodeTimeout, punycodeTimeout;
unicodeInput.addEventListener('input', () => {
clearTimeout(unicodeTimeout);
unicodeTimeout = setTimeout(convertUnicodeToPunycode, 500);
});
punycodeInput.addEventListener('input', () => {
clearTimeout(punycodeTimeout);
punycodeTimeout = setTimeout(convertPunycodeToUnicode, 500);
});
// Paste support
unicodeInput.addEventListener('paste', () => {
setTimeout(convertUnicodeToPunycode, 100);
});
punycodeInput.addEventListener('paste', () => {
setTimeout(convertPunycodeToUnicode, 100);
});
}

197
assets/js/theme.js Normal file
View File

@@ -0,0 +1,197 @@
// DNS Things - Theme and UI Utilities
// Theme toggle functionality
function initTheme() {
const themeToggleBtn = document.getElementById('theme-toggle');
const lightIcon = document.getElementById('theme-toggle-light-icon');
const darkIcon = document.getElementById('theme-toggle-dark-icon');
if (!themeToggleBtn || !lightIcon || !darkIcon) return;
// Check for saved theme preference or default to system preference
const savedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
let currentTheme = savedTheme || (systemPrefersDark ? 'dark' : 'light');
// Apply initial theme
if (currentTheme === 'dark') {
document.documentElement.classList.add('dark');
lightIcon.classList.add('hidden');
darkIcon.classList.remove('hidden');
} else {
document.documentElement.classList.remove('dark');
lightIcon.classList.remove('hidden');
darkIcon.classList.add('hidden');
}
// Toggle theme function
function toggleTheme() {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
lightIcon.classList.remove('hidden');
darkIcon.classList.add('hidden');
localStorage.setItem('theme', 'light');
} else {
document.documentElement.classList.add('dark');
lightIcon.classList.add('hidden');
darkIcon.classList.remove('hidden');
localStorage.setItem('theme', 'dark');
}
}
themeToggleBtn.addEventListener('click', toggleTheme);
}
// Tab descriptions for dynamic updates
const tabDescriptions = {
'dns-lookup-tab': 'Perform DNS lookups to resolve domain names and check DNS records',
'reverse-dns-tab': 'Find domain names associated with IP addresses using reverse DNS',
'whois-tab': 'Get detailed registration and ownership information for domains and IPs',
'punycode-tab': 'Convert international domain names between Unicode and Punycode formats',
'ip-tools-tab': 'Comprehensive IP address utilities including validation, conversion, and analysis'
};
// Tab switching functionality
function initTabs() {
const tabButtons = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
const descriptionElement = document.querySelector('p.text-lg.text-gray-500.mt-2');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const targetTab = button.getAttribute('data-tab');
// Remove active class from all buttons and contents
tabButtons.forEach(btn => {
btn.classList.remove('active', 'text-indigo-600', 'dark:text-indigo-400', 'bg-white', 'dark:bg-gray-800', 'shadow');
btn.classList.add('text-gray-600', 'dark:text-gray-300', 'hover:bg-white', 'hover:dark:bg-gray-800/50');
});
tabContents.forEach(content => {
content.classList.remove('active');
content.classList.add('hidden');
});
// Add active class to clicked button and show corresponding content
button.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:bg-white', 'hover:dark:bg-gray-800/50');
button.classList.add('active', 'text-indigo-600', 'dark:text-indigo-400', 'bg-white', 'dark:bg-gray-800', 'shadow');
const targetContent = document.getElementById(targetTab);
if (targetContent) {
targetContent.classList.remove('hidden');
targetContent.classList.add('active');
}
// Update description text
if (descriptionElement && tabDescriptions[targetTab]) {
descriptionElement.textContent = tabDescriptions[targetTab];
}
});
});
// Set initial description based on active tab
const activeTab = document.querySelector('.tab-btn.active');
if (activeTab && descriptionElement) {
const activeTabId = activeTab.getAttribute('data-tab');
if (tabDescriptions[activeTabId]) {
descriptionElement.textContent = tabDescriptions[activeTabId];
}
}
}
// Copy to clipboard functionality
function copyToClipboard(text, button) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => {
showCopyFeedback(button);
}).catch(err => {
console.error('Failed to copy: ', err);
fallbackCopyTextToClipboard(text, button);
});
} else {
fallbackCopyTextToClipboard(text, button);
}
}
// Make copyToClipboard globally available
window.copyToClipboard = copyToClipboard;
function fallbackCopyTextToClipboard(text, button) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showCopyFeedback(button);
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
}
document.body.removeChild(textArea);
}
function showCopyFeedback(button) {
const originalHTML = button.innerHTML;
button.innerHTML = `
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
`;
button.classList.add('bg-green-500');
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('bg-green-500');
}, 1000);
}
// Initialize copy buttons for the original HTML structure
function initCopyButtons() {
document.querySelectorAll('.copy-btn').forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('data-copy-target');
const targetElement = document.getElementById(targetId);
const copyIcon = this.querySelector('.copy-icon');
const copyFeedback = this.querySelector('.copy-feedback');
if (targetElement) {
const textToCopy = targetElement.textContent.trim();
// Skip if text is loading or not available
if (!textToCopy || textToCopy === 'loading...' || textToCopy === 'Not available') {
return;
}
// Try to use the modern clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(textToCopy).then(() => {
showCopyFeedback(copyIcon, copyFeedback);
}).catch(err => {
console.error('Failed to copy: ', err);
fallbackCopyTextToClipboard(textToCopy, copyIcon, copyFeedback);
});
} else {
// Fallback for older browsers
fallbackCopyTextToClipboard(textToCopy, copyIcon, copyFeedback);
}
}
});
});
}
function showCopyFeedback(copyIcon, copyFeedback) {
if (copyIcon) copyIcon.classList.add('hidden');
if (copyFeedback) copyFeedback.classList.remove('hidden');
setTimeout(() => {
if (copyIcon) copyIcon.classList.remove('hidden');
if (copyFeedback) copyFeedback.classList.add('hidden');
}, 2000);
}

196
assets/js/whois.js Normal file
View File

@@ -0,0 +1,196 @@
// DNS Things - Whois Module
// Initialize Whois lookup form
function initWhoisForm() {
const form = document.getElementById('whois-form');
const input = document.getElementById('whois-input');
const results = document.getElementById('whois-results');
const loader = document.getElementById('whois-loader');
if (!form || !input || !results || !loader) return;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const domain = input.value.trim();
if (!domain) return;
// Show loader
loader.classList.remove('hidden');
results.innerHTML = '';
try {
// Use DNS-based domain information lookup as a free alternative
const recordTypes = ['A', 'AAAA', 'MX', 'TXT', 'NS', 'SOA'];
const queries = recordTypes.map(type =>
fetch(`https://dns.google/resolve?name=${encodeURIComponent(domain)}&type=${type}`)
.then(response => response.json())
.then(data => ({ type, data }))
.catch(error => ({ type, error }))
);
const responses = await Promise.all(queries);
// Process DNS data
const dnsData = {};
const ipAddresses = { ipv4: [], ipv6: [] };
responses.forEach(({ type, data, error }) => {
if (!error && data.Answer && data.Answer.length > 0) {
dnsData[type] = data.Answer.map(record => record.data);
if (type === 'A') {
ipAddresses.ipv4.push(...data.Answer.map(record => record.data));
} else if (type === 'AAAA') {
ipAddresses.ipv6.push(...data.Answer.map(record => record.data));
}
}
});
// Determine TLD registry
const tld = domain.split('.').pop().toLowerCase();
const tldRegistries = {
'com': 'Verisign',
'net': 'Verisign',
'org': 'Public Interest Registry (PIR)',
'info': 'Afilias',
'biz': 'NeuStar',
'name': 'Verisign',
'mobi': 'Afilias',
'travel': 'Tralliance',
'museum': 'MuseDoma',
'coop': 'DotCooperation',
'aero': 'SITA',
'pro': 'RegistryPro',
'edu': 'Educause',
'gov': 'General Services Administration',
'mil': 'DoD Network Information Center',
'int': 'IANA'
};
// Build results
let resultsHTML = '<div class="space-y-4">';
// Domain Information
resultsHTML += `
<div class="p-4 bg-blue-50 dark:bg-blue-900/30 rounded-lg">
<h4 class="font-semibold text-blue-800 dark:text-blue-200 mb-3">Domain Information</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<div><strong>Domain:</strong> ${domain}</div>
<div><strong>TLD Registry:</strong> ${tldRegistries[tld] || 'Unknown'}</div>
<div><strong>Website:</strong> ${dnsData.A || dnsData.AAAA ? '✅ Available' : '❌ Not available'}</div>
<div><strong>Email:</strong> ${dnsData.MX ? '✅ Supported' : '❌ Not supported'}</div>
</div>
<div class="text-xs text-blue-600 dark:text-blue-400">
<p><strong>Note:</strong> This is a DNS-based lookup providing practical domain information. For detailed registration data, use official whois services.</p>
</div>
</div>
</div>
`;
// Name Servers
if (dnsData.NS) {
resultsHTML += `
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 class="font-semibold text-gray-800 dark:text-white mb-3">Name Servers (${dnsData.NS.length})</h4>
<ul class="space-y-1">
`;
dnsData.NS.forEach(ns => {
const cleanNS = ns.replace(/\.$/, '');
resultsHTML += `
<li class="flex justify-between items-center p-2 bg-gray-50 dark:bg-gray-700 rounded">
<span class="font-mono text-sm">${cleanNS}</span>
<button onclick="copyToClipboard('${cleanNS}', this)" class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" title="Copy">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</li>
`;
});
resultsHTML += '</ul></div>';
}
// DNS Records Summary
resultsHTML += `
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 class="font-semibold text-gray-800 dark:text-white mb-3">DNS Records Summary</h4>
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
`;
recordTypes.forEach(type => {
const hasRecord = dnsData[type] && dnsData[type].length > 0;
resultsHTML += `
<div class="text-center p-2 rounded ${hasRecord ? 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200' : 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400'}">
<div class="font-semibold">${type}</div>
<div class="text-sm">${hasRecord ? '✅' : '❌'}</div>
</div>
`;
});
resultsHTML += '</div></div>';
// IP Addresses
if (ipAddresses.ipv4.length > 0 || ipAddresses.ipv6.length > 0) {
resultsHTML += `
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 class="font-semibold text-gray-800 dark:text-white mb-3">IP Addresses</h4>
<div class="space-y-2">
`;
ipAddresses.ipv4.forEach(ip => {
resultsHTML += `
<div class="flex justify-between items-center">
<span class="whois-badge ipv4">${ip}</span>
<button onclick="copyToClipboard('${ip}', this)" class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" title="Copy">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
`;
});
ipAddresses.ipv6.forEach(ip => {
resultsHTML += `
<div class="flex justify-between items-center">
<span class="whois-badge ipv6">${ip}</span>
<button onclick="copyToClipboard('${ip}', this)" class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" title="Copy">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
`;
});
resultsHTML += '</div></div>';
}
resultsHTML += '</div>';
results.innerHTML = resultsHTML;
} catch (error) {
console.error('Whois lookup error:', error);
results.innerHTML = `
<div class="p-4 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg">
<h3 class="font-medium mb-1">Error</h3>
<p class="text-sm">Failed to lookup domain information. Please try again.</p>
</div>
`;
} finally {
loader.classList.add('hidden');
}
});
// Auto-submit on paste
input.addEventListener('paste', () => {
setTimeout(() => {
if (input.value.trim()) {
form.dispatchEvent(new Event('submit'));
}
}, 100);
});
}

292
index.html Normal file
View File

@@ -0,0 +1,292 @@
<!DOCTYPE html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DNS Things</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- Punycode library for proper IDN conversion -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/punycode/2.3.1/punycode.min.js"></script>
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/main.css">
<script>
tailwind.config = {
darkMode: 'class',
}
</script>
<style>
/* Simple transition for theme change */
body, .bg-white, .bg-gray-800, .text-gray-800, .text-white, .bg-gray-50, .bg-gray-700 {
transition: background-color 0.3s ease, color 0.3s ease;
}
.nav-link {
padding: 0.75rem;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
transition: background-color 0.15s ease-in-out;
}
.nav-link:hover {
background-color: #e5e7eb;
}
.dark .nav-link {
color: #d1d5db;
}
.dark .nav-link:hover {
background-color: #374151;
}
</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200">
<!-- Header -->
<header class="fixed top-0 left-0 right-0 bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm shadow-md z-50">
<nav class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex-shrink-0">
<a href="#" class="text-2xl font-bold text-gray-800 dark:text-white">DNS Things</a>
</div>
<div class="hidden md:flex items-center space-x-4">
<a href="#your-ip" class="nav-link">Your IP</a>
<a href="#dns-lookup" class="nav-link">DNS Tools</a>
<button id="theme-toggle" class="p-2 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200">
<svg id="theme-toggle-light-icon" class="w-6 h-6 hidden" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 5.05a1 1 0 00-1.414 1.414l.707.707a1 1 0 001.414-1.414l-.707-.707zM3 11a1 1 0 100-2H2a1 1 0 100 2h1z"></path></svg>
<svg id="theme-toggle-dark-icon" class="w-6 h-6 hidden" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
</button>
</div>
</div>
</nav>
</header>
<main class="pt-16">
<!-- Hero Section: Your IP -->
<section id="your-ip" class="py-16 md:py-20">
<div class="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 md:grid-cols-10 gap-8 items-start">
<div class="hidden md:flex md:col-span-4 items-start justify-center mt-12">
<img src="/assets/img/hero.webp" class="w-full h-auto max-w-md" alt="DNS Things">
</div>
<div class="w-full md:col-span-6 p-8 space-y-6 bg-white rounded-xl shadow-lg dark:bg-gray-800">
<div class="text-center">
<h1 class="text-4xl font-bold text-gray-800 dark:text-white">Your IP Address</h1>
<p class="text-gray-500 dark:text-gray-400">Your public IP addresses</p>
</div>
<div class="space-y-4">
<div class="p-4 bg-gray-50 rounded-lg dark:bg-gray-700">
<div class="flex justify-between items-center mb-1">
<label for="ipv4" class="text-sm font-medium text-gray-700 dark:text-gray-300">IPv4 Address</label>
<button data-copy-target="ipv4" class="copy-btn p-1 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600"><span class="copy-icon"><svg class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg></span><span class="copy-feedback hidden text-sm text-green-600 dark:text-green-400">Copied!</span></button>
</div>
<div id="ipv4" class="p-2 w-full text-lg font-mono text-blue-600 bg-blue-100 rounded-md dark:bg-blue-900 dark:text-blue-300 break-all">loading...</div>
</div>
<div class="p-4 bg-gray-50 rounded-lg dark:bg-gray-700">
<div class="flex justify-between items-center mb-1">
<label for="ipv6" class="text-sm font-medium text-gray-700 dark:text-gray-300">IPv6 Address</label>
<button data-copy-target="ipv6" class="copy-btn p-1 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600"><span class="copy-icon"><svg class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg></span><span class="copy-feedback hidden text-sm text-green-600 dark:text-green-400">Copied!</span></button>
</div>
<div id="ipv6" class="p-2 w-full text-lg font-mono text-purple-600 bg-purple-100 rounded-md dark:bg-purple-900 dark:text-purple-300 break-all">loading...</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- DNS Tools Section -->
<section id="dns-lookup" class="py-20 bg-white dark:bg-gray-800">
<div class="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h2 class="text-4xl font-bold text-gray-800 dark:text-white">DNS Tools</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-2">Various DNS utilities to help you analyze domains and IPs</p>
</div>
<div class="flex flex-col md:flex-row gap-8">
<!-- Vertical Tabs -->
<div class="w-full md:w-1/3 lg:w-1/4">
<div class="flex flex-col space-y-2 p-2 bg-gray-50 dark:bg-gray-700 rounded-lg">
<button class="tab-btn active p-3 text-left rounded-md font-medium text-indigo-600 dark:text-indigo-400 bg-white dark:bg-gray-800 shadow" data-tab="dns-lookup-tab">
DNS Lookup
</button>
<button class="tab-btn p-3 text-left rounded-md font-medium text-gray-600 dark:text-gray-300 hover:bg-white hover:dark:bg-gray-800/50" data-tab="reverse-dns-tab">
Reverse DNS Lookup
</button>
<button class="tab-btn p-3 text-left rounded-md font-medium text-gray-600 dark:text-gray-300 hover:bg-white hover:dark:bg-gray-800/50" data-tab="whois-tab">
Whois Lookup
</button>
<button class="tab-btn p-3 text-left rounded-md font-medium text-gray-600 dark:text-gray-300 hover:bg-white hover:dark:bg-gray-800/50" data-tab="punycode-tab">
IDN Punycode
</button>
<button class="tab-btn p-3 text-left rounded-md font-medium text-gray-600 dark:text-gray-300 hover:bg-white hover:dark:bg-gray-800/50" data-tab="ip-tools-tab">
IP Tools
</button>
</div>
</div>
<!-- Tab Content -->
<div class="w-full md:w-2/3 lg:w-3/4">
<!-- DNS Lookup Tab -->
<div id="dns-lookup-tab" class="tab-content active">
<h3 class="text-2xl font-semibold mb-4 text-gray-800 dark:text-white">DNS Lookup</h3>
<form id="dns-form" class="flex flex-col sm:flex-row gap-4">
<input type="text" id="domain-input" placeholder="e.g., google.com" class="flex-1 px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" autocomplete="off" spellcheck="false" autocorrect="off" autocapitalize="off">
<button type="submit" class="bg-indigo-600 text-white font-bold px-6 py-3 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 whitespace-nowrap">Check DNS</button>
</form>
<div id="dns-results-container" class="mt-6"></div>
<div id="dns-loader" class="hidden text-center mt-8">
<p class="text-gray-600 dark:text-gray-300">Loading DNS records...</p>
</div>
</div>
<!-- Reverse DNS Tab -->
<div id="reverse-dns-tab" class="tab-content hidden">
<h3 class="text-2xl font-semibold mb-4 text-gray-800 dark:text-white">Reverse DNS Lookup</h3>
<form id="reverse-dns-form" class="flex flex-col sm:flex-row gap-4">
<input type="text" id="ip-input" placeholder="e.g., 8.8.8.8" class="flex-1 px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" autocomplete="off" spellcheck="false" autocorrect="off" autocapitalize="off">
<button type="submit" class="bg-indigo-600 text-white font-bold px-6 py-3 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 whitespace-nowrap">Lookup</button>
</form>
<div id="reverse-dns-results" class="mt-6"></div>
<div id="reverse-dns-loader" class="hidden text-center mt-8">
<p class="text-gray-600 dark:text-gray-300">Looking up reverse DNS...</p>
</div>
</div>
<!-- Whois Lookup Tab -->
<div id="whois-tab" class="tab-content hidden">
<h3 class="text-2xl font-semibold mb-4 text-gray-800 dark:text-white">Whois Lookup</h3>
<form id="whois-form" class="flex flex-col sm:flex-row gap-4">
<input type="text" id="whois-input" placeholder="e.g., google.com" class="flex-1 px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" autocomplete="off" spellcheck="false" autocorrect="off" autocapitalize="off">
<button type="submit" class="bg-indigo-600 text-white font-bold px-6 py-3 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 whitespace-nowrap">Lookup</button>
</form>
<div id="whois-results" class="mt-6"></div>
<div id="whois-loader" class="hidden text-center mt-8">
<p class="text-gray-600 dark:text-gray-300">Looking up domain information...</p>
</div>
</div>
<!-- IDN Punycode Converter Tab -->
<div id="punycode-tab" class="tab-content hidden">
<h3 class="text-2xl font-semibold mb-4 text-gray-800 dark:text-white">IDN Punycode Converter</h3>
<!-- Unicode to Punycode -->
<div class="mb-8">
<h4 class="text-lg font-medium mb-3 text-gray-800 dark:text-white">Unicode to Punycode</h4>
<div class="flex flex-col sm:flex-row gap-4">
<input type="text" id="unicode-input" placeholder="e.g., 中国.com, россия.рф, العربية.com" class="flex-1 px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" autocomplete="off" spellcheck="false" autocorrect="off" autocapitalize="off">
<button type="button" id="unicode-to-punycode-btn" class="bg-indigo-600 text-white font-bold px-6 py-3 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 whitespace-nowrap">Convert</button>
</div>
<div id="punycode-result" class="mt-4"></div>
</div>
<!-- Punycode to Unicode -->
<div class="mb-6">
<h4 class="text-lg font-medium mb-3 text-gray-800 dark:text-white">Punycode to Unicode</h4>
<div class="flex flex-col sm:flex-row gap-4">
<input type="text" id="punycode-input" placeholder="e.g., xn--fiqs8s.com, xn--e1afmkfd.xn--p1ai" class="flex-1 px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" autocomplete="off" spellcheck="false" autocorrect="off" autocapitalize="off">
<button type="button" id="punycode-to-unicode-btn" class="bg-indigo-600 text-white font-bold px-6 py-3 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 whitespace-nowrap">Convert</button>
</div>
<div id="unicode-result" class="mt-4"></div>
</div>
<!-- Examples -->
<div class="p-4 bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-lg">
<h4 class="font-medium mb-2">📚 Examples</h4>
<div class="text-sm space-y-1">
<div><strong>Chinese:</strong> 中国.com → xn--fiqs8s.com</div>
<div><strong>Russian:</strong> россия.рф → xn--e1afmkfd.xn--p1ai</div>
<div><strong>Arabic:</strong> العربية.com → xn--mgbqf7g0ndx.com</div>
<div><strong>Japanese:</strong> 日本.com → xn--wgbl6a.com</div>
</div>
</div>
</div>
<!-- IP Tools Tab -->
<div id="ip-tools-tab" class="tab-content hidden">
<h3 class="text-2xl font-semibold mb-4 text-gray-800 dark:text-white">IP Address Tools</h3>
<!-- IP Validation -->
<div class="mb-8">
<h4 class="text-lg font-medium mb-3 text-gray-800 dark:text-white">IP Address Validation</h4>
<div class="flex flex-col sm:flex-row gap-4">
<input type="text" id="ip-validate-input" placeholder="e.g., 192.168.1.1, 2001:db8::1" class="flex-1 px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" autocomplete="off" spellcheck="false" autocorrect="off" autocapitalize="off">
<button type="button" id="ip-validate-btn" class="bg-indigo-600 text-white font-bold px-6 py-3 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 whitespace-nowrap">Validate</button>
</div>
<div id="ip-validate-result" class="mt-4"></div>
</div>
<!-- IPv4 to IPv6 Mapping -->
<div class="mb-8">
<h4 class="text-lg font-medium mb-3 text-gray-800 dark:text-white">IPv4 to IPv6 Mapping</h4>
<div class="flex flex-col sm:flex-row gap-4">
<input type="text" id="ipv4-to-ipv6-input" placeholder="e.g., 192.168.1.1" class="flex-1 px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" autocomplete="off" spellcheck="false" autocorrect="off" autocapitalize="off">
<button type="button" id="ipv4-to-ipv6-btn" class="bg-indigo-600 text-white font-bold px-6 py-3 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 whitespace-nowrap">Convert</button>
</div>
<div id="ipv4-to-ipv6-result" class="mt-4"></div>
</div>
<!-- IPv6 Compression/Expansion -->
<div class="mb-8">
<h4 class="text-lg font-medium mb-3 text-gray-800 dark:text-white">IPv6 Compression/Expansion</h4>
<div class="flex flex-col sm:flex-row gap-4">
<input type="text" id="ipv6-compress-input" placeholder="e.g., 2001:0db8:0000:0000:0000:0000:0000:0001" class="flex-1 px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" autocomplete="off" spellcheck="false" autocorrect="off" autocapitalize="off">
<button type="button" id="ipv6-compress-btn" class="bg-indigo-600 text-white font-bold px-6 py-3 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 whitespace-nowrap">Process</button>
</div>
<div id="ipv6-compress-result" class="mt-4"></div>
</div>
<!-- IP to Binary/Decimal -->
<div class="mb-8">
<h4 class="text-lg font-medium mb-3 text-gray-800 dark:text-white">IP to Binary/Decimal</h4>
<div class="flex flex-col sm:flex-row gap-4">
<input type="text" id="ip-binary-input" placeholder="e.g., 192.168.1.1" class="flex-1 px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" autocomplete="off" spellcheck="false" autocorrect="off" autocapitalize="off">
<button type="button" id="ip-binary-btn" class="bg-indigo-600 text-white font-bold px-6 py-3 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 whitespace-nowrap">Convert</button>
</div>
<div id="ip-binary-result" class="mt-4"></div>
</div>
<!-- Subnet Calculator -->
<div class="mb-6">
<h4 class="text-lg font-medium mb-3 text-gray-800 dark:text-white">Subnet Calculator</h4>
<div class="flex flex-col sm:flex-row gap-4">
<input type="text" id="subnet-input" placeholder="e.g., 192.168.1.0/24" class="flex-1 px-4 py-3 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" autocomplete="off" spellcheck="false" autocorrect="off" autocapitalize="off">
<button type="button" id="subnet-btn" class="bg-indigo-600 text-white font-bold px-6 py-3 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 whitespace-nowrap">Calculate</button>
</div>
<div id="subnet-result" class="mt-4"></div>
</div>
<!-- Examples -->
<div class="p-4 bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-lg">
<h4 class="font-medium mb-2">🔧 Examples</h4>
<div class="text-sm space-y-1">
<div><strong>IPv4:</strong> 192.168.1.1, 10.0.0.1, 172.16.0.1</div>
<div><strong>IPv6:</strong> 2001:db8::1, ::1, fe80::1</div>
<div><strong>CIDR:</strong> 192.168.1.0/24, 10.0.0.0/8</div>
<div><strong>IPv6 Full:</strong> 2001:0db8:0000:0000:0000:0000:0000:0001</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Footer -->
<footer class="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 py-6">
<p class="text-center text-sm text-gray-500">
&copy; <span id="current-year">2025</span> Dewe Toolsites - DNS Things.
</footer>
<!-- JavaScript Files -->
<script src="assets/js/theme.js"></script>
<script src="assets/js/dns-tools.js"></script>
<script src="assets/js/whois.js"></script>
<script src="assets/js/punycode.js"></script>
<script src="assets/js/ip-tools.js"></script>
<script src="assets/js/main.js"></script>
</body>
</html>