From 1d9fd2280db9a9d7197858e2a6727c22691cb1d8 Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 19 Jun 2026 13:27:55 +0200 Subject: [PATCH] feat(03-02): add domaincheck frontend - input form, results display, i18n - DomainInput component with text input and submit button - ResultList component with green/red status badges (D-01) - checkDomainAction fetches POST /modules/domaincheck/check with auth cookie - Page renders within portal AppShell at /modules/domaincheck - i18n keys added for both DE and EN locales --- .../(portal)/modules/domaincheck/actions.ts | 46 +++++++++++++ .../domaincheck/components/DomainInput.tsx | 48 ++++++++++++++ .../domaincheck/components/ResultList.tsx | 51 +++++++++++++++ .../app/(portal)/modules/domaincheck/page.tsx | 64 +++++++++++++++++++ apps/web/src/messages/de.json | 11 ++++ apps/web/src/messages/en.json | 11 ++++ 6 files changed, 231 insertions(+) create mode 100644 apps/web/src/app/(portal)/modules/domaincheck/actions.ts create mode 100644 apps/web/src/app/(portal)/modules/domaincheck/components/DomainInput.tsx create mode 100644 apps/web/src/app/(portal)/modules/domaincheck/components/ResultList.tsx create mode 100644 apps/web/src/app/(portal)/modules/domaincheck/page.tsx diff --git a/apps/web/src/app/(portal)/modules/domaincheck/actions.ts b/apps/web/src/app/(portal)/modules/domaincheck/actions.ts new file mode 100644 index 0000000..b1b4673 --- /dev/null +++ b/apps/web/src/app/(portal)/modules/domaincheck/actions.ts @@ -0,0 +1,46 @@ +/** + * 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'; +} + +export interface DomainCheckResponse { + domain: string; + 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 { + const response = await fetch(`${API_URL}/modules/domaincheck/check`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ domain }), + credentials: 'include', + }); + + if (!response.ok) { + const errorBody = await response.text().catch(() => ''); + throw new Error( + `Domain check failed: ${response.status} ${errorBody}`.trim(), + ); + } + + return response.json(); +} diff --git a/apps/web/src/app/(portal)/modules/domaincheck/components/DomainInput.tsx b/apps/web/src/app/(portal)/modules/domaincheck/components/DomainInput.tsx new file mode 100644 index 0000000..0e635d1 --- /dev/null +++ b/apps/web/src/app/(portal)/modules/domaincheck/components/DomainInput.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { useTranslations } from 'next-intl'; +import { type FormEvent, useState } from 'react'; + +interface DomainInputProps { + onSubmit: (domain: string) => void; + 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 trimmed = domain.trim(); + if (trimmed) { + onSubmit(trimmed); + } + }; + + return ( +
+ setDomain(e.target.value)} + placeholder={t('inputPlaceholder')} + disabled={isLoading} + className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" + /> + +
+ ); +} diff --git a/apps/web/src/app/(portal)/modules/domaincheck/components/ResultList.tsx b/apps/web/src/app/(portal)/modules/domaincheck/components/ResultList.tsx new file mode 100644 index 0000000..d3693a2 --- /dev/null +++ b/apps/web/src/app/(portal)/modules/domaincheck/components/ResultList.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { useTranslations } from 'next-intl'; +import type { DomainCheckResult } from '../actions'; + +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'); + + if (results.length === 0) { + return ( +

+ {t('noResults')} +

+ ); + } + + return ( +
+ {results.map((result) => ( +
+ {result.fqdn} + + {result.status === 'available' + ? t('statusAvailable') + : t('statusRegistered')} + +
+ ))} +
+ ); +} diff --git a/apps/web/src/app/(portal)/modules/domaincheck/page.tsx b/apps/web/src/app/(portal)/modules/domaincheck/page.tsx new file mode 100644 index 0000000..530173f --- /dev/null +++ b/apps/web/src/app/(portal)/modules/domaincheck/page.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { useTranslations } from 'next-intl'; +import { useState } from 'react'; +import { type DomainCheckResult, checkDomainAction } from './actions'; +import { DomainInput } from './components/DomainInput'; +import { ResultList } from './components/ResultList'; + +/** + * Domaincheck module page within the portal. + * + * Provides a form to enter a domain label, calls the API to check + * TLD variant availability, and displays color-coded results. + * + * Per D-05a: uses Card-style layout consistent with portal modules. + */ +export default function DomaincheckPage() { + const t = useTranslations('domaincheck'); + + const [results, setResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async (domain: string) => { + setIsLoading(true); + setError(null); + + try { + const response = await checkDomainAction(domain); + setResults(response.results); + } catch { + setError(t('error')); + setResults([]); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* Header */} +
+

{t('title')}

+

+ {t('description')} +

+
+ + {/* Card panel (per D-05a) */} +
+ {/* Domain input */} + + + {/* Error message */} + {error && ( +

{error}

+ )} + + {/* Results */} + +
+
+ ); +} diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index b65314a..10c8283 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -134,5 +134,16 @@ "locale": { "de": "Deutsch", "en": "English" + }, + "domaincheck": { + "title": "Domaincheck", + "description": "Pruefe ob eine Domain verfuegbar ist", + "inputPlaceholder": "Domain eingeben (z.B. beispiel)", + "checkButton": "Pruefen", + "statusAvailable": "Verfuegbar", + "statusRegistered": "Registriert", + "noResults": "Gib einen Domain-Namen ein um die Verfuegbarkeit zu pruefen", + "checking": "Pruefe...", + "error": "Fehler bei der Pruefung" } } diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index dde14f2..14762c8 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -133,5 +133,16 @@ "locale": { "de": "Deutsch", "en": "English" + }, + "domaincheck": { + "title": "Domain Check", + "description": "Check if a domain is available", + "inputPlaceholder": "Enter domain (e.g. example)", + "checkButton": "Check", + "statusAvailable": "Available", + "statusRegistered": "Registered", + "noResults": "Enter a domain name to check availability", + "checking": "Checking...", + "error": "Error checking domain" } }