EMAILOUT / frontend /src /pages /CtaFormEmbed.jsx
Seth
update
7fe6a68
Raw
History Blame Contribute Delete
5.5 kB
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>
);
}