Import aus Monorepo (frischer Start)
All checks were successful
Build & Deploy / deploy (push) Successful in 26s

This commit is contained in:
Lucas Orth
2026-08-24 12:13:54 +02:00
commit 32aa0a549f
38 changed files with 7881 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
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({ 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,
name: [c.first_name, c.last_name].filter(Boolean).join(' ') || f.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({
name: form.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-name">Name</label>
<input id="a-name" className="sk-input" value={form.name} onChange={set('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>
</>
);
}