import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
ArrowLeft,
ClipboardCopy,
ExternalLink,
ImagePlus,
Loader2,
Plus,
Sparkles,
Trash2,
} 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 LandingPageTemplate from '@/components/landing/LandingPageTemplate';
import { apiFetch } from '@/lib/api';
import { cn } from '@/lib/utils';
const AUTOSAVE_MS = 700;
function emptyDraft() {
return {
name: 'New landing page',
prompt: '',
content: {
headline: '',
subheadline: '',
benefits: [
{ title: 'Benefit 1', body: '' },
{ title: 'Benefit 2', body: '' },
{ title: 'Benefit 3', body: '' },
],
form_heading: 'Book a demo',
form_subtext: '',
footer_text: '',
},
cta_form_id: null,
banner_url: null,
logo_url: null,
public_url: '',
cta_form_public_id: null,
};
}
function draftFromPage(p) {
const c = p.content || {};
const benefits = (c.benefits || []).map((b) => ({ ...b }));
while (benefits.length < 3) {
benefits.push({ title: `Benefit ${benefits.length + 1}`, body: '' });
}
return {
name: p.name,
prompt: p.prompt || '',
content: {
headline: c.headline || '',
subheadline: c.subheadline || '',
benefits: benefits.slice(0, 3),
form_heading: c.form_heading || 'Book a demo',
form_subtext: c.form_subtext || '',
footer_text: c.footer_text || '',
},
cta_form_id: p.cta_form_id ?? null,
banner_url: p.banner_url || null,
logo_url: p.logo_url || null,
public_url: p.public_url || '',
cta_form_public_id: p.cta_form_public_id || null,
};
}
function draftSnapshot(draft) {
return JSON.stringify({
name: (draft.name || '').trim(),
prompt: draft.prompt || '',
content: draft.content,
cta_form_id: draft.cta_form_id,
});
}
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 LandingPagesSettings({ onBack }) {
const [pages, setPages] = useState([]);
const [ctaForms, setCtaForms] = 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 [generating, setGenerating] = useState(false);
const [uploadingBanner, setUploadingBanner] = useState(false);
const [uploadingLogo, setUploadingLogo] = useState(false);
const [error, setError] = useState('');
const skipAutosaveRef = useRef(true);
const saveTimerRef = useRef(null);
const lastSavedSnapshotRef = useRef('');
const savedFadeTimerRef = useRef(null);
const selected = useMemo(
() => pages.find((p) => p.id === selectedId) || null,
[pages, selectedId]
);
const loadPages = useCallback(async () => {
setLoading(true);
setError('');
try {
const [pr, fr] = await Promise.all([
apiFetch('/api/landing-pages'),
apiFetch('/api/landing-pages/cta-form-options'),
]);
const pdata = pr.ok ? await pr.json() : { pages: [] };
const fdata = fr.ok ? await fr.json() : { forms: [] };
if (!pr.ok) throw new Error(pdata.detail || 'Could not load pages');
const list = pdata.pages || [];
setPages(list);
setCtaForms(fdata.forms || []);
setSelectedId((prev) => {
if (prev && list.some((p) => p.id === prev)) return prev;
return list.length ? list[0].id : null;
});
} catch (e) {
setError(e.message || 'Load failed');
setPages([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadPages();
}, [loadPages]);
useEffect(() => {
if (!selected) return;
const next = draftFromPage(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 () => {
if (snapshot !== draftSnapshot(draft)) return;
setSaveState('saving');
try {
const payload = JSON.parse(snapshot);
const res = await apiFetch(`/api/landing-pages/${selectedId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: payload.name,
prompt: payload.prompt,
content: payload.content,
cta_form_id: payload.cta_form_id,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Save failed');
lastSavedSnapshotRef.current = snapshot;
setPages((prev) => prev.map((p) => (p.id === data.id ? data : p)));
setDraft((d) => ({
...d,
public_url: data.public_url || d.public_url,
banner_url: data.banner_url ?? d.banner_url,
logo_url: data.logo_url ?? d.logo_url,
cta_form_public_id: data.cta_form_public_id ?? d.cta_form_public_id,
}));
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 selectPage = (p) => {
const next = draftFromPage(p);
setSelectedId(p.id);
skipAutosaveRef.current = true;
lastSavedSnapshotRef.current = draftSnapshot(next);
setDraft(next);
setSaveState('idle');
};
const createPage = async () => {
setCreating(true);
setError('');
try {
const res = await apiFetch('/api/landing-pages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'New landing page', prompt: '' }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Create failed');
await loadPages();
setSelectedId(data.id);
const next = draftFromPage(data);
skipAutosaveRef.current = true;
lastSavedSnapshotRef.current = draftSnapshot(next);
setDraft(next);
} catch (e) {
setError(e.message || 'Create failed');
} finally {
setCreating(false);
}
};
const deletePage = async () => {
if (!selectedId) return;
if (!window.confirm('Delete this landing page? The public link will stop working.')) return;
try {
const res = await apiFetch(`/api/landing-pages/${selectedId}`, { method: 'DELETE' });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || 'Delete failed');
}
setSelectedId(null);
setDraft(emptyDraft());
await loadPages();
} catch (e) {
setError(e.message || 'Delete failed');
}
};
const generateFromPrompt = async () => {
if (!selectedId) return;
const prompt = draft.prompt?.trim();
if (!prompt || prompt.length < 8) {
setError('Enter a prompt with at least 8 characters to generate.');
return;
}
setGenerating(true);
setError('');
try {
const res = await apiFetch(`/api/landing-pages/${selectedId}/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Generation failed');
setPages((prev) => prev.map((p) => (p.id === data.id ? data : p)));
const next = draftFromPage(data);
skipAutosaveRef.current = true;
lastSavedSnapshotRef.current = draftSnapshot(next);
setDraft(next);
setSaveState('saved');
} catch (e) {
setError(e.message || 'Generation failed');
} finally {
setGenerating(false);
}
};
const uploadAsset = async (kind, file) => {
if (!selectedId || !file) return;
const setter = kind === 'banner' ? setUploadingBanner : setUploadingLogo;
setter(true);
setError('');
try {
const fd = new FormData();
fd.append('file', file);
const res = await apiFetch(`/api/landing-pages/${selectedId}/${kind}`, {
method: 'POST',
body: fd,
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Upload failed');
setPages((prev) => prev.map((p) => (p.id === data.id ? data : p)));
setDraft((d) => ({
...d,
banner_url: data.banner_url ?? d.banner_url,
logo_url: data.logo_url ?? d.logo_url,
public_url: data.public_url || d.public_url,
}));
} catch (e) {
setError(e.message || 'Upload failed');
} finally {
setter(false);
}
};
const updateContent = (key, value) => {
setDraft((d) => ({
...d,
content: { ...d.content, [key]: value },
}));
};
const updateBenefit = (idx, patch) => {
setDraft((d) => ({
...d,
content: {
...d.content,
benefits: d.content.benefits.map((b, i) => (i === idx ? { ...b, ...patch } : b)),
},
}));
};
const previewCtaPublicId = useMemo(() => {
if (draft.cta_form_public_id) return draft.cta_form_public_id;
const form = ctaForms.find((f) => f.id === draft.cta_form_id);
return form?.public_id || null;
}, [draft.cta_form_id, draft.cta_form_public_id, ctaForms]);
const publicUrl = draft.public_url || (selected?.public_url ?? '');
return (
{error ? (
{error}
) : null}
{loading ? (
) : (
{!selected ? (
Create a landing page to generate copy and share a public link.
) : (
<>
Describe your offer, audience, and goal — AI fills the
page template
{ctaForms.length === 0 ? (
Create a CTA form first under Manage CTA forms.
) : null}
{publicUrl ? (
Public link
Share on LinkedIn, email, or ads. Submissions go to your
embedded CTA form → Leads.
{publicUrl}
Open
) : null}
>
)}
{selected ? (
) : null}
)}
);
}
function PreviewBlock({ draft, previewCtaPublicId, publicUrl }) {
return (
Live preview
{publicUrl ? (
Open tab
) : null}
);
}