feat(domaincheck): support all TLDs with suggestion alternatives
- 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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user