Files
InsanityLab-web/src/components/ContactForm.astro
T
Adriano b17b5c831a Fix contatti + rinomina servizi + orario sabato
- Contatti: rate-limit per IP reale (X-Forwarded-For) invece dell'IP del
  proxy Traefik, condiviso da tutti; controllo spostato dopo la validazione
  così bot/honeypot non consumano la quota. Honeypot rinominato da "website"
  a "hp_field" per non farlo riempire dall'autofill del browser (falsi 400).
- Servizi: "Performance Class" -> "Class" (titolo/card/H1/footer); migrazione
  DB invertita e guardata.
- One to One: migrazione titolo prod "Personal training" -> "One to One".
- Footer: orario sabato 8-17 (non 8-19), seed + migrazione guardata.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 17:58:26 +02:00

59 lines
2.5 KiB
Plaintext

---
import { t } from '../lib/content';
import T from './content/T.astro';
interface Props { variant?: 'full' | 'footer' }
const { variant = 'full' } = Astro.props;
---
<form
class:list={['cform', `cform--${variant}`]}
method="post"
action="/api/contact"
novalidate
data-msg-success={t('form.msg.success')}
data-msg-error-generic={t('form.msg.error-generic')}
data-msg-error-network={t('form.msg.error-network')}
>
<input class="field" name="firstName" placeholder={t('form.field.first-name')} required maxlength="100" />
<input class="field" name="lastName" placeholder={t('form.field.last-name')} required maxlength="100" />
<input class="field" type="tel" name="phone" placeholder={t('form.field.phone')} required maxlength="40" />
<input class="field" type="email" name="email" placeholder={t('form.field.email')} required maxlength="200" />
<textarea class="field" name="message" placeholder={t('form.field.message')} required maxlength="5000"></textarea>
<input class="hp" type="text" name="hp_field" tabindex="-1" autocomplete="off" aria-hidden="true" />
<button class="btn" type="submit"><T tag="form.submit" as="span" /></button>
<p class="cform__privacy">Inviando il modulo dichiari di aver letto l'<a href="/privacy">informativa sulla privacy</a>.</p>
<p class="form-msg" hidden></p>
</form>
<style>
.cform__privacy { font-size: .74rem; opacity: .8; margin: 8px 0 0; line-height: 1.5; }
.cform__privacy a { text-decoration: underline; }
</style>
<script>
document.querySelectorAll<HTMLFormElement>('.cform').forEach((form) => {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const msg = form.querySelector<HTMLParagraphElement>('.form-msg')!;
const btn = form.querySelector<HTMLButtonElement>('button')!;
btn.disabled = true;
msg.hidden = true;
try {
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(Object.fromEntries(new FormData(form))),
});
const data = await res.json();
msg.textContent = res.ok ? form.dataset.msgSuccess! : (data.error ?? form.dataset.msgErrorGeneric);
msg.className = `form-msg ${res.ok ? 'form-msg--ok' : 'form-msg--err'}`;
if (res.ok) form.reset();
} catch {
msg.textContent = form.dataset.msgErrorNetwork!;
msg.className = 'form-msg form-msg--err';
}
msg.hidden = false;
btn.disabled = false;
});
});
</script>