rewrite UI: static-first with hidden attr, no classitus

This commit is contained in:
AJ ONeal 2026-05-27 02:22:25 -06:00
parent 98938936fc
commit ede27c582c
No known key found for this signature in database
3 changed files with 249 additions and 232 deletions

View File

@ -1,221 +1,192 @@
(function () { const App = {
"use strict"; myIpMode: false,
function $(sel, $parent) { submit(event) {
return ($parent || document).querySelector(sel); event.preventDefault();
} const input = event.target.querySelector('[data-input]');
const host = input.value.trim();
this.check(host);
},
function $$(sel, $parent) { check(host) {
return Array.from(($parent || document).querySelectorAll(sel)); const btn = document.querySelector('[data-submit-btn]');
}
const form = $(".search-form");
const input = $("input[type=\"text\"]", form);
const btn = $("button", form);
const myIpLink = $("#my-ip-link");
const loading = $(".loading");
const errorEl = $(".error-msg");
const resultEl = $(".result");
let myIpVisible = false;
function show(el) { el.classList.add("visible"); }
function hide(el) { el.classList.remove("visible"); }
function render(result, isMyIp) {
hide(errorEl);
hide(resultEl);
if (isMyIp) myIpVisible = true;
const host = result.ip;
const resolvedFrom = result.resolved_from || "";
const blocked = result.blocked;
const blockedIn = result.blocked_inbound;
const blockedOut = result.blocked_outbound;
const whitelisted = result.allowlisted;
const geo = result.geo || {};
// Verdict
let verdictClass = "unknown";
let verdictText = "UNKNOWN";
if (whitelisted) {
verdictClass = "whitelist";
verdictText = "ALLOWED (whitelist)";
} else if (blocked) {
verdictClass = "blocked";
verdictText = "BLOCKED";
} else {
verdictClass = "allowed";
verdictText = "ALLOWED";
}
// Geo info
let geoParts = [];
if (geo.city) geoParts.push(geo.city);
if (geo.region) {
geoParts.push(geo.region + (geo.region_iso ? " (" + geo.region_iso + ")" : ""));
} else if (geo.region_iso) {
geoParts.push(geo.region_iso);
}
if (geo.country) {
geoParts.push(geo.country + (geo.country_iso ? " (" + geo.country_iso + ")" : ""));
}
if (geo.continent) {
geoParts.push(geo.continent + (geo.continent_code ? " (" + geo.continent_code + ")" : ""));
}
let geoStr = geoParts.length ? geoParts.join(", ") : "—";
let latLonStr = "";
if (geo.latitude || geo.longitude) {
latLonStr = geo.latitude.toFixed(4) + ", " + geo.longitude.toFixed(4);
}
let asnStr = geo.asn ? "AS" + geo.asn + " " + (geo.asn_org || "") : "—";
let postalStr = geo.postal_code || "";
let tzStr = geo.timezone || "";
let metroStr = geo.metro_code ? String(geo.metro_code) : "";
let accuracyStr = geo.accuracy_radius ? String(geo.accuracy_radius) : "";
let registeredStr = geo.registered_country ? (geo.registered_country + (geo.registered_country_iso ? " (" + geo.registered_country_iso + ")" : "")) : "";
// Trait badges
let traitBadges = "";
if (geo.anycast) {
traitBadges += '<span class="trait-badge anycast">anycast</span>';
}
if (geo.anonymous_proxy) {
traitBadges += '<span class="trait-badge anonymous">anonymous proxy</span>';
}
if (geo.satellite) {
traitBadges += '<span class="trait-badge satellite">satellite</span>';
}
if (geo.country_in_eu) {
traitBadges += '<span class="trait-badge eu">EU</span>';
}
// Block details
let blockHtml = "";
if (blockedIn || blockedOut) {
const parts = [];
if (blockedIn) parts.push("inbound");
if (blockedOut) parts.push("outbound");
blockHtml = '<span class="block-badge">BLOCKED: ' + parts.join(" + ") + '</span>';
} else {
blockHtml = '<span class="block-badge clean">clean</span>';
}
resultEl.innerHTML =
'<div class="result-header">' +
'<span class="verdict ' + verdictClass + '">' + verdictText + '</span>' +
'<span class="host">' + escapeHtml(host) + '</span>' +
(resolvedFrom ? '<span class="resolved">→ ' + escapeHtml(resolvedFrom) + '</span>' : '') +
'</div>' +
'<div class="result-body">' +
(geoStr !== "—" ?
'<div class="result-section">' +
'<h3>Location</h3>' +
'<p class="mono">' + escapeHtml(geoStr) + '</p>' +
'</div>' : '') +
(latLonStr ?
'<div class="result-section">' +
'<h3>Coordinates</h3>' +
'<p class="mono">' + escapeHtml(latLonStr) + '</p>' +
'</div>' : '') +
(postalStr || tzStr || metroStr || accuracyStr ?
'<div class="result-section">' +
'<h3>Details</h3>' +
'<p class="mono">' +
(postalStr ? escapeHtml(postalStr) + " " : "") +
(tzStr ? escapeHtml(tzStr) + " " : "") +
(metroStr ? "Metro " + escapeHtml(metroStr) + " " : "") +
(accuracyStr ? "±" + escapeHtml(accuracyStr) + "km" : "") +
'</p>' +
'</div>' : '') +
(traitBadges ?
'<div class="result-section">' +
'<h3>Traits</h3>' +
'<div class="block-list">' + traitBadges + '</div>' +
'</div>' : '') +
(registeredStr ?
'<div class="result-section">' +
'<h3>Registered Country</h3>' +
'<p class="mono">' + escapeHtml(registeredStr) + '</p>' +
'</div>' : '') +
'<div class="result-section">' +
'<h3>ASN</h3>' +
'<p class="mono">' + escapeHtml(asnStr) + '</p>' +
'</div>' +
'<div class="result-section">' +
'<h3>Blocklist</h3>' +
'<div class="block-list">' + blockHtml + '</div>' +
'</div>' +
'</div>';
show(resultEl);
}
function escapeHtml(s) {
const d = document.createElement("div");
d.textContent = s;
return d.innerHTML;
}
function submitHost(host) {
hide(errorEl);
hide(resultEl);
show(loading);
btn.disabled = true; btn.disabled = true;
var isMyIp = host === ""; this.clearState();
document.querySelector('[data-state="loading"]').hidden = false;
this.myIpMode = host === '';
fetch("/check?host=" + encodeURIComponent(host) + "&format=json") fetch('/check?host=' + encodeURIComponent(host) + '&format=json')
.then(function (r) { .then(function(r) {
if (!r.ok) throw new Error("HTTP " + r.status); if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json(); return r.json();
}) })
.then(function (data) { .then(function(data) {
hide(loading); document.querySelector('[data-state="loading"]').hidden = true;
render(data, isMyIp); document.querySelector('[data-state="result"]').hidden = false;
App.render(data);
}) })
.catch(function (err) { .catch(function(err) {
hide(loading); document.querySelector('[data-state="loading"]').hidden = true;
errorEl.textContent = "Error: " + err.message; document.querySelector('[data-state="error"]').hidden = false;
show(errorEl); document.querySelector('[data-error-text]').textContent = err.message;
}) })
.finally(function () { .finally(function() {
btn.disabled = false; btn.disabled = false;
}); });
} },
form.addEventListener("submit", function (e) { clearState() {
e.preventDefault(); document.querySelector('[data-state="result"]').hidden = true;
submitHost(input.value.trim()); document.querySelector('[data-state="error"]').hidden = true;
}); document.querySelectorAll('[data-verdict]').forEach(function(el) { el.hidden = true; });
document.querySelectorAll('[data-section]').forEach(function(el) { el.hidden = true; });
document.querySelectorAll('[data-trait]').forEach(function(el) { el.hidden = true; });
document.querySelectorAll('[data-block]').forEach(function(el) { el.hidden = true; });
document.querySelector('[data-resolved-from]').hidden = true;
},
myIpLink.addEventListener("click", function (e) { render(data) {
e.preventDefault(); /* ── Verdict ──────────────────────────────────────────── */
if (myIpVisible) { document.querySelectorAll('[data-verdict]').forEach(function(el) { el.hidden = true; });
hide(resultEl); var verdictKey = 'allowed';
myIpLink.textContent = "Show my IP Info"; if (data.allowlisted) {
myIpVisible = false; verdictKey = 'whitelist';
} else { } else if (data.blocked_inbound && data.blocked_outbound) {
myIpLink.textContent = "Hide my IP Info"; verdictKey = 'blocked-inbound-outbound';
submitHost(""); } else if (data.blocked_inbound) {
verdictKey = 'blocked-inbound';
} else if (data.blocked_outbound) {
verdictKey = 'blocked-outbound';
} }
}); document.querySelector('[data-verdict="' + verdictKey + '"]').hidden = false;
// Pre-fill from URL query param or hash fragment and auto-submit /* ── IP address ───────────────────────────────────────── */
(function init() { document.querySelector('[data-value="ip"]').textContent = data.ip;
let host = new URLSearchParams(window.location.search).get("host");
// Also check hash fragment (e.g. /#/?host=google.com) /* ── Resolved from ────────────────────────────────────── */
if (data.resolved_from) {
document.querySelector('[data-value="resolved-from"]').textContent = data.resolved_from;
document.querySelector('[data-resolved-from]').hidden = false;
}
/* ── Geo ──────────────────────────────────────────────── */
var geo = data.geo || {};
/* Location */
var locParts = [];
if (geo.city) locParts.push(geo.city);
if (geo.region) {
locParts.push(geo.region + (geo.region_iso ? ' (' + geo.region_iso + ')' : ''));
} else if (geo.region_iso) {
locParts.push(geo.region_iso);
}
if (geo.country) {
locParts.push(geo.country + (geo.country_iso ? ' (' + geo.country_iso + ')' : ''));
}
if (geo.continent) {
locParts.push(geo.continent + (geo.continent_code ? ' (' + geo.continent_code + ')' : ''));
}
if (locParts.length) {
document.querySelector('[data-value="location"]').textContent = locParts.join(', ');
document.querySelector('[data-section="location"]').hidden = false;
}
/* Coordinates */
if (geo.latitude || geo.longitude) {
document.querySelector('[data-value="coordinates"]').textContent =
geo.latitude.toFixed(4) + ', ' + geo.longitude.toFixed(4);
document.querySelector('[data-section="coordinates"]').hidden = false;
}
/* Details */
var detailParts = [];
if (geo.postal_code) detailParts.push(geo.postal_code);
if (geo.timezone) detailParts.push(geo.timezone);
if (geo.metro_code) detailParts.push('Metro ' + String(geo.metro_code));
if (geo.accuracy_radius) detailParts.push('±' + String(geo.accuracy_radius) + 'km');
if (detailParts.length) {
document.querySelector('[data-value="details"]').textContent = detailParts.join(' ');
document.querySelector('[data-section="details"]').hidden = false;
}
/* Traits */
var traitMap = {
anycast: 'anycast',
anonymous: 'anonymous_proxy',
satellite: 'satellite',
eu: 'country_in_eu'
};
var hasTraits = false;
var traitKeys = Object.keys(traitMap);
for (var i = 0; i < traitKeys.length; i++) {
if (geo[traitMap[traitKeys[i]]]) {
document.querySelector('[data-trait="' + traitKeys[i] + '"]').hidden = false;
hasTraits = true;
}
}
if (hasTraits) {
document.querySelector('[data-section="traits"]').hidden = false;
}
/* Registered country */
if (geo.registered_country) {
var rcParts = geo.registered_country +
(geo.registered_country_iso ? ' (' + geo.registered_country_iso + ')' : '');
document.querySelector('[data-value="registered-country"]').textContent = rcParts;
document.querySelector('[data-section="registered-country"]').hidden = false;
}
/* ASN */
if (geo.asn) {
document.querySelector('[data-value="asn"]').textContent =
'AS' + geo.asn + ' ' + (geo.asn_org || '');
} else {
document.querySelector('[data-value="asn"]').textContent = '—';
}
/* Blocklist */
var hasBlock = false;
if (data.blocked_inbound) {
document.querySelector('[data-block="inbound"]').hidden = false;
hasBlock = true;
}
if (data.blocked_outbound) {
document.querySelector('[data-block="outbound"]').hidden = false;
hasBlock = true;
}
if (!hasBlock) {
document.querySelector('[data-block="clean"]').hidden = false;
}
},
toggleIpInfo(event) {
event.preventDefault();
var link = document.querySelector('[data-my-ip-link]');
if (this.myIpMode) {
document.querySelector('[data-state="result"]').hidden = true;
link.textContent = 'Show my IP Info';
this.myIpMode = false;
} else {
link.textContent = 'Hide my IP Info';
this.check('');
}
},
init() {
var host = new URLSearchParams(window.location.search).get('host');
if (!host) { if (!host) {
const hashQuery = window.location.hash.split("?")[1]; var hashQuery = window.location.hash.split('?')[1];
if (hashQuery) { if (hashQuery) {
host = new URLSearchParams(hashQuery).get("host"); host = new URLSearchParams(hashQuery).get('host');
} }
} }
if (host) { if (host) {
input.value = host; document.querySelector('[data-input]').value = host;
submitHost(host); this.check(host);
} else {
document.querySelector('[data-input]').focus();
} }
// Focus input on load (only if no auto-submit triggered) }
if (!host) { };
input.focus();
} App.init();
})();
})();

View File

@ -3,33 +3,88 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>IP Location & Status</title> <title>IP Location &amp; Status</title>
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
</head> </head>
<body> <body>
<div class="container"> <main class="container" data-app>
<h1>IP Location & Status</h1> <h1>IP Location &amp; Status</h1>
<p class="subtitle">Look up an IP address, domain, or your own IP against blocklists and GeoIP databases.</p> <p class="subtitle">Look up an IP address, domain, or your own IP against blocklists and GeoIP databases.</p>
<form class="search-form" autocomplete="off"> <form class="search-form" data-form autocomplete="off" onsubmit="App.submit(window.event)">
<input type="text" name="host" placeholder="Enter IP or domain…"> <input type="text" name="host" placeholder="Enter IP or domain…" data-input>
<button type="submit">Check</button> <button type="submit" data-submit-btn>Check</button>
</form> </form>
<a href="#" class="my-ip-link" id="my-ip-link">Show my IP Info</a> <a href="#" class="my-ip-link" data-my-ip-link onclick="App.toggleIpInfo(window.event)">Show my IP Info</a>
<div class="loading"> <div class="loading" data-state="loading" hidden>
<span class="spinner"></span> Resolving and checking… <span class="spinner"></span>
Resolving and checking…
</div> </div>
<div class="error-msg"></div> <div class="error-msg" data-state="error" hidden>
<span>Error: </span>
<span data-error-text></span>
</div>
<div class="result"></div> <div class="result" data-state="result" hidden>
<div class="result-header">
<span data-verdict-badge>
<span data-verdict="whitelist" hidden class="verdict verdict-whitelist">ALLOWED (whitelist)</span>
<span data-verdict="blocked-inbound-outbound" hidden class="verdict verdict-blocked">BLOCKED (inbound + outbound)</span>
<span data-verdict="blocked-inbound" hidden class="verdict verdict-blocked">BLOCKED (inbound)</span>
<span data-verdict="blocked-outbound" hidden class="verdict verdict-blocked">BLOCKED (outbound)</span>
<span data-verdict="allowed" hidden class="verdict verdict-allowed">ALLOWED</span>
</span>
<span class="host" data-value="ip"></span>
<span data-resolved-from hidden class="resolved">→ <span data-value="resolved-from"></span></span>
</div>
<div class="result-body">
<div class="result-section" data-section="location" hidden>
<h3>Location</h3>
<p class="mono" data-value="location"></p>
</div>
<div class="result-section" data-section="coordinates" hidden>
<h3>Coordinates</h3>
<p class="mono" data-value="coordinates"></p>
</div>
<div class="result-section" data-section="details" hidden>
<h3>Details</h3>
<p class="mono" data-value="details"></p>
</div>
<div class="result-section" data-section="traits" hidden>
<h3>Traits</h3>
<div class="block-list">
<span data-trait="anycast" hidden class="trait-badge anycast">anycast</span>
<span data-trait="anonymous" hidden class="trait-badge anonymous">anonymous proxy</span>
<span data-trait="satellite" hidden class="trait-badge satellite">satellite</span>
<span data-trait="eu" hidden class="trait-badge eu">EU</span>
</div>
</div>
<div class="result-section" data-section="registered-country" hidden>
<h3>Registered Country</h3>
<p class="mono" data-value="registered-country"></p>
</div>
<div class="result-section" data-section="asn">
<h3>ASN</h3>
<p class="mono" data-value="asn">—</p>
</div>
<div class="result-section" data-section="blocklist">
<h3>Blocklist</h3>
<div class="block-list">
<span data-block="inbound" hidden class="block-badge">inbound</span>
<span data-block="outbound" hidden class="block-badge">outbound</span>
<span data-block="clean" hidden class="block-badge clean">clean</span>
</div>
</div>
</div>
</div>
<footer> <footer>
<a href="/healthz">/healthz</a> <a href="/healthz">/healthz</a>
</footer> </footer>
</div> </main>
<script src="/app.js"></script> <script src="/app.js"></script>
</body> </body>

View File

@ -1,5 +1,6 @@
/* ── Reset & base ─────────────────────────────────────────── */ /* ── Reset & base ─────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
[hidden] { display: none !important; }
:root { :root {
--bg: #fafafa; --bg: #fafafa;
@ -60,7 +61,7 @@ html {
} }
body { body {
min-height: 100dvh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -126,6 +127,7 @@ button {
button:hover { background: var(--primary-hover); } button:hover { background: var(--primary-hover); }
button:active { transform: scale(.98); } button:active { transform: scale(.98); }
button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
button:disabled { button:disabled {
opacity: .6; opacity: .6;
cursor: not-allowed; cursor: not-allowed;
@ -133,7 +135,6 @@ button:disabled {
/* ── Result card ──────────────────────────────────────────── */ /* ── Result card ──────────────────────────────────────────── */
.result { .result {
display: none;
background: var(--surface); background: var(--surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
@ -141,8 +142,6 @@ button:disabled {
overflow: hidden; overflow: hidden;
} }
.result.visible { display: block; }
.result-header { .result-header {
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
@ -165,6 +164,7 @@ button:disabled {
word-break: break-all; word-break: break-all;
} }
/* ── Verdict badge ────────────────────────────────────────── */
.verdict { .verdict {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -178,30 +178,25 @@ button:disabled {
white-space: nowrap; white-space: nowrap;
} }
.verdict.allowed { .verdict.verdict-allowed {
background: var(--ok-bg); background: var(--ok-bg);
color: var(--ok); color: var(--ok);
border: 1px solid var(--ok-border); border: 1px solid var(--ok-border);
} }
.verdict.blocked { .verdict.verdict-blocked {
background: var(--bad-bg); background: var(--bad-bg);
color: var(--bad); color: var(--bad);
border: 1px solid var(--bad-border); border: 1px solid var(--bad-border);
} }
.verdict.whitelist { .verdict.verdict-whitelist {
background: var(--warn-bg); background: var(--warn-bg);
color: var(--warn); color: var(--warn);
border: 1px solid var(--warn-border); border: 1px solid var(--warn-border);
} }
.verdict.unknown { /* ── Result body ──────────────────────────────────────────── */
background: var(--bg);
color: var(--muted);
border: 1px solid var(--border);
}
.result-body { .result-body {
padding: 1.25rem; padding: 1.25rem;
} }
@ -237,6 +232,7 @@ button:disabled {
gap: .375rem; gap: .375rem;
} }
/* ── Block badge ──────────────────────────────────────────── */
.block-badge { .block-badge {
font-size: .75rem; font-size: .75rem;
font-weight: 600; font-weight: 600;
@ -313,15 +309,12 @@ button:disabled {
/* ── Loading spinner ──────────────────────────────────────── */ /* ── Loading spinner ──────────────────────────────────────── */
.loading { .loading {
display: none;
text-align: center; text-align: center;
padding: 2rem; padding: 2rem;
color: var(--muted); color: var(--muted);
font-size: .875rem; font-size: .875rem;
} }
.loading.visible { display: block; }
.spinner { .spinner {
display: inline-block; display: inline-block;
width: 1.25rem; width: 1.25rem;
@ -338,7 +331,6 @@ button:disabled {
/* ── Error ────────────────────────────────────────────────── */ /* ── Error ────────────────────────────────────────────────── */
.error-msg { .error-msg {
display: none;
background: var(--bad-bg); background: var(--bad-bg);
color: var(--bad); color: var(--bad);
border: 1px solid var(--bad-border); border: 1px solid var(--bad-border);
@ -348,8 +340,6 @@ button:disabled {
font-size: .875rem; font-size: .875rem;
} }
.error-msg.visible { display: block; }
/* ── My IP link ───────────────────────────────────────────── */ /* ── My IP link ───────────────────────────────────────────── */
.my-ip-link { .my-ip-link {
display: inline-block; display: inline-block;
@ -364,6 +354,7 @@ button:disabled {
} }
.my-ip-link:hover { text-decoration-color: var(--primary); } .my-ip-link:hover { text-decoration-color: var(--primary); }
.my-ip-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
/* ── Footer ───────────────────────────────────────────────── */ /* ── Footer ───────────────────────────────────────────────── */
footer { footer {