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: "

Write something...

" }; 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(null); const [sections, setSections] = useState([]); 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

Admins only.

; if (loading) return
; if (!page) return

Page not found.

; return (
{page.status === "published" && ( )}
Page settings
setPage({ ...page, title: e.target.value })} />
setPage({ ...page, slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "") })} />