All checks were successful
Build & Deploy / deploy (push) Successful in 21s
- Muster bestellen + individuelles Angebot: getrennte Felder Vorname/Nachname (statt einem Namensfeld). - Muster-Formular: Feld „Stadt" → „Ort". - „Individuelles Angebot" öffnet jetzt eine eigene Seite (/angebot) statt Popup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
142 lines
6.1 KiB
TypeScript
142 lines
6.1 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { ApiError, fetchMyCustomer, loadSession, sendOfferRequest } from '../lib/api';
|
||
|
||
const istEmail = (e: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e.trim());
|
||
|
||
/**
|
||
* Anfrageformular „Individuelles Angebot".
|
||
*
|
||
* Wird an zwei Stellen genutzt: im Konfigurator als eigene Seite (nach der Wahl
|
||
* eines Anfrageprodukts) und in der Seitenleiste als Dialog. Deshalb rendert es
|
||
* nur den Inhalt – die Umrahmung (Karte bzw. Overlay) kommt von außen.
|
||
*
|
||
* `onClose` blendet im Dialog den Schließen-Button ein; inline (im Konfigurator)
|
||
* bleibt er weg.
|
||
*/
|
||
export default function OfferForm({ produkt, onClose }: { produkt?: string; onClose?: () => void }) {
|
||
const gewaehltesProdukt = produkt?.trim() || '';
|
||
const [form, setForm] = useState({ first_name: '', last_name: '', email: '', phone: '', quantity: '', message: '' });
|
||
const [datenschutz, setDatenschutz] = useState(false);
|
||
const [status, setStatus] = useState<'formular' | 'sendet' | 'fertig'>('formular');
|
||
const [fehler, setFehler] = useState<string[]>([]);
|
||
|
||
// Bei angemeldeten Kunden Name und E-Mail vorausfüllen.
|
||
useEffect(() => {
|
||
if (!loadSession()) return;
|
||
fetchMyCustomer()
|
||
.then((c) => {
|
||
if (!c) return;
|
||
setForm((f) => ({
|
||
...f,
|
||
first_name: c.first_name || f.first_name,
|
||
last_name: c.last_name || f.last_name,
|
||
email: c.email || f.email,
|
||
phone: c.phone || f.phone,
|
||
}));
|
||
})
|
||
.catch(() => { /* Vorausfüllen ist optional */ });
|
||
}, []);
|
||
|
||
const set = (k: keyof typeof form) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||
const roh = e.target.value;
|
||
setForm({ ...form, [k]: k === 'phone' ? roh.replace(/[^0-9+ ]/g, '') : roh });
|
||
};
|
||
|
||
async function absenden(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
if (!istEmail(form.email)) { setFehler(['Bitte geben Sie eine gültige E-Mail-Adresse an.']); return; }
|
||
if (!form.phone.trim()) { setFehler(['Bitte geben Sie eine Telefonnummer an.']); return; }
|
||
setFehler([]);
|
||
setStatus('sendet');
|
||
try {
|
||
await sendOfferRequest({
|
||
first_name: form.first_name.trim(),
|
||
last_name: form.last_name.trim(),
|
||
email: form.email.trim(),
|
||
phone: form.phone.trim() || undefined,
|
||
quantity: form.quantity ? Number(form.quantity) : null,
|
||
message: form.message.trim(),
|
||
product: gewaehltesProdukt || undefined,
|
||
});
|
||
setStatus('fertig');
|
||
} catch (err) {
|
||
setFehler(err instanceof ApiError ? (err.details.length ? err.details : [err.message]) : ['Anfrage konnte nicht gesendet werden.']);
|
||
setStatus('formular');
|
||
}
|
||
}
|
||
|
||
if (status === 'fertig') {
|
||
return (
|
||
<>
|
||
<p>Vielen Dank – wir melden uns werktags innerhalb eines Tages mit einem Angebot bei Ihnen.</p>
|
||
{onClose && (
|
||
<button className="sk-btn sk-btn--primary" style={{ marginTop: 'var(--sk-space-4)' }} onClick={onClose}>
|
||
Schließen
|
||
</button>
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<p className="sk-note">
|
||
Größere Mengen, Sonderformate, Unterschriftenservice oder automatisierte Follow-ups – schildern Sie
|
||
kurz Ihr Vorhaben.
|
||
</p>
|
||
{gewaehltesProdukt && (
|
||
<div className="sk-menu-head" style={{ borderRadius: 'var(--sk-radius)', border: '1px solid var(--sk-border)', marginTop: 'var(--sk-space-3)' }}>
|
||
<div className="sk-menu-sub">Ihre Anfrage betrifft</div>
|
||
<div className="sk-menu-name">{gewaehltesProdukt}</div>
|
||
</div>
|
||
)}
|
||
<form style={{ marginTop: 'var(--sk-space-4)' }} onSubmit={absenden}>
|
||
<div className="sk-field">
|
||
<label className="sk-label" htmlFor="a-vorname">Vorname</label>
|
||
<input id="a-vorname" className="sk-input" value={form.first_name} onChange={set('first_name')} required />
|
||
</div>
|
||
<div className="sk-field">
|
||
<label className="sk-label" htmlFor="a-nachname">Nachname</label>
|
||
<input id="a-nachname" className="sk-input" value={form.last_name} onChange={set('last_name')} required />
|
||
</div>
|
||
<div className="sk-field">
|
||
<label className="sk-label" htmlFor="a-mail">E-Mail</label>
|
||
<input id="a-mail" className="sk-input" type="email" value={form.email} onChange={set('email')} required />
|
||
</div>
|
||
<div className="sk-field">
|
||
<label className="sk-label sk-label--required" htmlFor="a-tel">Telefon</label>
|
||
<input id="a-tel" className="sk-input" type="tel" value={form.phone} onChange={set('phone')} required />
|
||
</div>
|
||
<div className="sk-field">
|
||
<label className="sk-label" htmlFor="a-menge">Ungefähre Menge <span className="sk-note">(optional)</span></label>
|
||
<input id="a-menge" className="sk-input" type="number" min={1} value={form.quantity} onChange={set('quantity')} />
|
||
</div>
|
||
<div className="sk-field">
|
||
<label className="sk-label" htmlFor="a-text">Ihr Vorhaben</label>
|
||
<textarea id="a-text" className="sk-input" rows={4} value={form.message} onChange={set('message')} required />
|
||
</div>
|
||
|
||
<label className="sk-check" style={{ marginBottom: 'var(--sk-space-4)' }}>
|
||
<input type="checkbox" checked={datenschutz} onChange={(e) => setDatenschutz(e.target.checked)} required />
|
||
<span>
|
||
Ich habe die{' '}
|
||
<a href="https://skrift.de/datenschutz" target="_blank" rel="noreferrer">Datenschutzerklärung</a>{' '}
|
||
zur Kenntnis genommen und bin mit der Verarbeitung meiner Daten zur Bearbeitung meiner
|
||
Anfrage einverstanden.
|
||
</span>
|
||
</label>
|
||
|
||
{fehler.length > 0 && (
|
||
<ul className="sk-error" style={{ margin: '0 0 var(--sk-space-3)', paddingLeft: '1.1em' }}>
|
||
{fehler.map((f) => <li key={f}>{f}</li>)}
|
||
</ul>
|
||
)}
|
||
|
||
<button className="sk-btn sk-btn--primary sk-btn--block" disabled={status === 'sendet' || !datenschutz || !form.phone.trim()}>
|
||
{status === 'sendet' ? 'Wird gesendet …' : 'Anfrage senden'}
|
||
</button>
|
||
</form>
|
||
</>
|
||
);
|
||
}
|