chore(examples): migrates draft-preview example to 3.0 (#9362)
This commit is contained in:
@@ -13,9 +13,15 @@ First you'll need a running Payload app. There is one made explicitly for this e
|
||||
### Next.js
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server
|
||||
|
||||
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
|
||||
|
||||
4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
5. `open http://localhost:3001` to see the result
|
||||
|
||||
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload) for full details.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
import { Page } from '../../payload-types'
|
||||
import type { Page as PageType } from '../../payload-types'
|
||||
import { fetchPage } from '../_api/fetchPage'
|
||||
import { fetchPages } from '../_api/fetchPages'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
@@ -10,10 +10,12 @@ import RichText from '../_components/RichText'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
interface PageParams {
|
||||
params: { slug: string }
|
||||
params: Promise<{
|
||||
slug?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export const PageTemplate: React.FC<{ page: Page | null | undefined }> = ({ page }) => (
|
||||
export const PageTemplate: React.FC<{ page: null | PageType | undefined }> = ({ page }) => (
|
||||
<main className={classes.page}>
|
||||
<Gutter>
|
||||
<h1>{page?.title}</h1>
|
||||
@@ -22,8 +24,10 @@ export const PageTemplate: React.FC<{ page: Page | null | undefined }> = ({ page
|
||||
</main>
|
||||
)
|
||||
|
||||
export default async function Page({ params: { slug = 'home' } }: PageParams) {
|
||||
const { isEnabled: isDraftMode } = draftMode()
|
||||
export default async function Page({ params }: PageParams) {
|
||||
const { slug = 'home' } = await params
|
||||
|
||||
const { isEnabled: isDraftMode } = await draftMode()
|
||||
|
||||
const page = await fetchPage(slug, isDraftMode)
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import type { Page } from '../../payload-types'
|
||||
export const fetchPage = async (
|
||||
slug: string,
|
||||
draft?: boolean,
|
||||
): Promise<Page | undefined | null> => {
|
||||
): Promise<null | Page | undefined> => {
|
||||
let payloadToken: RequestCookie | undefined
|
||||
|
||||
if (draft) {
|
||||
const { cookies } = await import('next/headers')
|
||||
payloadToken = cookies().get('payload-token')
|
||||
payloadToken = (await cookies()).get('payload-token')
|
||||
}
|
||||
|
||||
const pageRes: {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { PayloadAdminBar, PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
|
||||
import type { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
|
||||
import { PayloadAdminBar } from 'payload-admin-bar'
|
||||
|
||||
import { Gutter } from '../Gutter'
|
||||
|
||||
@@ -17,24 +18,24 @@ export const AdminBarClient: React.FC<PayloadAdminBarProps> = (props) => {
|
||||
<Gutter className={classes.container}>
|
||||
<PayloadAdminBar
|
||||
{...props}
|
||||
logo={<Title />}
|
||||
className={classes.payloadAdminBar}
|
||||
classNames={{
|
||||
controls: classes.controls,
|
||||
logo: classes.logo,
|
||||
user: classes.user,
|
||||
}}
|
||||
cmsURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
|
||||
logo={<Title />}
|
||||
onAuthChange={setUser}
|
||||
onPreviewExit={async () => {
|
||||
await fetch(`/api/exit-preview`)
|
||||
window.location.reload()
|
||||
}}
|
||||
onAuthChange={setUser}
|
||||
className={classes.payloadAdminBar}
|
||||
classNames={{
|
||||
user: classes.user,
|
||||
logo: classes.logo,
|
||||
controls: classes.controls,
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
zIndex: 'unset',
|
||||
padding: 0,
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
/>
|
||||
</Gutter>
|
||||
|
||||
@@ -3,14 +3,14 @@ import { draftMode } from 'next/headers'
|
||||
|
||||
import { AdminBarClient } from './index.client'
|
||||
|
||||
export function AdminBar() {
|
||||
const { isEnabled: isPreviewMode } = draftMode()
|
||||
export async function AdminBar() {
|
||||
const { isEnabled: isPreviewMode } = await draftMode()
|
||||
|
||||
return (
|
||||
<AdminBarClient
|
||||
preview={isPreviewMode}
|
||||
// id={page?.id} // TODO: is there any way to do this?!
|
||||
collection="pages"
|
||||
preview={isPreviewMode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
import React, { ElementType } from 'react'
|
||||
import type { ElementType } from 'react'
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export type Props = {
|
||||
label?: string
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
el?: 'button' | 'link' | 'a'
|
||||
onClick?: () => void
|
||||
href?: string
|
||||
newTab?: boolean
|
||||
className?: string
|
||||
type?: 'submit' | 'button'
|
||||
disabled?: boolean
|
||||
el?: 'a' | 'button' | 'link'
|
||||
href?: string
|
||||
label?: string
|
||||
newTab?: boolean | null
|
||||
onClick?: () => void
|
||||
type?: 'button' | 'submit'
|
||||
}
|
||||
|
||||
export const Button: React.FC<Props> = ({
|
||||
el: elFromProps = 'link',
|
||||
label,
|
||||
newTab,
|
||||
href,
|
||||
type = 'button',
|
||||
appearance,
|
||||
className: classNameFromProps,
|
||||
onClick,
|
||||
type = 'button',
|
||||
disabled,
|
||||
el: elFromProps = 'link',
|
||||
href,
|
||||
label,
|
||||
newTab,
|
||||
onClick,
|
||||
}) => {
|
||||
let el = elFromProps
|
||||
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
const className = [
|
||||
classes.button,
|
||||
classNameFromProps,
|
||||
@@ -44,11 +45,13 @@ export const Button: React.FC<Props> = ({
|
||||
</div>
|
||||
)
|
||||
|
||||
if (onClick || type === 'submit') el = 'button'
|
||||
if (onClick || type === 'submit') {
|
||||
el = 'button'
|
||||
}
|
||||
|
||||
if (el === 'link') {
|
||||
return (
|
||||
<Link href={href || ''} className={className} {...newTabProps} onClick={onClick}>
|
||||
<Link className={className} href={href || ''} {...newTabProps} onClick={onClick}>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
@@ -58,12 +61,12 @@ export const Button: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<Element
|
||||
href={href}
|
||||
className={className}
|
||||
href={href}
|
||||
type={type}
|
||||
{...newTabProps}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{content}
|
||||
</Element>
|
||||
|
||||
@@ -1,46 +1,52 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Page } from '../../../payload-types'
|
||||
import type { Page } from '../../../payload-types'
|
||||
import { Button } from '../Button'
|
||||
|
||||
export type CMSLinkType = {
|
||||
type?: 'custom' | 'reference'
|
||||
url?: string
|
||||
newTab?: boolean
|
||||
reference?: {
|
||||
value: string | Page
|
||||
relationTo: 'pages'
|
||||
}
|
||||
label?: string
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
label?: string
|
||||
newTab?: boolean | null
|
||||
reference?: {
|
||||
relationTo: 'pages'
|
||||
value: number | Page | string
|
||||
} | null
|
||||
type?: 'custom' | 'reference' | null
|
||||
url?: null | string
|
||||
}
|
||||
|
||||
export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
type,
|
||||
url,
|
||||
newTab,
|
||||
reference,
|
||||
label,
|
||||
appearance,
|
||||
children,
|
||||
className,
|
||||
label,
|
||||
newTab,
|
||||
reference,
|
||||
url,
|
||||
}) => {
|
||||
const href =
|
||||
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
|
||||
? `/${reference.value.slug === 'home' ? '' : reference.value.slug}`
|
||||
? `${reference?.relationTo !== 'pages' ? `/${reference?.relationTo}` : ''}/${
|
||||
reference.value.slug
|
||||
}`
|
||||
: url
|
||||
|
||||
if (!href) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!appearance) {
|
||||
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
|
||||
if (type === 'custom') {
|
||||
return (
|
||||
<a href={url} {...newTabProps} className={className}>
|
||||
<a href={url || ''} {...newTabProps} className={className}>
|
||||
{label && label}
|
||||
{children && children}
|
||||
{children ? <>{children}</> : null}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -49,17 +55,17 @@ export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
return (
|
||||
<Link href={href} {...newTabProps} className={className} prefetch={false}>
|
||||
{label && label}
|
||||
{children && children}
|
||||
{children ? <>{children}</> : null}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const buttonProps = {
|
||||
newTab,
|
||||
href,
|
||||
appearance,
|
||||
href,
|
||||
label,
|
||||
newTab,
|
||||
}
|
||||
|
||||
return <Button className={className} {...buttonProps} el="link" />
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import React, { forwardRef, Ref } from 'react'
|
||||
import type { Ref } from 'react'
|
||||
import React, { forwardRef } from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
left?: boolean
|
||||
right?: boolean
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
left?: boolean
|
||||
ref?: Ref<HTMLDivElement>
|
||||
right?: boolean
|
||||
}
|
||||
|
||||
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const { left = true, right = true, className, children } = props
|
||||
const { children, className, left = true, right = true } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={[
|
||||
classes.gutter,
|
||||
left && classes.gutterLeft,
|
||||
@@ -24,6 +24,7 @@ export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import type { MainMenu } from '../../../payload-types'
|
||||
|
||||
import { CMSLink } from '../CMSLink'
|
||||
import { Gutter } from '../Gutter'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export async function Header() {
|
||||
|
||||
@@ -6,14 +6,14 @@ import { Text } from 'slate'
|
||||
type Children = Leaf[]
|
||||
|
||||
type Leaf = {
|
||||
type: string
|
||||
value?: {
|
||||
url: string
|
||||
alt: string
|
||||
}
|
||||
children: Children
|
||||
url?: string
|
||||
[key: string]: unknown
|
||||
children: Children
|
||||
type: string
|
||||
url?: string
|
||||
value?: {
|
||||
alt: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
const serialize = (children: Children): React.ReactNode[] =>
|
||||
@@ -35,7 +35,7 @@ const serialize = (children: Children): React.ReactNode[] =>
|
||||
|
||||
if (node.underline) {
|
||||
text = (
|
||||
<span style={{ textDecoration: 'underline' }} key={i}>
|
||||
<span key={i} style={{ textDecoration: 'underline' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
@@ -43,7 +43,7 @@ const serialize = (children: Children): React.ReactNode[] =>
|
||||
|
||||
if (node.strikethrough) {
|
||||
text = (
|
||||
<span style={{ textDecoration: 'line-through' }} key={i}>
|
||||
<span key={i} style={{ textDecoration: 'line-through' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
@@ -57,6 +57,8 @@ const serialize = (children: Children): React.ReactNode[] =>
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case 'blockquote':
|
||||
return <blockquote key={i}>{serialize(node.children)}</blockquote>
|
||||
case 'h1':
|
||||
return <h1 key={i}>{serialize(node.children)}</h1>
|
||||
case 'h2':
|
||||
@@ -69,12 +71,6 @@ const serialize = (children: Children): React.ReactNode[] =>
|
||||
return <h5 key={i}>{serialize(node.children)}</h5>
|
||||
case 'h6':
|
||||
return <h6 key={i}>{serialize(node.children)}</h6>
|
||||
case 'blockquote':
|
||||
return <blockquote key={i}>{serialize(node.children)}</blockquote>
|
||||
case 'ul':
|
||||
return <ul key={i}>{serialize(node.children)}</ul>
|
||||
case 'ol':
|
||||
return <ol key={i}>{serialize(node.children)}</ol>
|
||||
case 'li':
|
||||
return <li key={i}>{serialize(node.children)}</li>
|
||||
case 'link':
|
||||
@@ -83,6 +79,10 @@ const serialize = (children: Children): React.ReactNode[] =>
|
||||
{serialize(node.children)}
|
||||
</a>
|
||||
)
|
||||
case 'ol':
|
||||
return <ol key={i}>{serialize(node.children)}</ol>
|
||||
case 'ul':
|
||||
return <ul key={i}>{serialize(node.children)}</ul>
|
||||
|
||||
default:
|
||||
return <p key={i}>{serialize(node.children)}</p>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
draftMode().disable()
|
||||
const draft = await draftMode()
|
||||
draft.disable()
|
||||
return new Response('Draft mode is disabled')
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { draftMode } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export async function GET(
|
||||
req: Request & {
|
||||
req: {
|
||||
cookies: {
|
||||
get: (name: string) => {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
},
|
||||
} & Request,
|
||||
): Promise<Response> {
|
||||
const payloadToken = req.cookies.get('payload-token')?.value
|
||||
const { searchParams } = new URL(req.url)
|
||||
@@ -32,8 +32,10 @@ export async function GET(
|
||||
|
||||
const userRes = await userReq.json()
|
||||
|
||||
const draft = await draftMode()
|
||||
|
||||
if (!userReq.ok || !userRes?.user) {
|
||||
draftMode().disable()
|
||||
draft.disable()
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
@@ -41,7 +43,7 @@ export async function GET(
|
||||
return new Response('Invalid token', { status: 401 })
|
||||
}
|
||||
|
||||
draftMode().enable()
|
||||
draft.enable()
|
||||
|
||||
redirect(url)
|
||||
}
|
||||
|
||||
@@ -5,19 +5,20 @@ import { NextResponse } from 'next/server'
|
||||
// this endpoint will revalidate a page by tag or path
|
||||
// this is to achieve on-demand revalidation of pages that use this data
|
||||
// send either `collection` and `slug` or `revalidatePath` as query params
|
||||
export async function GET(request: NextRequest): Promise<unknown> {
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
export async function GET(request: NextRequest): Promise<Response> {
|
||||
const collection = request.nextUrl.searchParams.get('collection')
|
||||
const slug = request.nextUrl.searchParams.get('slug')
|
||||
const path = request.nextUrl.searchParams.get('path')
|
||||
const secret = request.nextUrl.searchParams.get('secret')
|
||||
|
||||
if (secret !== process.env.NEXT_PRIVATE_REVALIDATION_KEY) {
|
||||
return NextResponse.json({ revalidated: false, now: Date.now() })
|
||||
return NextResponse.json({ now: Date.now(), revalidated: false })
|
||||
}
|
||||
|
||||
if (typeof collection === 'string' && typeof slug === 'string') {
|
||||
revalidateTag(`${collection}_${slug}`)
|
||||
return NextResponse.json({ revalidated: true, now: Date.now() })
|
||||
return NextResponse.json({ now: Date.now(), revalidated: true })
|
||||
}
|
||||
|
||||
// there is a known limitation with `revalidatePath` where it will not revalidate exact paths of dynamic routes
|
||||
@@ -27,8 +28,8 @@ export async function GET(request: NextRequest): Promise<unknown> {
|
||||
// - https://github.com/vercel/next.js/issues/49778#issuecomment-1547028830
|
||||
if (typeof path === 'string') {
|
||||
revalidatePath(path)
|
||||
return NextResponse.json({ revalidated: true, now: Date.now() })
|
||||
return NextResponse.json({ now: Date.now(), revalidated: true })
|
||||
}
|
||||
|
||||
return NextResponse.json({ revalidated: false, now: Date.now() })
|
||||
return NextResponse.json({ now: Date.now(), revalidated: false })
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import { Header } from './_components/Header'
|
||||
import './app.scss'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
title: 'Create Next App',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||
const { children } = props
|
||||
|
||||
@@ -23,7 +24,6 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||
Update: this is fixed in `@types/react` v18.2.14 but still requires `@ts-expect-error` to build :shrug:
|
||||
See my comment here: https://github.com/vercel/next.js/issues/42292#issuecomment-1622979777
|
||||
*/}
|
||||
{/* @ts-expect-error */}
|
||||
<Header />
|
||||
{children}
|
||||
</body>
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
const nextConfig = {
|
||||
sassOptions: {
|
||||
silenceDeprecations: ['legacy-js-api'],
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
|
||||
@@ -3,29 +3,29 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3001",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3001",
|
||||
"lint": "next lint"
|
||||
"dev": "next dev -p 3001",
|
||||
"lint": "next lint",
|
||||
"start": "next start -p 3001"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-html": "^1.0.3",
|
||||
"next": "^13.5.1",
|
||||
"next": "^15.0.0",
|
||||
"payload-admin-bar": "^1.0.6",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
"react": "19.0.0-rc-65a56d0e-20241020",
|
||||
"react-dom": "19.0.0-rc-65a56d0e-20241020"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^13.4.8",
|
||||
"@next/eslint-plugin-next": "^15.0.0",
|
||||
"@payloadcms/eslint-config": "^0.0.2",
|
||||
"@types/escape-html": "^1.0.2",
|
||||
"@types/node": "18.11.3",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"eslint": "8.41.0",
|
||||
"eslint-config-next": "13.4.3",
|
||||
"eslint-config-next": "15.0.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-filenames": "^1.3.2",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
@@ -33,8 +33,8 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.62.1",
|
||||
"sass": "^1.81.0",
|
||||
"slate": "^0.82.0",
|
||||
"typescript": "^4.8.4"
|
||||
"typescript": "5.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,95 +7,259 @@
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
collections: {
|
||||
pages: Page
|
||||
users: User
|
||||
'payload-preferences': PayloadPreference
|
||||
'payload-migrations': PayloadMigration
|
||||
}
|
||||
pages: Page;
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
pages: PagesSelect<false> | PagesSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {
|
||||
'main-menu': MainMenu
|
||||
}
|
||||
'main-menu': MainMenu;
|
||||
};
|
||||
globalsSelect: {
|
||||
'main-menu': MainMenuSelect<false> | MainMenuSelect<true>;
|
||||
};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: string
|
||||
title: string
|
||||
slug?: string
|
||||
id: string;
|
||||
title: string;
|
||||
slug?: string | null;
|
||||
richText: {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
_status?: 'draft' | 'published'
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
email: string
|
||||
resetPasswordToken?: string
|
||||
resetPasswordExpiration?: string
|
||||
salt?: string
|
||||
hash?: string
|
||||
loginAttempts?: number
|
||||
lockUntil?: string
|
||||
password?: string
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
export interface PayloadPreference {
|
||||
id: string
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users'
|
||||
value: string | User
|
||||
}
|
||||
key?: string
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string
|
||||
name?: string
|
||||
batch?: number
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages_select".
|
||||
*/
|
||||
export interface PagesSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
slug?: T;
|
||||
richText?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "main-menu".
|
||||
*/
|
||||
export interface MainMenu {
|
||||
id: string
|
||||
navItems?: {
|
||||
link: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
label: string
|
||||
}
|
||||
id?: string
|
||||
}[]
|
||||
updatedAt?: string
|
||||
createdAt?: string
|
||||
id: string;
|
||||
navItems?:
|
||||
| {
|
||||
link: {
|
||||
type?: ('reference' | 'custom') | null;
|
||||
newTab?: boolean | null;
|
||||
reference?: {
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
} | null;
|
||||
url?: string | null;
|
||||
label: string;
|
||||
};
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
|
||||
declare module 'payload' {
|
||||
export interface GeneratedTypes {
|
||||
collections: {
|
||||
pages: Page
|
||||
users: User
|
||||
'payload-preferences': PayloadPreference
|
||||
'payload-migrations': PayloadMigration
|
||||
}
|
||||
globals: {
|
||||
'main-menu': MainMenu
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "main-menu_select".
|
||||
*/
|
||||
export interface MainMenuSelect<T extends boolean = true> {
|
||||
navItems?:
|
||||
| T
|
||||
| {
|
||||
link?:
|
||||
| T
|
||||
| {
|
||||
type?: T;
|
||||
newTab?: T;
|
||||
reference?: T;
|
||||
url?: T;
|
||||
label?: T;
|
||||
};
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
3495
examples/draft-preview/next-app/pnpm-lock.yaml
generated
Normal file
3495
examples/draft-preview/next-app/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,6 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next.config.js"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,9 +13,15 @@ First you'll need a running Payload app. There is one made explicitly for this e
|
||||
### Next.js
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server
|
||||
|
||||
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
|
||||
|
||||
4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
5. `open http://localhost:3001` to see the result
|
||||
|
||||
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload) for full details.
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
images: {
|
||||
domains: ['localhost', process.env.NEXT_PUBLIC_PAYLOAD_URL || ''].filter(Boolean),
|
||||
sassOptions: {
|
||||
silenceDeprecations: ['legacy-js-api'],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,29 +3,29 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3001",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3001",
|
||||
"lint": "next lint"
|
||||
"dev": "next dev -p 3001",
|
||||
"lint": "next lint",
|
||||
"start": "next start -p 3001"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-html": "^1.0.3",
|
||||
"next": "^13.5.1",
|
||||
"next": "^15.0.0",
|
||||
"payload-admin-bar": "^1.0.6",
|
||||
"qs": "^6.11.0",
|
||||
"react": "^18.2.0",
|
||||
"react": "19.0.0-rc-65a56d0e-20241020",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "19.0.0-rc-65a56d0e-20241020"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^13.4.8",
|
||||
"@next/eslint-plugin-next": "^15.0.0",
|
||||
"@payloadcms/eslint-config": "^0.0.2",
|
||||
"@types/node": "18.11.3",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"eslint": "8.25.0",
|
||||
"eslint": "8.41.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-filenames": "^1.3.2",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
@@ -33,8 +33,8 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.55.0",
|
||||
"sass": "^1.81.0",
|
||||
"slate": "^0.82.0",
|
||||
"typescript": "4.8.4"
|
||||
"typescript": "5.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
3064
examples/draft-preview/next-pages/pnpm-lock.yaml
generated
Normal file
3064
examples/draft-preview/next-pages/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import { PayloadAdminBar, PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
|
||||
import type { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
|
||||
import { PayloadAdminBar } from 'payload-admin-bar'
|
||||
|
||||
import { Gutter } from '../Gutter'
|
||||
|
||||
@@ -9,30 +10,30 @@ const Title: React.FC = () => <span>Dashboard</span>
|
||||
|
||||
export const AdminBar: React.FC<{
|
||||
adminBarProps?: PayloadAdminBarProps
|
||||
setUser?: (user: PayloadMeUser) => void
|
||||
user?: PayloadMeUser
|
||||
setUser?: (user: PayloadMeUser) => void // eslint-disable-line no-unused-vars
|
||||
}> = (props) => {
|
||||
const { adminBarProps, user, setUser } = props
|
||||
const { adminBarProps, setUser, user } = props
|
||||
|
||||
return (
|
||||
<div className={[classes.adminBar, user && classes.show].filter(Boolean).join(' ')}>
|
||||
<Gutter className={classes.container}>
|
||||
<PayloadAdminBar
|
||||
{...adminBarProps}
|
||||
logo={<Title />}
|
||||
cmsURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
|
||||
onAuthChange={setUser}
|
||||
className={classes.payloadAdminBar}
|
||||
classNames={{
|
||||
user: classes.user,
|
||||
logo: classes.logo,
|
||||
controls: classes.controls,
|
||||
logo: classes.logo,
|
||||
user: classes.user,
|
||||
}}
|
||||
cmsURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
|
||||
logo={<Title />}
|
||||
onAuthChange={setUser}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
zIndex: 'unset',
|
||||
padding: 0,
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
/>
|
||||
</Gutter>
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
import React, { ElementType } from 'react'
|
||||
import type { ElementType } from 'react'
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export type Props = {
|
||||
label: string
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
el?: 'button' | 'link' | 'a'
|
||||
onClick?: () => void
|
||||
href?: string
|
||||
newTab?: boolean
|
||||
className?: string
|
||||
type?: 'submit' | 'button'
|
||||
disabled?: boolean
|
||||
el?: 'a' | 'button' | 'link'
|
||||
href?: string
|
||||
label: string
|
||||
newTab?: boolean
|
||||
onClick?: () => void
|
||||
type?: 'button' | 'submit'
|
||||
}
|
||||
|
||||
export const Button: React.FC<Props> = ({
|
||||
el: elFromProps = 'link',
|
||||
label,
|
||||
newTab,
|
||||
href,
|
||||
type = 'button',
|
||||
appearance,
|
||||
className: classNameFromProps,
|
||||
onClick,
|
||||
type = 'button',
|
||||
disabled,
|
||||
el: elFromProps = 'link',
|
||||
href,
|
||||
label,
|
||||
newTab,
|
||||
onClick,
|
||||
}) => {
|
||||
let el = elFromProps
|
||||
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
const className = [
|
||||
classes.button,
|
||||
classNameFromProps,
|
||||
@@ -44,11 +45,13 @@ export const Button: React.FC<Props> = ({
|
||||
</div>
|
||||
)
|
||||
|
||||
if (onClick || type === 'submit') el = 'button'
|
||||
if (onClick || type === 'submit') {
|
||||
el = 'button'
|
||||
}
|
||||
|
||||
if (el === 'link') {
|
||||
return (
|
||||
<Link href={href} className={className} {...newTabProps} onClick={onClick}>
|
||||
<Link className={className} href={href} {...newTabProps} onClick={onClick}>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
@@ -58,12 +61,12 @@ export const Button: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<Element
|
||||
href={href}
|
||||
className={className}
|
||||
href={href}
|
||||
type={type}
|
||||
{...newTabProps}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{content}
|
||||
</Element>
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Page } from '../../payload-types'
|
||||
import type { Page } from '../../payload-types'
|
||||
import { Button } from '../Button'
|
||||
|
||||
export type CMSLinkType = {
|
||||
type?: 'custom' | 'reference'
|
||||
url?: string
|
||||
newTab?: boolean
|
||||
reference?: {
|
||||
value: string | Page
|
||||
relationTo: 'pages'
|
||||
}
|
||||
label?: string
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
label?: string
|
||||
newTab?: boolean
|
||||
reference?: {
|
||||
relationTo: 'pages'
|
||||
value: Page | string
|
||||
}
|
||||
type?: 'custom' | 'reference'
|
||||
url?: string
|
||||
}
|
||||
|
||||
export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
type,
|
||||
url,
|
||||
newTab,
|
||||
reference,
|
||||
label,
|
||||
appearance,
|
||||
children,
|
||||
className,
|
||||
label,
|
||||
newTab,
|
||||
reference,
|
||||
url,
|
||||
}) => {
|
||||
const href =
|
||||
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
|
||||
@@ -34,7 +34,7 @@ export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
: url
|
||||
|
||||
if (!appearance) {
|
||||
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
|
||||
if (type === 'custom') {
|
||||
return (
|
||||
@@ -56,10 +56,10 @@ export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
}
|
||||
|
||||
const buttonProps = {
|
||||
newTab,
|
||||
href,
|
||||
appearance,
|
||||
href,
|
||||
label,
|
||||
newTab,
|
||||
}
|
||||
|
||||
return <Button className={className} {...buttonProps} el="link" />
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import React, { forwardRef, Ref } from 'react'
|
||||
import type { Ref } from 'react'
|
||||
import React, { forwardRef } from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
left?: boolean
|
||||
right?: boolean
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
left?: boolean
|
||||
ref?: Ref<HTMLDivElement>
|
||||
right?: boolean
|
||||
}
|
||||
|
||||
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const { left = true, right = true, className, children } = props
|
||||
const { children, className, left = true, right = true } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={[
|
||||
classes.gutter,
|
||||
left && classes.gutterLeft,
|
||||
@@ -24,6 +24,7 @@ export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React, { useState } from 'react'
|
||||
import type { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
|
||||
|
||||
import type { MainMenu } from '../../payload-types'
|
||||
|
||||
import { AdminBar } from '../AdminBar'
|
||||
import { CMSLink } from '../CMSLink'
|
||||
import { Gutter } from '../Gutter'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type HeaderBarProps = {
|
||||
|
||||
@@ -6,14 +6,14 @@ import { Text } from 'slate'
|
||||
type Children = Leaf[]
|
||||
|
||||
type Leaf = {
|
||||
type: string
|
||||
value?: {
|
||||
url: string
|
||||
alt: string
|
||||
}
|
||||
children?: Children
|
||||
url?: string
|
||||
[key: string]: unknown
|
||||
children?: Children
|
||||
type: string
|
||||
url?: string
|
||||
value?: {
|
||||
alt: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
const serialize = (children: Children): React.ReactElement[] =>
|
||||
@@ -35,7 +35,7 @@ const serialize = (children: Children): React.ReactElement[] =>
|
||||
|
||||
if (node.underline) {
|
||||
text = (
|
||||
<span style={{ textDecoration: 'underline' }} key={i}>
|
||||
<span key={i} style={{ textDecoration: 'underline' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
@@ -43,7 +43,7 @@ const serialize = (children: Children): React.ReactElement[] =>
|
||||
|
||||
if (node.strikethrough) {
|
||||
text = (
|
||||
<span style={{ textDecoration: 'line-through' }} key={i}>
|
||||
<span key={i} style={{ textDecoration: 'line-through' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
@@ -57,6 +57,8 @@ const serialize = (children: Children): React.ReactElement[] =>
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case 'blockquote':
|
||||
return <blockquote key={i}>{serialize(node.children)}</blockquote>
|
||||
case 'h1':
|
||||
return <h1 key={i}>{serialize(node.children)}</h1>
|
||||
case 'h2':
|
||||
@@ -69,12 +71,6 @@ const serialize = (children: Children): React.ReactElement[] =>
|
||||
return <h5 key={i}>{serialize(node.children)}</h5>
|
||||
case 'h6':
|
||||
return <h6 key={i}>{serialize(node.children)}</h6>
|
||||
case 'blockquote':
|
||||
return <blockquote key={i}>{serialize(node.children)}</blockquote>
|
||||
case 'ul':
|
||||
return <ul key={i}>{serialize(node.children)}</ul>
|
||||
case 'ol':
|
||||
return <ol key={i}>{serialize(node.children)}</ol>
|
||||
case 'li':
|
||||
return <li key={i}>{serialize(node.children)}</li>
|
||||
case 'link':
|
||||
@@ -83,6 +79,10 @@ const serialize = (children: Children): React.ReactElement[] =>
|
||||
{serialize(node.children)}
|
||||
</a>
|
||||
)
|
||||
case 'ol':
|
||||
return <ol key={i}>{serialize(node.children)}</ol>
|
||||
case 'ul':
|
||||
return <ul key={i}>{serialize(node.children)}</ul>
|
||||
|
||||
default:
|
||||
return <p key={i}>{serialize(node.children)}</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next'
|
||||
import type { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next'
|
||||
import QueryString from 'qs'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
import type { ParsedUrlQuery } from 'querystring'
|
||||
|
||||
import { Gutter } from '../components/Gutter'
|
||||
import RichText from '../components/RichText'
|
||||
@@ -10,12 +10,12 @@ import type { MainMenu, Page as PageType } from '../payload-types'
|
||||
import classes from './[slug].module.scss'
|
||||
|
||||
const Page: React.FC<
|
||||
PageType & {
|
||||
{
|
||||
mainMenu: MainMenu
|
||||
preview?: boolean
|
||||
}
|
||||
} & PageType
|
||||
> = (props) => {
|
||||
const { title, richText } = props
|
||||
const { richText, title } = props
|
||||
|
||||
return (
|
||||
<main className={classes.page}>
|
||||
@@ -35,7 +35,7 @@ interface IParams extends ParsedUrlQuery {
|
||||
|
||||
// when 'preview' cookies are set in the browser, getStaticProps runs on every request :)
|
||||
export const getStaticProps: GetStaticProps = async (context: GetStaticPropsContext) => {
|
||||
const { preview, previewData, params } = context
|
||||
const { params, preview, previewData } = context
|
||||
|
||||
const { payloadToken } =
|
||||
(previewData as {
|
||||
@@ -43,7 +43,9 @@ export const getStaticProps: GetStaticProps = async (context: GetStaticPropsCont
|
||||
}) || {}
|
||||
|
||||
let { slug } = (params as IParams) || {}
|
||||
if (!slug) slug = 'home'
|
||||
if (!slug) {
|
||||
slug = 'home'
|
||||
}
|
||||
|
||||
let doc = {}
|
||||
const notFound = false
|
||||
@@ -52,17 +54,17 @@ export const getStaticProps: GetStaticProps = async (context: GetStaticPropsCont
|
||||
|
||||
const searchParams = QueryString.stringify(
|
||||
{
|
||||
depth: 1,
|
||||
draft: preview ? true : undefined,
|
||||
where: {
|
||||
slug: {
|
||||
equals: lowerCaseSlug,
|
||||
},
|
||||
},
|
||||
depth: 1,
|
||||
draft: preview ? true : undefined,
|
||||
},
|
||||
{
|
||||
encode: false,
|
||||
addQueryPrefix: true,
|
||||
encode: false,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -88,12 +90,12 @@ export const getStaticProps: GetStaticProps = async (context: GetStaticPropsCont
|
||||
}
|
||||
|
||||
return {
|
||||
notFound,
|
||||
props: {
|
||||
...doc,
|
||||
preview: preview || null,
|
||||
collection: 'pages',
|
||||
preview: preview || null,
|
||||
},
|
||||
notFound,
|
||||
revalidate: 3600, // in seconds
|
||||
}
|
||||
}
|
||||
@@ -124,7 +126,7 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||
}
|
||||
|
||||
return {
|
||||
paths,
|
||||
fallback: true,
|
||||
paths,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { CookiesProvider } from 'react-cookie'
|
||||
import App, { AppContext, AppProps as NextAppProps } from 'next/app'
|
||||
import type { AppContext, AppProps as NextAppProps } from 'next/app'
|
||||
import App from 'next/app'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Header } from '../components/Header'
|
||||
import { MainMenu } from '../payload-types'
|
||||
import type { MainMenu } from '../payload-types'
|
||||
|
||||
import './app.scss'
|
||||
|
||||
@@ -26,13 +27,13 @@ type AppProps<P = any> = {
|
||||
} & Omit<NextAppProps<P>, 'pageProps'>
|
||||
|
||||
const PayloadApp = (
|
||||
appProps: AppProps & {
|
||||
appProps: {
|
||||
globals: IGlobals
|
||||
},
|
||||
} & AppProps,
|
||||
): React.ReactElement => {
|
||||
const { Component, pageProps, globals } = appProps
|
||||
const { Component, globals, pageProps } = appProps
|
||||
|
||||
const { collection, id, preview } = pageProps
|
||||
const { id, collection, preview } = pageProps
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -43,25 +44,27 @@ const PayloadApp = (
|
||||
router.reload()
|
||||
}
|
||||
}
|
||||
exit()
|
||||
exit().catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to exit preview:', error)
|
||||
})
|
||||
}, [router])
|
||||
|
||||
return (
|
||||
<CookiesProvider>
|
||||
<Header
|
||||
globals={globals}
|
||||
adminBarProps={{
|
||||
collection,
|
||||
id,
|
||||
preview,
|
||||
collection,
|
||||
onPreviewExit,
|
||||
preview,
|
||||
}}
|
||||
globals={globals}
|
||||
/>
|
||||
{/* typescript flags this `@ts-expect-error` declaration as unneeded, but types are breaking the build process
|
||||
Remove these comments when the issue is resolved
|
||||
See more here: https://github.com/facebook/react/issues/24304
|
||||
*/}
|
||||
{/* @ts-expect-error */}
|
||||
<Component {...pageProps} />
|
||||
</CookiesProvider>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { GetStaticProps } from 'next'
|
||||
import type { GetStaticProps } from 'next'
|
||||
|
||||
import Page, { getStaticProps as sharedGetStaticProps } from './[slug]'
|
||||
|
||||
export default Page
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export const getStaticProps: GetStaticProps = async (ctx) => {
|
||||
const func = sharedGetStaticProps.bind(this)
|
||||
return func(ctx)
|
||||
|
||||
@@ -7,95 +7,259 @@
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
collections: {
|
||||
pages: Page
|
||||
users: User
|
||||
'payload-preferences': PayloadPreference
|
||||
'payload-migrations': PayloadMigration
|
||||
}
|
||||
pages: Page;
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
pages: PagesSelect<false> | PagesSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {
|
||||
'main-menu': MainMenu
|
||||
}
|
||||
'main-menu': MainMenu;
|
||||
};
|
||||
globalsSelect: {
|
||||
'main-menu': MainMenuSelect<false> | MainMenuSelect<true>;
|
||||
};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: string
|
||||
title: string
|
||||
slug?: string
|
||||
id: string;
|
||||
title: string;
|
||||
slug?: string | null;
|
||||
richText: {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
_status?: 'draft' | 'published'
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
email: string
|
||||
resetPasswordToken?: string
|
||||
resetPasswordExpiration?: string
|
||||
salt?: string
|
||||
hash?: string
|
||||
loginAttempts?: number
|
||||
lockUntil?: string
|
||||
password?: string
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
export interface PayloadPreference {
|
||||
id: string
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users'
|
||||
value: string | User
|
||||
}
|
||||
key?: string
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string
|
||||
name?: string
|
||||
batch?: number
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages_select".
|
||||
*/
|
||||
export interface PagesSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
slug?: T;
|
||||
richText?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "main-menu".
|
||||
*/
|
||||
export interface MainMenu {
|
||||
id: string
|
||||
navItems?: {
|
||||
link: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
label: string
|
||||
}
|
||||
id?: string
|
||||
}[]
|
||||
updatedAt?: string
|
||||
createdAt?: string
|
||||
id: string;
|
||||
navItems?:
|
||||
| {
|
||||
link: {
|
||||
type?: ('reference' | 'custom') | null;
|
||||
newTab?: boolean | null;
|
||||
reference?: {
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
} | null;
|
||||
url?: string | null;
|
||||
label: string;
|
||||
};
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
|
||||
declare module 'payload' {
|
||||
export interface GeneratedTypes {
|
||||
collections: {
|
||||
pages: Page
|
||||
users: User
|
||||
'payload-preferences': PayloadPreference
|
||||
'payload-migrations': PayloadMigration
|
||||
}
|
||||
globals: {
|
||||
'main-menu': MainMenu
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "main-menu_select".
|
||||
*/
|
||||
export interface MainMenuSelect<T extends boolean = true> {
|
||||
navItems?:
|
||||
| T
|
||||
| {
|
||||
link?:
|
||||
| T
|
||||
| {
|
||||
type?: T;
|
||||
newTab?: T;
|
||||
reference?: T;
|
||||
url?: T;
|
||||
label?: T;
|
||||
};
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
"**/*.tsx",
|
||||
"next.config.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,16 @@
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-example-draft-preview
|
||||
PAYLOAD_SECRET=ENTER-STRING-HERE
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
# Database connection string
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
|
||||
|
||||
# Used to encrypt JWT tokens
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
|
||||
# Used to configure CORS, format links and more. No trailing slash
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
|
||||
|
||||
# Add the following environment variables when running your payload server & app separately
|
||||
# i.e. next-app || next-pages on localhost:3001 and payload server on localhost:3000
|
||||
|
||||
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3001
|
||||
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
|
||||
COOKIE_DOMAIN=localhost
|
||||
REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
|
||||
PAYLOAD_PUBLIC_SEED=true
|
||||
PAYLOAD_DROP_DATABASE=true
|
||||
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
module.exports = {
|
||||
extends: 'next',
|
||||
root: true,
|
||||
extends: ['@payloadcms'],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
|
||||
24
examples/draft-preview/payload/.swcrc
Normal file
24
examples/draft-preview/payload/.swcrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"dts": true
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"runtime": "automatic",
|
||||
"pragmaFrag": "React.Fragment",
|
||||
"throwIfNamespace": true,
|
||||
"development": false,
|
||||
"useBuiltins": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Preview Example CMS",
|
||||
"program": "${workspaceFolder}/src/server.ts",
|
||||
"preLaunchTask": "npm: build:server",
|
||||
"env": {
|
||||
"PAYLOAD_CONFIG_PATH": "${workspaceFolder}/src/payload.config.ts"
|
||||
}
|
||||
// "outFiles": [
|
||||
// "${workspaceFolder}/dist/**/*.js"
|
||||
// ]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,10 +9,18 @@ Follow the instructions in each respective README to get started. If you are set
|
||||
|
||||
## Quick Start
|
||||
|
||||
To spin up this example locally, follow these steps:
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
|
||||
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
|
||||
|
||||
4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
@@ -22,6 +30,33 @@ That's it! Changes made in `./src` will be reflected in your app. See the [Devel
|
||||
|
||||
Draft preview works by sending the user to your front-end with a `secret` along with their http-only cookies. Your front-end catches the request, verifies the authenticity, then enters into it's own preview mode. Once in preview mode, your front-end can begin securely requesting draft documents from Payload. See [Preview Mode](#preview-mode) for more details.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Depending on how you run this example, you need different environment variables:
|
||||
|
||||
- #### Running Payload and the Front-End Together
|
||||
|
||||
When the Payload server and front-end run on the same domain and port:
|
||||
|
||||
```ts
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
- #### Running Payload and the Front-End Separately
|
||||
|
||||
When running Payload on one domain (e.g., `localhost:3000`) and the front-end on another (e.g., `localhost:3001`):
|
||||
|
||||
```ts
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3001
|
||||
REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
|
||||
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
|
||||
```
|
||||
|
||||
### Collections
|
||||
|
||||
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend any of this functionality.
|
||||
@@ -53,11 +88,11 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
})
|
||||
```
|
||||
|
||||
For more details on how to extend this functionality, see the [Authentication](https://payloadcms.com/docs/authentication) docs.
|
||||
For more details on how to extend this functionality, see the [Authentication](https://payloadcms.com/docs/authentication/overview) docs.
|
||||
|
||||
### Preview Mode
|
||||
|
||||
To preview draft documents, the user first needs to have at least one draft document saved. When they click the "preview" button from the Payload admin panel, a custom [preview function](https://payloadcms.com/docs/configuration/collections#preview) routes them to your front-end with a `secret` along with their http-only cookies. An API route on your front-end will verify the secret and token before entering into it's own preview mode. Once in preview mode, it can begin requesting drafts from Payload using the `Authorization` header. See [Pages](#pages) for more details.
|
||||
To preview draft documents, the user first needs to have at least one draft document saved. When they click the "preview" button from the Payload admin panel, a custom [preview function](https://payloadcms.com/docs/admin/collections#preview) routes them to your front-end with a `secret` along with their http-only cookies. An API route on your front-end will verify the secret and token before entering into it's own preview mode. Once in preview mode, it can begin requesting drafts from Payload using the `Authorization` header. See [Pages](#pages) for more details.
|
||||
|
||||
> "Preview mode" looks differently for every front-end framework. For instance, check out the differences between Next.js [Preview Mode](https://nextjs.org/docs/pages/building-your-application/configuring/preview-mode) in the Pages Router and [Draft Mode](https://nextjs.org/docs/pages/building-your-application/configuring/draft-mode) in the App Router. In Next.js, methods are provided that set cookies in your browser, but this may not be the case for all frameworks.
|
||||
|
||||
@@ -83,16 +118,16 @@ To spin up this example locally, follow the [Quick Start](#quick-start).
|
||||
|
||||
### Seed
|
||||
|
||||
On boot, a seed script is included to scaffold a basic database for you to use as an example. This is done by setting the `PAYLOAD_DROP_DATABASE` and `PAYLOAD_PUBLIC_SEED` environment variables which are included in the `.env.example` by default. You can remove these from your `.env` to prevent this behavior. You can also freshly seed your project at any time by running `yarn seed`. This seed creates a user with email `demo@payloadcms.com` and password `demo` along with a home page and an example page with two versions, one published and the other draft.
|
||||
On boot, a seed script is included to scaffold a basic database for you to use as an example. You can remove `pnpm seed` from the `dev` script in the `package.json` to prevent this behavior. You can also freshly seed your project at any time by running `pnpm seed`. This seed creates a user with email `demo@payloadcms.com` and password `demo` along with a home page and an example page with two versions, one published and the other draft.
|
||||
|
||||
> NOTICE: seeding the database is destructive because it drops your current database to populate a fresh one from the seed template. Only run this command if you are starting a new project or can afford to lose your current data.
|
||||
|
||||
## Production
|
||||
|
||||
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
|
||||
To run Payload in production, you need to build and start the Admin panel. To do so, follow these steps:
|
||||
|
||||
1. First invoke the `payload build` script by running `yarn build` or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
|
||||
1. Then run `yarn serve` or `npm run serve` to run Node in production and serve Payload from the `./build` directory.
|
||||
1. Invoke the `next build` script by running `pnpm build` or `npm run build` in your project root. This creates a `.next` directory with a production-ready admin bundle.
|
||||
1. Finally run `pnpm start` or `npm run start` to run Node in production and serve Payload from the `.build` directory.
|
||||
|
||||
### Deployment
|
||||
|
||||
|
||||
5
examples/draft-preview/payload/next-env.d.ts
vendored
Normal file
5
examples/draft-preview/payload/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
8
examples/draft-preview/payload/next.config.mjs
Normal file
8
examples/draft-preview/payload/next.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
import { withPayload } from '@payloadcms/next/withPayload'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Your Next.js config here
|
||||
}
|
||||
|
||||
export default withPayload(nextConfig)
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"ext": "ts",
|
||||
"exec": "ts-node src/server.ts -- -I",
|
||||
"stdin": false
|
||||
}
|
||||
@@ -1,49 +1,57 @@
|
||||
{
|
||||
"name": "payload-example-preview",
|
||||
"description": "Payload preview example.",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/server.js",
|
||||
"description": "Payload preview example.",
|
||||
"license": "MIT",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
|
||||
"seed": "rm -rf media && cross-env PAYLOAD_PUBLIC_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts ts-node src/server.ts",
|
||||
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
||||
"build:server": "tsc",
|
||||
"build": "yarn copyfiles && yarn build:payload && yarn build:server",
|
||||
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation && pnpm seed && next dev",
|
||||
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||
"generate:schema": "payload-graphql generate:schema",
|
||||
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||
"seed": "npm run payload migrate:fresh",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/next": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"@payloadcms/ui": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest"
|
||||
"escape-html": "^1.0.3",
|
||||
"graphql": "^16.9.0",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"next": "^15.0.0",
|
||||
"payload": "latest",
|
||||
"payload-admin-bar": "^1.0.6",
|
||||
"react": "19.0.0-rc-65a56d0e-20241020",
|
||||
"react-dom": "19.0.0-rc-65a56d0e-20241020"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "^0.0.2",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/node": "18.11.3",
|
||||
"@types/react": "18.0.21",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-filenames": "^1.3.2",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"nodemon": "^2.0.6",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.8.4"
|
||||
"@payloadcms/graphql": "latest",
|
||||
"@swc/core": "^1.6.13",
|
||||
"@types/escape-html": "^1.0.2",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^15.0.0",
|
||||
"slate": "^0.82.0",
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "5.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||
}
|
||||
}
|
||||
|
||||
5380
examples/draft-preview/payload/pnpm-lock.yaml
generated
Normal file
5380
examples/draft-preview/payload/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
.page {
|
||||
margin-top: calc(var(--base) * 2);
|
||||
}
|
||||
89
examples/draft-preview/payload/src/app/(app)/[slug]/page.tsx
Normal file
89
examples/draft-preview/payload/src/app/(app)/[slug]/page.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
import React, { cache, Fragment } from 'react'
|
||||
|
||||
import type { Page as PageType } from '../../../payload-types'
|
||||
|
||||
import { Gutter } from '../../../components/Gutter'
|
||||
import RichText from '../../../components/RichText'
|
||||
import config from '../../../payload.config'
|
||||
import { home as homeStatic } from '../../../seed/home'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const payload = await getPayload({ config })
|
||||
const pages = await payload.find({
|
||||
collection: 'pages',
|
||||
draft: false,
|
||||
limit: 1000,
|
||||
overrideAccess: false,
|
||||
})
|
||||
|
||||
const params = pages.docs
|
||||
?.filter((doc) => {
|
||||
return doc.slug !== 'home'
|
||||
})
|
||||
.map(({ slug }) => {
|
||||
return { slug }
|
||||
})
|
||||
|
||||
return params || []
|
||||
}
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
slug?: string
|
||||
}>
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default async function Page({ params: paramsPromise }: Args) {
|
||||
const { slug = 'home' } = await paramsPromise
|
||||
|
||||
let page: null | PageType
|
||||
|
||||
page = await queryPageBySlug({
|
||||
slug,
|
||||
})
|
||||
|
||||
// Remove this code once your website is seeded
|
||||
if (!page && slug === 'home') {
|
||||
page = homeStatic
|
||||
}
|
||||
|
||||
if (page === null) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<main className={classes.page}>
|
||||
<Gutter>
|
||||
<h1>{page?.title}</h1>
|
||||
<RichText content={page?.richText} />
|
||||
</Gutter>
|
||||
</main>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const queryPageBySlug = cache(async ({ slug }: { slug: string }) => {
|
||||
const { isEnabled: draft } = await draftMode()
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
const result = await payload.find({
|
||||
collection: 'pages',
|
||||
draft,
|
||||
limit: 1,
|
||||
overrideAccess: draft,
|
||||
where: {
|
||||
slug: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return result.docs?.[0] || null
|
||||
})
|
||||
118
examples/draft-preview/payload/src/app/(app)/app.scss
Normal file
118
examples/draft-preview/payload/src/app/(app)/app.scss
Normal file
@@ -0,0 +1,118 @@
|
||||
$breakpoint: 1000px;
|
||||
|
||||
:root {
|
||||
--max-width: 1600px;
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-rgb: 255, 255, 255;
|
||||
--block-spacing: 2rem;
|
||||
--gutter-h: 4rem;
|
||||
--base: 1rem;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
--block-spacing: 1rem;
|
||||
--gutter-h: 2rem;
|
||||
--base: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-rgb: 7, 7, 7;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 20px;
|
||||
line-height: 1.5;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Open Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: rgb(var(--background-rgb));
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 2.5rem 0;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
font-size: 3rem;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 3.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 2.5rem 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 2rem 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
31
examples/draft-preview/payload/src/app/(app)/layout.tsx
Normal file
31
examples/draft-preview/payload/src/app/(app)/layout.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
import { AdminBar } from '../../components/AdminBar'
|
||||
import { Header } from '../../components/Header'
|
||||
import './app.scss'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description: 'Generated by create next app',
|
||||
title: 'Create Next App',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const { isEnabled } = await draftMode()
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<AdminBar
|
||||
adminBarProps={{
|
||||
preview: isEnabled,
|
||||
}}
|
||||
/>
|
||||
<Header />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const draft = await draftMode()
|
||||
draft.disable()
|
||||
return new Response('Draft mode is disabled')
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const draft = await draftMode()
|
||||
draft.disable()
|
||||
return new Response('Draft mode is disabled')
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { CollectionSlug } from 'payload'
|
||||
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { draftMode } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
import configPromise from '../../../../payload.config'
|
||||
|
||||
const payloadToken = 'payload-token'
|
||||
|
||||
export async function GET(
|
||||
req: {
|
||||
cookies: {
|
||||
get: (name: string) => {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
} & Request,
|
||||
): Promise<Response> {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
const token = req.cookies.get(payloadToken)?.value
|
||||
const { searchParams } = new URL(req.url)
|
||||
const path = searchParams.get('path')
|
||||
const collection = searchParams.get('collection') as CollectionSlug
|
||||
const slug = searchParams.get('slug')
|
||||
|
||||
const previewSecret = searchParams.get('previewSecret')
|
||||
|
||||
if (previewSecret) {
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
} else {
|
||||
if (!path) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!collection) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
new Response('This endpoint can only be used for internal previews', { status: 500 })
|
||||
}
|
||||
|
||||
let user
|
||||
|
||||
try {
|
||||
user = jwt.verify(token, payload.secret)
|
||||
} catch (error) {
|
||||
payload.logger.error({
|
||||
err: error,
|
||||
msg: 'Error verifying token for live preview',
|
||||
})
|
||||
}
|
||||
|
||||
const draft = await draftMode()
|
||||
|
||||
// You can add additional checks here to see if the user is allowed to preview this page
|
||||
if (!user) {
|
||||
draft.disable()
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
// Verify the given slug exists
|
||||
try {
|
||||
const docs = await payload.find({
|
||||
collection,
|
||||
draft: true,
|
||||
where: {
|
||||
slug: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!docs.docs.length) {
|
||||
return new Response('Document not found', { status: 404 })
|
||||
}
|
||||
} catch (error) {
|
||||
payload.logger.error({
|
||||
err: error,
|
||||
msg: 'Error verifying token for live preview:',
|
||||
})
|
||||
}
|
||||
|
||||
draft.enable()
|
||||
|
||||
redirect(path)
|
||||
}
|
||||
}
|
||||
3
examples/draft-preview/payload/src/app/(app)/page.tsx
Normal file
3
examples/draft-preview/payload/src/app/(app)/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import Page from './[slug]/page'
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,25 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) =>
|
||||
NotFoundPage({ config, importMap, params, searchParams })
|
||||
|
||||
export default NotFound
|
||||
@@ -0,0 +1,25 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { generatePageMetadata, RootPage } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config, importMap, params, searchParams })
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,121 @@
|
||||
import { RscEntrySlateCell as RscEntrySlateCell_0e78253914a550fdacd75626f1dabe17 } from '@payloadcms/richtext-slate/rsc'
|
||||
import { RscEntrySlateField as RscEntrySlateField_0e78253914a550fdacd75626f1dabe17 } from '@payloadcms/richtext-slate/rsc'
|
||||
import { BoldLeafButton as BoldLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { BoldLeaf as BoldLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { CodeLeafButton as CodeLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { CodeLeaf as CodeLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { ItalicLeafButton as ItalicLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { ItalicLeaf as ItalicLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { StrikethroughLeafButton as StrikethroughLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { StrikethroughLeaf as StrikethroughLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { UnderlineLeafButton as UnderlineLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { UnderlineLeaf as UnderlineLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { BlockquoteElementButton as BlockquoteElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { BlockquoteElement as BlockquoteElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { H1ElementButton as H1ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { Heading1Element as Heading1Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { H2ElementButton as H2ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { Heading2Element as Heading2Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { H3ElementButton as H3ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { Heading3Element as Heading3Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { H4ElementButton as H4ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { Heading4Element as Heading4Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { H5ElementButton as H5ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { Heading5Element as Heading5Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { H6ElementButton as H6ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { Heading6Element as Heading6Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { IndentButton as IndentButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { IndentElement as IndentElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { ListItemElement as ListItemElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { LinkButton as LinkButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { LinkElement as LinkElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { WithLinks as WithLinks_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { OLElementButton as OLElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { OrderedListElement as OrderedListElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { RelationshipButton as RelationshipButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { RelationshipElement as RelationshipElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { WithRelationship as WithRelationship_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { TextAlignElementButton as TextAlignElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { ULElementButton as ULElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { UnorderedListElement as UnorderedListElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { UploadElementButton as UploadElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { UploadElement as UploadElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
import { WithUpload as WithUpload_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
|
||||
|
||||
export const importMap = {
|
||||
'@payloadcms/richtext-slate/rsc#RscEntrySlateCell':
|
||||
RscEntrySlateCell_0e78253914a550fdacd75626f1dabe17,
|
||||
'@payloadcms/richtext-slate/rsc#RscEntrySlateField':
|
||||
RscEntrySlateField_0e78253914a550fdacd75626f1dabe17,
|
||||
'@payloadcms/richtext-slate/client#BoldLeafButton':
|
||||
BoldLeafButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#BoldLeaf': BoldLeaf_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#CodeLeafButton':
|
||||
CodeLeafButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#CodeLeaf': CodeLeaf_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#ItalicLeafButton':
|
||||
ItalicLeafButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#ItalicLeaf': ItalicLeaf_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#StrikethroughLeafButton':
|
||||
StrikethroughLeafButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#StrikethroughLeaf':
|
||||
StrikethroughLeaf_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#UnderlineLeafButton':
|
||||
UnderlineLeafButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#UnderlineLeaf': UnderlineLeaf_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#BlockquoteElementButton':
|
||||
BlockquoteElementButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#BlockquoteElement':
|
||||
BlockquoteElement_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#H1ElementButton':
|
||||
H1ElementButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#Heading1Element':
|
||||
Heading1Element_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#H2ElementButton':
|
||||
H2ElementButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#Heading2Element':
|
||||
Heading2Element_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#H3ElementButton':
|
||||
H3ElementButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#Heading3Element':
|
||||
Heading3Element_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#H4ElementButton':
|
||||
H4ElementButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#Heading4Element':
|
||||
Heading4Element_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#H5ElementButton':
|
||||
H5ElementButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#Heading5Element':
|
||||
Heading5Element_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#H6ElementButton':
|
||||
H6ElementButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#Heading6Element':
|
||||
Heading6Element_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#IndentButton': IndentButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#IndentElement': IndentElement_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#ListItemElement':
|
||||
ListItemElement_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#LinkButton': LinkButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#LinkElement': LinkElement_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#WithLinks': WithLinks_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#OLElementButton':
|
||||
OLElementButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#OrderedListElement':
|
||||
OrderedListElement_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#RelationshipButton':
|
||||
RelationshipButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#RelationshipElement':
|
||||
RelationshipElement_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#WithRelationship':
|
||||
WithRelationship_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#TextAlignElementButton':
|
||||
TextAlignElementButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#ULElementButton':
|
||||
ULElementButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#UnorderedListElement':
|
||||
UnorderedListElement_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#UploadElementButton':
|
||||
UploadElementButton_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#UploadElement': UploadElement_0b388c087d9de8c4f011dd323a130cfb,
|
||||
'@payloadcms/richtext-slate/client#WithUpload': WithUpload_0b388c087d9de8c4f011dd323a130cfb,
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = REST_GET(config)
|
||||
export const POST = REST_POST(config)
|
||||
export const DELETE = REST_DELETE(config)
|
||||
export const PATCH = REST_PATCH(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
@@ -0,0 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||
@@ -0,0 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
32
examples/draft-preview/payload/src/app/(payload)/layout.tsx
Normal file
32
examples/draft-preview/payload/src/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
|
||||
import '@payloadcms/next/css'
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
import './custom.scss'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server'
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
export default Layout
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access } from 'payload/config'
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const loggedIn: Access = ({ req: { user } }) => {
|
||||
return Boolean(user)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access } from 'payload/config'
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const publishedOrLoggedIn: Access = ({ req: { user } }) => {
|
||||
if (user) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FieldHook } from 'payload/types'
|
||||
import type { FieldHook } from 'payload'
|
||||
|
||||
const format = (val: string): string =>
|
||||
val
|
||||
@@ -6,9 +6,9 @@ const format = (val: string): string =>
|
||||
.replace(/[^\w-]+/g, '')
|
||||
.toLowerCase()
|
||||
|
||||
const formatSlug =
|
||||
export const formatSlug =
|
||||
(fallback: string): FieldHook =>
|
||||
({ operation, value, originalDoc, data }) => {
|
||||
({ data, operation, originalDoc, value }) => {
|
||||
if (typeof value === 'string') {
|
||||
return format(value)
|
||||
}
|
||||
@@ -23,5 +23,3 @@ const formatSlug =
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export default formatSlug
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { AfterChangeHook } from 'payload/dist/collections/config/types'
|
||||
import type { CollectionAfterChangeHook } from 'payload'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
import type { Page } from '../../../payload-types'
|
||||
|
||||
// ensure that the home page is revalidated at '/' instead of '/home'
|
||||
export const formatAppURL = ({ doc }): string => {
|
||||
@@ -7,32 +11,52 @@ export const formatAppURL = ({ doc }): string => {
|
||||
return pathname
|
||||
}
|
||||
|
||||
// revalidate the page in the background, so the user doesn't have to wait
|
||||
// notice that the hook itself is not async and we are not awaiting `revalidate`
|
||||
// only revalidate existing docs that are published (not drafts)
|
||||
// send `revalidatePath`, `collection`, and `slug` to the frontend to use in its revalidate route
|
||||
// frameworks may have different ways of doing this, but the idea is the same
|
||||
export const revalidatePage: AfterChangeHook = ({ doc, req, operation }) => {
|
||||
if (operation === 'update' && doc._status === 'published') {
|
||||
const url = formatAppURL({ doc })
|
||||
export const revalidatePage: CollectionAfterChangeHook<Page> = ({
|
||||
doc,
|
||||
operation,
|
||||
previousDoc,
|
||||
req,
|
||||
}) => {
|
||||
if (process.env.PAYLOAD_PUBLIC_SITE_URL && process.env.REVALIDATION_KEY) {
|
||||
// Revalidate externally if payload is configured separately from the next app
|
||||
if (operation === 'update' && doc._status === 'published') {
|
||||
const url = formatAppURL({ doc })
|
||||
|
||||
const revalidate = async (): Promise<void> => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/revalidate?secret=${process.env.REVALIDATION_KEY}&collection=pages&slug=${doc?.slug}&path=${url}`,
|
||||
)
|
||||
const revalidate = async (): Promise<void> => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/revalidate?secret=${process.env.REVALIDATION_KEY}&collection=pages&slug=${doc?.slug}&path=${url}`,
|
||||
)
|
||||
|
||||
if (res.ok) {
|
||||
req.payload.logger.info(`Revalidated path ${url}`)
|
||||
} else {
|
||||
req.payload.logger.error(`Error revalidating path ${url}`)
|
||||
if (res.ok) {
|
||||
req.payload.logger.info(`Revalidated path ${url}`)
|
||||
} else {
|
||||
req.payload.logger.error(`Error revalidating path ${url}`)
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error(`Error hitting revalidate route for ${url}`)
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error(`Error hitting revalidate route for ${url}`)
|
||||
}
|
||||
|
||||
void revalidate()
|
||||
}
|
||||
} else {
|
||||
// Revalidate internally with next/cache if your payload app is installed within /app folder
|
||||
if (req.context.skipRevalidate) {
|
||||
return doc
|
||||
}
|
||||
|
||||
revalidate()
|
||||
if (doc._status === 'published') {
|
||||
const path = doc.slug === 'home' ? '/' : `/${doc.slug}`
|
||||
req.payload.logger.info(`Revalidating page at path: ${path}`)
|
||||
revalidatePath(path)
|
||||
}
|
||||
|
||||
if (previousDoc?._status === 'published' && doc._status !== 'published') {
|
||||
const oldPath = previousDoc.slug === 'home' ? '/' : `/${previousDoc.slug}`
|
||||
req.payload.logger.info(`Revalidating old page at path: ${oldPath}`)
|
||||
revalidatePath(oldPath)
|
||||
}
|
||||
}
|
||||
|
||||
return doc
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import richText from '../../fields/richText'
|
||||
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
|
||||
import { loggedIn } from './access/loggedIn'
|
||||
import { publishedOrLoggedIn } from './access/publishedOrLoggedIn'
|
||||
import formatSlug from './hooks/formatSlug'
|
||||
import { formatSlug } from './hooks/formatSlug'
|
||||
import { formatAppURL, revalidatePage } from './hooks/revalidatePage'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
access: {
|
||||
create: loggedIn,
|
||||
delete: loggedIn,
|
||||
read: publishedOrLoggedIn,
|
||||
update: loggedIn,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'slug', 'updatedAt'],
|
||||
preview: (doc) => {
|
||||
return `${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/preview?url=${encodeURIComponent(
|
||||
formatAppURL({
|
||||
doc,
|
||||
}),
|
||||
)}&collection=pages&slug=${doc.slug}&secret=${process.env.PAYLOAD_PUBLIC_DRAFT_SECRET}`
|
||||
if (process.env.PAYLOAD_PUBLIC_SITE_URL && process.env.PAYLOAD_PUBLIC_DRAFT_SECRET) {
|
||||
// Separate Payload and front-end setup
|
||||
return `${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/preview?url=${encodeURIComponent(
|
||||
formatAppURL({ doc }),
|
||||
)}&collection=pages&slug=${doc.slug}&secret=${process.env.PAYLOAD_PUBLIC_DRAFT_SECRET}`
|
||||
} else if (process.env.NEXT_PUBLIC_SERVER_URL) {
|
||||
// Unified Payload and front-end setup
|
||||
const path = generatePreviewPath({
|
||||
slug: typeof doc?.slug === 'string' ? doc.slug : '',
|
||||
collection: 'pages',
|
||||
})
|
||||
return `${process.env.NEXT_PUBLIC_SERVER_URL}${path}`
|
||||
}
|
||||
|
||||
// Fallback for missing environment variables
|
||||
throw new Error(
|
||||
'Environment variables for preview functionality are not set. Ensure that either PAYLOAD_PUBLIC_SITE_URL and PAYLOAD_PUBLIC_DRAFT_SECRET, or NEXT_PUBLIC_SERVER_URL are defined.',
|
||||
)
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
access: {
|
||||
read: publishedOrLoggedIn,
|
||||
create: loggedIn,
|
||||
update: loggedIn,
|
||||
delete: loggedIn,
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [revalidatePage],
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
@@ -39,16 +47,22 @@ export const Pages: CollectionConfig = {
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
label: 'Slug',
|
||||
type: 'text',
|
||||
index: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate: [formatSlug('title')],
|
||||
},
|
||||
index: true,
|
||||
label: 'Slug',
|
||||
},
|
||||
richText(),
|
||||
],
|
||||
hooks: {
|
||||
afterChange: [revalidatePage],
|
||||
},
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: {
|
||||
tokenExpiration: 28800, // 8 hours
|
||||
cookies: {
|
||||
sameSite: 'none',
|
||||
secure: true,
|
||||
domain: process.env.COOKIE_DOMAIN,
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
auth: true,
|
||||
fields: [],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
.adminBar {
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
background-color: rgba(var(--foreground-rgb), 0.075);
|
||||
padding: calc(var(--base) * 0.5) 0;
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 150ms linear;
|
||||
}
|
||||
|
||||
.payloadAdminBar {
|
||||
color: rgb(var(--foreground-rgb)) !important;
|
||||
}
|
||||
|
||||
.show {
|
||||
display: block;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.controls {
|
||||
& > *:not(:last-child) {
|
||||
margin-right: calc(var(--base) * 0.5) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
margin-right: calc(var(--base) * 0.5) !important;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-right: calc(var(--base) * 0.5) !important;
|
||||
}
|
||||
|
||||
.innerLogo {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hr {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: rbg(var(--background-rgb));
|
||||
height: 2px;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
|
||||
import type { PayloadAdminBarProps } from 'payload-admin-bar'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { PayloadAdminBar } from 'payload-admin-bar'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { Gutter } from '../Gutter'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const collectionLabels = {
|
||||
pages: {
|
||||
plural: 'Pages',
|
||||
singular: 'Page',
|
||||
},
|
||||
}
|
||||
|
||||
const Title: React.FC = () => <span>Dashboard</span>
|
||||
|
||||
export const AdminBar: React.FC<{
|
||||
adminBarProps?: PayloadAdminBarProps
|
||||
}> = (props) => {
|
||||
const { adminBarProps } = props || {}
|
||||
const [show, setShow] = useState(false)
|
||||
const collection = 'pages'
|
||||
const router = useRouter()
|
||||
|
||||
const onAuthChange = React.useCallback((user) => {
|
||||
setShow(user?.id)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={[classes.adminBar, show && classes.show].filter(Boolean).join(' ')}>
|
||||
<Gutter className={classes.container}>
|
||||
<PayloadAdminBar
|
||||
{...adminBarProps}
|
||||
className={classes.payloadAdminBar}
|
||||
classNames={{
|
||||
controls: classes.controls,
|
||||
logo: classes.logo,
|
||||
user: classes.user,
|
||||
}}
|
||||
cmsURL={process.env.NEXT_PUBLIC_SERVER_URL}
|
||||
collection={collection}
|
||||
collectionLabels={{
|
||||
plural: collectionLabels[collection]?.plural || 'Pages',
|
||||
singular: collectionLabels[collection]?.singular || 'Page',
|
||||
}}
|
||||
logo={<Title />}
|
||||
onAuthChange={onAuthChange}
|
||||
onPreviewExit={() => {
|
||||
fetch('/next/exit-preview')
|
||||
.then(() => {
|
||||
router.push('/')
|
||||
router.refresh()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error exiting preview:', error)
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
zIndex: 'unset',
|
||||
}}
|
||||
/>
|
||||
</Gutter>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
const BeforeLogin: React.FC = () => {
|
||||
if (process.env.PAYLOAD_PUBLIC_SEED === 'true') {
|
||||
return (
|
||||
<p>
|
||||
{'Log in with the email '}
|
||||
<strong>demo@payloadcms.com</strong>
|
||||
{' and the password '}
|
||||
<strong>demo</strong>.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export default BeforeLogin
|
||||
@@ -0,0 +1,55 @@
|
||||
.button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
svg {
|
||||
margin-right: calc(var(--base) / 2);
|
||||
width: var(--base);
|
||||
height: var(--base);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
.primary--white {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.primary--black {
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.secondary--white {
|
||||
background-color: white;
|
||||
box-shadow: inset 0 0 0 1px black;
|
||||
}
|
||||
|
||||
.secondary--black {
|
||||
background-color: black;
|
||||
box-shadow: inset 0 0 0 1px white;
|
||||
}
|
||||
|
||||
.appearance--default {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { ElementType } from 'react'
|
||||
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export type Props = {
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
el?: 'a' | 'button' | 'link'
|
||||
href?: string
|
||||
label?: string
|
||||
newTab?: boolean | null
|
||||
onClick?: () => void
|
||||
type?: 'button' | 'submit'
|
||||
}
|
||||
|
||||
export const Button: React.FC<Props> = ({
|
||||
type = 'button',
|
||||
appearance,
|
||||
className: classNameFromProps,
|
||||
disabled,
|
||||
el: elFromProps = 'link',
|
||||
href,
|
||||
label,
|
||||
newTab,
|
||||
onClick,
|
||||
}) => {
|
||||
let el = elFromProps
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
const className = [
|
||||
classes.button,
|
||||
classNameFromProps,
|
||||
classes[`appearance--${appearance}`],
|
||||
classes.button,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
const content = (
|
||||
<div className={classes.content}>
|
||||
{/* <Chevron /> */}
|
||||
<span className={classes.label}>{label}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (onClick || type === 'submit') {
|
||||
el = 'button'
|
||||
}
|
||||
|
||||
if (el === 'link') {
|
||||
return (
|
||||
<Link className={className} href={href || ''} {...newTabProps} onClick={onClick}>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const Element: ElementType = el
|
||||
|
||||
return (
|
||||
<Element
|
||||
className={className}
|
||||
href={href}
|
||||
type={type}
|
||||
{...newTabProps}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{content}
|
||||
</Element>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import type { Page } from '../../payload-types'
|
||||
|
||||
import { Button } from '../Button'
|
||||
|
||||
export type CMSLinkType = {
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
label?: string
|
||||
newTab?: boolean | null
|
||||
reference?: {
|
||||
relationTo: 'pages'
|
||||
value: number | Page | string
|
||||
} | null
|
||||
type?: 'custom' | 'reference' | null
|
||||
url?: null | string
|
||||
}
|
||||
|
||||
export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
type,
|
||||
appearance,
|
||||
children,
|
||||
className,
|
||||
label,
|
||||
newTab,
|
||||
reference,
|
||||
url,
|
||||
}) => {
|
||||
const href =
|
||||
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
|
||||
? `${reference?.relationTo !== 'pages' ? `/${reference?.relationTo}` : ''}/${
|
||||
reference.value.slug
|
||||
}`
|
||||
: url
|
||||
|
||||
if (!href) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!appearance) {
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
|
||||
if (type === 'custom') {
|
||||
return (
|
||||
<a href={url || ''} {...newTabProps} className={className}>
|
||||
{label && label}
|
||||
{children ? <>{children}</> : null}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={href} {...newTabProps} className={className} prefetch={false}>
|
||||
{label && label}
|
||||
{children ? <>{children}</> : null}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const buttonProps = {
|
||||
appearance,
|
||||
href,
|
||||
label,
|
||||
newTab,
|
||||
}
|
||||
|
||||
return <Button className={className} {...buttonProps} el="link" />
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
.gutter {
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.gutterLeft {
|
||||
padding-left: var(--gutter-h);
|
||||
}
|
||||
|
||||
.gutterRight {
|
||||
padding-right: var(--gutter-h);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Ref } from 'react'
|
||||
|
||||
import React, { forwardRef } from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
left?: boolean
|
||||
ref?: Ref<HTMLDivElement>
|
||||
right?: boolean
|
||||
}
|
||||
|
||||
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const { children, className, left = true, right = true } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
classes.gutter,
|
||||
left && classes.gutterLeft,
|
||||
right && classes.gutterRight,
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
Gutter.displayName = 'Gutter'
|
||||
@@ -0,0 +1,32 @@
|
||||
.header {
|
||||
padding: var(--base) 0;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: calc(var(--base) / 2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--base);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
gap: 0 calc(var(--base) / 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import type { MainMenu } from '../../payload-types'
|
||||
|
||||
import { getCachedGlobal } from '../../utilities/getGlobals'
|
||||
import { CMSLink } from '../CMSLink'
|
||||
import { Gutter } from '../Gutter'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export async function Header() {
|
||||
const header: MainMenu = await getCachedGlobal('main-menu', 1)()
|
||||
|
||||
const navItems = header?.navItems || []
|
||||
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<Gutter className={classes.wrap}>
|
||||
<Link className={classes.logo} href="/">
|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-light.svg"
|
||||
/>
|
||||
<Image
|
||||
alt="Payload Logo"
|
||||
height={30}
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-dark.svg"
|
||||
width={150}
|
||||
/>
|
||||
</picture>
|
||||
</Link>
|
||||
<nav className={classes.nav}>
|
||||
{navItems.map(({ link }, i) => {
|
||||
const sanitizedLink = {
|
||||
...link,
|
||||
type: link.type ?? undefined,
|
||||
newTab: link.newTab ?? false,
|
||||
url: link.url ?? undefined,
|
||||
}
|
||||
|
||||
return <CMSLink key={i} {...sanitizedLink} />
|
||||
})}
|
||||
</nav>
|
||||
</Gutter>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -0,0 +1,9 @@
|
||||
.richText {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
import serialize from './serialize'
|
||||
|
||||
const RichText: React.FC<{ className?: string; content: any }> = ({ className, content }) => {
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
|
||||
{serialize(content)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RichText
|
||||
@@ -0,0 +1,91 @@
|
||||
import escapeHTML from 'escape-html'
|
||||
import React, { Fragment } from 'react'
|
||||
import { Text } from 'slate'
|
||||
|
||||
type Children = Leaf[]
|
||||
|
||||
type Leaf = {
|
||||
[key: string]: unknown
|
||||
children: Children
|
||||
type: string
|
||||
url?: string
|
||||
value?: {
|
||||
alt: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
const serialize = (children: Children): React.ReactNode[] =>
|
||||
children.map((node, i) => {
|
||||
if (Text.isText(node)) {
|
||||
let text = <span dangerouslySetInnerHTML={{ __html: escapeHTML(node.text) }} />
|
||||
|
||||
if (node.bold) {
|
||||
text = <strong key={i}>{text}</strong>
|
||||
}
|
||||
|
||||
if (node.code) {
|
||||
text = <code key={i}>{text}</code>
|
||||
}
|
||||
|
||||
if (node.italic) {
|
||||
text = <em key={i}>{text}</em>
|
||||
}
|
||||
|
||||
if (node.underline) {
|
||||
text = (
|
||||
<span key={i} style={{ textDecoration: 'underline' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (node.strikethrough) {
|
||||
text = (
|
||||
<span key={i} style={{ textDecoration: 'line-through' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return <Fragment key={i}>{text}</Fragment>
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case 'blockquote':
|
||||
return <blockquote key={i}>{serialize(node.children)}</blockquote>
|
||||
case 'h1':
|
||||
return <h1 key={i}>{serialize(node.children)}</h1>
|
||||
case 'h2':
|
||||
return <h2 key={i}>{serialize(node.children)}</h2>
|
||||
case 'h3':
|
||||
return <h3 key={i}>{serialize(node.children)}</h3>
|
||||
case 'h4':
|
||||
return <h4 key={i}>{serialize(node.children)}</h4>
|
||||
case 'h5':
|
||||
return <h5 key={i}>{serialize(node.children)}</h5>
|
||||
case 'h6':
|
||||
return <h6 key={i}>{serialize(node.children)}</h6>
|
||||
case 'li':
|
||||
return <li key={i}>{serialize(node.children)}</li>
|
||||
case 'link':
|
||||
return (
|
||||
<a href={escapeHTML(node.url)} key={i}>
|
||||
{serialize(node.children)}
|
||||
</a>
|
||||
)
|
||||
case 'ol':
|
||||
return <ol key={i}>{serialize(node.children)}</ol>
|
||||
case 'ul':
|
||||
return <ul key={i}>{serialize(node.children)}</ul>
|
||||
|
||||
default:
|
||||
return <p key={i}>{serialize(node.children)}</p>
|
||||
}
|
||||
})
|
||||
|
||||
export default serialize
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { Field } from 'payload/types'
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import deepMerge from '../utilities/deepMerge'
|
||||
|
||||
export const appearanceOptions = {
|
||||
default: {
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
primary: {
|
||||
label: 'Primary Button',
|
||||
value: 'primary',
|
||||
@@ -11,16 +15,12 @@ export const appearanceOptions = {
|
||||
label: 'Secondary Button',
|
||||
value: 'secondary',
|
||||
},
|
||||
default: {
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
}
|
||||
|
||||
export type LinkAppearances = 'primary' | 'secondary' | 'default'
|
||||
export type LinkAppearances = 'default' | 'primary' | 'secondary'
|
||||
|
||||
type LinkType = (options?: {
|
||||
appearances?: LinkAppearances[] | false
|
||||
appearances?: false | LinkAppearances[]
|
||||
disableLabel?: boolean
|
||||
overrides?: Record<string, unknown>
|
||||
}) => Field
|
||||
@@ -39,6 +39,11 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
|
||||
{
|
||||
name: 'type',
|
||||
type: 'radio',
|
||||
admin: {
|
||||
layout: 'horizontal',
|
||||
width: '50%',
|
||||
},
|
||||
defaultValue: 'reference',
|
||||
options: [
|
||||
{
|
||||
label: 'Internal link',
|
||||
@@ -49,22 +54,17 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
|
||||
value: 'custom',
|
||||
},
|
||||
],
|
||||
defaultValue: 'reference',
|
||||
admin: {
|
||||
layout: 'horizontal',
|
||||
width: '50%',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'newTab',
|
||||
label: 'Open in new tab',
|
||||
type: 'checkbox',
|
||||
admin: {
|
||||
width: '50%',
|
||||
style: {
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
width: '50%',
|
||||
},
|
||||
label: 'Open in new tab',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -74,29 +74,33 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
|
||||
const linkTypes: Field[] = [
|
||||
{
|
||||
name: 'reference',
|
||||
label: 'Document to link to',
|
||||
type: 'relationship',
|
||||
relationTo: ['pages'],
|
||||
required: true,
|
||||
maxDepth: 1,
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === 'reference',
|
||||
},
|
||||
label: 'Document to link to',
|
||||
maxDepth: 1,
|
||||
relationTo: ['pages'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
label: 'Custom URL',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === 'custom',
|
||||
},
|
||||
label: 'Custom URL',
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
|
||||
if (!disableLabel) {
|
||||
linkTypes[0].admin.width = '50%'
|
||||
linkTypes[1].admin.width = '50%'
|
||||
if (linkTypes[0].admin) {
|
||||
linkTypes[0].admin.width = '50%'
|
||||
}
|
||||
if (linkTypes[1].admin) {
|
||||
linkTypes[1].admin.width = '50%'
|
||||
}
|
||||
|
||||
linkResult.fields.push({
|
||||
type: 'row',
|
||||
@@ -104,12 +108,12 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
|
||||
...linkTypes,
|
||||
{
|
||||
name: 'label',
|
||||
label: 'Label',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: {
|
||||
width: '50%',
|
||||
},
|
||||
label: 'Label',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -131,11 +135,11 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
|
||||
linkResult.fields.push({
|
||||
name: 'appearance',
|
||||
type: 'select',
|
||||
defaultValue: 'default',
|
||||
options: appearanceOptionsToUse,
|
||||
admin: {
|
||||
description: 'Choose how the link should be rendered.',
|
||||
},
|
||||
defaultValue: 'default',
|
||||
options: appearanceOptionsToUse,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { RichTextElement, RichTextLeaf } from '@payloadcms/richtext-slate'
|
||||
import type { RichTextField } from 'payload'
|
||||
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
import type { RichTextElement, RichTextLeaf } from '@payloadcms/richtext-slate/dist/types'
|
||||
import type { RichTextField } from 'payload/types'
|
||||
|
||||
import deepMerge from '../../utilities/deepMerge'
|
||||
import link from '../link'
|
||||
@@ -16,7 +17,7 @@ type RichText = (
|
||||
) => RichTextField
|
||||
|
||||
const richText: RichText = (
|
||||
overrides,
|
||||
overrides = {},
|
||||
additions = {
|
||||
elements: [],
|
||||
leaves: [],
|
||||
@@ -26,27 +27,28 @@ const richText: RichText = (
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
required: true,
|
||||
editor: slateEditor({
|
||||
admin: {
|
||||
elements: [...elements, ...(additions.elements || [])],
|
||||
leaves: [...leaves, ...(additions.leaves || [])],
|
||||
upload: {
|
||||
collections: {
|
||||
media: {
|
||||
fields: [
|
||||
{
|
||||
type: 'richText',
|
||||
name: 'caption',
|
||||
label: 'Caption',
|
||||
type: 'richText',
|
||||
editor: slateEditor({
|
||||
admin: {
|
||||
elements: [...elements],
|
||||
leaves: [...leaves],
|
||||
},
|
||||
}),
|
||||
label: 'Caption',
|
||||
},
|
||||
{
|
||||
type: 'radio',
|
||||
name: 'alignment',
|
||||
type: 'radio',
|
||||
label: 'Alignment',
|
||||
options: [
|
||||
{
|
||||
@@ -73,7 +75,7 @@ const richText: RichText = (
|
||||
disableLabel: true,
|
||||
overrides: {
|
||||
admin: {
|
||||
condition: (_, data) => Boolean(data?.enableLink),
|
||||
condition: (_: any, data: { enableLink: any }) => Boolean(data?.enableLink),
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -81,10 +83,9 @@ const richText: RichText = (
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: [...elements, ...(additions.elements || [])],
|
||||
leaves: [...leaves, ...(additions.leaves || [])],
|
||||
},
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
overrides,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RichTextLeaf } from '@payloadcms/richtext-slate'
|
||||
import type { RichTextLeaf } from '@payloadcms/richtext-slate'
|
||||
|
||||
const defaultLeaves: RichTextLeaf[] = ['bold', 'italic', 'underline']
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { GlobalAfterChangeHook } from 'payload'
|
||||
|
||||
import { revalidateTag } from 'next/cache'
|
||||
|
||||
export const revalidateMainMenu: GlobalAfterChangeHook = ({ doc, req, req: { payload } }) => {
|
||||
payload.logger.info(`Revalidating main menu`)
|
||||
|
||||
if (req.context.skipRevalidate) {
|
||||
return doc
|
||||
}
|
||||
|
||||
revalidateTag('global_main-menu')
|
||||
|
||||
return doc
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GlobalConfig } from 'payload/types'
|
||||
import type { GlobalConfig } from 'payload'
|
||||
|
||||
import link from '../fields/link'
|
||||
import link from '../../fields/link'
|
||||
import { revalidateMainMenu } from './hooks/revalidateMainMenu'
|
||||
|
||||
export const MainMenu: GlobalConfig = {
|
||||
slug: 'main-menu',
|
||||
@@ -11,12 +12,15 @@ export const MainMenu: GlobalConfig = {
|
||||
{
|
||||
name: 'navItems',
|
||||
type: 'array',
|
||||
maxRows: 6,
|
||||
fields: [
|
||||
link({
|
||||
appearances: false,
|
||||
}),
|
||||
],
|
||||
maxRows: 6,
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
afterChange: [revalidateMainMenu],
|
||||
},
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Payload } from 'payload'
|
||||
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
|
||||
|
||||
import { home } from './home'
|
||||
import { examplePage } from './page'
|
||||
import { examplePageDraft } from './pageDraft'
|
||||
import { home } from '../seed/home'
|
||||
import { examplePage } from '../seed/page'
|
||||
import { examplePageDraft } from '../seed/pageDraft'
|
||||
|
||||
export const seed = async (payload: Payload): Promise<void> => {
|
||||
export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
@@ -15,54 +15,66 @@ export const seed = async (payload: Payload): Promise<void> => {
|
||||
|
||||
const { id: examplePageID } = await payload.create({
|
||||
collection: 'pages',
|
||||
context: {
|
||||
skipRevalidate: true,
|
||||
},
|
||||
data: examplePage as any, // eslint-disable-line
|
||||
})
|
||||
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: examplePageID,
|
||||
draft: true,
|
||||
collection: 'pages',
|
||||
context: {
|
||||
skipRevalidate: true,
|
||||
},
|
||||
data: examplePageDraft as any, // eslint-disable-line
|
||||
draft: true,
|
||||
})
|
||||
|
||||
const homepageJSON = JSON.parse(JSON.stringify(home).replace('{{DRAFT_PAGE_ID}}', examplePageID))
|
||||
|
||||
const { id: homePageID } = await payload.create({
|
||||
collection: 'pages',
|
||||
context: {
|
||||
skipRevalidate: true,
|
||||
},
|
||||
data: homepageJSON,
|
||||
})
|
||||
|
||||
await payload.updateGlobal({
|
||||
slug: 'main-menu',
|
||||
context: {
|
||||
skipRevalidate: true,
|
||||
},
|
||||
data: {
|
||||
navItems: [
|
||||
{
|
||||
link: {
|
||||
type: 'reference',
|
||||
label: 'Home',
|
||||
reference: {
|
||||
relationTo: 'pages',
|
||||
value: homePageID,
|
||||
},
|
||||
label: 'Home',
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
link: {
|
||||
type: 'reference',
|
||||
label: 'Example Page',
|
||||
reference: {
|
||||
relationTo: 'pages',
|
||||
value: examplePageID,
|
||||
},
|
||||
label: 'Example Page',
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
link: {
|
||||
type: 'custom',
|
||||
reference: null,
|
||||
label: 'Dashboard',
|
||||
reference: undefined,
|
||||
url: 'http://localhost:3000/admin',
|
||||
},
|
||||
},
|
||||
@@ -7,95 +7,264 @@
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
collections: {
|
||||
pages: Page
|
||||
users: User
|
||||
'payload-preferences': PayloadPreference
|
||||
'payload-migrations': PayloadMigration
|
||||
}
|
||||
pages: Page;
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
pages: PagesSelect<false> | PagesSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {
|
||||
'main-menu': MainMenu
|
||||
}
|
||||
'main-menu': MainMenu;
|
||||
};
|
||||
globalsSelect: {
|
||||
'main-menu': MainMenuSelect<false> | MainMenuSelect<true>;
|
||||
};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: string
|
||||
title: string
|
||||
slug?: string
|
||||
id: string;
|
||||
title: string;
|
||||
slug?: string | null;
|
||||
richText: {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
_status?: 'draft' | 'published'
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
email: string
|
||||
resetPasswordToken?: string
|
||||
resetPasswordExpiration?: string
|
||||
salt?: string
|
||||
hash?: string
|
||||
loginAttempts?: number
|
||||
lockUntil?: string
|
||||
password?: string
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
export interface PayloadPreference {
|
||||
id: string
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users'
|
||||
value: string | User
|
||||
}
|
||||
key?: string
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string
|
||||
name?: string
|
||||
batch?: number
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages_select".
|
||||
*/
|
||||
export interface PagesSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
slug?: T;
|
||||
richText?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "main-menu".
|
||||
*/
|
||||
export interface MainMenu {
|
||||
id: string
|
||||
navItems?: {
|
||||
link: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
label: string
|
||||
}
|
||||
id?: string
|
||||
}[]
|
||||
updatedAt?: string
|
||||
createdAt?: string
|
||||
id: string;
|
||||
navItems?:
|
||||
| {
|
||||
link: {
|
||||
type?: ('reference' | 'custom') | null;
|
||||
newTab?: boolean | null;
|
||||
reference?: {
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
} | null;
|
||||
url?: string | null;
|
||||
label: string;
|
||||
};
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "main-menu_select".
|
||||
*/
|
||||
export interface MainMenuSelect<T extends boolean = true> {
|
||||
navItems?:
|
||||
| T
|
||||
| {
|
||||
link?:
|
||||
| T
|
||||
| {
|
||||
type?: T;
|
||||
newTab?: T;
|
||||
reference?: T;
|
||||
url?: T;
|
||||
label?: T;
|
||||
};
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
export interface GeneratedTypes {
|
||||
collections: {
|
||||
pages: Page
|
||||
users: User
|
||||
'payload-preferences': PayloadPreference
|
||||
'payload-migrations': PayloadMigration
|
||||
}
|
||||
globals: {
|
||||
'main-menu': MainMenu
|
||||
}
|
||||
}
|
||||
}
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
@@ -1,37 +1,42 @@
|
||||
import { webpackBundler } from '@payloadcms/bundler-webpack'
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
import { Pages } from './collections/Pages'
|
||||
import { Users } from './collections/Users'
|
||||
import BeforeLogin from './components/BeforeLogin'
|
||||
import { MainMenu } from './globals/MainMenu'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default buildConfig({
|
||||
collections: [Pages, Users],
|
||||
admin: {
|
||||
bundler: webpackBundler(),
|
||||
components: {
|
||||
beforeLogin: [BeforeLogin],
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
editor: slateEditor({}),
|
||||
db: mongooseAdapter({
|
||||
url: process.env.DATABASE_URI,
|
||||
}),
|
||||
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
|
||||
collections: [Pages, Users],
|
||||
cors: [
|
||||
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
|
||||
process.env.NEXT_PUBLIC_SERVER_URL || '',
|
||||
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
|
||||
].filter(Boolean),
|
||||
csrf: [
|
||||
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
|
||||
process.env.NEXT_PUBLIC_SERVER_URL || '',
|
||||
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
|
||||
].filter(Boolean),
|
||||
db: mongooseAdapter({
|
||||
url: process.env.DATABASE_URI || '',
|
||||
}),
|
||||
editor: slateEditor({}),
|
||||
globals: [MainMenu],
|
||||
graphQL: {
|
||||
schemaOutputFile: path.resolve(dirname, 'generated-schema.graphql'),
|
||||
},
|
||||
secret: process.env.PAYLOAD_SECRET || '',
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, 'payload-types.ts'),
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Page } from '../payload-types'
|
||||
|
||||
export const home: Partial<Page> = {
|
||||
// Used for pre-seeded content so that the homepage is not empty
|
||||
// @ts-expect-error: Page type is not fully compatible with the provided object structure
|
||||
export const home: Page = {
|
||||
slug: 'home',
|
||||
_status: 'published',
|
||||
richText: [
|
||||
@@ -59,7 +61,7 @@ export const home: Partial<Page> = {
|
||||
type: 'link',
|
||||
children: [{ text: 'example page' }],
|
||||
linkType: 'custom',
|
||||
url: 'http://localhost:3001/example-page',
|
||||
url: 'http://localhost:3000/example-page',
|
||||
},
|
||||
{ text: ' to see how we control access to draft content. ' },
|
||||
],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Page } from '../payload-types'
|
||||
|
||||
export const examplePage: Partial<Page> = {
|
||||
title: 'Example Page (Published)',
|
||||
slug: 'example-page',
|
||||
_status: 'published',
|
||||
richText: [
|
||||
@@ -11,18 +10,18 @@ export const examplePage: Partial<Page> = {
|
||||
text: 'This is an example page with two versions, draft and published. You are currently seeing ',
|
||||
},
|
||||
{
|
||||
text: 'published',
|
||||
bold: true,
|
||||
text: 'published',
|
||||
},
|
||||
{
|
||||
text: ' content because you are not in preview mode. ',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
url: 'http://localhost:3000/admin',
|
||||
newTab: true,
|
||||
children: [{ text: 'Log in to the admin panel' }],
|
||||
linkType: 'custom',
|
||||
newTab: true,
|
||||
url: 'http://localhost:3000/admin',
|
||||
},
|
||||
{
|
||||
text: ' and click "preview" to return to this page and view the latest draft content in Next.js preview mode. To make additional changes to the draft, click "save draft" before returning to the preview.',
|
||||
@@ -30,4 +29,5 @@ export const examplePage: Partial<Page> = {
|
||||
],
|
||||
},
|
||||
],
|
||||
title: 'Example Page (Published)',
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Page } from '../payload-types'
|
||||
|
||||
export const examplePageDraft: Partial<Page> = {
|
||||
title: 'Example Page (Draft)',
|
||||
richText: [
|
||||
{
|
||||
children: [
|
||||
@@ -9,18 +8,18 @@ export const examplePageDraft: Partial<Page> = {
|
||||
text: 'This page is an example page with two versions, draft and published. You are currently seeing ',
|
||||
},
|
||||
{
|
||||
text: 'draft',
|
||||
bold: true,
|
||||
text: 'draft',
|
||||
},
|
||||
{
|
||||
text: ' content because you in preview mode. ',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
url: 'http://localhost:3000/admin/logout',
|
||||
newTab: true,
|
||||
children: [{ text: 'Log out' }],
|
||||
linkType: 'custom',
|
||||
newTab: true,
|
||||
url: 'http://localhost:3000/admin/logout',
|
||||
},
|
||||
{
|
||||
text: ' or click "exit preview mode" from the Payload Admin Bar to see the latest published content. To make additional changes to the draft, click "save draft" before returning to the preview.',
|
||||
@@ -28,4 +27,5 @@ export const examplePageDraft: Partial<Page> = {
|
||||
],
|
||||
},
|
||||
],
|
||||
title: 'Example Page (Draft)',
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
|
||||
dotenv.config({
|
||||
path: path.resolve(__dirname, '../.env'),
|
||||
})
|
||||
|
||||
import express from 'express'
|
||||
import payload from 'payload'
|
||||
|
||||
import { seed } from './seed'
|
||||
|
||||
const app = express()
|
||||
|
||||
app.get('/', (_, res) => {
|
||||
res.redirect('/admin')
|
||||
})
|
||||
|
||||
const start = async (): Promise<void> => {
|
||||
await payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
express: app,
|
||||
onInit: () => {
|
||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
|
||||
},
|
||||
})
|
||||
|
||||
if (process.env.PAYLOAD_PUBLIC_SEED === 'true') {
|
||||
payload.logger.info('---- SEEDING DATABASE ----')
|
||||
await seed(payload)
|
||||
}
|
||||
|
||||
app.listen(3000)
|
||||
}
|
||||
|
||||
start()
|
||||
@@ -14,6 +14,7 @@ export function isObject(item: unknown): boolean {
|
||||
* @param target
|
||||
* @param ...sources
|
||||
*/
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default function deepMerge<T, R>(target: T, source: R): T {
|
||||
const output = { ...target }
|
||||
if (isObject(target) && isObject(source)) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { CollectionSlug } from 'payload'
|
||||
|
||||
const collectionPrefixMap: Partial<Record<CollectionSlug, string>> = {
|
||||
pages: '',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
collection: keyof typeof collectionPrefixMap
|
||||
slug: string
|
||||
}
|
||||
|
||||
export const generatePreviewPath = ({ slug, collection }: Props) => {
|
||||
const path = `${collectionPrefixMap[collection]}/${slug}`
|
||||
|
||||
const params = {
|
||||
slug,
|
||||
collection,
|
||||
path,
|
||||
}
|
||||
|
||||
const encodedParams = new URLSearchParams()
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
encodedParams.append(key, value)
|
||||
})
|
||||
|
||||
return `/next/preview?${encodedParams.toString()}`
|
||||
}
|
||||
27
examples/draft-preview/payload/src/utilities/getGlobals.ts
Normal file
27
examples/draft-preview/payload/src/utilities/getGlobals.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { Config } from 'src/payload-types'
|
||||
|
||||
import { unstable_cache } from 'next/cache'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
import configPromise from '../payload.config'
|
||||
|
||||
type Global = keyof Config['globals']
|
||||
|
||||
async function getGlobal(slug: Global, depth = 0) {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const global = await payload.findGlobal({
|
||||
slug,
|
||||
depth,
|
||||
})
|
||||
|
||||
return global
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unstable_cache function mapped with the cache tag for the slug
|
||||
*/
|
||||
export const getCachedGlobal = (slug: Global, depth = 0) =>
|
||||
unstable_cache(async () => getGlobal(slug, depth), [slug], {
|
||||
tags: [`global_${slug}`],
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"esModuleInterop": true,
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"DOM",
|
||||
@@ -7,27 +9,43 @@
|
||||
"ES2022"
|
||||
],
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"strict": false,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"jsx": "preserve",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"isolatedModules": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"node_modules/*": ["./node_modules/*"]
|
||||
},
|
||||
"@payload-config": [
|
||||
"./src/payload.config.ts"
|
||||
],
|
||||
"react": [
|
||||
"./node_modules/@types/react"
|
||||
],
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"redirects.js",
|
||||
"next.config.mjs"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
],
|
||||
"ts-node": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user