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.
) : ( <>
setDraft((d) => ({ ...d, name: e.target.value })) } className="mt-1" />

Describe your offer, audience, and goal — AI fills the page template