Download frontend/src/pages/CtaFormEmbed.jsx from Seth0330/EMAILOUT: direct link, hf CLI and curl.
- Browser
- Download file 5.5 kB
-
https://huggingface.co/spaces/Seth0330/EMAILOUT/resolve/main/frontend/src/pages/CtaFormEmbed.jsx
- Command line
-
hf download hf://spaces/Seth0330/EMAILOUT/frontend/src/pages/CtaFormEmbed.jsx
-
curl -L -o CtaFormEmbed.jsx https://huggingface.co/spaces/Seth0330/EMAILOUT/resolve/main/frontend/src/pages/CtaFormEmbed.jsx
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> | |
| ); | |
| } | |