File size: 5,496 Bytes
b1324bf 32f4dc0 b1324bf 32f4dc0 b1324bf 32f4dc0 b1324bf 7fe6a68 32f4dc0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | import React, { useEffect, useState } from 'react';
import { Loader2 } from 'lucide-react';
/**
* Public embed page (iframe). No sign-in required.
* Route: /embed/cta/:publicId
*/
export default function CtaFormEmbed() {
const publicId = window.location.pathname.split('/').filter(Boolean).pop() || '';
const [phase, setPhase] = useState('loading');
const [form, setForm] = useState(null);
const [error, setError] = useState('');
const [values, setValues] = useState({});
const [submitting, setSubmitting] = useState(false);
const [done, setDone] = useState(false);
const [doneMsg, setDoneMsg] = useState('');
useEffect(() => {
if (!publicId) {
setError('Invalid form link');
setPhase('error');
return;
}
fetch(`/api/public/cta-forms/${encodeURIComponent(publicId)}`)
.then((r) => r.json().then((j) => ({ ok: r.ok, j })))
.then(({ ok, j }) => {
if (!ok) throw new Error(j.detail || 'Form not found');
setForm(j);
const init = {};
(j.fields || []).forEach((f) => {
init[f.key] = '';
});
setValues(init);
setPhase('ready');
})
.catch((e) => {
setError(e.message || 'Could not load form');
setPhase('error');
});
}, [publicId]);
const onChange = (key, val) => {
setValues((prev) => ({ ...prev, [key]: val }));
};
const onSubmit = async (e) => {
e.preventDefault();
setSubmitting(true);
setError('');
try {
const res = await fetch(`/api/public/cta-forms/${encodeURIComponent(publicId)}/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fields: values,
page_url: window.location.href,
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(
typeof data.detail === 'string' ? data.detail : 'Submission failed'
);
}
setDone(true);
setDoneMsg(data.message || 'Thank you — we will be in touch soon.');
} catch (err) {
setError(err.message || 'Submission failed');
} finally {
setSubmitting(false);
}
};
if (phase === 'loading') {
return (
<div className="flex min-h-[12rem] items-center justify-center bg-transparent p-4">
<Loader2 className="h-8 w-8 animate-spin text-violet-600" />
</div>
);
}
if (phase === 'error') {
return (
<div className="flex min-h-[12rem] items-center justify-center bg-transparent p-4">
<p className="text-sm text-red-600">{error}</p>
</div>
);
}
return (
<div className="bg-transparent p-4">
<div className="mx-auto w-full max-w-lg rounded-xl border border-slate-200 bg-white p-4">
{done ? (
<p className="text-sm text-green-700 text-center py-6">{doneMsg}</p>
) : (
<form onSubmit={onSubmit} className="space-y-3">
{(form.fields || []).map((f) => (
<div key={f.key}>
<label className="block text-xs font-medium text-slate-600 mb-1">
{f.label}
{f.required ? ' *' : ''}
</label>
{f.type === 'textarea' ? (
<textarea
className="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm"
rows={4}
required={!!f.required}
value={values[f.key] || ''}
onChange={(e) => onChange(f.key, e.target.value)}
/>
) : (
<input
type={f.type || 'text'}
className="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm"
required={!!f.required}
value={values[f.key] || ''}
onChange={(e) => onChange(f.key, e.target.value)}
/>
)}
</div>
))}
{error ? <p className="text-xs text-red-600">{error}</p> : null}
<button
type="submit"
disabled={submitting}
className="w-full rounded-lg bg-violet-600 py-2.5 text-sm font-semibold text-white hover:bg-violet-700 disabled:opacity-60"
>
{submitting ? 'Sending…' : 'Submit'}
</button>
</form>
)}
</div>
</div>
);
}
|