front: poprawny adres kontaktowy + walidacja formularza

- zamiana wszystkich adresów e-mail @gethumanai.com -> @gethumanai.pl
  (CTASection, Footer, i18n pl/en)
- walidacja po stronie klienta przed wysłaniem (name niepuste,
  email zawiera "@", message >= 5 znaków — zgodne z mailer.mjs)
- mapowanie kodów błędów mailera na czytelne komunikaty w i18n (pl/en),
  pokazywane zamiast generycznego błędu; honeypot company bez zmian

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Oskar Kapala 2026-06-23 16:26:22 +02:00
parent 2067c79c67
commit 09b9bf90bd
4 changed files with 85 additions and 8 deletions

View file

@ -12,9 +12,46 @@ const CTASection = () => {
const [message, setMessage] = useState('');
const [company, setCompany] = useState(''); // honeypot
const [status, setStatus] = useState<Status>('idle');
const [fieldErrors, setFieldErrors] = useState<{
name?: string;
email?: string;
message?: string;
}>({});
const [errorMsg, setErrorMsg] = useState('');
// Map mailer error codes (mailer.mjs) to human-readable, localized text.
const mapBackendError = (code: string) => {
switch (code) {
case 'name required':
return t.contact.errors.nameRequired;
case 'valid email required':
return t.contact.errors.emailInvalid;
case 'message too short':
return t.contact.errors.messageTooShort;
case 'Too many requests':
return t.contact.errors.rateLimited;
case 'mail delivery failed':
return t.contact.errors.mailFailed;
default:
return t.contact.errors.generic;
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Client-side validation — kept in sync with mailer.mjs thresholds.
const errs: { name?: string; email?: string; message?: string } = {};
if (!name.trim()) errs.name = t.contact.errors.nameRequired;
if (!email.includes('@')) errs.email = t.contact.errors.emailInvalid;
if (message.trim().length < 5) errs.message = t.contact.errors.messageTooShort;
setFieldErrors(errs);
if (Object.keys(errs).length > 0) {
if (status !== 'idle') setStatus('idle');
return;
}
setErrorMsg('');
setStatus('sending');
try {
const res = await fetch('/api/contact', {
@ -22,9 +59,21 @@ const CTASection = () => {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, message, company }),
});
if (!res.ok) throw new Error(`${res.status}`);
if (!res.ok) {
let code = '';
try {
const data = await res.json();
code = typeof data?.error === 'string' ? data.error : '';
} catch {
/* response had no JSON body */
}
setErrorMsg(mapBackendError(code));
setStatus('error');
return;
}
setStatus('success');
} catch {
setErrorMsg(t.contact.errors.generic);
setStatus('error');
}
};
@ -50,8 +99,8 @@ const CTASection = () => {
<div className="space-y-4">
<div className="flex items-center gap-3 text-primary">
<Mail size={20} />
<a href="mailto:hello@gethumanai.com" className="font-semibold hover:underline">
hello@gethumanai.com
<a href="mailto:hello@gethumanai.pl" className="font-semibold hover:underline">
hello@gethumanai.pl
</a>
</div>
<p className="text-sm text-muted italic">
@ -97,8 +146,12 @@ const CTASection = () => {
value={name}
onChange={(e) => setName(e.target.value)}
required
aria-invalid={!!fieldErrors.name}
className="w-full bg-background border border-white/10 rounded-lg px-4 py-3 focus:outline-none focus:border-primary/50 transition-colors"
/>
{fieldErrors.name && (
<p className="text-sm text-red-400">{fieldErrors.name}</p>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium">{t.contact.labelEmail}</label>
@ -108,8 +161,12 @@ const CTASection = () => {
value={email}
onChange={(e) => setEmail(e.target.value)}
required
aria-invalid={!!fieldErrors.email}
className="w-full bg-background border border-white/10 rounded-lg px-4 py-3 focus:outline-none focus:border-primary/50 transition-colors"
/>
{fieldErrors.email && (
<p className="text-sm text-red-400">{fieldErrors.email}</p>
)}
</div>
</div>
<div className="space-y-2">
@ -120,14 +177,18 @@ const CTASection = () => {
value={message}
onChange={(e) => setMessage(e.target.value)}
required
aria-invalid={!!fieldErrors.message}
className="w-full bg-background border border-white/10 rounded-lg px-4 py-3 focus:outline-none focus:border-primary/50 transition-colors resize-none"
/>
{fieldErrors.message && (
<p className="text-sm text-red-400">{fieldErrors.message}</p>
)}
</div>
{status === 'error' && (
<p className="text-sm text-red-400">
<span className="font-semibold">{t.contact.errorTitle}</span>{' '}
{t.contact.errorBody}
{errorMsg || t.contact.errorBody}
</p>
)}

View file

@ -14,8 +14,8 @@ const Footer = () => {
<p className="text-muted max-w-xs mb-6">
ludzka strona AI budujemy praktyczne systemy AI dla ludzi i organizacji.
</p>
<a href="mailto:hello@gethumanai.com" className="text-foreground/80 hover:text-primary transition-colors">
hello@gethumanai.com
<a href="mailto:hello@gethumanai.pl" className="text-foreground/80 hover:text-primary transition-colors">
hello@gethumanai.pl
</a>
</div>

View file

@ -11,6 +11,14 @@ export const en = {
successTitle: 'Message sent!',
successBody: "We'll get back to you shortly.",
errorTitle: 'Something went wrong.',
errorBody: 'Please try again or email us directly at hello@gethumanai.com.',
errorBody: 'Please try again or email us directly at hello@gethumanai.pl.',
errors: {
nameRequired: 'Please enter your name.',
emailInvalid: 'Please enter a valid email address.',
messageTooShort: 'Your message must be at least 5 characters long.',
rateLimited: 'Too many attempts. Please wait a moment and try again.',
mailFailed: 'We could not send your message. Please try again or email us directly at hello@gethumanai.pl.',
generic: 'Please try again or email us directly at hello@gethumanai.pl.',
},
},
};

View file

@ -11,6 +11,14 @@ export const pl = {
successTitle: 'Wiadomość wysłana!',
successBody: 'Odezwiemy się wkrótce.',
errorTitle: 'Coś poszło nie tak.',
errorBody: 'Spróbuj ponownie lub napisz bezpośrednio na hello@gethumanai.com.',
errorBody: 'Spróbuj ponownie lub napisz bezpośrednio na hello@gethumanai.pl.',
errors: {
nameRequired: 'Podaj imię.',
emailInvalid: 'Podaj poprawny adres e-mail.',
messageTooShort: 'Wiadomość musi mieć co najmniej 5 znaków.',
rateLimited: 'Zbyt wiele prób. Odczekaj chwilę i spróbuj ponownie.',
mailFailed: 'Nie udało się wysłać wiadomości. Spróbuj ponownie lub napisz bezpośrednio na hello@gethumanai.pl.',
generic: 'Spróbuj ponownie lub napisz bezpośrednio na hello@gethumanai.pl.',
},
},
};