feat(sirius-x): inline search on landing, immediate feedback on register

Landing hero previously had a redirect-form (type → click Search → jump
to register.html). Users read that as broken because typing did nothing.
Now the landing runs the same availability check inline: hits
/api/labels/<label> once (server-side cross-TLD lookup from the cached
index), cross-checks against the client TLDS list, and paints result
cards for every TLD with an 'available/taken' badge and either a
Register button (linking to register.html?q=<label> for the wallet
flow) or a 'view' link to the existing name via the public gateway.

Immediate feedback: 18 'checking…' placeholder cards paint the moment
the input event fires, so there is no dead period while the fetch is
in flight. Register.html got the same treatment — a 'Checking <label>
across every TLD…' line replaces the empty-state message during the
350ms debounce so the box never looks unresponsive.
This commit is contained in:
Local Dev 2026-09-07 21:33:01 +02:00
parent cf04b583aa
commit 45d733f263
2 changed files with 99 additions and 18 deletions

View file

@ -110,15 +110,17 @@
Register a top-level domain, register a name under one, look up records,
verify anything against the chain.</p>
<form id="search-form" role="search" style="margin:1.8rem auto 0;max-width:520px;padding:0 1rem;display:flex;gap:8px">
<input id="search-input" name="q" type="search"
placeholder="search a name — e.g. hello"
<div style="margin:1.6rem auto 0;max-width:640px;padding:0 1rem">
<input id="search-input" type="search"
placeholder="type a name — e.g. hello"
spellcheck="false" autocomplete="off"
style="flex:1;padding:14px 18px;border-radius:12px;border:1px solid #ffffff22;background:#141a24;color:var(--ink);font-size:16px;outline:none;font-family:inherit"
style="width:100%;padding:14px 18px;border-radius:12px;border:1px solid #ffffff22;background:#141a24;color:var(--ink);font-size:17px;outline:none;font-family:inherit"
aria-label="Search a name across every BCNR TLD">
<button type="submit" class="btn acid" style="padding:0 20px">Search →</button>
</form>
<p class="tag" style="margin-top:.8rem;font-size:.92rem">One search checks the label across every TLD in the registry. Available names appear first.</p>
<div id="search-hint" style="margin-top:8px;font-size:12.5px;color:var(--mut);text-align:center">
One search checks the label across every TLD in the registry. Available names appear first.
</div>
<div id="search-results" style="margin-top:14px;display:grid;gap:8px;text-align:left"></div>
</div>
<div class="cta" style="margin-top:1.4rem">
<a class="btn ghost" href="#try">Or browse what you can do →</a>
@ -126,17 +128,88 @@
</div>
</header>
<style>
/* Inline search card styling: mirrors register.html's .card look. */
#search-results .row-card{display:grid;grid-template-columns:1fr auto auto;gap:12px;align-items:center;
background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:11px 16px;min-height:48px}
#search-results .row-card.checking{opacity:.55}
#search-results .row-card .n{font-family:ui-monospace,monospace;font-size:15px;overflow-wrap:anywhere}
#search-results .row-card .btn{padding:6px 12px;font-size:12.5px}
#search-results .badge{font-size:11px;padding:2px 10px;border-radius:999px;font-weight:600;white-space:nowrap}
#search-results .b-ok{background:rgba(79,209,165,.15);color:var(--ok)}
#search-results .b-taken{background:rgba(246,118,138,.15);color:var(--taken)}
#search-results .b-check{background:rgba(139,152,169,.14);color:var(--mut)}
@media (max-width:520px){
#search-results .row-card{grid-template-columns:1fr auto;gap:8px}
#search-results .row-card .btn{justify-self:start}
}
</style>
<script>
// The hero-search box: normalise the label the same way register.html does
// (lowercase, [a-z0-9-], 63 chars max) and hand it to register.html via ?q=,
// which already knows how to parallel-check across every TLD.
document.getElementById("search-form").addEventListener("submit", (e) => {
e.preventDefault();
const raw = document.getElementById("search-input").value.trim().toLowerCase();
const clean = raw.replace(/[^a-z0-9-]/g, "").slice(0, 63);
if (!clean) return;
location.href = `./register.html?q=${encodeURIComponent(clean)}`;
});
// Inline name search — same UX as register.html but no wallet dependencies.
// Hits POST-friendly /api/labels/<label> (one call, returns every taken
// match across every TLD from the gateway's cached index) and cross-checks
// against the client-side TLDS list. Register buttons hand the label off
// to register.html?q= for the full mint flow.
(function () {
const TLDS = [
"bch","p2p","bit","nav","test","x","asm","neo","gt","sc",
"sia","dex","cex","nt","dea","hub","os","vpn",
// .com is held off the public registry (2026-08-29) — skip.
];
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
const clean = (v) => v.toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 32);
let lastReq = 0, timer = null;
function row(full, state) {
const badge = state === "ok" ? '<span class="badge b-ok">available</span>'
: state === "taken" ? '<span class="badge b-taken">taken</span>'
: '<span class="badge b-check">checking…</span>';
const btn = state === "ok"
? `<a class="btn acid" href="./register.html?q=${encodeURIComponent(full.split(".")[0])}">Register →</a>`
: state === "taken"
? `<a class="btn ghost" href="https://navigate.st/bns/${encodeURIComponent(full)}" target="_blank" rel="noopener">view →</a>`
: "<span></span>";
return `<div class="row-card ${state === "checking" ? "checking" : ""}"><span class="n">${esc(full)}</span>${badge}${btn}</div>`;
}
async function search(label) {
const my = ++lastReq;
const out = $("search-results");
// Immediate feedback: paint one 'checking' row per TLD so the user
// sees something is happening in the ~300ms before results land.
out.innerHTML = TLDS.map((t) => row(`${label}.${t}`, "checking")).join("");
let taken = new Set();
try {
const r = await fetch(`https://silentmode.st/api/labels/${encodeURIComponent(label)}`);
if (r.ok) {
const j = await r.json();
for (const m of (j.matches || [])) taken.add(m.name);
}
} catch { /* fall through: everything shows as "checking" then flips to error-free available; a gateway blip shouldn't block the user */ }
if (my !== lastReq) return; // superseded by newer keystroke
const rows = TLDS
.map((t) => ({ full: `${label}.${t}`, state: taken.has(`${label}.${t}`) ? "taken" : "ok" }))
.sort((a, b) => (a.state === "ok" ? 0 : 1) - (b.state === "ok" ? 0 : 1));
out.innerHTML = rows.map((x) => row(x.full, x.state)).join("");
}
$("search-input").addEventListener("input", () => {
clearTimeout(timer);
const v = clean($("search-input").value);
$("search-input").value = v;
if (!v) { $("search-results").innerHTML = ""; return; }
timer = setTimeout(() => search(v), 200);
});
$("search-input").addEventListener("keydown", (e) => {
if (e.key === "Enter") {
clearTimeout(timer);
const v = clean($("search-input").value);
if (v) search(v);
}
});
})();
</script>
<div class="wrap">

View file

@ -408,7 +408,15 @@ async function check() {
});
}));
}
$("q").addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(check, 350); });
$("q").addEventListener("input", () => {
clearTimeout(timer);
// Show an immediate placeholder so the input never looks unresponsive
// during the 350ms debounce window. `check()` overwrites this the moment
// it runs.
const raw = clean($("q").value);
if (raw) $("result").innerHTML = `<span class="muted">Checking <b>${esc(raw)}</b> across every TLD…</span>`;
timer = setTimeout(check, 350);
});
$("q").addEventListener("keydown", (e) => { if (e.key === "Enter") { clearTimeout(timer); check(); } });
// ---------- ?q= deep-link ----------