Add page builder admin UI + public page route (step 5 + step 6 client)
- PagesAdmin: list view of pages (title/slug/status/protected/updated) with new/edit navigation and a View link to the live page. - PageBuilder: full-page block canvas — palette (adds any registered block), per-block editor cards with show/hide, up/down + native drag reorder, and remove; Content / Settings tabs; SEO metadata + layout/nav settings panels; publish/unpublish; protect (PATCH) and password-gated unprotect (modal); draft preview (mints a token, opens /preview/:id/:token); delete (blocked while protected). Surfaces server block-validation details on save. - CmsPage: public renderer for /:slug (published; staff see drafts) and the token-gated /preview/:id/:token, rendering blocks via BlockList and reflecting the page title/meta. - Routing: /:slug catch-all after all named routes + /preview/:id/:token outside the maintenance gate; admin /admin/pages, /pages/new, /pages/:id. - api client: public page/pagePreview + admin pages CRUD/unprotect/preview. - AdminLayout: "Pages" nav entry (Content group) with icon. - theme.css: builder canvas + preview-banner + shell-wide styles. Client builds clean (216 modules). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
56
client/src/routes/public/CmsPage.jsx
Normal file
56
client/src/routes/public/CmsPage.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import PublicLayout from '../../components/PublicLayout.jsx'
|
||||
import { Loading, ErrorState } from '../../components/PageState.jsx'
|
||||
import { useAsync } from '../../lib/useAsync.js'
|
||||
import { api } from '../../api/client.js'
|
||||
import '../../blocks/index.js' // registers all block types
|
||||
import { BlockList } from '../../blocks/BlockRenderer.jsx'
|
||||
|
||||
// Renders a CMS page composed of blocks. Two modes:
|
||||
// - live: /:slug → fetches the published page (staff see drafts)
|
||||
// - preview: /preview/:id/:token → fetches the current state via a token,
|
||||
// regardless of publish status (draft-preview links).
|
||||
export default function CmsPage({ preview = false }) {
|
||||
const params = useParams()
|
||||
const { loading, error, data: page } = useAsync(
|
||||
() => (preview ? api.pagePreview(params.id, params.token) : api.page(params.slug)),
|
||||
[preview, params.id, params.token, params.slug],
|
||||
)
|
||||
|
||||
// Reflect the page's title + meta description while it's mounted, then restore.
|
||||
useEffect(() => {
|
||||
if (!page) return
|
||||
const prevTitle = document.title
|
||||
document.title = page.metadata?.seoTitle || page.title || prevTitle
|
||||
return () => {
|
||||
document.title = prevTitle
|
||||
}
|
||||
}, [page])
|
||||
|
||||
const layout = page?.settings?.layout || 'default'
|
||||
const widthClass = layout === 'full_width' || layout === 'landing' ? 'shell-wide' : 'shell'
|
||||
|
||||
return (
|
||||
<PublicLayout section="website">
|
||||
<div className={`${widthClass} page-body`} style={{ paddingTop: 40 }}>
|
||||
{preview && page && (
|
||||
<div className="page-preview-banner sans">
|
||||
Preview — this is the current draft state and isn’t publicly visible.
|
||||
</div>
|
||||
)}
|
||||
{loading && <Loading />}
|
||||
{error && (
|
||||
<ErrorState
|
||||
message={error.status === 404 ? 'That page could not be found.' : 'Could not load this page.'}
|
||||
/>
|
||||
)}
|
||||
{page && (
|
||||
<article className={`page-blocks page-layout--${layout}`}>
|
||||
<BlockList blocks={page.blocks} />
|
||||
</article>
|
||||
)}
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user