diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index de7924c..de641cc 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -2775,6 +2775,86 @@ export type Database = { } Relationships: [] } + public_page_sections: { + Row: { + content: Json + created_at: string + id: string + page_id: string + section_type: string + sort_order: number + updated_at: string + } + Insert: { + content?: Json + created_at?: string + id?: string + page_id: string + section_type: string + sort_order?: number + updated_at?: string + } + Update: { + content?: Json + created_at?: string + id?: string + page_id?: string + section_type?: string + sort_order?: number + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "public_page_sections_page_id_fkey" + columns: ["page_id"] + isOneToOne: false + referencedRelation: "public_pages" + referencedColumns: ["id"] + }, + ] + } + public_pages: { + Row: { + created_at: string + created_by: string | null + id: string + meta_description: string | null + published_at: string | null + show_in_nav: boolean + slug: string + sort_order: number + status: string + title: string + updated_at: string + } + Insert: { + created_at?: string + created_by?: string | null + id?: string + meta_description?: string | null + published_at?: string | null + show_in_nav?: boolean + slug: string + sort_order?: number + status?: string + title: string + updated_at?: string + } + Update: { + created_at?: string + created_by?: string | null + id?: string + meta_description?: string | null + published_at?: string | null + show_in_nav?: boolean + slug?: string + sort_order?: number + status?: string + title?: string + updated_at?: string + } + Relationships: [] + } smtp_settings: { Row: { created_at: string diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 5d98e10..7cda72c 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -41,6 +41,7 @@ import { Route as SettingsFeesRouteImport } from './routes/settings.fees' import { Route as SettingsCustomFieldsRouteImport } from './routes/settings.custom-fields' import { Route as SettingsClientFieldsRouteImport } from './routes/settings.client-fields' import { Route as PayIdRouteImport } from './routes/pay.$id' +import { Route as PSlugRouteImport } from './routes/p.$slug' import { Route as InvoicesNewRouteImport } from './routes/invoices.new' import { Route as InvoicesInvoiceIdRouteImport } from './routes/invoices.$invoiceId' import { Route as HooksPollImapRouteImport } from './routes/hooks/poll-imap' @@ -52,7 +53,9 @@ import { Route as CasesNewRouteImport } from './routes/cases.new' import { Route as CasesCaseIdRouteImport } from './routes/cases.$caseId' import { Route as ApiStripeWebhookRouteImport } from './routes/api.stripe-webhook' import { Route as AdminUsersRouteImport } from './routes/admin.users' +import { Route as SettingsPublicPagesIndexRouteImport } from './routes/settings.public-pages.index' import { Route as DocumentsTemplatesIndexRouteImport } from './routes/documents.templates.index' +import { Route as SettingsPublicPagesPageIdRouteImport } from './routes/settings.public-pages.$pageId' import { Route as LovableEmailSuppressionRouteImport } from './routes/lovable/email/suppression' import { Route as InvoicesNewClientIdRouteImport } from './routes/invoices.new.$clientId' import { Route as DocumentsTemplatesNewRouteImport } from './routes/documents.templates.new' @@ -224,6 +227,11 @@ const PayIdRoute = PayIdRouteImport.update({ path: '/pay/$id', getParentRoute: () => rootRouteImport, } as any) +const PSlugRoute = PSlugRouteImport.update({ + id: '/p/$slug', + path: '/p/$slug', + getParentRoute: () => rootRouteImport, +} as any) const InvoicesNewRoute = InvoicesNewRouteImport.update({ id: '/invoices/new', path: '/invoices/new', @@ -279,11 +287,23 @@ const AdminUsersRoute = AdminUsersRouteImport.update({ path: '/admin/users', getParentRoute: () => rootRouteImport, } as any) +const SettingsPublicPagesIndexRoute = + SettingsPublicPagesIndexRouteImport.update({ + id: '/public-pages/', + path: '/public-pages/', + getParentRoute: () => SettingsRoute, + } as any) const DocumentsTemplatesIndexRoute = DocumentsTemplatesIndexRouteImport.update({ id: '/documents/templates/', path: '/documents/templates/', getParentRoute: () => rootRouteImport, } as any) +const SettingsPublicPagesPageIdRoute = + SettingsPublicPagesPageIdRouteImport.update({ + id: '/public-pages/$pageId', + path: '/public-pages/$pageId', + getParentRoute: () => SettingsRoute, + } as any) const LovableEmailSuppressionRoute = LovableEmailSuppressionRouteImport.update({ id: '/lovable/email/suppression', path: '/lovable/email/suppression', @@ -356,6 +376,7 @@ export interface FileRoutesByFullPath { '/hooks/poll-imap': typeof HooksPollImapRoute '/invoices/$invoiceId': typeof InvoicesInvoiceIdRoute '/invoices/new': typeof InvoicesNewRouteWithChildren + '/p/$slug': typeof PSlugRoute '/pay/$id': typeof PayIdRoute '/settings/client-fields': typeof SettingsClientFieldsRoute '/settings/custom-fields': typeof SettingsCustomFieldsRoute @@ -391,7 +412,9 @@ export interface FileRoutesByFullPath { '/documents/templates/new': typeof DocumentsTemplatesNewRoute '/invoices/new/$clientId': typeof InvoicesNewClientIdRoute '/lovable/email/suppression': typeof LovableEmailSuppressionRoute + '/settings/public-pages/$pageId': typeof SettingsPublicPagesPageIdRoute '/documents/templates/': typeof DocumentsTemplatesIndexRoute + '/settings/public-pages/': typeof SettingsPublicPagesIndexRoute '/lovable/email/queue/process': typeof LovableEmailQueueProcessRoute '/lovable/email/transactional/preview': typeof LovableEmailTransactionalPreviewRoute '/lovable/email/transactional/send': typeof LovableEmailTransactionalSendRoute @@ -411,6 +434,7 @@ export interface FileRoutesByTo { '/hooks/poll-imap': typeof HooksPollImapRoute '/invoices/$invoiceId': typeof InvoicesInvoiceIdRoute '/invoices/new': typeof InvoicesNewRouteWithChildren + '/p/$slug': typeof PSlugRoute '/pay/$id': typeof PayIdRoute '/settings/client-fields': typeof SettingsClientFieldsRoute '/settings/custom-fields': typeof SettingsCustomFieldsRoute @@ -446,7 +470,9 @@ export interface FileRoutesByTo { '/documents/templates/new': typeof DocumentsTemplatesNewRoute '/invoices/new/$clientId': typeof InvoicesNewClientIdRoute '/lovable/email/suppression': typeof LovableEmailSuppressionRoute + '/settings/public-pages/$pageId': typeof SettingsPublicPagesPageIdRoute '/documents/templates': typeof DocumentsTemplatesIndexRoute + '/settings/public-pages': typeof SettingsPublicPagesIndexRoute '/lovable/email/queue/process': typeof LovableEmailQueueProcessRoute '/lovable/email/transactional/preview': typeof LovableEmailTransactionalPreviewRoute '/lovable/email/transactional/send': typeof LovableEmailTransactionalSendRoute @@ -468,6 +494,7 @@ export interface FileRoutesById { '/hooks/poll-imap': typeof HooksPollImapRoute '/invoices/$invoiceId': typeof InvoicesInvoiceIdRoute '/invoices/new': typeof InvoicesNewRouteWithChildren + '/p/$slug': typeof PSlugRoute '/pay/$id': typeof PayIdRoute '/settings/client-fields': typeof SettingsClientFieldsRoute '/settings/custom-fields': typeof SettingsCustomFieldsRoute @@ -503,7 +530,9 @@ export interface FileRoutesById { '/documents/templates/new': typeof DocumentsTemplatesNewRoute '/invoices/new/$clientId': typeof InvoicesNewClientIdRoute '/lovable/email/suppression': typeof LovableEmailSuppressionRoute + '/settings/public-pages/$pageId': typeof SettingsPublicPagesPageIdRoute '/documents/templates/': typeof DocumentsTemplatesIndexRoute + '/settings/public-pages/': typeof SettingsPublicPagesIndexRoute '/lovable/email/queue/process': typeof LovableEmailQueueProcessRoute '/lovable/email/transactional/preview': typeof LovableEmailTransactionalPreviewRoute '/lovable/email/transactional/send': typeof LovableEmailTransactionalSendRoute @@ -526,6 +555,7 @@ export interface FileRouteTypes { | '/hooks/poll-imap' | '/invoices/$invoiceId' | '/invoices/new' + | '/p/$slug' | '/pay/$id' | '/settings/client-fields' | '/settings/custom-fields' @@ -561,7 +591,9 @@ export interface FileRouteTypes { | '/documents/templates/new' | '/invoices/new/$clientId' | '/lovable/email/suppression' + | '/settings/public-pages/$pageId' | '/documents/templates/' + | '/settings/public-pages/' | '/lovable/email/queue/process' | '/lovable/email/transactional/preview' | '/lovable/email/transactional/send' @@ -581,6 +613,7 @@ export interface FileRouteTypes { | '/hooks/poll-imap' | '/invoices/$invoiceId' | '/invoices/new' + | '/p/$slug' | '/pay/$id' | '/settings/client-fields' | '/settings/custom-fields' @@ -616,7 +649,9 @@ export interface FileRouteTypes { | '/documents/templates/new' | '/invoices/new/$clientId' | '/lovable/email/suppression' + | '/settings/public-pages/$pageId' | '/documents/templates' + | '/settings/public-pages' | '/lovable/email/queue/process' | '/lovable/email/transactional/preview' | '/lovable/email/transactional/send' @@ -637,6 +672,7 @@ export interface FileRouteTypes { | '/hooks/poll-imap' | '/invoices/$invoiceId' | '/invoices/new' + | '/p/$slug' | '/pay/$id' | '/settings/client-fields' | '/settings/custom-fields' @@ -672,7 +708,9 @@ export interface FileRouteTypes { | '/documents/templates/new' | '/invoices/new/$clientId' | '/lovable/email/suppression' + | '/settings/public-pages/$pageId' | '/documents/templates/' + | '/settings/public-pages/' | '/lovable/email/queue/process' | '/lovable/email/transactional/preview' | '/lovable/email/transactional/send' @@ -694,6 +732,7 @@ export interface RootRouteChildren { HooksPollImapRoute: typeof HooksPollImapRoute InvoicesInvoiceIdRoute: typeof InvoicesInvoiceIdRoute InvoicesNewRoute: typeof InvoicesNewRouteWithChildren + PSlugRoute: typeof PSlugRoute PayIdRoute: typeof PayIdRoute ArchiveIndexRoute: typeof ArchiveIndexRoute CalendarIndexRoute: typeof CalendarIndexRoute @@ -948,6 +987,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PayIdRouteImport parentRoute: typeof rootRouteImport } + '/p/$slug': { + id: '/p/$slug' + path: '/p/$slug' + fullPath: '/p/$slug' + preLoaderRoute: typeof PSlugRouteImport + parentRoute: typeof rootRouteImport + } '/invoices/new': { id: '/invoices/new' path: '/invoices/new' @@ -1025,6 +1071,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AdminUsersRouteImport parentRoute: typeof rootRouteImport } + '/settings/public-pages/': { + id: '/settings/public-pages/' + path: '/public-pages' + fullPath: '/settings/public-pages/' + preLoaderRoute: typeof SettingsPublicPagesIndexRouteImport + parentRoute: typeof SettingsRoute + } '/documents/templates/': { id: '/documents/templates/' path: '/documents/templates' @@ -1032,6 +1085,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DocumentsTemplatesIndexRouteImport parentRoute: typeof rootRouteImport } + '/settings/public-pages/$pageId': { + id: '/settings/public-pages/$pageId' + path: '/public-pages/$pageId' + fullPath: '/settings/public-pages/$pageId' + preLoaderRoute: typeof SettingsPublicPagesPageIdRouteImport + parentRoute: typeof SettingsRoute + } '/lovable/email/suppression': { id: '/lovable/email/suppression' path: '/lovable/email/suppression' @@ -1117,6 +1177,8 @@ interface SettingsRouteChildren { SettingsWorkflowRoute: typeof SettingsWorkflowRoute SettingsWorkflowsRoute: typeof SettingsWorkflowsRoute SettingsIndexRoute: typeof SettingsIndexRoute + SettingsPublicPagesPageIdRoute: typeof SettingsPublicPagesPageIdRoute + SettingsPublicPagesIndexRoute: typeof SettingsPublicPagesIndexRoute } const SettingsRouteChildren: SettingsRouteChildren = { @@ -1131,6 +1193,8 @@ const SettingsRouteChildren: SettingsRouteChildren = { SettingsWorkflowRoute: SettingsWorkflowRoute, SettingsWorkflowsRoute: SettingsWorkflowsRoute, SettingsIndexRoute: SettingsIndexRoute, + SettingsPublicPagesPageIdRoute: SettingsPublicPagesPageIdRoute, + SettingsPublicPagesIndexRoute: SettingsPublicPagesIndexRoute, } const SettingsRouteWithChildren = SettingsRoute._addFileChildren( @@ -1177,6 +1241,7 @@ const rootRouteChildren: RootRouteChildren = { HooksPollImapRoute: HooksPollImapRoute, InvoicesInvoiceIdRoute: InvoicesInvoiceIdRoute, InvoicesNewRoute: InvoicesNewRouteWithChildren, + PSlugRoute: PSlugRoute, PayIdRoute: PayIdRoute, ArchiveIndexRoute: ArchiveIndexRoute, CalendarIndexRoute: CalendarIndexRoute, diff --git a/src/routes/p.$slug.tsx b/src/routes/p.$slug.tsx new file mode 100644 index 0000000..67a10d3 --- /dev/null +++ b/src/routes/p.$slug.tsx @@ -0,0 +1,218 @@ +import { createFileRoute, Link } 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"; + +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(null); + const [sections, setSections] = useState([]); + const [firm, setFirm] = useState(null); + const [navPages, setNavPages] = useState([]); + const [logoUrl, setLogoUrl] = useState(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
; + } + if (!page) { + return ( +
+
+

Page not found

+

This page doesn't exist or hasn't been published yet.

+
+
+ ); + } + + const firmName = firm?.company_name ?? "Stage Law Firm, PLLC"; + const cityLine = [firm?.city, firm?.state, firm?.postal_code].filter(Boolean).join(", "); + + return ( +
+
+
+ + {logoUrl ? ( + {firmName} + ) : ( +
Logo
+ )} + {firmName} + + +
+
+ +
+ {sections.length === 0 ? ( +
+

{page.title}

+

No content yet.

+
+ ) : sections.map((s) => )} +
+ +
+
+
+ {logoUrl && {firmName}} +
+

{firmName}

+
+
+
+ {firm?.contact_phone && ( + + )} + {firm?.contact_email && ( + + )} + {(firm?.address_line1 || cityLine) && ( +
+
{firm?.address_line1}{firm?.address_line2 ? <>, {firm.address_line2} : null}{cityLine ? <>
{cityLine} : null}
+
+ )} +
+
+
+ Copyright {new Date().getFullYear()}. All Rights Reserved. +
+
+
+ ); +} + +function SectionRenderer({ section }: { section: SectionRow }) { + const c = section.content || {}; + switch (section.section_type) { + case "hero": + return ( +
+
+ {c.eyebrow &&

{c.eyebrow}

} +

{c.heading}

+ {c.subheading &&

{c.subheading}

} +
+
+ ); + case "rich_text": + return ( +
+
+
+ ); + case "feature_grid": { + const items = Array.isArray(c.items) ? c.items : []; + return ( +
+ {c.heading &&

{c.heading}

} +
+ {items.map((it: any, i: number) => ( +
+

{it.title}

+

{it.body}

+
+ ))} +
+
+ ); + } + case "cta": + return ( +
+
+

{c.heading}

+ {c.subheading &&

{c.subheading}

} + {c.button_label && c.button_url && ( + + )} +
+
+ ); + case "contact": { + return ( +
+ {c.heading &&

{c.heading}

} +
+ {c.phone && } + {c.email && } + {c.address &&
{c.address}
} +
+
+ ); + } + case "faq": { + const items = Array.isArray(c.items) ? c.items : []; + return ( +
+ {c.heading &&

{c.heading}

} +
+ {items.map((it: any, i: number) => ( +
+

{it.q}

+

{it.a}

+
+ ))} +
+
+ ); + } + default: return null; + } +} diff --git a/src/routes/settings.public-pages.$pageId.tsx b/src/routes/settings.public-pages.$pageId.tsx new file mode 100644 index 0000000..3cf7426 --- /dev/null +++ b/src/routes/settings.public-pages.$pageId.tsx @@ -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: "

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, "") })} /> +
+
+
+ +