Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-23 21:53:26 +00:00
co-authored by renee-png
parent 95e84ad842
commit 515bf890c6
3 changed files with 601 additions and 0 deletions
+219
View File
@@ -0,0 +1,219 @@
import { createFileRoute, Link, notFound } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { supabase } from "@/integrations/supabase/client";
import { Loader2, Phone, Mail, MapPin } from "lucide-react";
import { Button } from "@/components/ui/button";
import justiceIcon from "@/components/justice-icon";
export const Route = createFileRoute("/p/$slug")({
component: PublicPagePage,
});
type PageRow = {
id: string; slug: string; title: string; meta_description: string | null; status: string;
};
type SectionRow = { id: string; section_type: string; sort_order: number; content: any };
type FirmRow = {
company_name: string | null; contact_email: string | null; contact_phone: string | null;
address_line1: string | null; address_line2: string | null; city: string | null;
state: string | null; postal_code: string | null; logo_storage_path: string | null;
};
type NavPage = { slug: string; title: string };
function PublicPagePage() {
const { slug } = Route.useParams();
const [loading, setLoading] = useState(true);
const [page, setPage] = useState<PageRow | null>(null);
const [sections, setSections] = useState<SectionRow[]>([]);
const [firm, setFirm] = useState<FirmRow | null>(null);
const [navPages, setNavPages] = useState<NavPage[]>([]);
const [logoUrl, setLogoUrl] = useState<string | null>(null);
useEffect(() => {
(async () => {
setLoading(true);
const [pRes, fRes, nRes] = await Promise.all([
supabase.from("public_pages").select("*").eq("slug", slug).eq("status", "published").maybeSingle(),
supabase.from("firm_settings").select("*").order("updated_at", { ascending: false }).limit(1).maybeSingle(),
supabase.from("public_pages").select("slug,title").eq("status", "published").eq("show_in_nav", true).order("sort_order", { ascending: true }),
]);
if (pRes.data) {
setPage(pRes.data as PageRow);
const { data: sData } = await supabase.from("public_page_sections")
.select("*").eq("page_id", (pRes.data as PageRow).id)
.order("sort_order", { ascending: true });
setSections((sData ?? []) as SectionRow[]);
}
if (fRes.data) {
setFirm(fRes.data as FirmRow);
if ((fRes.data as FirmRow).logo_storage_path) {
const { data } = supabase.storage.from("firm-logos").getPublicUrl((fRes.data as FirmRow).logo_storage_path!);
setLogoUrl(data.publicUrl);
}
}
setNavPages((nRes.data ?? []) as NavPage[]);
setLoading(false);
if (pRes.data) {
document.title = `${(pRes.data as PageRow).title} | ${(fRes.data as FirmRow | null)?.company_name ?? "Stage Law Firm"}`;
}
})();
}, [slug]);
if (loading) {
return <div className="min-h-screen flex items-center justify-center"><Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /></div>;
}
if (!page) {
return (
<div className="min-h-screen flex items-center justify-center bg-background px-4">
<div className="text-center">
<h1 className="text-2xl font-semibold">Page not found</h1>
<p className="text-sm text-muted-foreground mt-2">This page doesn't exist or hasn't been published yet.</p>
</div>
</div>
);
}
const firmName = firm?.company_name ?? "Stage Law Firm, PLLC";
const cityLine = [firm?.city, firm?.state, firm?.postal_code].filter(Boolean).join(", ");
return (
<div className="min-h-screen bg-white text-slate-900 flex flex-col">
<header className="border-b border-slate-200 bg-white">
<div className="max-w-6xl mx-auto px-6 py-5 flex items-center justify-between">
<Link to="/p/$slug" params={{ slug: navPages[0]?.slug ?? slug }} className="flex items-center gap-3">
{logoUrl ? (
<img src={logoUrl} alt={firmName} className="h-12 w-auto" />
) : (
<div className="h-12 w-12 bg-slate-100 rounded flex items-center justify-center text-xs">Logo</div>
)}
<span className="font-serif text-xl tracking-wide uppercase">{firmName}</span>
</Link>
<nav className="hidden md:flex items-center gap-6 text-sm font-medium uppercase tracking-wide">
{navPages.map((np) => (
<Link key={np.slug} to="/p/$slug" params={{ slug: np.slug }} className="hover:text-slate-600">
{np.title}
</Link>
))}
</nav>
</div>
</header>
<main className="flex-1">
{sections.length === 0 ? (
<div className="max-w-3xl mx-auto px-6 py-16">
<h1 className="text-3xl font-serif">{page.title}</h1>
<p className="text-muted-foreground mt-2">No content yet.</p>
</div>
) : sections.map((s) => <SectionRenderer key={s.id} section={s} />)}
</main>
<footer className="bg-[hsl(220_60%_15%)] text-white">
<div className="max-w-6xl mx-auto px-6 py-12 grid md:grid-cols-2 gap-8">
<div className="flex items-start gap-4">
{logoUrl && <img src={logoUrl} alt={firmName} className="h-16 w-auto bg-white/5 rounded p-1" />}
<div>
<h3 className="font-serif text-2xl">{firmName}</h3>
</div>
</div>
<div className="space-y-3 text-sm">
{firm?.contact_phone && (
<div className="flex items-center gap-3"><Phone className="h-4 w-4 opacity-70" /><a href={`tel:${firm.contact_phone}`}>{firm.contact_phone}</a></div>
)}
{firm?.contact_email && (
<div className="flex items-center gap-3"><Mail className="h-4 w-4 opacity-70" /><a href={`mailto:${firm.contact_email}`}>{firm.contact_email}</a></div>
)}
{(firm?.address_line1 || cityLine) && (
<div className="flex items-start gap-3"><MapPin className="h-4 w-4 opacity-70 mt-0.5" />
<div>{firm?.address_line1}{firm?.address_line2 ? <>, {firm.address_line2}</> : null}{cityLine ? <><br />{cityLine}</> : null}</div>
</div>
)}
</div>
</div>
<div className="bg-[hsl(15_60%_55%)] text-white py-3 text-center text-sm">
Copyright {new Date().getFullYear()}. All Rights Reserved.
</div>
</footer>
</div>
);
}
function SectionRenderer({ section }: { section: SectionRow }) {
const c = section.content || {};
switch (section.section_type) {
case "hero":
return (
<section className="bg-slate-50 border-b border-slate-200">
<div className="max-w-4xl mx-auto px-6 py-20 text-center">
{c.eyebrow && <p className="text-xs uppercase tracking-[0.3em] text-slate-500 mb-4">{c.eyebrow}</p>}
<h1 className="font-serif text-4xl md:text-5xl text-[hsl(220_60%_15%)]">{c.heading}</h1>
{c.subheading && <p className="mt-4 text-lg text-slate-600 max-w-2xl mx-auto">{c.subheading}</p>}
</div>
</section>
);
case "rich_text":
return (
<section className="max-w-3xl mx-auto px-6 py-12">
<div className="prose prose-slate max-w-none" dangerouslySetInnerHTML={{ __html: c.html ?? "" }} />
</section>
);
case "feature_grid": {
const items = Array.isArray(c.items) ? c.items : [];
return (
<section className="max-w-5xl mx-auto px-6 py-16">
{c.heading && <h2 className="font-serif text-3xl text-center mb-10 text-[hsl(220_60%_15%)]">{c.heading}</h2>}
<div className="grid md:grid-cols-3 gap-8">
{items.map((it: any, i: number) => (
<div key={i} className="text-center">
<h3 className="font-serif text-xl mb-2">{it.title}</h3>
<p className="text-slate-600 text-sm">{it.body}</p>
</div>
))}
</div>
</section>
);
}
case "cta":
return (
<section className="bg-[hsl(220_60%_15%)] text-white">
<div className="max-w-3xl mx-auto px-6 py-16 text-center">
<h2 className="font-serif text-3xl">{c.heading}</h2>
{c.subheading && <p className="mt-3 text-white/80">{c.subheading}</p>}
{c.button_label && c.button_url && (
<Button asChild size="lg" className="mt-6 bg-white text-[hsl(220_60%_15%)] hover:bg-white/90">
<a href={c.button_url}>{c.button_label}</a>
</Button>
)}
</div>
</section>
);
case "contact": {
return (
<section className="max-w-3xl mx-auto px-6 py-16">
{c.heading && <h2 className="font-serif text-3xl text-center mb-8 text-[hsl(220_60%_15%)]">{c.heading}</h2>}
<div className="grid sm:grid-cols-3 gap-6 text-center">
{c.phone && <div><Phone className="h-5 w-5 mx-auto mb-2 text-slate-500" /><a href={`tel:${c.phone}`} className="font-medium">{c.phone}</a></div>}
{c.email && <div><Mail className="h-5 w-5 mx-auto mb-2 text-slate-500" /><a href={`mailto:${c.email}`} className="font-medium">{c.email}</a></div>}
{c.address && <div><MapPin className="h-5 w-5 mx-auto mb-2 text-slate-500" /><div className="font-medium whitespace-pre-line">{c.address}</div></div>}
</div>
</section>
);
}
case "faq": {
const items = Array.isArray(c.items) ? c.items : [];
return (
<section className="max-w-3xl mx-auto px-6 py-16">
{c.heading && <h2 className="font-serif text-3xl text-center mb-8 text-[hsl(220_60%_15%)]">{c.heading}</h2>}
<div className="space-y-4">
{items.map((it: any, i: number) => (
<div key={i} className="border-b border-slate-200 pb-4">
<h3 className="font-serif text-lg mb-2">{it.q}</h3>
<p className="text-slate-600 text-sm">{it.a}</p>
</div>
))}
</div>
</section>
);
}
default: return null;
}
}
@@ -0,0 +1,351 @@
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { RichTextEditor } from "@/components/documents/rich-text-editor";
import {
Loader2, ArrowLeft, Plus, Trash2, ChevronUp, ChevronDown, Save, ExternalLink, Image as ImageIcon, X,
} from "lucide-react";
import { toast } from "sonner";
export const Route = createFileRoute("/settings/public-pages/$pageId")({
component: PublicPageEditor,
});
type PageRow = {
id: string; slug: string; title: string; meta_description: string | null;
status: string; show_in_nav: boolean; sort_order: number;
};
type SectionRow = {
id: string; page_id: string; section_type: string; sort_order: number; content: any;
};
const SECTION_TYPES = [
{ value: "hero", label: "Hero (heading + subtext)" },
{ value: "rich_text", label: "Rich text block" },
{ value: "feature_grid", label: "Feature grid" },
{ value: "cta", label: "Call-to-action" },
{ value: "contact", label: "Contact info" },
{ value: "faq", label: "FAQ list" },
];
function defaultContent(type: string): any {
switch (type) {
case "hero": return { heading: "Heading", subheading: "Subheading", eyebrow: "" };
case "rich_text": return { html: "<p>Write something...</p>" };
case "feature_grid": return { heading: "Features", items: [{ title: "Feature 1", body: "Description" }] };
case "cta": return { heading: "Ready to start?", subheading: "", button_label: "Contact us", button_url: "/p/contact" };
case "contact": return { heading: "Get in touch", phone: "", email: "", address: "" };
case "faq": return { heading: "FAQ", items: [{ q: "Question?", a: "Answer." }] };
default: return {};
}
}
function PublicPageEditor() {
const { pageId } = Route.useParams();
const { isAdmin } = useAuth();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [page, setPage] = useState<PageRow | null>(null);
const [sections, setSections] = useState<SectionRow[]>([]);
const load = async () => {
setLoading(true);
const [pRes, sRes] = await Promise.all([
supabase.from("public_pages").select("*").eq("id", pageId).maybeSingle(),
supabase.from("public_page_sections").select("*").eq("page_id", pageId).order("sort_order", { ascending: true }),
]);
if (pRes.data) setPage(pRes.data as PageRow);
setSections((sRes.data ?? []) as SectionRow[]);
setLoading(false);
};
useEffect(() => { load(); }, [pageId]);
const savePage = async () => {
if (!page) return;
setSaving(true);
const updates: any = {
title: page.title, slug: page.slug, meta_description: page.meta_description,
status: page.status, show_in_nav: page.show_in_nav, sort_order: page.sort_order,
};
if (page.status === "published") updates.published_at = new Date().toISOString();
const { error } = await supabase.from("public_pages").update(updates).eq("id", pageId);
setSaving(false);
if (error) { toast.error("Could not save", { description: error.message }); return; }
toast.success("Page saved");
};
const addSection = async (type: string) => {
const sort_order = sections.length > 0 ? Math.max(...sections.map(s => s.sort_order)) + 1 : 0;
const { data, error } = await supabase
.from("public_page_sections")
.insert({ page_id: pageId, section_type: type, sort_order, content: defaultContent(type) })
.select("*").single();
if (error) { toast.error("Could not add section", { description: error.message }); return; }
setSections([...sections, data as SectionRow]);
};
const updateSection = (id: string, content: any) => {
setSections(sections.map(s => s.id === id ? { ...s, content } : s));
};
const persistSection = async (id: string) => {
const s = sections.find(x => x.id === id);
if (!s) return;
const { error } = await supabase.from("public_page_sections").update({ content: s.content }).eq("id", id);
if (error) toast.error("Could not save section", { description: error.message });
else toast.success("Section saved");
};
const removeSection = async (id: string) => {
if (!confirm("Delete this section?")) return;
const { error } = await supabase.from("public_page_sections").delete().eq("id", id);
if (error) { toast.error("Could not delete", { description: error.message }); return; }
setSections(sections.filter(s => s.id !== id));
};
const move = async (id: string, dir: -1 | 1) => {
const idx = sections.findIndex(s => s.id === id);
const swapIdx = idx + dir;
if (swapIdx < 0 || swapIdx >= sections.length) return;
const a = sections[idx]; const b = sections[swapIdx];
const newSections = [...sections];
newSections[idx] = { ...b, sort_order: a.sort_order };
newSections[swapIdx] = { ...a, sort_order: b.sort_order };
setSections(newSections);
await Promise.all([
supabase.from("public_page_sections").update({ sort_order: b.sort_order }).eq("id", a.id),
supabase.from("public_page_sections").update({ sort_order: a.sort_order }).eq("id", b.id),
]);
};
if (!isAdmin) return <p className="text-sm text-muted-foreground">Admins only.</p>;
if (loading) return <div className="flex justify-center py-10"><Loader2 className="h-5 w-5 animate-spin" /></div>;
if (!page) return <p className="text-sm text-muted-foreground">Page not found.</p>;
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<Button variant="ghost" size="sm" asChild>
<Link to="/settings/public-pages"><ArrowLeft className="h-4 w-4 mr-2" />All pages</Link>
</Button>
<div className="flex gap-2">
{page.status === "published" && (
<Button variant="outline" size="sm" asChild>
<a href={`/p/${page.slug}`} target="_blank" rel="noreferrer"><ExternalLink className="h-4 w-4 mr-2" />View</a>
</Button>
)}
<Button onClick={savePage} disabled={saving}>
{saving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
Save page
</Button>
</div>
</div>
<Card>
<CardHeader><CardTitle className="text-base">Page settings</CardTitle></CardHeader>
<CardContent className="space-y-3">
<div className="grid sm:grid-cols-2 gap-3">
<div>
<Label>Title</Label>
<Input value={page.title} onChange={(e) => setPage({ ...page, title: e.target.value })} />
</div>
<div>
<Label>Slug</Label>
<Input value={page.slug} onChange={(e) => setPage({ ...page, slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "") })} />
</div>
</div>
<div>
<Label>Meta description (SEO)</Label>
<Textarea rows={2} value={page.meta_description ?? ""} onChange={(e) => setPage({ ...page, meta_description: e.target.value })} />
</div>
<div className="grid sm:grid-cols-3 gap-3">
<div>
<Label>Status</Label>
<Select value={page.status} onValueChange={(v) => setPage({ ...page, status: v })}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="draft">Draft</SelectItem>
<SelectItem value="published">Published</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Sort order</Label>
<Input type="number" value={page.sort_order} onChange={(e) => setPage({ ...page, sort_order: parseInt(e.target.value) || 0 })} />
</div>
<div className="flex items-end gap-2 pb-2">
<Switch checked={page.show_in_nav} onCheckedChange={(v) => setPage({ ...page, show_in_nav: v })} id="nav" />
<Label htmlFor="nav">Show in nav</Label>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-base">Sections</CardTitle>
<Select onValueChange={addSection}>
<SelectTrigger className="w-[220px]"><SelectValue placeholder="+ Add section" /></SelectTrigger>
<SelectContent>
{SECTION_TYPES.map(t => <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>)}
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent className="space-y-4">
{sections.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-6">No sections yet. Add one above.</p>
)}
{sections.map((s, i) => (
<Card key={s.id} className="border-2">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge variant="outline">{SECTION_TYPES.find(t => t.value === s.section_type)?.label ?? s.section_type}</Badge>
</div>
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" onClick={() => move(s.id, -1)} disabled={i === 0}><ChevronUp className="h-4 w-4" /></Button>
<Button variant="ghost" size="sm" onClick={() => move(s.id, 1)} disabled={i === sections.length - 1}><ChevronDown className="h-4 w-4" /></Button>
<Button variant="ghost" size="sm" onClick={() => persistSection(s.id)}><Save className="h-4 w-4" /></Button>
<Button variant="ghost" size="sm" onClick={() => removeSection(s.id)}><Trash2 className="h-4 w-4 text-destructive" /></Button>
</div>
</div>
</CardHeader>
<CardContent>
<SectionEditor section={s} onChange={(c) => updateSection(s.id, c)} />
</CardContent>
</Card>
))}
</CardContent>
</Card>
</div>
);
}
function SectionEditor({ section, onChange }: { section: SectionRow; onChange: (c: any) => void }) {
const c = section.content || {};
const upd = (patch: any) => onChange({ ...c, ...patch });
const uploadImage = async (file: File): Promise<string | null> => {
const ext = file.name.split(".").pop() || "png";
const path = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
const { error } = await supabase.storage.from("public-page-images").upload(path, file, { upsert: false, contentType: file.type });
if (error) { toast.error("Upload failed", { description: error.message }); return null; }
const { data } = supabase.storage.from("public-page-images").getPublicUrl(path);
return data.publicUrl;
};
switch (section.section_type) {
case "hero":
return (
<div className="space-y-3">
<div><Label>Eyebrow (small label above heading)</Label><Input value={c.eyebrow ?? ""} onChange={(e) => upd({ eyebrow: e.target.value })} /></div>
<div><Label>Heading</Label><Input value={c.heading ?? ""} onChange={(e) => upd({ heading: e.target.value })} /></div>
<div><Label>Subheading</Label><Textarea rows={2} value={c.subheading ?? ""} onChange={(e) => upd({ subheading: e.target.value })} /></div>
</div>
);
case "rich_text":
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label>Body</Label>
<Button type="button" variant="outline" size="sm" onClick={() => {
const input = document.createElement("input");
input.type = "file"; input.accept = "image/*";
input.onchange = async () => {
const file = input.files?.[0]; if (!file) return;
const url = await uploadImage(file);
if (url) upd({ html: (c.html ?? "") + `<p><img src="${url}" alt="" style="max-width:100%;" /></p>` });
};
input.click();
}}><ImageIcon className="h-4 w-4 mr-2" />Insert image</Button>
</div>
<RichTextEditor value={c.html ?? ""} onChange={(html) => upd({ html })} minHeight={250} />
</div>
);
case "feature_grid": {
const items = Array.isArray(c.items) ? c.items : [];
return (
<div className="space-y-3">
<div><Label>Heading</Label><Input value={c.heading ?? ""} onChange={(e) => upd({ heading: e.target.value })} /></div>
<div className="space-y-2">
{items.map((it: any, idx: number) => (
<div key={idx} className="flex gap-2 items-start border rounded-md p-2">
<div className="flex-1 space-y-2">
<Input placeholder="Title" value={it.title ?? ""} onChange={(e) => {
const next = [...items]; next[idx] = { ...it, title: e.target.value }; upd({ items: next });
}} />
<Textarea rows={2} placeholder="Description" value={it.body ?? ""} onChange={(e) => {
const next = [...items]; next[idx] = { ...it, body: e.target.value }; upd({ items: next });
}} />
</div>
<Button variant="ghost" size="sm" onClick={() => upd({ items: items.filter((_: any, i: number) => i !== idx) })}><X className="h-4 w-4" /></Button>
</div>
))}
<Button variant="outline" size="sm" onClick={() => upd({ items: [...items, { title: "", body: "" }] })}><Plus className="h-4 w-4 mr-2" />Add feature</Button>
</div>
</div>
);
}
case "cta":
return (
<div className="space-y-3">
<div><Label>Heading</Label><Input value={c.heading ?? ""} onChange={(e) => upd({ heading: e.target.value })} /></div>
<div><Label>Subheading</Label><Textarea rows={2} value={c.subheading ?? ""} onChange={(e) => upd({ subheading: e.target.value })} /></div>
<div className="grid sm:grid-cols-2 gap-3">
<div><Label>Button label</Label><Input value={c.button_label ?? ""} onChange={(e) => upd({ button_label: e.target.value })} /></div>
<div><Label>Button URL</Label><Input value={c.button_url ?? ""} onChange={(e) => upd({ button_url: e.target.value })} /></div>
</div>
</div>
);
case "contact":
return (
<div className="space-y-3">
<div><Label>Heading</Label><Input value={c.heading ?? ""} onChange={(e) => upd({ heading: e.target.value })} /></div>
<div className="grid sm:grid-cols-2 gap-3">
<div><Label>Phone</Label><Input value={c.phone ?? ""} onChange={(e) => upd({ phone: e.target.value })} /></div>
<div><Label>Email</Label><Input value={c.email ?? ""} onChange={(e) => upd({ email: e.target.value })} /></div>
</div>
<div><Label>Address</Label><Textarea rows={2} value={c.address ?? ""} onChange={(e) => upd({ address: e.target.value })} /></div>
</div>
);
case "faq": {
const items = Array.isArray(c.items) ? c.items : [];
return (
<div className="space-y-3">
<div><Label>Heading</Label><Input value={c.heading ?? ""} onChange={(e) => upd({ heading: e.target.value })} /></div>
<div className="space-y-2">
{items.map((it: any, idx: number) => (
<div key={idx} className="flex gap-2 items-start border rounded-md p-2">
<div className="flex-1 space-y-2">
<Input placeholder="Question" value={it.q ?? ""} onChange={(e) => {
const next = [...items]; next[idx] = { ...it, q: e.target.value }; upd({ items: next });
}} />
<Textarea rows={2} placeholder="Answer" value={it.a ?? ""} onChange={(e) => {
const next = [...items]; next[idx] = { ...it, a: e.target.value }; upd({ items: next });
}} />
</div>
<Button variant="ghost" size="sm" onClick={() => upd({ items: items.filter((_: any, i: number) => i !== idx) })}><X className="h-4 w-4" /></Button>
</div>
))}
<Button variant="outline" size="sm" onClick={() => upd({ items: [...items, { q: "", a: "" }] })}><Plus className="h-4 w-4 mr-2" />Add Q&amp;A</Button>
</div>
</div>
);
}
default: return <p className="text-sm text-muted-foreground">Unknown section type.</p>;
}
}