From 475f1cad85bd2a2694b72bac4aa021c39da77a3e Mon Sep 17 00:00:00 2001 From: gibbyb Date: Thu, 26 Mar 2026 16:30:28 -0500 Subject: [PATCH] Live preview when editting website! Very cool! --- apps/next/package.json | 1 + apps/next/payload-types.ts | 2 ++ apps/next/src/app/(frontend)/page.tsx | 17 ++++++++-- .../payload/refresh-route-on-save.tsx | 16 ++++++++++ .../lib/payload/get-landing-page-content.ts | 9 ++++-- apps/next/src/payload/globals/landing-page.ts | 32 +++++++++++++++++++ apps/next/src/proxy.ts | 2 +- bun.lock | 5 +++ 8 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 apps/next/src/components/payload/refresh-route-on-save.tsx diff --git a/apps/next/package.json b/apps/next/package.json index 2bbc43f..816b6fb 100644 --- a/apps/next/package.json +++ b/apps/next/package.json @@ -21,6 +21,7 @@ "@gib/backend": "workspace:*", "@gib/ui": "workspace:*", "@payloadcms/db-postgres": "^3.80.0", + "@payloadcms/live-preview-react": "^3.80.0", "@payloadcms/next": "^3.80.0", "@payloadcms/richtext-lexical": "^3.80.0", "@sentry/nextjs": "^10.43.0", diff --git a/apps/next/payload-types.ts b/apps/next/payload-types.ts index bf164f8..0ac6d75 100644 --- a/apps/next/payload-types.ts +++ b/apps/next/payload-types.ts @@ -333,6 +333,7 @@ export interface LandingPage { commandLabel?: string | null; command?: string | null; }; + _status?: ('draft' | 'published') | null; updatedAt?: string | null; createdAt?: string | null; } @@ -403,6 +404,7 @@ export interface LandingPageSelect { commandLabel?: T; command?: T; }; + _status?: T; updatedAt?: T; createdAt?: T; globalType?: T; diff --git a/apps/next/src/app/(frontend)/page.tsx b/apps/next/src/app/(frontend)/page.tsx index 233277d..279f02b 100644 --- a/apps/next/src/app/(frontend)/page.tsx +++ b/apps/next/src/app/(frontend)/page.tsx @@ -1,11 +1,24 @@ import { CTA, Features, Hero, TechStack } from '@/components/landing'; +import { RefreshRouteOnSave } from '@/components/payload/refresh-route-on-save'; import { getLandingPageContent } from '@/lib/payload/get-landing-page-content'; -const Home = async () => { - const content = await getLandingPageContent(); +type HomeProps = { + searchParams: Promise<{ + preview?: string | string[]; + }>; +}; + +const Home = async ({ searchParams }: HomeProps) => { + const resolvedSearchParams = await searchParams; + const previewParam = resolvedSearchParams.preview; + const isPreview = Array.isArray(previewParam) + ? previewParam.includes('true') + : previewParam === 'true'; + const content = await getLandingPageContent(isPreview); return (
+ {isPreview ? : null} diff --git a/apps/next/src/components/payload/refresh-route-on-save.tsx b/apps/next/src/components/payload/refresh-route-on-save.tsx new file mode 100644 index 0000000..77cd6b3 --- /dev/null +++ b/apps/next/src/components/payload/refresh-route-on-save.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { env } from '@/env'; +import { RefreshRouteOnSave as PayloadRefreshRouteOnSave } from '@payloadcms/live-preview-react'; + +export const RefreshRouteOnSave = () => { + const router = useRouter(); + + return ( + router.refresh()} + serverURL={env.NEXT_PUBLIC_SITE_URL} + /> + ); +}; diff --git a/apps/next/src/lib/payload/get-landing-page-content.ts b/apps/next/src/lib/payload/get-landing-page-content.ts index 9f9ee9c..9c5e8eb 100644 --- a/apps/next/src/lib/payload/get-landing-page-content.ts +++ b/apps/next/src/lib/payload/get-landing-page-content.ts @@ -8,13 +8,16 @@ import { import { getPayloadClient } from './get-payload'; export const getLandingPageContent = cache( - async (): Promise => { + async (isPreview = false): Promise => { const payload = await getPayloadClient(); const landingPage = await ( payload as { - findGlobal: (args: { slug: string }) => Promise; + findGlobal: (args: { + slug: string; + draft?: boolean; + }) => Promise; } - ).findGlobal({ slug: 'landing-page' }); + ).findGlobal({ slug: 'landing-page', draft: isPreview }); return mergeLandingPageContent( (landingPage as Partial | null | undefined) ?? diff --git a/apps/next/src/payload/globals/landing-page.ts b/apps/next/src/payload/globals/landing-page.ts index 7896ded..3acdea8 100644 --- a/apps/next/src/payload/globals/landing-page.ts +++ b/apps/next/src/payload/globals/landing-page.ts @@ -8,6 +8,38 @@ export const LandingPage: GlobalConfig = { access: { read: () => true, }, + admin: { + livePreview: { + url: '/?preview=true', + breakpoints: [ + { + label: 'Mobile', + name: 'mobile', + width: 390, + height: 844, + }, + { + label: 'Tablet', + name: 'tablet', + width: 768, + height: 1024, + }, + { + label: 'Desktop', + name: 'desktop', + width: 1440, + height: 1024, + }, + ], + }, + }, + versions: { + drafts: { + autosave: { + interval: 500, + }, + }, + }, fields: [ { type: 'tabs', diff --git a/apps/next/src/proxy.ts b/apps/next/src/proxy.ts index 60b71eb..0e729e0 100644 --- a/apps/next/src/proxy.ts +++ b/apps/next/src/proxy.ts @@ -6,7 +6,7 @@ import { } from '@convex-dev/auth/nextjs/server'; const isSignInPage = createRouteMatcher(['/sign-in']); -const isProtectedRoute = createRouteMatcher(['/profile']); +const isProtectedRoute = createRouteMatcher(['/profile', '/admin']); export default convexAuthNextjsMiddleware( async (request, { convexAuth }) => { diff --git a/bun.lock b/bun.lock index 2608931..ec329ba 100644 --- a/bun.lock +++ b/bun.lock @@ -73,6 +73,7 @@ "@gib/backend": "workspace:*", "@gib/ui": "workspace:*", "@payloadcms/db-postgres": "^3.80.0", + "@payloadcms/live-preview-react": "^3.80.0", "@payloadcms/next": "^3.80.0", "@payloadcms/richtext-lexical": "^3.80.0", "@sentry/nextjs": "^10.43.0", @@ -1155,6 +1156,10 @@ "@payloadcms/graphql": ["@payloadcms/graphql@3.80.0", "", { "dependencies": { "graphql-scalars": "1.22.2", "pluralize": "8.0.0", "ts-essentials": "10.0.3", "tsx": "4.21.0" }, "peerDependencies": { "graphql": "^16.8.1", "payload": "3.80.0" }, "bin": { "payload-graphql": "bin.js" } }, "sha512-AlJcFI/R+4SRPWW51ny2BsIj+4j6qVxyn0W5Kz1f5MMfheuD844tc92O+IwmUsrRsb0l6zv3zI3G3M6V2WdFFQ=="], + "@payloadcms/live-preview": ["@payloadcms/live-preview@3.80.0", "", {}, "sha512-O28f7DoiE7n5z1ukiquVNNyqJcLgzNP1Qb/g9tmY1ppe16c+jni0NbSRw55uAsZ0PdfZuiJNZ+50iubEtsXCdw=="], + + "@payloadcms/live-preview-react": ["@payloadcms/live-preview-react@3.80.0", "", { "dependencies": { "@payloadcms/live-preview": "3.80.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.1 || ^19.1.2 || ^19.2.1", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.1 || ^19.1.2 || ^19.2.1" } }, "sha512-J0IUH9gow3wWJqUitJdS56z5Dj2cxBHZZjrnHwb4qF5bV3wChSDTLZvzRB8lZ1/m2vB7yGTgateHDLBZ6Q5PpA=="], + "@payloadcms/next": ["@payloadcms/next@3.80.0", "", { "dependencies": { "@dnd-kit/core": "6.3.1", "@dnd-kit/modifiers": "9.0.0", "@dnd-kit/sortable": "10.0.0", "@payloadcms/graphql": "3.80.0", "@payloadcms/translations": "3.80.0", "@payloadcms/ui": "3.80.0", "busboy": "^1.6.0", "dequal": "2.0.3", "file-type": "19.3.0", "graphql-http": "^1.22.0", "graphql-playground-html": "1.6.30", "http-status": "2.1.0", "path-to-regexp": "6.3.0", "qs-esm": "7.0.2", "sass": "1.77.4", "uuid": "10.0.0" }, "peerDependencies": { "graphql": "^16.8.1", "next": ">=15.2.9 <15.3.0 || >=15.3.9 <15.4.0 || >=15.4.11 <15.5.0 || >=16.2.0-canary.10 <17.0.0", "payload": "3.80.0" } }, "sha512-JL7Ydi/rm7hoLDzMO6H1mBQdvxJ5Dww26aomMOYMziUr9YzU6w+W5EnJgN3x2vH0myvDr9VUfNTTBNK616G5LQ=="], "@payloadcms/richtext-lexical": ["@payloadcms/richtext-lexical@3.80.0", "", { "dependencies": { "@lexical/clipboard": "0.41.0", "@lexical/headless": "0.41.0", "@lexical/html": "0.41.0", "@lexical/link": "0.41.0", "@lexical/list": "0.41.0", "@lexical/mark": "0.41.0", "@lexical/react": "0.41.0", "@lexical/rich-text": "0.41.0", "@lexical/selection": "0.41.0", "@lexical/table": "0.41.0", "@lexical/utils": "0.41.0", "@payloadcms/translations": "3.80.0", "@payloadcms/ui": "3.80.0", "@types/uuid": "10.0.0", "acorn": "8.16.0", "bson-objectid": "2.0.4", "csstype": "3.1.3", "dequal": "2.0.3", "escape-html": "1.0.3", "jsox": "1.2.121", "lexical": "0.41.0", "mdast-util-from-markdown": "2.0.2", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-mdx-jsx": "3.0.1", "qs-esm": "7.0.2", "react-error-boundary": "4.1.2", "ts-essentials": "10.0.3", "uuid": "10.0.0" }, "peerDependencies": { "@faceless-ui/modal": "3.0.0", "@faceless-ui/scroll-info": "2.0.0", "@payloadcms/next": "3.80.0", "payload": "3.80.0", "react": "^19.0.1 || ^19.1.2 || ^19.2.1", "react-dom": "^19.0.1 || ^19.1.2 || ^19.2.1" } }, "sha512-P7F7VoCS4dZFxdausUdxc79t6tlMpeusf2QwdtyBnB4FwuC8sAc2TW9dxsP+N/gvAnEuPR6rAd/kUma1kQoJug=="],