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, sort_order: parseInt(e.target.value) || 0 })} />
setPage({ ...page, show_in_nav: v })} id="nav" />
Sections
{sections.length === 0 && (
No sections yet. Add one above.
)}
{sections.map((s, i) => (
{SECTION_TYPES.find(t => t.value === s.section_type)?.label ?? s.section_type}
updateSection(s.id, c)} />
))}
);
}
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 => {
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 (
);
case "rich_text":
return (
upd({ html })} minHeight={250} />
);
case "feature_grid": {
const items = Array.isArray(c.items) ? c.items : [];
return (
upd({ heading: e.target.value })} />
{items.map((it: any, idx: number) => (
))}
);
}
case "cta":
return (
);
case "contact":
return (
);
case "faq": {
const items = Array.isArray(c.items) ? c.items : [];
return (
upd({ heading: e.target.value })} />
{items.map((it: any, idx: number) => (
))}
);
}
default: return Unknown section type.
;
}
}