import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
ArrowLeft,
ClipboardCopy,
ExternalLink,
FileInput,
Loader2,
Plus,
Trash2,
GripVertical,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import AppShell from '@/components/layout/AppShell';
import CtaFormPreview from '@/components/settings/CtaFormPreview';
import { apiFetch } from '@/lib/api';
import { buildCtaEmbedCodes } from '@/lib/ctaFormEmbed';
import { cn } from '@/lib/utils';
const FIELD_PRESETS = [
{ key: 'name', label: 'Name', type: 'text', required: true, map_to: 'name' },
{ key: 'email', label: 'Email', type: 'email', required: true, map_to: 'email' },
{
key: 'company_name',
label: 'Company Name',
type: 'text',
required: false,
map_to: 'company_name',
},
{ key: 'title', label: 'Job Title', type: 'text', required: false, map_to: 'title' },
{ key: 'phone', label: 'Phone', type: 'tel', required: false, map_to: 'phone' },
{ key: 'message', label: 'Message', type: 'textarea', required: false, map_to: 'message' },
];
const MAP_OPTIONS = [
{ value: 'name', label: 'Full name' },
{ value: 'email', label: 'Email' },
{ value: 'company_name', label: 'Company' },
{ value: 'title', label: 'Job title' },
{ value: 'phone', label: 'Phone' },
{ value: 'message', label: 'Message' },
{ value: 'custom', label: 'Custom (in submission only)' },
];
const AUTOSAVE_MS = 600;
function emptyDraft() {
return {
name: 'New contact form',
fields: FIELD_PRESETS.slice(0, 3).map((f) => ({ ...f })),
};
}
function draftFromForm(form) {
return {
name: form.name,
fields: (form.fields || []).map((f) => ({ ...f })),
};
}
function draftSnapshot(draft) {
return JSON.stringify({
name: (draft.name || '').trim(),
fields: draft.fields || [],
});
}
function CopyBlock({ label, code }) {
return (
{label}
code && navigator.clipboard.writeText(code)}
>
Copy
);
}
function SaveIndicator({ state }) {
if (state === 'saving') {
return (
Saving…
);
}
if (state === 'saved') {
return Saved ;
}
if (state === 'error') {
return Could not save ;
}
return null;
}
export default function CtaFormsSettings({ onBack }) {
const [forms, setForms] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedId, setSelectedId] = useState(null);
const [draft, setDraft] = useState(emptyDraft());
const [saveState, setSaveState] = useState('idle');
const [creating, setCreating] = useState(false);
const [error, setError] = useState('');
const skipAutosaveRef = useRef(true);
const saveTimerRef = useRef(null);
const lastSavedSnapshotRef = useRef('');
const savedFadeTimerRef = useRef(null);
const dragFieldIdxRef = useRef(null);
const [dropFieldIdx, setDropFieldIdx] = useState(null);
const selected = useMemo(
() => forms.find((f) => f.id === selectedId) || null,
[forms, selectedId]
);
const embedCodes = useMemo(() => {
if (!selected?.public_id) {
return { embed_iframe: '', embed_script: '' };
}
return buildCtaEmbedCodes({
publicId: selected.public_id,
name: draft.name,
});
}, [selected?.public_id, draft.name]);
const previewUrl = selected?.public_id ? `/embed/cta/${selected.public_id}` : '';
const loadForms = useCallback(async () => {
setLoading(true);
setError('');
try {
const r = await apiFetch('/api/cta-forms');
const data = r.ok ? await r.json() : { forms: [] };
if (!r.ok) throw new Error(data.detail || 'Could not load forms');
const list = data.forms || [];
setForms(list);
setSelectedId((prev) => {
if (prev && list.some((f) => f.id === prev)) return prev;
return list.length ? list[0].id : null;
});
} catch (e) {
setError(e.message || 'Load failed');
setForms([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadForms();
}, [loadForms]);
useEffect(() => {
if (!selected) return;
const next = draftFromForm(selected);
skipAutosaveRef.current = true;
lastSavedSnapshotRef.current = draftSnapshot(next);
setDraft(next);
setSaveState('idle');
}, [selected?.id]);
useEffect(() => {
if (!selectedId) return;
if (skipAutosaveRef.current) {
skipAutosaveRef.current = false;
return;
}
const snapshot = draftSnapshot(draft);
if (snapshot === lastSavedSnapshotRef.current) return;
const name = draft.name?.trim();
if (!name) return;
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
saveTimerRef.current = setTimeout(async () => {
const payload = JSON.parse(snapshot);
if (payload.name !== (draft.name || '').trim()) return;
if (snapshot !== draftSnapshot(draft)) return;
setSaveState('saving');
try {
const res = await apiFetch(`/api/cta-forms/${selectedId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: payload.name,
fields: payload.fields,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Save failed');
lastSavedSnapshotRef.current = snapshot;
setForms((prev) => prev.map((f) => (f.id === data.id ? data : f)));
if (savedFadeTimerRef.current) clearTimeout(savedFadeTimerRef.current);
setSaveState('saved');
savedFadeTimerRef.current = window.setTimeout(() => setSaveState('idle'), 2000);
} catch (e) {
setError(e.message || 'Save failed');
setSaveState('error');
}
}, AUTOSAVE_MS);
return () => {
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
};
}, [draft, selectedId]);
const selectForm = (f) => {
const next = draftFromForm(f);
setSelectedId(f.id);
skipAutosaveRef.current = true;
lastSavedSnapshotRef.current = draftSnapshot(next);
setDraft(next);
setSaveState('idle');
};
const createForm = async () => {
setCreating(true);
setError('');
try {
const res = await apiFetch('/api/cta-forms', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'New contact form',
fields: FIELD_PRESETS.slice(0, 3),
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Create failed');
await loadForms();
setSelectedId(data.id);
const next = draftFromForm(data);
skipAutosaveRef.current = true;
lastSavedSnapshotRef.current = draftSnapshot(next);
setDraft(next);
} catch (e) {
setError(e.message || 'Create failed');
} finally {
setCreating(false);
}
};
const deleteForm = async () => {
if (!selectedId) return;
if (!window.confirm('Delete this form? Embed codes will stop working.')) return;
try {
const res = await apiFetch(`/api/cta-forms/${selectedId}`, { method: 'DELETE' });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || 'Delete failed');
}
setSelectedId(null);
setDraft(emptyDraft());
await loadForms();
} catch (e) {
setError(e.message || 'Delete failed');
}
};
const addPreset = (preset) => {
setDraft((d) => {
const keys = new Set(d.fields.map((f) => f.key));
let key = preset.key;
let n = 2;
while (keys.has(key)) {
key = `${preset.key}_${n}`;
n += 1;
}
return {
...d,
fields: [...d.fields, { ...preset, key }],
};
});
};
const updateField = (idx, patch) => {
setDraft((d) => ({
...d,
fields: d.fields.map((f, i) => (i === idx ? { ...f, ...patch } : f)),
}));
};
const removeField = (idx) => {
setDraft((d) => ({
...d,
fields: d.fields.filter((_, i) => i !== idx),
}));
};
const reorderFields = (fromIdx, toIdx) => {
if (fromIdx === toIdx || fromIdx == null || toIdx == null) return;
setDraft((d) => {
const fields = [...d.fields];
const [moved] = fields.splice(fromIdx, 1);
fields.splice(toIdx, 0, moved);
return { ...d, fields };
});
};
const onFieldDragStart = (idx) => (e) => {
dragFieldIdxRef.current = idx;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', String(idx));
};
const onFieldDragOver = (idx) => (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
setDropFieldIdx(idx);
};
const onFieldDrop = (idx) => (e) => {
e.preventDefault();
const fromIdx = Number(e.dataTransfer.getData('text/plain'));
reorderFields(fromIdx, idx);
dragFieldIdxRef.current = null;
setDropFieldIdx(null);
};
const onFieldDragEnd = () => {
dragFieldIdxRef.current = null;
setDropFieldIdx(null);
};
return (
Back to settings
{creating ? (
) : (
)}
New form
{error ? (
{error}
) : null}
{loading ? (
) : (
{!selected ? (
Create a form to configure fields and copy embed codes for WordPress.
) : (
<>
Add field
{FIELD_PRESETS.map((p) => (
addPreset(p)}
>
+ {p.label}
))}
Fields
{draft.fields.length > 1 ? (
Drag the handle to reorder fields
) : null}
{draft.fields.length === 0 ? (
Add at least Name and Email.
) : (
draft.fields.map((f, idx) => (
))
)}
Embed on WordPress
Paste either code into a WordPress HTML block, page builder, or
theme widget. The form sizes itself to fit your page width.
Submissions go straight to Leads in this workspace.
{previewUrl ? (
Full-page preview:{' '}
Open in new tab
) : null}
>
)}
{selected ? (
) : null}
)}
);
}
function PreviewPanel({ draft, previewUrl }) {
return (
Live preview
{previewUrl ? (
Open tab
) : null}
Updates as you edit · matches embed appearance
);
}