feat(domaincheck): support all TLDs with suggestion alternatives
Tessera CI/CD / Lint & Type Check (push) Successful in 1m8s
Tessera CI/CD / Tests (push) Successful in 1m13s
Tessera CI/CD / Build & Deploy (push) Successful in 2m39s

- Accept full domains (e.g. "example.xyz") not just labels
- Check the entered TLD as primary result
- Show .de, .com, .net, .org as alternative suggestions below
- Primary result highlighted with accent border
- Input without TLD still works (shows all 4 suggestions)
- Updated i18n placeholders and added "suggestions" label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 14:54:30 +02:00
parent 95423497ad
commit 9f78580606
8 changed files with 95 additions and 134 deletions
@@ -1,16 +1,10 @@
/**
* Client-side action to check domain availability via the API.
*
* Calls POST /modules/domaincheck/check with the domain label.
* Auth cookie is forwarded via credentials: 'include'.
*/
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
export interface DomainCheckResult {
tld: string;
fqdn: string;
status: 'available' | 'registered';
primary: boolean;
}
export interface DomainCheckResponse {
@@ -18,13 +12,6 @@ export interface DomainCheckResponse {
results: DomainCheckResult[];
}
/**
* Check domain availability across TLD variants.
*
* @param domain - Base domain label (e.g., "example")
* @returns Response with domain and results array
* @throws Error if the API call fails
*/
export async function checkDomainAction(
domain: string,
): Promise<DomainCheckResponse> {
@@ -8,21 +8,15 @@ interface DomainInputProps {
isLoading: boolean;
}
/**
* Input form for entering a domain label to check.
*
* Renders a text input with placeholder from i18n and a submit button
* that shows loading state while the check is in progress.
*/
export function DomainInput({ onSubmit, isLoading }: DomainInputProps) {
const t = useTranslations('domaincheck');
const [domain, setDomain] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
const label = domain.trim().split('.')[0];
if (label) {
onSubmit(label);
const trimmed = domain.trim().toLowerCase();
if (trimmed) {
onSubmit(trimmed);
}
};
@@ -7,13 +7,6 @@ interface ResultListProps {
results: DomainCheckResult[];
}
/**
* Displays domain availability results as a list with color-coded status.
*
* Per D-01: green badge for "available", red badge for "registered".
* Per D-02: shows all TLD variants with their status.
* Shows empty state message when no results are available.
*/
export function ResultList({ results }: ResultListProps) {
const t = useTranslations('domaincheck');
@@ -25,27 +18,58 @@ export function ResultList({ results }: ResultListProps) {
);
}
const primary = results.filter((r) => r.primary);
const suggestions = results.filter((r) => !r.primary);
return (
<div className="space-y-2">
{results.map((result) => (
<div
key={result.fqdn}
className="flex items-center justify-between rounded-lg border border-border bg-card p-3"
>
<span className="text-sm font-medium">{result.fqdn}</span>
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${
result.status === 'available'
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'
}`}
>
{result.status === 'available'
? t('statusAvailable')
: t('statusRegistered')}
</span>
<div className="space-y-4">
{primary.length > 0 && (
<div className="space-y-2">
{primary.map((result) => (
<ResultRow key={result.fqdn} result={result} />
))}
</div>
))}
)}
{suggestions.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
{t('suggestions')}
</p>
{suggestions.map((result) => (
<ResultRow key={result.fqdn} result={result} />
))}
</div>
)}
</div>
);
}
function ResultRow({ result }: { result: DomainCheckResult }) {
const t = useTranslations('domaincheck');
return (
<div
className={`flex items-center justify-between rounded-lg border p-3 ${
result.primary
? 'border-primary/30 bg-primary/5'
: 'border-border bg-card'
}`}
>
<span className={`text-sm ${result.primary ? 'font-semibold' : 'font-medium'}`}>
{result.fqdn}
</span>
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${
result.status === 'available'
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'
}`}
>
{result.status === 'available'
? t('statusAvailable')
: t('statusRegistered')}
</span>
</div>
);
}
+2 -1
View File
@@ -221,7 +221,8 @@
"domaincheck": {
"title": "Domaincheck",
"description": "Pruefe ob eine Domain verfuegbar ist",
"inputPlaceholder": "Domain eingeben (z.B. beispiel)",
"inputPlaceholder": "Domain eingeben (z.B. beispiel.de)",
"suggestions": "Alternativen",
"checkButton": "Pruefen",
"statusAvailable": "Verfuegbar",
"statusRegistered": "Registriert",
+2 -1
View File
@@ -221,7 +221,8 @@
"domaincheck": {
"title": "Domain Check",
"description": "Check if a domain is available",
"inputPlaceholder": "Enter domain (e.g. example)",
"inputPlaceholder": "Enter domain (e.g. example.com)",
"suggestions": "Alternatives",
"checkButton": "Check",
"statusAvailable": "Available",
"statusRegistered": "Registered",