Skip to content

Commit 6842eb8

Browse files
committed
feat(cms): add live preview
closed cod-377
1 parent 6d17f5a commit 6842eb8

11 files changed

Lines changed: 165 additions & 8 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use client';
2+
3+
import { RenderPage } from '@codeware/shared/ui/cms-renderer';
4+
import type { Page } from '@codeware/shared/util/payload-types';
5+
import type { BlocksData } from '@codeware/shared/util/payload-utils';
6+
7+
import { LivePreview } from '../../../components/LivePreview.client';
8+
9+
type Props = {
10+
page: Page;
11+
blocksData?: BlocksData;
12+
};
13+
14+
export function PagePreview({ page, blocksData }: Props) {
15+
return (
16+
<LivePreview initialData={page}>
17+
{(data) => <RenderPage page={data} blocksData={blocksData} />}
18+
</LivePreview>
19+
);
20+
}

apps/cms/src/app/(site)/[...slug]/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { notFound } from 'next/navigation';
22

33
import { getPageData } from '@codeware/app-cms/data-access';
4-
import { RenderPage } from '@codeware/shared/ui/cms-renderer';
54

65
import { payloadRuntime } from '../../../security/payload-runtime';
76

7+
import { PagePreview } from './page-preview.client';
8+
89
interface Props {
910
params: Promise<{
1011
slug: string[];
@@ -22,5 +23,5 @@ export default async function Page({ params }: Props) {
2223
notFound();
2324
}
2425

25-
return <RenderPage page={data.page} blocksData={data.blocksData} />;
26+
return <PagePreview page={data.page} blocksData={data.blocksData} />;
2627
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use client';
2+
3+
import { RenderLandingPage } from '@codeware/shared/ui/cms-renderer';
4+
import type { Page } from '@codeware/shared/util/payload-types';
5+
6+
import { LivePreview } from '../../components/LivePreview.client';
7+
8+
type Props = {
9+
landingPage: Page;
10+
};
11+
12+
export function LandingPagePreview({ landingPage }: Props) {
13+
return (
14+
<LivePreview initialData={landingPage}>
15+
{(data) => <RenderLandingPage landingPage={data} />}
16+
</LivePreview>
17+
);
18+
}

apps/cms/src/app/(site)/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { notFound } from 'next/navigation';
22

33
import { getPage } from '@codeware/app-cms/data-access';
4-
import { RenderLandingPage } from '@codeware/shared/ui/cms-renderer';
54

65
import { payloadRuntime } from '../../security/payload-runtime';
76

7+
import { LandingPagePreview } from './landing-page-preview.client';
8+
89
// TODO: metadata
910

1011
export default async function SiteIndexPage() {
@@ -19,5 +20,5 @@ export default async function SiteIndexPage() {
1920
notFound();
2021
}
2122

22-
return <RenderLandingPage landingPage={page} />;
23+
return <LandingPagePreview landingPage={page} />;
2324
}

apps/cms/src/app/(site)/posts/[...slug]/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { notFound } from 'next/navigation';
22

33
import { getPost } from '@codeware/app-cms/data-access';
4-
import { RenderPost } from '@codeware/shared/ui/cms-renderer';
54

65
import { payloadRuntime } from '../../../../security/payload-runtime';
76

7+
import { PostPreview } from './post-preview.client';
8+
89
interface Props {
910
params: Promise<{
1011
slug: string[];
@@ -16,12 +17,11 @@ export default async function Post({ params }: Props) {
1617
const slugString = slug.join('/');
1718

1819
const runtime = await payloadRuntime();
19-
2020
const post = await getPost(runtime, slugString);
2121

2222
if (!post) {
2323
notFound();
2424
}
2525

26-
return <RenderPost post={post} />;
26+
return <PostPreview post={post} />;
2727
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use client';
2+
3+
import { RenderPost } from '@codeware/shared/ui/cms-renderer';
4+
import type { Post } from '@codeware/shared/util/payload-types';
5+
6+
import { LivePreview } from '../../../../components/LivePreview.client';
7+
8+
type Props = {
9+
post: Post;
10+
};
11+
12+
export function PostPreview({ post }: Props) {
13+
return (
14+
<LivePreview initialData={post} depth={1}>
15+
{(data) => <RenderPost post={data} />}
16+
</LivePreview>
17+
);
18+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use client';
2+
3+
import { useLivePreview } from '@payloadcms/live-preview-react';
4+
5+
import { usePayload } from '@codeware/shared/ui/cms-renderer';
6+
7+
interface LivePreviewProps<T extends Record<string, any>> {
8+
initialData: T;
9+
depth?: number;
10+
children: (data: T) => React.ReactNode;
11+
}
12+
13+
/**
14+
* Generic client component for Payload live preview.
15+
*
16+
* Subscribes to live preview updates from the CMS admin panel and passes
17+
* the current data (updated as-you-type) to the render function.
18+
* Falls back to `initialData` when not in preview mode.
19+
*
20+
* @example
21+
* ```tsx
22+
* <LivePreview initialData={page}>
23+
* {(data) => <RenderPage page={data} blocksData={blocksData} />}
24+
* </LivePreview>
25+
* ```
26+
*/
27+
export function LivePreview<T extends Record<string, any>>({
28+
initialData,
29+
depth = 2,
30+
children
31+
}: LivePreviewProps<T>) {
32+
const { payloadUrl } = usePayload();
33+
const { data } = useLivePreview<T>({
34+
initialData,
35+
serverURL: payloadUrl,
36+
depth
37+
});
38+
39+
return <>{children(data)}</>;
40+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use client';
2+
3+
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react';
4+
import { useRouter } from 'next/navigation';
5+
import React from 'react';
6+
7+
import { usePayload } from '@codeware/shared/ui/cms-renderer';
8+
9+
/**
10+
* Client component for server-side live preview.
11+
* Listens for save events from Payload CMS and triggers a route refresh.
12+
*
13+
* Add this component to your layout or page to enable live preview updates when content is saved in the CMS.
14+
*/
15+
export const RefreshRouteOnSave: React.FC = () => {
16+
const { payloadUrl } = usePayload();
17+
const router = useRouter();
18+
19+
return (
20+
<PayloadLivePreview
21+
refresh={() => router.refresh()}
22+
serverURL={payloadUrl}
23+
/>
24+
);
25+
};

apps/cms/src/payload.config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,20 @@ export default buildConfig({
5858
graphics: {
5959
Logo: '@codeware/apps/cms/components/Logo.client'
6060
}
61+
},
62+
livePreview: {
63+
breakpoints: [
64+
{ label: 'Mobile', name: 'mobile', width: 375, height: 667 },
65+
{ label: 'Tablet', name: 'tablet', width: 768, height: 1024 },
66+
{ label: 'Desktop', name: 'desktop', width: 1440, height: 900 }
67+
],
68+
collections: ['pages', 'posts'],
69+
url: ({ data, collectionConfig, locale }) => {
70+
if (collectionConfig?.slug === 'posts') {
71+
return `posts/${data.slug}?locale=${locale.code}`;
72+
}
73+
return `${data.slug}?locale=${locale.code}`;
74+
}
6175
}
6276
},
6377
// Declare blocks globally and reference then by slug elsewhere

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@payloadcms/drizzle": "~3.69.0",
3232
"@payloadcms/email-nodemailer": "~3.69.0",
3333
"@payloadcms/next": "~3.69.0",
34+
"@payloadcms/live-preview-react": "~3.69.0",
3435
"@payloadcms/plugin-form-builder": "~3.69.0",
3536
"@payloadcms/plugin-multi-tenant": "~3.69.0",
3637
"@payloadcms/plugin-seo": "~3.69.0",

0 commit comments

Comments
 (0)