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:
parent
2067c79c67
commit
09b9bf90bd
|
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue