Files
website/client/src/routes/public/CmsPage.jsx
Claude 1dd7603f54 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>
2026-07-09 20:54:36 -05:00

57 lines
2.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 isnt 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>
)
}