Add frontend /whois page for domain RDAP/WHOIS information

Implements a new /whois/[[domain]] route displaying registrar,
creation/expiration dates, nameservers and RDAP status codes
retrieved from the /api/domaininfo/{domain} endpoint. Includes
translations for en, fr, de, es, zh and hi, and a header menu
entry between DNS resolver and Domain Checkers.
This commit is contained in:
nemunaire 2026-03-01 15:13:32 +07:00
commit f804404ac6
12 changed files with 774 additions and 0 deletions

View file

@ -0,0 +1,30 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2022-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { postDomaininfoByDomain } from "$lib/api-base/sdk.gen";
import type { DomainInfo } from "$lib/model/domaininfo";
import { unwrapSdkResponse } from "./errors";
export async function getDomainInfo(domain: string): Promise<DomainInfo> {
return unwrapSdkResponse(
await postDomaininfoByDomain({ path: { domain } }),
) as DomainInfo;
}

View file

@ -0,0 +1,227 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2026 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {
Badge,
Card,
CardBody,
CardHeader,
CardTitle,
Col,
ListGroup,
ListGroupItem,
Row,
} from "@sveltestrap/sveltestrap";
import type { DomainInfo } from "$lib/model/domaininfo";
import { t } from "$lib/translations";
interface Props {
info: DomainInfo;
domain: string;
}
let { info, domain }: Props = $props();
function statusColor(code: string): string {
const lc = code.toLowerCase();
if (lc === "ok" || lc === "active") return "success";
if (lc.includes("hold")) return "danger";
if (lc.includes("prohibited") || lc.includes("pending")) return "info";
return "secondary";
}
function expirationProgress(expiration?: string): number {
if (!expiration) return 0;
const days = daysUntilExpiration(expiration);
if (days <= 0) return 0;
return Math.min(100, Math.round((days / 365) * 100));
}
function expirationColor(expiration?: string): string {
if (!expiration) return "secondary";
const days = Math.round((new Date(expiration).getTime() - Date.now()) / 86400000);
if (days < 0) return "danger";
if (days < 30) return "danger";
if (days < 90) return "warning";
return "success";
}
function daysUntilExpiration(expiration?: string): number {
if (!expiration) return 0;
return Math.round((new Date(expiration).getTime() - Date.now()) / 86400000);
}
function formatDate(iso?: string): string {
if (!iso) return "";
return new Date(iso).toLocaleDateString(undefined, {
year: "numeric",
month: "long",
day: "numeric",
});
}
</script>
<h2 class="display-7 fw-bold mt-3 mb-1 font-monospace">
{info.name ?? domain}
</h2>
<!-- Status badges -->
{#if info.status && info.status.length > 0}
<div class="mb-4">
<p class="text-muted small mb-1">{$t("domaininfo.status")}</p>
<div class="d-flex flex-wrap gap-2">
{#each info.status as code}
<Badge
color={statusColor(code)}
title={$t(`domaininfo.status-descriptions.${code}`) || code}
>
{code}
</Badge>
{/each}
</div>
{#if info.status.length === 1}
{@const desc = $t(
`domaininfo.status-descriptions.${info.status[0]}`,
)}
{#if desc && !desc.startsWith("domaininfo.")}
<p class="text-muted small mt-1 mb-0">{desc}</p>
{/if}
{/if}
</div>
{/if}
<!-- Dates card -->
<div class="card mb-4">
<div class="card-body">
<Row>
<Col sm="6" class="mb-3 mb-sm-0">
<p class="text-muted small mb-1">
<i class="bi bi-calendar-check me-1"></i>
{$t("domaininfo.creation-date")}
</p>
{#if info.creation}
<p class="fw-semibold mb-0">{formatDate(info.creation)}</p>
{:else}
<p class="text-muted mb-0">
{$t("domaininfo.no-creation")}
</p>
{/if}
</Col>
<Col sm="6">
<p class="text-muted small mb-1">
<i class="bi bi-calendar-x me-1"></i>
{$t("domaininfo.expiration-date")}
</p>
{#if info.expiration}
{@const days = daysUntilExpiration(info.expiration)}
{@const color = expirationColor(info.expiration)}
{@const expiresLabel = days < 0 ? $t("domaininfo.expired", { days: Math.abs(days) }) : days === 0 ? $t("domaininfo.expires-today") : $t("domaininfo.expires-in", { days })}
<p class="fw-semibold mb-1">
{formatDate(info.expiration)}
</p>
<div
class="progress mb-1"
style="height: 6px;"
title={expiresLabel}
>
<div
class="progress-bar bg-{color}"
role="progressbar"
style="width: {expirationProgress(info.expiration)}%"
></div>
</div>
<p class="text-{color} small mb-0">
{#if days < 0}
{$t("domaininfo.expired", { days: Math.abs(days) })}
{:else if days === 0}
{$t("domaininfo.expires-today")}
{:else}
{$t("domaininfo.expires-in", { days })}
{/if}
</p>
{:else}
<p class="text-muted mb-0">
{$t("domaininfo.no-expiration")}
</p>
{/if}
</Col>
</Row>
</div>
</div>
<Row>
<Col md={6}>
<!-- Registrar card -->
<Card class="mb-4">
<CardHeader>
<CardTitle class="h6 mb-0 fw-bold">
<i class="bi bi-building me-1"></i>
{$t("domaininfo.registrar")}
</CardTitle>
</CardHeader>
<CardBody>
{#if info.registrar}
<p class="fw-semibold mb-1">{info.registrar}</p>
{#if info.registrar_url}
<a
href={info.registrar_url}
target="_blank"
rel="noopener noreferrer"
class="btn btn-outline-secondary btn-sm"
>
<i class="bi bi-box-arrow-up-right me-1"></i>
{$t("domaininfo.registrar-url")}
</a>
{/if}
{:else}
<p class="text-muted mb-0">
{$t("domaininfo.no-registrar")}
</p>
{/if}
</CardBody>
</Card>
</Col>
<!-- Nameservers card -->
{#if info.nameservers && info.nameservers.length > 0}
<Col md={6}>
<Card class="mb-4">
<CardHeader>
<CardTitle class="h6 mb-0 fw-bold">
<i class="bi bi-hdd-network me-1"></i>
{$t("domaininfo.nameservers")}
</CardTitle>
</CardHeader>
<ListGroup flush>
{#each info.nameservers as ns}
<ListGroupItem class="font-monospace py-2"
>{ns}</ListGroupItem
>
{/each}
</ListGroup>
</Card>
</Col>
{/if}
</Row>

View file

@ -138,6 +138,12 @@
>
{$t("menu.dns-resolver")}
</DropdownItem>
<DropdownItem
active={page.route && page.route.id == "/whois/[[domain]]"}
href="/whois"
>
{$t("menu.whois")}
</DropdownItem>
<DropdownItem
active={page.route &&
(page.route.id == "/checkers" ||

View file

@ -166,6 +166,40 @@
"captcha": {
"human-check": "Sind Sie ein Mensch? Bitte aktivieren Sie das untenstehende Kästchen, um fortzufahren:"
},
"domaininfo": {
"page-title": "Registrierungsdaten-Abfrage",
"page-description": "Registrierungsinformationen für jeden Domainnamen nachschlagen",
"domain-description": "Geben Sie den Domainnamen ein, den Sie nachschlagen möchten, z.B. {{domain}}.",
"lookup": "Nachschlagen",
"registrar": "Registrar",
"registrar-url": "Registrar-Website",
"nameservers": "Nameserver",
"creation-date": "Registriert am",
"expiration-date": "Läuft ab am",
"status": "Domain-Status",
"no-expiration": "Kein Ablaufdatum verfügbar",
"no-creation": "Kein Registrierungsdatum verfügbar",
"no-registrar": "Unbekannter Registrar",
"expires-in": "Läuft in {{days}} Tagen ab",
"expired": "Vor {{days}} Tagen abgelaufen",
"expires-today": "Läuft heute ab",
"domain-not-found": "Dieser Domainname scheint nicht registriert zu sein.",
"error": "Domain-Informationen konnten nicht abgerufen werden.",
"status-descriptions": {
"ok": "Die Domain ist ohne Einschränkungen aktiv.",
"active": "Die Domain ist aktiv.",
"clientTransferProhibited": "Der Transfer zu einem anderen Registrar ist gesperrt.",
"serverTransferProhibited": "Der Transfer ist durch die Registry verboten.",
"clientUpdateProhibited": "Domain-Änderungen sind gesperrt.",
"serverUpdateProhibited": "Domain-Änderungen sind durch die Registry verboten.",
"clientDeleteProhibited": "Das Löschen der Domain ist gesperrt.",
"serverDeleteProhibited": "Das Löschen der Domain ist durch die Registry verboten.",
"clientHold": "Die Domain ist gesperrt (wird nicht aufgelöst).",
"serverHold": "Die Domain ist durch die Registry gesperrt.",
"pendingTransfer": "Ein Transfer zu einem anderen Registrar steht aus.",
"pendingDelete": "Die Domain steht vor der Löschung."
}
},
"errors": {
"404": {
"title": "Seite nicht gefunden",
@ -233,6 +267,7 @@
"my-domains": "Meine Domains",
"my-providers": "Meine Domain-Anbieter",
"dns-resolver": "DNS-Resolver",
"whois": "Registrierungsdaten-Abfrage",
"my-account": "Mein Konto",
"logout": "Abmelden",
"provider-features": "Unterstützte Anbieter",

View file

@ -189,6 +189,40 @@
"captcha": {
"human-check": "Are you a human? Please check the box below to continue:"
},
"domaininfo": {
"page-title": "Registration Data Lookup",
"page-description": "Look up registration information for any domain name",
"domain-description": "Enter the domain name you want to look up, e.g. {{domain}}.",
"lookup": "Look up",
"registrar": "Registrar",
"registrar-url": "Registrar website",
"nameservers": "Nameservers",
"creation-date": "Registered on",
"expiration-date": "Expires on",
"status": "Domain status",
"no-expiration": "No expiration date available",
"no-creation": "No registration date available",
"no-registrar": "Unknown registrar",
"expires-in": "Expires in {{days}} days",
"expired": "Expired {{days}} days ago",
"expires-today": "Expires today",
"domain-not-found": "This domain name does not appear to be registered.",
"error": "Could not retrieve domain information.",
"status-descriptions": {
"ok": "The domain is active with no restrictions.",
"active": "The domain is active.",
"clientTransferProhibited": "Transfer to another registrar is locked.",
"serverTransferProhibited": "Transfer is prohibited by the registry.",
"clientUpdateProhibited": "Domain updates are locked.",
"serverUpdateProhibited": "Domain updates are prohibited by the registry.",
"clientDeleteProhibited": "Domain deletion is locked.",
"serverDeleteProhibited": "Domain deletion is prohibited by the registry.",
"clientHold": "The domain is on hold (not resolving).",
"serverHold": "The domain is on hold by the registry.",
"pendingTransfer": "A transfer to another registrar is pending.",
"pendingDelete": "The domain is pending deletion."
}
},
"errors": {
"404": {
"title": "Page not found",
@ -256,6 +290,7 @@
"my-domains": "My domains",
"my-providers": "My domain providers",
"dns-resolver": "DNS resolver",
"whois": "Registration Data Lookup",
"checkers": "Configure Checkers",
"my-account": "My account",
"logout": "Sign out",

View file

@ -166,6 +166,40 @@
"captcha": {
"human-check": "¿Eres humano? Por favor, marca la casilla de abajo para continuar:"
},
"domaininfo": {
"page-title": "Consulta de datos de registro",
"page-description": "Busca información de registro para cualquier nombre de dominio",
"domain-description": "Introduce el nombre de dominio que quieres buscar, p.ej. {{domain}}.",
"lookup": "Buscar",
"registrar": "Registrador",
"registrar-url": "Sitio web del registrador",
"nameservers": "Servidores de nombres",
"creation-date": "Registrado el",
"expiration-date": "Expira el",
"status": "Estado del dominio",
"no-expiration": "No hay fecha de vencimiento disponible",
"no-creation": "No hay fecha de registro disponible",
"no-registrar": "Registrador desconocido",
"expires-in": "Expira en {{days}} días",
"expired": "Expiró hace {{days}} días",
"expires-today": "Expira hoy",
"domain-not-found": "Este nombre de dominio no parece estar registrado.",
"error": "No se pudo recuperar la información del dominio.",
"status-descriptions": {
"ok": "El dominio está activo sin restricciones.",
"active": "El dominio está activo.",
"clientTransferProhibited": "La transferencia a otro registrador está bloqueada.",
"serverTransferProhibited": "La transferencia está prohibida por el registro.",
"clientUpdateProhibited": "Las actualizaciones del dominio están bloqueadas.",
"serverUpdateProhibited": "Las actualizaciones del dominio están prohibidas por el registro.",
"clientDeleteProhibited": "La eliminación del dominio está bloqueada.",
"serverDeleteProhibited": "La eliminación del dominio está prohibida por el registro.",
"clientHold": "El dominio está en espera (no resuelve).",
"serverHold": "El dominio está en espera por el registro.",
"pendingTransfer": "Hay una transferencia a otro registrador pendiente.",
"pendingDelete": "El dominio está pendiente de eliminación."
}
},
"errors": {
"404": {
"title": "Página no encontrada",
@ -233,6 +267,7 @@
"my-domains": "Mis dominios",
"my-providers": "Mis proveedores de dominios",
"dns-resolver": "Resolvedor DNS",
"whois": "Consulta de datos de registro",
"my-account": "Mi cuenta",
"logout": "Cerrar sesión",
"provider-features": "Proveedores compatibles",

View file

@ -162,6 +162,40 @@
"captcha": {
"human-check": "Êtes-vous un humain ? Veuillez cocher la case ci-dessous pour continuer :"
},
"domaininfo": {
"page-title": "Données d'enregistrement d'un domaine",
"page-description": "Recherchez les informations d'enregistrement pour n'importe quel nom de domaine",
"domain-description": "Entrez le nom de domaine que vous souhaitez rechercher, par ex. {{domain}}.",
"lookup": "Rechercher",
"registrar": "Bureau d'enregistrement",
"registrar-url": "Site du registrar",
"nameservers": "Serveurs de noms",
"creation-date": "Enregistré le",
"expiration-date": "Expire le",
"status": "Statut du domaine",
"no-expiration": "Aucune date d'expiration disponible",
"no-creation": "Aucune date d'enregistrement disponible",
"no-registrar": "Registrar inconnu",
"expires-in": "Expire dans {{days}} jours",
"expired": "Expiré il y a {{days}} jours",
"expires-today": "Expire aujourd'hui",
"domain-not-found": "Ce nom de domaine ne semble pas être enregistré.",
"error": "Impossible de récupérer les informations du domaine.",
"status-descriptions": {
"ok": "Le domaine est actif sans restrictions.",
"active": "Le domaine est actif.",
"clientTransferProhibited": "Le transfert vers un autre registrar est verrouillé.",
"serverTransferProhibited": "Le transfert est interdit par le registre.",
"clientUpdateProhibited": "Les modifications du domaine sont verrouillées.",
"serverUpdateProhibited": "Les modifications du domaine sont interdites par le registre.",
"clientDeleteProhibited": "La suppression du domaine est verrouillée.",
"serverDeleteProhibited": "La suppression du domaine est interdite par le registre.",
"clientHold": "Le domaine est suspendu (ne résout pas).",
"serverHold": "Le domaine est suspendu par le registre.",
"pendingTransfer": "Un transfert vers un autre registrar est en cours.",
"pendingDelete": "Le domaine est en attente de suppression."
}
},
"errors": {
"404": {
"title": "Page introuvable",
@ -228,6 +262,7 @@
"menu": {
"my-domains": "Mes domaines",
"dns-resolver": "Résolveur DNS",
"whois": "Infos d'enregistrement d'un domaine",
"my-account": "Mon compte",
"logout": "Se déconnecter",
"provider-features": "Fournisseurs supportés",

View file

@ -166,6 +166,40 @@
"captcha": {
"human-check": "क्या आप इंसान हैं? जारी रखने के लिए कृपया नीचे दिया गया बॉक्स चेक करें:"
},
"domaininfo": {
"page-title": "पंजीकरण डेटा खोज",
"page-description": "किसी भी डोमेन नाम की पंजीकरण जानकारी देखें",
"domain-description": "वह डोमेन नाम दर्ज करें जिसे आप देखना चाहते हैं, जैसे {{domain}}।",
"lookup": "खोजें",
"registrar": "रजिस्ट्रार",
"registrar-url": "रजिस्ट्रार वेबसाइट",
"nameservers": "नेमसर्वर",
"creation-date": "पंजीकृत तिथि",
"expiration-date": "समाप्ति तिथि",
"status": "डोमेन स्थिति",
"no-expiration": "समाप्ति तिथि उपलब्ध नहीं",
"no-creation": "पंजीकरण तिथि उपलब्ध नहीं",
"no-registrar": "अज्ञात रजिस्ट्रार",
"expires-in": "{{days}} दिनों में समाप्त होगा",
"expired": "{{days}} दिन पहले समाप्त हो गया",
"expires-today": "आज समाप्त होगा",
"domain-not-found": "यह डोमेन नाम पंजीकृत नहीं लगता।",
"error": "डोमेन जानकारी प्राप्त नहीं हो सकी।",
"status-descriptions": {
"ok": "डोमेन बिना किसी प्रतिबंध के सक्रिय है।",
"active": "डोमेन सक्रिय है।",
"clientTransferProhibited": "किसी अन्य रजिस्ट्रार को स्थानांतरण लॉक है।",
"serverTransferProhibited": "रजिस्ट्री द्वारा स्थानांतरण निषिद्ध है।",
"clientUpdateProhibited": "डोमेन अपडेट लॉक हैं।",
"serverUpdateProhibited": "रजिस्ट्री द्वारा डोमेन अपडेट निषिद्ध हैं।",
"clientDeleteProhibited": "डोमेन हटाना लॉक है।",
"serverDeleteProhibited": "रजिस्ट्री द्वारा डोमेन हटाना निषिद्ध है।",
"clientHold": "डोमेन होल्ड पर है (रिज़ॉल्व नहीं हो रहा)।",
"serverHold": "रजिस्ट्री द्वारा डोमेन होल्ड पर है।",
"pendingTransfer": "किसी अन्य रजिस्ट्रार को स्थानांतरण लंबित है।",
"pendingDelete": "डोमेन हटाने की प्रतीक्षा में है।"
}
},
"errors": {
"404": {
"title": "पृष्ठ नहीं मिला",
@ -233,6 +267,7 @@
"my-domains": "मेरे डोमेन",
"my-providers": "मेरे डोमेन प्रदाता",
"dns-resolver": "DNS रिज़ॉल्वर",
"whois": "पंजीकरण डेटा खोज",
"my-account": "मेरा खाता",
"logout": "साइन आउट",
"provider-features": "समर्थित प्रदाता",

View file

@ -175,6 +175,40 @@
"captcha": {
"human-check": "您是人类吗?请勾选下方的框以继续:"
},
"domaininfo": {
"page-title": "域名注册信息查询",
"page-description": "查询任意域名的注册信息",
"domain-description": "输入您要查询的域名,例如 {{domain}}。",
"lookup": "查询",
"registrar": "注册商",
"registrar-url": "注册商网站",
"nameservers": "域名服务器",
"creation-date": "注册日期",
"expiration-date": "到期日期",
"status": "域名状态",
"no-expiration": "无到期日期信息",
"no-creation": "无注册日期信息",
"no-registrar": "未知注册商",
"expires-in": "{{days}} 天后到期",
"expired": "已于 {{days}} 天前到期",
"expires-today": "今日到期",
"domain-not-found": "该域名似乎未被注册。",
"error": "无法获取域名信息。",
"status-descriptions": {
"ok": "域名处于活跃状态,无任何限制。",
"active": "域名处于活跃状态。",
"clientTransferProhibited": "转移至其他注册商的功能已锁定。",
"serverTransferProhibited": "注册局禁止转移。",
"clientUpdateProhibited": "域名更新功能已锁定。",
"serverUpdateProhibited": "注册局禁止域名更新。",
"clientDeleteProhibited": "域名删除功能已锁定。",
"serverDeleteProhibited": "注册局禁止删除域名。",
"clientHold": "域名已暂停(无法解析)。",
"serverHold": "注册局已暂停该域名。",
"pendingTransfer": "正在等待转移至其他注册商。",
"pendingDelete": "域名等待删除中。"
}
},
"errors": {
"404": {
"title": "页面未找到",
@ -242,6 +276,7 @@
"my-domains": "我的域名",
"my-providers": "我的提供商",
"dns-resolver": "DNS解析器",
"whois": "域名注册信息查询",
"my-account": "个人中心",
"logout": "退出",
"provider-features": "支持的提供商",

View file

@ -0,0 +1,24 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2022-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import type { HappydnsDomainInfo } from "$lib/api-base/types.gen";
export type DomainInfo = HappydnsDomainInfo;

View file

@ -0,0 +1,249 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2026 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import { navigate } from "$lib/stores/config";
import { untrack } from "svelte";
import { preventDefault } from "svelte/legacy";
import {
Button,
Col,
Container,
FormGroup,
Input,
Row,
Spinner,
} from "@sveltestrap/sveltestrap";
import { getDomainInfo } from "$lib/api/domaininfo";
import type { DomainInfo } from "$lib/model/domaininfo";
import DomainInfoDisplay from "$lib/components/DomainInfoDisplay.svelte";
import { domains } from "$lib/stores/domains";
import { t } from "$lib/translations";
import PageTitle from "$lib/components/PageTitle.svelte";
interface Props {
data: { domain?: string };
}
let { data }: Props = $props();
let domain = $derived(data.domain ?? "");
let inputDomain = $state("");
let error_response: string | null = $state(null);
let not_found = $state(false);
let request_pending = $state(false);
let info: DomainInfo | null = $state(null);
function fetchInfo(d: string) {
if (!d) return;
request_pending = true;
error_response = null;
not_found = false;
info = null;
getDomainInfo(d).then(
(result) => {
info = result;
request_pending = false;
},
(error: unknown) => {
const msg = error instanceof Error ? error.message : String(error);
if (msg.toLowerCase().includes("not found") || msg.toLowerCase().includes("doesn't exist")) {
not_found = true;
} else {
error_response = msg;
}
request_pending = false;
},
);
}
$effect(() => {
if (domain) {
untrack(() => {
inputDomain = domain;
fetchInfo(domain);
});
}
});
function submit() {
if (!inputDomain) return;
if (inputDomain === domain) {
fetchInfo(inputDomain);
} else {
navigate("/whois/" + encodeURIComponent(inputDomain), { noScroll: true });
}
}
</script>
<svelte:head>
<title>
{$t("domaininfo.page-title")}
{domain ? domain : ""}
- happyDomain
</title>
</svelte:head>
{#if domain}
<Container fluid class="flex-fill d-flex flex-column">
<Row class="flex-grow-1">
<Col md={{ offset: 0, size: 4 }} class="bg-light pt-3 pb-5">
<div class="sticky-top">
<PageTitle title={$t("domaininfo.page-title")} domain={domain} />
<form onsubmit={preventDefault(submit)}>
<FormGroup>
<label for="domain-input">
{$t("common.domain")}
</label>
<Input
id="domain-input"
class="font-monospace"
list="my-domains"
required
placeholder="example.com"
bind:value={inputDomain}
/>
<div class="form-text">
{@html $t("domaininfo.domain-description", {
domain: `<span class="font-monospace">example.com</span>`,
})}
</div>
<datalist id="my-domains">
{#if $domains}
{#each $domains as dn (dn.id)}
<option>{dn.domain}</option>
{/each}
{/if}
</datalist>
</FormGroup>
<div class="mx-3 d-flex justify-content-end">
<Button type="submit" color="primary" disabled={request_pending}>
{#if request_pending}
<Spinner size="sm" />
{/if}
{$t("domaininfo.lookup")}
</Button>
</div>
</form>
</div>
</Col>
{#if request_pending}
<Col md="8" class="pt-5 pb-5 d-flex align-items-center justify-content-center">
<div class="text-center text-muted">
<Spinner />
<p class="mt-3">{$t("common.spinning")}</p>
</div>
</Col>
{:else if not_found}
<Col md="8" class="pt-3 pb-5">
<h2 class="display-7 fw-bold mt-3">
<i class="bi bi-question-circle"></i>
{domain}
</h2>
<div class="card border-warning mt-3">
<div class="card-body">
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle text-warning fs-3 me-3"></i>
<p class="card-text mb-0">{$t("domaininfo.domain-not-found")}</p>
</div>
</div>
</div>
</Col>
{:else if error_response !== null}
<Col md="8" class="pt-3 pb-5">
<h2 class="display-7 fw-bold mt-3">
<i class="bi bi-exclamation-triangle"></i>
{$t("domaininfo.error")}
</h2>
<div class="card border-danger mt-3">
<div class="card-body">
<div class="d-flex align-items-center">
<i class="bi bi-x-circle text-danger fs-3 me-3"></i>
<p class="card-text mb-0">{error_response}</p>
</div>
</div>
</div>
</Col>
{:else if info !== null}
<Col md="8" class="pt-3 pb-5">
<DomainInfoDisplay {info} {domain} />
</Col>
{/if}
</Row>
</Container>
{:else}
<div class="my-5 container flex-fill">
<PageTitle title={$t("domaininfo.page-title")} subtitle={$t("domaininfo.page-description")} />
<Row class="justify-content-center mt-4">
<Col md="10" lg="8">
<div class="card rounded-4 p-2">
<div class="card-body">
<form onsubmit={preventDefault(submit)}>
<FormGroup>
<label for="domain-input-landing">
{$t("common.domain")}
</label>
<Input
id="domain-input-landing"
class="font-monospace"
list="my-domains-landing"
required
placeholder="example.com"
bind:value={inputDomain}
/>
<div class="form-text">
{@html $t("domaininfo.domain-description", {
domain: `<span class="font-monospace">example.com</span>`,
})}
</div>
<datalist id="my-domains-landing">
{#if $domains}
{#each $domains as dn (dn.id)}
<option>{dn.domain}</option>
{/each}
{/if}
</datalist>
</FormGroup>
<div class="d-flex justify-content-end">
<Button type="submit" color="primary" disabled={request_pending}>
{#if request_pending}
<Spinner size="sm" />
{/if}
{$t("domaininfo.lookup")}
</Button>
</div>
</form>
</div>
</div>
</Col>
</Row>
</div>
{/if}

View file

@ -0,0 +1,28 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2022-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import type { Load } from "@sveltejs/kit";
export const load: Load = async ({ params }) => {
return {
domain: params.domain,
};
};