humanai-web/src/components/CTASection.tsx
oskar 39d0662197 Add contact form mailer microservice and i18n support
- mailer/: standalone Express+nodemailer service on port 3000 with
  validation, honeypot (company field), per-IP rate limiting (5/min),
  Fastmail SMTP. Returns 400 on bad input, 429 on rate limit, 502 on
  SMTP failure, 200 {ok:true} on success or honeypot hit.
- Dockerfile: multi-stage nginx build for humanai-landing container.
- mailer/Dockerfile: node:22-alpine, no exposed host ports.
- deploy.sh: idempotent deploy of both containers to piha.local via
  Docker context 'piha', with smoke tests (400 on empty body, 200 on
  honeypot fill).
- src/i18n/: minimal pl/en translation files; useT() hook picks
  language from navigator.language.
- CTASection: replaced dead onSubmit with fetch POST /api/contact,
  added hidden honeypot field, success/error states using i18n strings.
- README: Mailer section with two manual steps (credentials + NPM proxy).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 15:12:05 +02:00

151 lines
6.1 KiB
TypeScript

import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { Mail, MessageSquare } from 'lucide-react';
import { useT } from '../i18n';
type Status = 'idle' | 'sending' | 'success' | 'error';
const CTASection = () => {
const t = useT();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [company, setCompany] = useState(''); // honeypot
const [status, setStatus] = useState<Status>('idle');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus('sending');
try {
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, message, company }),
});
if (!res.ok) throw new Error(`${res.status}`);
setStatus('success');
} catch {
setStatus('error');
}
};
return (
<section id="contact" className="py-24 relative overflow-hidden">
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-px bg-gradient-to-r from-transparent via-primary/30 to-transparent" />
<div className="container mx-auto px-6">
<div className="max-w-5xl mx-auto glass-card p-12 relative overflow-hidden">
<div className="absolute top-0 right-0 p-12 opacity-10 pointer-events-none">
<MessageSquare size={200} className="text-primary" />
</div>
<div className="grid md:grid-cols-2 gap-12 relative z-10">
<div>
<h2 className="text-4xl font-bold mb-6">
Masz proces, który powinien już pracować sam?
</h2>
<p className="text-xl text-muted mb-8">
Opowiedz, co robisz ręcznie, powtarzalnie albo zbyt wolno. Sprawdzimy, czy AI może to uprościć.
</p>
<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>
</div>
<p className="text-sm text-muted italic">
"Na start wystarczy jeden proces, jeden problem albo jeden pomysł."
</p>
</div>
</div>
{status === 'success' ? (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col justify-center items-center text-center gap-4"
>
<p className="text-2xl font-bold text-primary">{t.contact.successTitle}</p>
<p className="text-muted">{t.contact.successBody}</p>
</motion.div>
) : (
<form className="space-y-4" onSubmit={handleSubmit}>
{/* Honeypot — hidden from real users */}
<div
aria-hidden="true"
style={{ position: 'absolute', left: '-9999px', top: '-9999px' }}
>
<label htmlFor="company">Company</label>
<input
id="company"
type="text"
name="company"
tabIndex={-1}
autoComplete="off"
value={company}
onChange={(e) => setCompany(e.target.value)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">{t.contact.labelName}</label>
<input
type="text"
placeholder={t.contact.placeholderName}
value={name}
onChange={(e) => setName(e.target.value)}
required
className="w-full bg-background border border-white/10 rounded-lg px-4 py-3 focus:outline-none focus:border-primary/50 transition-colors"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">{t.contact.labelEmail}</label>
<input
type="email"
placeholder={t.contact.placeholderEmail}
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full bg-background border border-white/10 rounded-lg px-4 py-3 focus:outline-none focus:border-primary/50 transition-colors"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">{t.contact.labelMessage}</label>
<textarea
rows={4}
placeholder={t.contact.placeholderMessage}
value={message}
onChange={(e) => setMessage(e.target.value)}
required
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"
/>
</div>
{status === 'error' && (
<p className="text-sm text-red-400">
<span className="font-semibold">{t.contact.errorTitle}</span>{' '}
{t.contact.errorBody}
</p>
)}
<button
type="submit"
disabled={status === 'sending'}
className="btn-primary w-full py-4 text-lg disabled:opacity-60"
>
{status === 'sending' ? t.contact.sending : t.contact.submit}
</button>
</form>
)}
</div>
</div>
</div>
</section>
);
};
export default CTASection;