chore(examples): migrates draft-preview example to 3.0 (#9362)

This commit is contained in:
Patrik
2024-11-25 22:13:15 -05:00
committed by GitHub
parent 82145f7bb0
commit d0af8e8d06
101 changed files with 14513 additions and 13511 deletions

View File

@@ -13,9 +13,15 @@ First you'll need a running Payload app. There is one made explicitly for this e
### Next.js ### Next.js
1. Clone this repo 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 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 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. 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.

View File

@@ -1,7 +1,7 @@
import { draftMode } from 'next/headers' import { draftMode } from 'next/headers'
import { notFound } from 'next/navigation' import { notFound } from 'next/navigation'
import { Page } from '../../payload-types' import type { Page as PageType } from '../../payload-types'
import { fetchPage } from '../_api/fetchPage' import { fetchPage } from '../_api/fetchPage'
import { fetchPages } from '../_api/fetchPages' import { fetchPages } from '../_api/fetchPages'
import { Gutter } from '../_components/Gutter' import { Gutter } from '../_components/Gutter'
@@ -10,10 +10,12 @@ import RichText from '../_components/RichText'
import classes from './index.module.scss' import classes from './index.module.scss'
interface PageParams { 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}> <main className={classes.page}>
<Gutter> <Gutter>
<h1>{page?.title}</h1> <h1>{page?.title}</h1>
@@ -22,8 +24,10 @@ export const PageTemplate: React.FC<{ page: Page | null | undefined }> = ({ page
</main> </main>
) )
export default async function Page({ params: { slug = 'home' } }: PageParams) { export default async function Page({ params }: PageParams) {
const { isEnabled: isDraftMode } = draftMode() const { slug = 'home' } = await params
const { isEnabled: isDraftMode } = await draftMode()
const page = await fetchPage(slug, isDraftMode) const page = await fetchPage(slug, isDraftMode)

View File

@@ -5,12 +5,12 @@ import type { Page } from '../../payload-types'
export const fetchPage = async ( export const fetchPage = async (
slug: string, slug: string,
draft?: boolean, draft?: boolean,
): Promise<Page | undefined | null> => { ): Promise<null | Page | undefined> => {
let payloadToken: RequestCookie | undefined let payloadToken: RequestCookie | undefined
if (draft) { if (draft) {
const { cookies } = await import('next/headers') const { cookies } = await import('next/headers')
payloadToken = cookies().get('payload-token') payloadToken = (await cookies()).get('payload-token')
} }
const pageRes: { const pageRes: {

View File

@@ -1,7 +1,8 @@
'use client' 'use client'
import React, { useState } from 'react' 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' import { Gutter } from '../Gutter'
@@ -17,24 +18,24 @@ export const AdminBarClient: React.FC<PayloadAdminBarProps> = (props) => {
<Gutter className={classes.container}> <Gutter className={classes.container}>
<PayloadAdminBar <PayloadAdminBar
{...props} {...props}
logo={<Title />} className={classes.payloadAdminBar}
classNames={{
controls: classes.controls,
logo: classes.logo,
user: classes.user,
}}
cmsURL={process.env.NEXT_PUBLIC_PAYLOAD_URL} cmsURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
logo={<Title />}
onAuthChange={setUser}
onPreviewExit={async () => { onPreviewExit={async () => {
await fetch(`/api/exit-preview`) await fetch(`/api/exit-preview`)
window.location.reload() window.location.reload()
}} }}
onAuthChange={setUser}
className={classes.payloadAdminBar}
classNames={{
user: classes.user,
logo: classes.logo,
controls: classes.controls,
}}
style={{ style={{
backgroundColor: 'transparent',
padding: 0,
position: 'relative', position: 'relative',
zIndex: 'unset', zIndex: 'unset',
padding: 0,
backgroundColor: 'transparent',
}} }}
/> />
</Gutter> </Gutter>

View File

@@ -3,14 +3,14 @@ import { draftMode } from 'next/headers'
import { AdminBarClient } from './index.client' import { AdminBarClient } from './index.client'
export function AdminBar() { export async function AdminBar() {
const { isEnabled: isPreviewMode } = draftMode() const { isEnabled: isPreviewMode } = await draftMode()
return ( return (
<AdminBarClient <AdminBarClient
preview={isPreviewMode}
// id={page?.id} // TODO: is there any way to do this?! // id={page?.id} // TODO: is there any way to do this?!
collection="pages" collection="pages"
preview={isPreviewMode}
/> />
) )
} }

View File

@@ -1,33 +1,34 @@
import React, { ElementType } from 'react' import type { ElementType } from 'react'
import React from 'react'
import Link from 'next/link' import Link from 'next/link'
import classes from './index.module.scss' import classes from './index.module.scss'
export type Props = { export type Props = {
label?: string
appearance?: 'default' | 'primary' | 'secondary' appearance?: 'default' | 'primary' | 'secondary'
el?: 'button' | 'link' | 'a'
onClick?: () => void
href?: string
newTab?: boolean
className?: string className?: string
type?: 'submit' | 'button'
disabled?: boolean disabled?: boolean
el?: 'a' | 'button' | 'link'
href?: string
label?: string
newTab?: boolean | null
onClick?: () => void
type?: 'button' | 'submit'
} }
export const Button: React.FC<Props> = ({ export const Button: React.FC<Props> = ({
el: elFromProps = 'link', type = 'button',
label,
newTab,
href,
appearance, appearance,
className: classNameFromProps, className: classNameFromProps,
onClick,
type = 'button',
disabled, disabled,
el: elFromProps = 'link',
href,
label,
newTab,
onClick,
}) => { }) => {
let el = elFromProps let el = elFromProps
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {} const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
const className = [ const className = [
classes.button, classes.button,
classNameFromProps, classNameFromProps,
@@ -44,11 +45,13 @@ export const Button: React.FC<Props> = ({
</div> </div>
) )
if (onClick || type === 'submit') el = 'button' if (onClick || type === 'submit') {
el = 'button'
}
if (el === 'link') { if (el === 'link') {
return ( return (
<Link href={href || ''} className={className} {...newTabProps} onClick={onClick}> <Link className={className} href={href || ''} {...newTabProps} onClick={onClick}>
{content} {content}
</Link> </Link>
) )
@@ -58,12 +61,12 @@ export const Button: React.FC<Props> = ({
return ( return (
<Element <Element
href={href}
className={className} className={className}
href={href}
type={type} type={type}
{...newTabProps} {...newTabProps}
onClick={onClick}
disabled={disabled} disabled={disabled}
onClick={onClick}
> >
{content} {content}
</Element> </Element>

View File

@@ -1,46 +1,52 @@
import React from 'react' import React from 'react'
import Link from 'next/link' import Link from 'next/link'
import { Page } from '../../../payload-types' import type { Page } from '../../../payload-types'
import { Button } from '../Button' import { Button } from '../Button'
export type CMSLinkType = { export type CMSLinkType = {
type?: 'custom' | 'reference'
url?: string
newTab?: boolean
reference?: {
value: string | Page
relationTo: 'pages'
}
label?: string
appearance?: 'default' | 'primary' | 'secondary' appearance?: 'default' | 'primary' | 'secondary'
children?: React.ReactNode children?: React.ReactNode
className?: string 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> = ({ export const CMSLink: React.FC<CMSLinkType> = ({
type, type,
url,
newTab,
reference,
label,
appearance, appearance,
children, children,
className, className,
label,
newTab,
reference,
url,
}) => { }) => {
const href = const href =
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug 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 : url
if (!href) {
return null
}
if (!appearance) { if (!appearance) {
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {} const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
if (type === 'custom') { if (type === 'custom') {
return ( return (
<a href={url} {...newTabProps} className={className}> <a href={url || ''} {...newTabProps} className={className}>
{label && label} {label && label}
{children && children} {children ? <>{children}</> : null}
</a> </a>
) )
} }
@@ -49,17 +55,17 @@ export const CMSLink: React.FC<CMSLinkType> = ({
return ( return (
<Link href={href} {...newTabProps} className={className} prefetch={false}> <Link href={href} {...newTabProps} className={className} prefetch={false}>
{label && label} {label && label}
{children && children} {children ? <>{children}</> : null}
</Link> </Link>
) )
} }
} }
const buttonProps = { const buttonProps = {
newTab,
href,
appearance, appearance,
href,
label, label,
newTab,
} }
return <Button className={className} {...buttonProps} el="link" /> return <Button className={className} {...buttonProps} el="link" />

View File

@@ -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' import classes from './index.module.scss'
type Props = { type Props = {
left?: boolean
right?: boolean
className?: string
children: React.ReactNode children: React.ReactNode
className?: string
left?: boolean
ref?: Ref<HTMLDivElement> ref?: Ref<HTMLDivElement>
right?: boolean
} }
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => { 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 ( return (
<div <div
ref={ref}
className={[ className={[
classes.gutter, classes.gutter,
left && classes.gutterLeft, left && classes.gutterLeft,
@@ -24,6 +24,7 @@ export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props,
] ]
.filter(Boolean) .filter(Boolean)
.join(' ')} .join(' ')}
ref={ref}
> >
{children} {children}
</div> </div>

View File

@@ -1,11 +1,11 @@
import React from 'react'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import React from 'react'
import type { MainMenu } from '../../../payload-types' import type { MainMenu } from '../../../payload-types'
import { CMSLink } from '../CMSLink' import { CMSLink } from '../CMSLink'
import { Gutter } from '../Gutter' import { Gutter } from '../Gutter'
import classes from './index.module.scss' import classes from './index.module.scss'
export async function Header() { export async function Header() {

View File

@@ -6,14 +6,14 @@ import { Text } from 'slate'
type Children = Leaf[] type Children = Leaf[]
type Leaf = { type Leaf = {
type: string
value?: {
url: string
alt: string
}
children: Children
url?: string
[key: string]: unknown [key: string]: unknown
children: Children
type: string
url?: string
value?: {
alt: string
url: string
}
} }
const serialize = (children: Children): React.ReactNode[] => const serialize = (children: Children): React.ReactNode[] =>
@@ -35,7 +35,7 @@ const serialize = (children: Children): React.ReactNode[] =>
if (node.underline) { if (node.underline) {
text = ( text = (
<span style={{ textDecoration: 'underline' }} key={i}> <span key={i} style={{ textDecoration: 'underline' }}>
{text} {text}
</span> </span>
) )
@@ -43,7 +43,7 @@ const serialize = (children: Children): React.ReactNode[] =>
if (node.strikethrough) { if (node.strikethrough) {
text = ( text = (
<span style={{ textDecoration: 'line-through' }} key={i}> <span key={i} style={{ textDecoration: 'line-through' }}>
{text} {text}
</span> </span>
) )
@@ -57,6 +57,8 @@ const serialize = (children: Children): React.ReactNode[] =>
} }
switch (node.type) { switch (node.type) {
case 'blockquote':
return <blockquote key={i}>{serialize(node.children)}</blockquote>
case 'h1': case 'h1':
return <h1 key={i}>{serialize(node.children)}</h1> return <h1 key={i}>{serialize(node.children)}</h1>
case 'h2': case 'h2':
@@ -69,12 +71,6 @@ const serialize = (children: Children): React.ReactNode[] =>
return <h5 key={i}>{serialize(node.children)}</h5> return <h5 key={i}>{serialize(node.children)}</h5>
case 'h6': case 'h6':
return <h6 key={i}>{serialize(node.children)}</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': case 'li':
return <li key={i}>{serialize(node.children)}</li> return <li key={i}>{serialize(node.children)}</li>
case 'link': case 'link':
@@ -83,6 +79,10 @@ const serialize = (children: Children): React.ReactNode[] =>
{serialize(node.children)} {serialize(node.children)}
</a> </a>
) )
case 'ol':
return <ol key={i}>{serialize(node.children)}</ol>
case 'ul':
return <ul key={i}>{serialize(node.children)}</ul>
default: default:
return <p key={i}>{serialize(node.children)}</p> return <p key={i}>{serialize(node.children)}</p>

View File

@@ -1,6 +1,7 @@
import { draftMode } from 'next/headers' import { draftMode } from 'next/headers'
export async function GET(): Promise<Response> { export async function GET(): Promise<Response> {
draftMode().disable() const draft = await draftMode()
draft.disable()
return new Response('Draft mode is disabled') return new Response('Draft mode is disabled')
} }

View File

@@ -2,13 +2,13 @@ import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation' import { redirect } from 'next/navigation'
export async function GET( export async function GET(
req: Request & { req: {
cookies: { cookies: {
get: (name: string) => { get: (name: string) => {
value: string value: string
} }
} }
}, } & Request,
): Promise<Response> { ): Promise<Response> {
const payloadToken = req.cookies.get('payload-token')?.value const payloadToken = req.cookies.get('payload-token')?.value
const { searchParams } = new URL(req.url) const { searchParams } = new URL(req.url)
@@ -32,8 +32,10 @@ export async function GET(
const userRes = await userReq.json() const userRes = await userReq.json()
const draft = await draftMode()
if (!userReq.ok || !userRes?.user) { if (!userReq.ok || !userRes?.user) {
draftMode().disable() draft.disable()
return new Response('You are not allowed to preview this page', { status: 403 }) 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 }) return new Response('Invalid token', { status: 401 })
} }
draftMode().enable() draft.enable()
redirect(url) redirect(url)
} }

View File

@@ -5,19 +5,20 @@ import { NextResponse } from 'next/server'
// this endpoint will revalidate a page by tag or path // this endpoint will revalidate a page by tag or path
// this is to achieve on-demand revalidation of pages that use this data // this is to achieve on-demand revalidation of pages that use this data
// send either `collection` and `slug` or `revalidatePath` as query params // 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 collection = request.nextUrl.searchParams.get('collection')
const slug = request.nextUrl.searchParams.get('slug') const slug = request.nextUrl.searchParams.get('slug')
const path = request.nextUrl.searchParams.get('path') const path = request.nextUrl.searchParams.get('path')
const secret = request.nextUrl.searchParams.get('secret') const secret = request.nextUrl.searchParams.get('secret')
if (secret !== process.env.NEXT_PRIVATE_REVALIDATION_KEY) { 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') { if (typeof collection === 'string' && typeof slug === 'string') {
revalidateTag(`${collection}_${slug}`) 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 // 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 // - https://github.com/vercel/next.js/issues/49778#issuecomment-1547028830
if (typeof path === 'string') { if (typeof path === 'string') {
revalidatePath(path) 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 })
} }

View File

@@ -4,10 +4,11 @@ import { Header } from './_components/Header'
import './app.scss' import './app.scss'
export const metadata = { export const metadata = {
title: 'Create Next App',
description: 'Generated by 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 }) { export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props 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: 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 See my comment here: https://github.com/vercel/next.js/issues/42292#issuecomment-1622979777
*/} */}
{/* @ts-expect-error */}
<Header /> <Header />
{children} {children}
</body> </body>

View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited // 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.

View File

@@ -1,4 +1,8 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = {} const nextConfig = {
sassOptions: {
silenceDeprecations: ['legacy-js-api'],
},
}
module.exports = nextConfig module.exports = nextConfig

View File

@@ -3,29 +3,29 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3001",
"build": "next build", "build": "next build",
"start": "next start -p 3001", "dev": "next dev -p 3001",
"lint": "next lint" "lint": "next lint",
"start": "next start -p 3001"
}, },
"dependencies": { "dependencies": {
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"next": "^13.5.1", "next": "^15.0.0",
"payload-admin-bar": "^1.0.6", "payload-admin-bar": "^1.0.6",
"react": "18.2.0", "react": "19.0.0-rc-65a56d0e-20241020",
"react-dom": "18.2.0" "react-dom": "19.0.0-rc-65a56d0e-20241020"
}, },
"devDependencies": { "devDependencies": {
"@next/eslint-plugin-next": "^13.4.8", "@next/eslint-plugin-next": "^15.0.0",
"@payloadcms/eslint-config": "^0.0.2", "@payloadcms/eslint-config": "^0.0.2",
"@types/escape-html": "^1.0.2", "@types/escape-html": "^1.0.2",
"@types/node": "18.11.3", "@types/node": "18.11.3",
"@types/react": "^18.2.14", "@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "^18.2.6", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0", "@typescript-eslint/parser": "^5.51.0",
"eslint": "8.41.0", "eslint": "8.41.0",
"eslint-config-next": "13.4.3", "eslint-config-next": "15.0.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-filenames": "^1.3.2", "eslint-plugin-filenames": "^1.3.2",
"eslint-plugin-import": "2.25.4", "eslint-plugin-import": "2.25.4",
@@ -33,8 +33,8 @@
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"sass": "^1.62.1", "sass": "^1.81.0",
"slate": "^0.82.0", "slate": "^0.82.0",
"typescript": "^4.8.4" "typescript": "5.5.2"
} }
} }

View File

@@ -7,95 +7,259 @@
*/ */
export interface Config { export interface Config {
auth: {
users: UserAuthOperations;
};
collections: { collections: {
pages: Page pages: Page;
users: User users: User;
'payload-preferences': PayloadPreference 'payload-locked-documents': PayloadLockedDocument;
'payload-migrations': PayloadMigration '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: { 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 { export interface Page {
id: string id: string;
title: string title: string;
slug?: string slug?: string | null;
richText: { richText: {
[k: string]: unknown [k: string]: unknown;
}[] }[];
updatedAt: string updatedAt: string;
createdAt: string createdAt: string;
_status?: 'draft' | 'published' _status?: ('draft' | 'published') | null;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User { export interface User {
id: string id: string;
updatedAt: string updatedAt: string;
createdAt: string createdAt: string;
email: string email: string;
resetPasswordToken?: string resetPasswordToken?: string | null;
resetPasswordExpiration?: string resetPasswordExpiration?: string | null;
salt?: string salt?: string | null;
hash?: string hash?: string | null;
loginAttempts?: number loginAttempts?: number | null;
lockUntil?: string lockUntil?: string | null;
password?: string 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: { user: {
relationTo: 'users' relationTo: 'users';
value: string | User value: string | User;
} };
key?: string 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?: value?:
| { | {
[k: string]: unknown [k: string]: unknown;
} }
| unknown[] | unknown[]
| string | string
| number | number
| boolean | boolean
| null | null;
updatedAt: string updatedAt: string;
createdAt: string createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration { export interface PayloadMigration {
id: string id: string;
name?: string name?: string | null;
batch?: number batch?: number | null;
updatedAt: string updatedAt: string;
createdAt: 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 { export interface MainMenu {
id: string id: string;
navItems?: { navItems?:
| {
link: { link: {
type?: 'reference' | 'custom' type?: ('reference' | 'custom') | null;
newTab?: boolean newTab?: boolean | null;
reference: { reference?: {
relationTo: 'pages' relationTo: 'pages';
value: string | Page value: string | Page;
} } | null;
url: string url?: string | null;
label: string label: string;
} };
id?: string id?: string | null;
}[] }[]
updatedAt?: string | null;
createdAt?: string updatedAt?: string | null;
createdAt?: string | null;
} }
/**
declare module 'payload' { * This interface was referenced by `Config`'s JSON-Schema
export interface GeneratedTypes { * via the `definition` "main-menu_select".
collections: { */
pages: Page export interface MainMenuSelect<T extends boolean = true> {
users: User navItems?:
'payload-preferences': PayloadPreference | T
'payload-migrations': PayloadMigration | {
} link?:
globals: { | T
'main-menu': MainMenu | {
} 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;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,6 @@
"@/*": ["./src/*"] "@/*": ["./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"] "exclude": ["node_modules"]
} }

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,15 @@ First you'll need a running Payload app. There is one made explicitly for this e
### Next.js ### Next.js
1. Clone this repo 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 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 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. 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.

View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited // 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.

View File

@@ -1,9 +1,7 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, sassOptions: {
swcMinify: true, silenceDeprecations: ['legacy-js-api'],
images: {
domains: ['localhost', process.env.NEXT_PUBLIC_PAYLOAD_URL || ''].filter(Boolean),
}, },
} }

View File

@@ -3,29 +3,29 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3001",
"build": "next build", "build": "next build",
"start": "next start -p 3001", "dev": "next dev -p 3001",
"lint": "next lint" "lint": "next lint",
"start": "next start -p 3001"
}, },
"dependencies": { "dependencies": {
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"next": "^13.5.1", "next": "^15.0.0",
"payload-admin-bar": "^1.0.6", "payload-admin-bar": "^1.0.6",
"qs": "^6.11.0", "qs": "^6.11.0",
"react": "^18.2.0", "react": "19.0.0-rc-65a56d0e-20241020",
"react-cookie": "^4.1.1", "react-cookie": "^4.1.1",
"react-dom": "^18.2.0" "react-dom": "19.0.0-rc-65a56d0e-20241020"
}, },
"devDependencies": { "devDependencies": {
"@next/eslint-plugin-next": "^13.4.8", "@next/eslint-plugin-next": "^15.0.0",
"@payloadcms/eslint-config": "^0.0.2", "@payloadcms/eslint-config": "^0.0.2",
"@types/node": "18.11.3", "@types/node": "18.11.3",
"@types/react": "^18.2.14", "@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "^18.2.6", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0", "@typescript-eslint/parser": "^5.51.0",
"eslint": "8.25.0", "eslint": "8.41.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-filenames": "^1.3.2", "eslint-plugin-filenames": "^1.3.2",
"eslint-plugin-import": "2.25.4", "eslint-plugin-import": "2.25.4",
@@ -33,8 +33,8 @@
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"sass": "^1.55.0", "sass": "^1.81.0",
"slate": "^0.82.0", "slate": "^0.82.0",
"typescript": "4.8.4" "typescript": "5.5.2"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import React from 'react' 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' import { Gutter } from '../Gutter'
@@ -9,30 +10,30 @@ const Title: React.FC = () => <span>Dashboard</span>
export const AdminBar: React.FC<{ export const AdminBar: React.FC<{
adminBarProps?: PayloadAdminBarProps adminBarProps?: PayloadAdminBarProps
setUser?: (user: PayloadMeUser) => void
user?: PayloadMeUser user?: PayloadMeUser
setUser?: (user: PayloadMeUser) => void // eslint-disable-line no-unused-vars
}> = (props) => { }> = (props) => {
const { adminBarProps, user, setUser } = props const { adminBarProps, setUser, user } = props
return ( return (
<div className={[classes.adminBar, user && classes.show].filter(Boolean).join(' ')}> <div className={[classes.adminBar, user && classes.show].filter(Boolean).join(' ')}>
<Gutter className={classes.container}> <Gutter className={classes.container}>
<PayloadAdminBar <PayloadAdminBar
{...adminBarProps} {...adminBarProps}
logo={<Title />}
cmsURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
onAuthChange={setUser}
className={classes.payloadAdminBar} className={classes.payloadAdminBar}
classNames={{ classNames={{
user: classes.user,
logo: classes.logo,
controls: classes.controls, controls: classes.controls,
logo: classes.logo,
user: classes.user,
}} }}
cmsURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
logo={<Title />}
onAuthChange={setUser}
style={{ style={{
backgroundColor: 'transparent',
padding: 0,
position: 'relative', position: 'relative',
zIndex: 'unset', zIndex: 'unset',
padding: 0,
backgroundColor: 'transparent',
}} }}
/> />
</Gutter> </Gutter>

View File

@@ -1,33 +1,34 @@
import React, { ElementType } from 'react' import type { ElementType } from 'react'
import React from 'react'
import Link from 'next/link' import Link from 'next/link'
import classes from './index.module.scss' import classes from './index.module.scss'
export type Props = { export type Props = {
label: string
appearance?: 'default' | 'primary' | 'secondary' appearance?: 'default' | 'primary' | 'secondary'
el?: 'button' | 'link' | 'a'
onClick?: () => void
href?: string
newTab?: boolean
className?: string className?: string
type?: 'submit' | 'button'
disabled?: boolean disabled?: boolean
el?: 'a' | 'button' | 'link'
href?: string
label: string
newTab?: boolean
onClick?: () => void
type?: 'button' | 'submit'
} }
export const Button: React.FC<Props> = ({ export const Button: React.FC<Props> = ({
el: elFromProps = 'link', type = 'button',
label,
newTab,
href,
appearance, appearance,
className: classNameFromProps, className: classNameFromProps,
onClick,
type = 'button',
disabled, disabled,
el: elFromProps = 'link',
href,
label,
newTab,
onClick,
}) => { }) => {
let el = elFromProps let el = elFromProps
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {} const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
const className = [ const className = [
classes.button, classes.button,
classNameFromProps, classNameFromProps,
@@ -44,11 +45,13 @@ export const Button: React.FC<Props> = ({
</div> </div>
) )
if (onClick || type === 'submit') el = 'button' if (onClick || type === 'submit') {
el = 'button'
}
if (el === 'link') { if (el === 'link') {
return ( return (
<Link href={href} className={className} {...newTabProps} onClick={onClick}> <Link className={className} href={href} {...newTabProps} onClick={onClick}>
{content} {content}
</Link> </Link>
) )
@@ -58,12 +61,12 @@ export const Button: React.FC<Props> = ({
return ( return (
<Element <Element
href={href}
className={className} className={className}
href={href}
type={type} type={type}
{...newTabProps} {...newTabProps}
onClick={onClick}
disabled={disabled} disabled={disabled}
onClick={onClick}
> >
{content} {content}
</Element> </Element>

View File

@@ -1,32 +1,32 @@
import React from 'react' import React from 'react'
import Link from 'next/link' import Link from 'next/link'
import { Page } from '../../payload-types' import type { Page } from '../../payload-types'
import { Button } from '../Button' import { Button } from '../Button'
export type CMSLinkType = { export type CMSLinkType = {
type?: 'custom' | 'reference'
url?: string
newTab?: boolean
reference?: {
value: string | Page
relationTo: 'pages'
}
label?: string
appearance?: 'default' | 'primary' | 'secondary' appearance?: 'default' | 'primary' | 'secondary'
children?: React.ReactNode children?: React.ReactNode
className?: string className?: string
label?: string
newTab?: boolean
reference?: {
relationTo: 'pages'
value: Page | string
}
type?: 'custom' | 'reference'
url?: string
} }
export const CMSLink: React.FC<CMSLinkType> = ({ export const CMSLink: React.FC<CMSLinkType> = ({
type, type,
url,
newTab,
reference,
label,
appearance, appearance,
children, children,
className, className,
label,
newTab,
reference,
url,
}) => { }) => {
const href = const href =
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
@@ -34,7 +34,7 @@ export const CMSLink: React.FC<CMSLinkType> = ({
: url : url
if (!appearance) { if (!appearance) {
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {} const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
if (type === 'custom') { if (type === 'custom') {
return ( return (
@@ -56,10 +56,10 @@ export const CMSLink: React.FC<CMSLinkType> = ({
} }
const buttonProps = { const buttonProps = {
newTab,
href,
appearance, appearance,
href,
label, label,
newTab,
} }
return <Button className={className} {...buttonProps} el="link" /> return <Button className={className} {...buttonProps} el="link" />

View File

@@ -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' import classes from './index.module.scss'
type Props = { type Props = {
left?: boolean
right?: boolean
className?: string
children: React.ReactNode children: React.ReactNode
className?: string
left?: boolean
ref?: Ref<HTMLDivElement> ref?: Ref<HTMLDivElement>
right?: boolean
} }
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => { 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 ( return (
<div <div
ref={ref}
className={[ className={[
classes.gutter, classes.gutter,
left && classes.gutterLeft, left && classes.gutterLeft,
@@ -24,6 +24,7 @@ export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props,
] ]
.filter(Boolean) .filter(Boolean)
.join(' ')} .join(' ')}
ref={ref}
> >
{children} {children}
</div> </div>

View File

@@ -1,14 +1,13 @@
import type { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar' import React, { useState } from 'react'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' 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 type { MainMenu } from '../../payload-types'
import { AdminBar } from '../AdminBar' import { AdminBar } from '../AdminBar'
import { CMSLink } from '../CMSLink' import { CMSLink } from '../CMSLink'
import { Gutter } from '../Gutter' import { Gutter } from '../Gutter'
import classes from './index.module.scss' import classes from './index.module.scss'
type HeaderBarProps = { type HeaderBarProps = {

View File

@@ -6,14 +6,14 @@ import { Text } from 'slate'
type Children = Leaf[] type Children = Leaf[]
type Leaf = { type Leaf = {
type: string
value?: {
url: string
alt: string
}
children?: Children
url?: string
[key: string]: unknown [key: string]: unknown
children?: Children
type: string
url?: string
value?: {
alt: string
url: string
}
} }
const serialize = (children: Children): React.ReactElement[] => const serialize = (children: Children): React.ReactElement[] =>
@@ -35,7 +35,7 @@ const serialize = (children: Children): React.ReactElement[] =>
if (node.underline) { if (node.underline) {
text = ( text = (
<span style={{ textDecoration: 'underline' }} key={i}> <span key={i} style={{ textDecoration: 'underline' }}>
{text} {text}
</span> </span>
) )
@@ -43,7 +43,7 @@ const serialize = (children: Children): React.ReactElement[] =>
if (node.strikethrough) { if (node.strikethrough) {
text = ( text = (
<span style={{ textDecoration: 'line-through' }} key={i}> <span key={i} style={{ textDecoration: 'line-through' }}>
{text} {text}
</span> </span>
) )
@@ -57,6 +57,8 @@ const serialize = (children: Children): React.ReactElement[] =>
} }
switch (node.type) { switch (node.type) {
case 'blockquote':
return <blockquote key={i}>{serialize(node.children)}</blockquote>
case 'h1': case 'h1':
return <h1 key={i}>{serialize(node.children)}</h1> return <h1 key={i}>{serialize(node.children)}</h1>
case 'h2': case 'h2':
@@ -69,12 +71,6 @@ const serialize = (children: Children): React.ReactElement[] =>
return <h5 key={i}>{serialize(node.children)}</h5> return <h5 key={i}>{serialize(node.children)}</h5>
case 'h6': case 'h6':
return <h6 key={i}>{serialize(node.children)}</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': case 'li':
return <li key={i}>{serialize(node.children)}</li> return <li key={i}>{serialize(node.children)}</li>
case 'link': case 'link':
@@ -83,6 +79,10 @@ const serialize = (children: Children): React.ReactElement[] =>
{serialize(node.children)} {serialize(node.children)}
</a> </a>
) )
case 'ol':
return <ol key={i}>{serialize(node.children)}</ol>
case 'ul':
return <ul key={i}>{serialize(node.children)}</ul>
default: default:
return <p key={i}>{serialize(node.children)}</p> return <p key={i}>{serialize(node.children)}</p>

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next' import type { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next'
import QueryString from 'qs' import QueryString from 'qs'
import { ParsedUrlQuery } from 'querystring' import type { ParsedUrlQuery } from 'querystring'
import { Gutter } from '../components/Gutter' import { Gutter } from '../components/Gutter'
import RichText from '../components/RichText' import RichText from '../components/RichText'
@@ -10,12 +10,12 @@ import type { MainMenu, Page as PageType } from '../payload-types'
import classes from './[slug].module.scss' import classes from './[slug].module.scss'
const Page: React.FC< const Page: React.FC<
PageType & { {
mainMenu: MainMenu mainMenu: MainMenu
preview?: boolean preview?: boolean
} } & PageType
> = (props) => { > = (props) => {
const { title, richText } = props const { richText, title } = props
return ( return (
<main className={classes.page}> <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 :) // when 'preview' cookies are set in the browser, getStaticProps runs on every request :)
export const getStaticProps: GetStaticProps = async (context: GetStaticPropsContext) => { export const getStaticProps: GetStaticProps = async (context: GetStaticPropsContext) => {
const { preview, previewData, params } = context const { params, preview, previewData } = context
const { payloadToken } = const { payloadToken } =
(previewData as { (previewData as {
@@ -43,7 +43,9 @@ export const getStaticProps: GetStaticProps = async (context: GetStaticPropsCont
}) || {} }) || {}
let { slug } = (params as IParams) || {} let { slug } = (params as IParams) || {}
if (!slug) slug = 'home' if (!slug) {
slug = 'home'
}
let doc = {} let doc = {}
const notFound = false const notFound = false
@@ -52,17 +54,17 @@ export const getStaticProps: GetStaticProps = async (context: GetStaticPropsCont
const searchParams = QueryString.stringify( const searchParams = QueryString.stringify(
{ {
depth: 1,
draft: preview ? true : undefined,
where: { where: {
slug: { slug: {
equals: lowerCaseSlug, equals: lowerCaseSlug,
}, },
}, },
depth: 1,
draft: preview ? true : undefined,
}, },
{ {
encode: false,
addQueryPrefix: true, addQueryPrefix: true,
encode: false,
}, },
) )
@@ -88,12 +90,12 @@ export const getStaticProps: GetStaticProps = async (context: GetStaticPropsCont
} }
return { return {
notFound,
props: { props: {
...doc, ...doc,
preview: preview || null,
collection: 'pages', collection: 'pages',
preview: preview || null,
}, },
notFound,
revalidate: 3600, // in seconds revalidate: 3600, // in seconds
} }
} }
@@ -124,7 +126,7 @@ export const getStaticPaths: GetStaticPaths = async () => {
} }
return { return {
paths,
fallback: true, fallback: true,
paths,
} }
} }

View File

@@ -1,10 +1,11 @@
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { CookiesProvider } from 'react-cookie' 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 { useRouter } from 'next/router'
import { Header } from '../components/Header' import { Header } from '../components/Header'
import { MainMenu } from '../payload-types' import type { MainMenu } from '../payload-types'
import './app.scss' import './app.scss'
@@ -26,13 +27,13 @@ type AppProps<P = any> = {
} & Omit<NextAppProps<P>, 'pageProps'> } & Omit<NextAppProps<P>, 'pageProps'>
const PayloadApp = ( const PayloadApp = (
appProps: AppProps & { appProps: {
globals: IGlobals globals: IGlobals
}, } & AppProps,
): React.ReactElement => { ): 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() const router = useRouter()
@@ -43,25 +44,27 @@ const PayloadApp = (
router.reload() router.reload()
} }
} }
exit() exit().catch((error) => {
// eslint-disable-next-line no-console
console.error('Failed to exit preview:', error)
})
}, [router]) }, [router])
return ( return (
<CookiesProvider> <CookiesProvider>
<Header <Header
globals={globals}
adminBarProps={{ adminBarProps={{
collection,
id, id,
preview, collection,
onPreviewExit, onPreviewExit,
preview,
}} }}
globals={globals}
/> />
{/* typescript flags this `@ts-expect-error` declaration as unneeded, but types are breaking the build process {/* typescript flags this `@ts-expect-error` declaration as unneeded, but types are breaking the build process
Remove these comments when the issue is resolved Remove these comments when the issue is resolved
See more here: https://github.com/facebook/react/issues/24304 See more here: https://github.com/facebook/react/issues/24304
*/} */}
{/* @ts-expect-error */}
<Component {...pageProps} /> <Component {...pageProps} />
</CookiesProvider> </CookiesProvider>
) )

View File

@@ -1,9 +1,10 @@
import { GetStaticProps } from 'next' import type { GetStaticProps } from 'next'
import Page, { getStaticProps as sharedGetStaticProps } from './[slug]' import Page, { getStaticProps as sharedGetStaticProps } from './[slug]'
export default Page export default Page
// eslint-disable-next-line @typescript-eslint/require-await
export const getStaticProps: GetStaticProps = async (ctx) => { export const getStaticProps: GetStaticProps = async (ctx) => {
const func = sharedGetStaticProps.bind(this) const func = sharedGetStaticProps.bind(this)
return func(ctx) return func(ctx)

View File

@@ -7,95 +7,259 @@
*/ */
export interface Config { export interface Config {
auth: {
users: UserAuthOperations;
};
collections: { collections: {
pages: Page pages: Page;
users: User users: User;
'payload-preferences': PayloadPreference 'payload-locked-documents': PayloadLockedDocument;
'payload-migrations': PayloadMigration '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: { 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 { export interface Page {
id: string id: string;
title: string title: string;
slug?: string slug?: string | null;
richText: { richText: {
[k: string]: unknown [k: string]: unknown;
}[] }[];
updatedAt: string updatedAt: string;
createdAt: string createdAt: string;
_status?: 'draft' | 'published' _status?: ('draft' | 'published') | null;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User { export interface User {
id: string id: string;
updatedAt: string updatedAt: string;
createdAt: string createdAt: string;
email: string email: string;
resetPasswordToken?: string resetPasswordToken?: string | null;
resetPasswordExpiration?: string resetPasswordExpiration?: string | null;
salt?: string salt?: string | null;
hash?: string hash?: string | null;
loginAttempts?: number loginAttempts?: number | null;
lockUntil?: string lockUntil?: string | null;
password?: string 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: { user: {
relationTo: 'users' relationTo: 'users';
value: string | User value: string | User;
} };
key?: string 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?: value?:
| { | {
[k: string]: unknown [k: string]: unknown;
} }
| unknown[] | unknown[]
| string | string
| number | number
| boolean | boolean
| null | null;
updatedAt: string updatedAt: string;
createdAt: string createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration { export interface PayloadMigration {
id: string id: string;
name?: string name?: string | null;
batch?: number batch?: number | null;
updatedAt: string updatedAt: string;
createdAt: 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 { export interface MainMenu {
id: string id: string;
navItems?: { navItems?:
| {
link: { link: {
type?: 'reference' | 'custom' type?: ('reference' | 'custom') | null;
newTab?: boolean newTab?: boolean | null;
reference: { reference?: {
relationTo: 'pages' relationTo: 'pages';
value: string | Page value: string | Page;
} } | null;
url: string url?: string | null;
label: string label: string;
} };
id?: string id?: string | null;
}[] }[]
updatedAt?: string | null;
createdAt?: string updatedAt?: string | null;
createdAt?: string | null;
} }
/**
declare module 'payload' { * This interface was referenced by `Config`'s JSON-Schema
export interface GeneratedTypes { * via the `definition` "main-menu_select".
collections: { */
pages: Page export interface MainMenuSelect<T extends boolean = true> {
users: User navItems?:
'payload-preferences': PayloadPreference | T
'payload-migrations': PayloadMigration | {
} link?:
globals: { | T
'main-menu': MainMenu | {
} 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;
} }

View File

@@ -22,7 +22,8 @@
"include": [ "include": [
"next-env.d.ts", "next-env.d.ts",
"**/*.ts", "**/*.ts",
"**/*.tsx" "**/*.tsx",
"next.config.js"
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules"

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,16 @@
DATABASE_URI=mongodb://127.0.0.1/payload-example-draft-preview # Database connection string
PAYLOAD_SECRET=ENTER-STRING-HERE DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
# 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_SITE_URL=http://localhost:3001
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
COOKIE_DOMAIN=localhost
REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
PAYLOAD_PUBLIC_SEED=true PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
PAYLOAD_DROP_DATABASE=true

View File

@@ -1,4 +1,8 @@
module.exports = { module.exports = {
extends: 'next',
root: true, root: true,
extends: ['@payloadcms'], parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
} }

View 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"
}
}

View File

@@ -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"
// ]
}
]
}

View File

@@ -9,10 +9,18 @@ Follow the instructions in each respective README to get started. If you are set
## Quick Start ## Quick Start
To spin up this example locally, follow these steps:
1. Clone this repo 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 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 5. `open http://localhost:3000/admin` to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo` 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. 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 ### Collections
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend any of this functionality. 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 ### 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. > "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 ### 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. > 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 ## 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. 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. Then run `yarn serve` or `npm run serve` to run Node in production and serve Payload from the `./build` directory. 1. Finally run `pnpm start` or `npm run start` to run Node in production and serve Payload from the `.build` directory.
### Deployment ### Deployment

View 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.

View 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)

View File

@@ -1,5 +0,0 @@
{
"ext": "ts",
"exec": "ts-node src/server.ts -- -I",
"stdin": false
}

View File

@@ -1,49 +1,57 @@
{ {
"name": "payload-example-preview", "name": "payload-example-preview",
"description": "Payload preview example.",
"version": "1.0.0", "version": "1.0.0",
"main": "dist/server.js", "description": "Payload preview example.",
"license": "MIT", "license": "MIT",
"main": "dist/server.js",
"scripts": { "scripts": {
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon", "build": "cross-env NODE_OPTIONS=--no-deprecation next build",
"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", "dev": "cross-env NODE_OPTIONS=--no-deprecation && pnpm seed && next dev",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build", "generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"build:server": "tsc", "generate:schema": "payload-graphql generate:schema",
"build": "yarn copyfiles && yarn build:payload && yarn build:server", "generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js", "payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/", "seed": "npm run payload migrate:fresh",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types", "start": "cross-env NODE_OPTIONS=--no-deprecation next start"
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
"lint": "eslint src",
"lint:fix": "eslint --fix --ext .ts,.tsx src"
}, },
"dependencies": { "dependencies": {
"@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "latest", "@payloadcms/db-mongodb": "latest",
"@payloadcms/next": "latest",
"@payloadcms/richtext-slate": "latest", "@payloadcms/richtext-slate": "latest",
"@payloadcms/ui": "latest",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "escape-html": "^1.0.3",
"payload": "latest" "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": { "devDependencies": {
"@payloadcms/eslint-config": "^0.0.2", "@payloadcms/graphql": "latest",
"@types/express": "^4.17.9", "@swc/core": "^1.6.13",
"@types/node": "18.11.3", "@types/escape-html": "^1.0.2",
"@types/react": "18.0.21", "@types/react": "npm:types-react@19.0.0-rc.1",
"@typescript-eslint/eslint-plugin": "^5.51.0", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@typescript-eslint/parser": "^5.51.0", "eslint": "^8.57.0",
"copyfiles": "^2.4.1", "eslint-config-next": "^15.0.0",
"cross-env": "^7.0.3", "slate": "^0.82.0",
"eslint": "^8.19.0", "tsx": "^4.16.2",
"eslint-config-prettier": "^8.5.0", "typescript": "5.5.2"
"eslint-plugin-filenames": "^1.3.2", },
"eslint-plugin-import": "2.25.4", "engines": {
"eslint-plugin-prettier": "^4.0.0", "node": "^18.20.2 || >=20.9.0"
"eslint-plugin-react-hooks": "^4.6.0", },
"eslint-plugin-simple-import-sort": "^10.0.0", "pnpm": {
"nodemon": "^2.0.6", "overrides": {
"prettier": "^2.7.1", "@types/react": "npm:types-react@19.0.0-rc.1",
"ts-node": "^9.1.1", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
"typescript": "^4.8.4" }
},
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
.page {
margin-top: calc(var(--base) * 2);
}

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

View 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;
}
}

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

View File

@@ -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')
}

View File

@@ -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')
}

View File

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

View File

@@ -0,0 +1,3 @@
import Page from './[slug]/page'
export default Page

View File

@@ -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

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View 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

View File

@@ -1,4 +1,4 @@
import type { Access } from 'payload/config' import type { Access } from 'payload'
export const loggedIn: Access = ({ req: { user } }) => { export const loggedIn: Access = ({ req: { user } }) => {
return Boolean(user) return Boolean(user)

View File

@@ -1,4 +1,4 @@
import type { Access } from 'payload/config' import type { Access } from 'payload'
export const publishedOrLoggedIn: Access = ({ req: { user } }) => { export const publishedOrLoggedIn: Access = ({ req: { user } }) => {
if (user) { if (user) {

View File

@@ -1,4 +1,4 @@
import type { FieldHook } from 'payload/types' import type { FieldHook } from 'payload'
const format = (val: string): string => const format = (val: string): string =>
val val
@@ -6,9 +6,9 @@ const format = (val: string): string =>
.replace(/[^\w-]+/g, '') .replace(/[^\w-]+/g, '')
.toLowerCase() .toLowerCase()
const formatSlug = export const formatSlug =
(fallback: string): FieldHook => (fallback: string): FieldHook =>
({ operation, value, originalDoc, data }) => { ({ data, operation, originalDoc, value }) => {
if (typeof value === 'string') { if (typeof value === 'string') {
return format(value) return format(value)
} }
@@ -23,5 +23,3 @@ const formatSlug =
return value return value
} }
export default formatSlug

View File

@@ -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' // ensure that the home page is revalidated at '/' instead of '/home'
export const formatAppURL = ({ doc }): string => { export const formatAppURL = ({ doc }): string => {
@@ -7,12 +11,14 @@ export const formatAppURL = ({ doc }): string => {
return pathname return pathname
} }
// revalidate the page in the background, so the user doesn't have to wait export const revalidatePage: CollectionAfterChangeHook<Page> = ({
// notice that the hook itself is not async and we are not awaiting `revalidate` doc,
// only revalidate existing docs that are published (not drafts) operation,
// send `revalidatePath`, `collection`, and `slug` to the frontend to use in its revalidate route previousDoc,
// frameworks may have different ways of doing this, but the idea is the same req,
export const revalidatePage: AfterChangeHook = ({ doc, req, operation }) => { }) => {
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') { if (operation === 'update' && doc._status === 'published') {
const url = formatAppURL({ doc }) const url = formatAppURL({ doc })
@@ -32,7 +38,25 @@ export const revalidatePage: AfterChangeHook = ({ doc, req, operation }) => {
} }
} }
revalidate() void revalidate()
}
} else {
// Revalidate internally with next/cache if your payload app is installed within /app folder
if (req.context.skipRevalidate) {
return doc
}
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 return doc

View File

@@ -1,35 +1,43 @@
import type { CollectionConfig } from 'payload/types' import type { CollectionConfig } from 'payload'
import richText from '../../fields/richText' import richText from '../../fields/richText'
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
import { loggedIn } from './access/loggedIn' import { loggedIn } from './access/loggedIn'
import { publishedOrLoggedIn } from './access/publishedOrLoggedIn' import { publishedOrLoggedIn } from './access/publishedOrLoggedIn'
import formatSlug from './hooks/formatSlug' import { formatSlug } from './hooks/formatSlug'
import { formatAppURL, revalidatePage } from './hooks/revalidatePage' import { formatAppURL, revalidatePage } from './hooks/revalidatePage'
export const Pages: CollectionConfig = { export const Pages: CollectionConfig = {
slug: 'pages', slug: 'pages',
access: {
create: loggedIn,
delete: loggedIn,
read: publishedOrLoggedIn,
update: loggedIn,
},
admin: { admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'slug', 'updatedAt'], defaultColumns: ['title', 'slug', 'updatedAt'],
preview: (doc) => { preview: (doc) => {
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( return `${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/preview?url=${encodeURIComponent(
formatAppURL({ formatAppURL({ doc }),
doc,
}),
)}&collection=pages&slug=${doc.slug}&secret=${process.env.PAYLOAD_PUBLIC_DRAFT_SECRET}` )}&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.',
)
}, },
}, useAsTitle: 'title',
versions: {
drafts: true,
},
access: {
read: publishedOrLoggedIn,
create: loggedIn,
update: loggedIn,
delete: loggedIn,
},
hooks: {
afterChange: [revalidatePage],
}, },
fields: [ fields: [
{ {
@@ -39,16 +47,22 @@ export const Pages: CollectionConfig = {
}, },
{ {
name: 'slug', name: 'slug',
label: 'Slug',
type: 'text', type: 'text',
index: true,
admin: { admin: {
position: 'sidebar', position: 'sidebar',
}, },
hooks: { hooks: {
beforeValidate: [formatSlug('title')], beforeValidate: [formatSlug('title')],
}, },
index: true,
label: 'Slug',
}, },
richText(), richText(),
], ],
hooks: {
afterChange: [revalidatePage],
},
versions: {
drafts: true,
},
} }

View File

@@ -1,17 +1,10 @@
import type { CollectionConfig } from 'payload/types' import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = { export const Users: CollectionConfig = {
slug: 'users', slug: 'users',
auth: {
tokenExpiration: 28800, // 8 hours
cookies: {
sameSite: 'none',
secure: true,
domain: process.env.COOKIE_DOMAIN,
},
},
admin: { admin: {
useAsTitle: 'email', useAsTitle: 'email',
}, },
auth: true,
fields: [], fields: [],
} }

View File

@@ -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;
}

View File

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

View File

@@ -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

View File

@@ -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;
}

View File

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

View File

@@ -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" />
}

View File

@@ -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);
}

View File

@@ -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'

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -0,0 +1,9 @@
.richText {
:first-child {
margin-top: 0;
}
a {
text-decoration: underline;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -1,8 +1,12 @@
import type { Field } from 'payload/types' import type { Field } from 'payload'
import deepMerge from '../utilities/deepMerge' import deepMerge from '../utilities/deepMerge'
export const appearanceOptions = { export const appearanceOptions = {
default: {
label: 'Default',
value: 'default',
},
primary: { primary: {
label: 'Primary Button', label: 'Primary Button',
value: 'primary', value: 'primary',
@@ -11,16 +15,12 @@ export const appearanceOptions = {
label: 'Secondary Button', label: 'Secondary Button',
value: 'secondary', value: 'secondary',
}, },
default: {
label: 'Default',
value: 'default',
},
} }
export type LinkAppearances = 'primary' | 'secondary' | 'default' export type LinkAppearances = 'default' | 'primary' | 'secondary'
type LinkType = (options?: { type LinkType = (options?: {
appearances?: LinkAppearances[] | false appearances?: false | LinkAppearances[]
disableLabel?: boolean disableLabel?: boolean
overrides?: Record<string, unknown> overrides?: Record<string, unknown>
}) => Field }) => Field
@@ -39,6 +39,11 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
{ {
name: 'type', name: 'type',
type: 'radio', type: 'radio',
admin: {
layout: 'horizontal',
width: '50%',
},
defaultValue: 'reference',
options: [ options: [
{ {
label: 'Internal link', label: 'Internal link',
@@ -49,22 +54,17 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
value: 'custom', value: 'custom',
}, },
], ],
defaultValue: 'reference',
admin: {
layout: 'horizontal',
width: '50%',
},
}, },
{ {
name: 'newTab', name: 'newTab',
label: 'Open in new tab',
type: 'checkbox', type: 'checkbox',
admin: { admin: {
width: '50%',
style: { style: {
alignSelf: 'flex-end', alignSelf: 'flex-end',
}, },
width: '50%',
}, },
label: 'Open in new tab',
}, },
], ],
}, },
@@ -74,29 +74,33 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
const linkTypes: Field[] = [ const linkTypes: Field[] = [
{ {
name: 'reference', name: 'reference',
label: 'Document to link to',
type: 'relationship', type: 'relationship',
relationTo: ['pages'],
required: true,
maxDepth: 1,
admin: { admin: {
condition: (_, siblingData) => siblingData?.type === 'reference', condition: (_, siblingData) => siblingData?.type === 'reference',
}, },
label: 'Document to link to',
maxDepth: 1,
relationTo: ['pages'],
required: true,
}, },
{ {
name: 'url', name: 'url',
label: 'Custom URL',
type: 'text', type: 'text',
required: true,
admin: { admin: {
condition: (_, siblingData) => siblingData?.type === 'custom', condition: (_, siblingData) => siblingData?.type === 'custom',
}, },
label: 'Custom URL',
required: true,
}, },
] ]
if (!disableLabel) { if (!disableLabel) {
if (linkTypes[0].admin) {
linkTypes[0].admin.width = '50%' linkTypes[0].admin.width = '50%'
}
if (linkTypes[1].admin) {
linkTypes[1].admin.width = '50%' linkTypes[1].admin.width = '50%'
}
linkResult.fields.push({ linkResult.fields.push({
type: 'row', type: 'row',
@@ -104,12 +108,12 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
...linkTypes, ...linkTypes,
{ {
name: 'label', name: 'label',
label: 'Label',
type: 'text', type: 'text',
required: true,
admin: { admin: {
width: '50%', width: '50%',
}, },
label: 'Label',
required: true,
}, },
], ],
}) })
@@ -131,11 +135,11 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
linkResult.fields.push({ linkResult.fields.push({
name: 'appearance', name: 'appearance',
type: 'select', type: 'select',
defaultValue: 'default',
options: appearanceOptionsToUse,
admin: { admin: {
description: 'Choose how the link should be rendered.', description: 'Choose how the link should be rendered.',
}, },
defaultValue: 'default',
options: appearanceOptionsToUse,
}) })
} }

View File

@@ -1,6 +1,7 @@
import type { RichTextElement, RichTextLeaf } from '@payloadcms/richtext-slate'
import type { RichTextField } from 'payload'
import { slateEditor } from '@payloadcms/richtext-slate' 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 deepMerge from '../../utilities/deepMerge'
import link from '../link' import link from '../link'
@@ -16,7 +17,7 @@ type RichText = (
) => RichTextField ) => RichTextField
const richText: RichText = ( const richText: RichText = (
overrides, overrides = {},
additions = { additions = {
elements: [], elements: [],
leaves: [], leaves: [],
@@ -26,27 +27,28 @@ const richText: RichText = (
{ {
name: 'richText', name: 'richText',
type: 'richText', type: 'richText',
required: true,
editor: slateEditor({ editor: slateEditor({
admin: { admin: {
elements: [...elements, ...(additions.elements || [])],
leaves: [...leaves, ...(additions.leaves || [])],
upload: { upload: {
collections: { collections: {
media: { media: {
fields: [ fields: [
{ {
type: 'richText',
name: 'caption', name: 'caption',
label: 'Caption', type: 'richText',
editor: slateEditor({ editor: slateEditor({
admin: { admin: {
elements: [...elements], elements: [...elements],
leaves: [...leaves], leaves: [...leaves],
}, },
}), }),
label: 'Caption',
}, },
{ {
type: 'radio',
name: 'alignment', name: 'alignment',
type: 'radio',
label: 'Alignment', label: 'Alignment',
options: [ options: [
{ {
@@ -73,7 +75,7 @@ const richText: RichText = (
disableLabel: true, disableLabel: true,
overrides: { overrides: {
admin: { 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, overrides,
) )

View File

@@ -1,4 +1,4 @@
import { RichTextLeaf } from '@payloadcms/richtext-slate' import type { RichTextLeaf } from '@payloadcms/richtext-slate'
const defaultLeaves: RichTextLeaf[] = ['bold', 'italic', 'underline'] const defaultLeaves: RichTextLeaf[] = ['bold', 'italic', 'underline']

View File

@@ -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
}

View File

@@ -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 = { export const MainMenu: GlobalConfig = {
slug: 'main-menu', slug: 'main-menu',
@@ -11,12 +12,15 @@ export const MainMenu: GlobalConfig = {
{ {
name: 'navItems', name: 'navItems',
type: 'array', type: 'array',
maxRows: 6,
fields: [ fields: [
link({ link({
appearances: false, appearances: false,
}), }),
], ],
maxRows: 6,
}, },
], ],
hooks: {
afterChange: [revalidateMainMenu],
},
} }

View File

@@ -1,10 +1,10 @@
import type { Payload } from 'payload' import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
import { home } from './home' import { home } from '../seed/home'
import { examplePage } from './page' import { examplePage } from '../seed/page'
import { examplePageDraft } from './pageDraft' import { examplePageDraft } from '../seed/pageDraft'
export const seed = async (payload: Payload): Promise<void> => { export async function up({ payload }: MigrateUpArgs): Promise<void> {
await payload.create({ await payload.create({
collection: 'users', collection: 'users',
data: { data: {
@@ -15,54 +15,66 @@ export const seed = async (payload: Payload): Promise<void> => {
const { id: examplePageID } = await payload.create({ const { id: examplePageID } = await payload.create({
collection: 'pages', collection: 'pages',
context: {
skipRevalidate: true,
},
data: examplePage as any, // eslint-disable-line data: examplePage as any, // eslint-disable-line
}) })
await payload.update({ await payload.update({
collection: 'pages',
id: examplePageID, id: examplePageID,
draft: true, collection: 'pages',
context: {
skipRevalidate: true,
},
data: examplePageDraft as any, // eslint-disable-line data: examplePageDraft as any, // eslint-disable-line
draft: true,
}) })
const homepageJSON = JSON.parse(JSON.stringify(home).replace('{{DRAFT_PAGE_ID}}', examplePageID)) const homepageJSON = JSON.parse(JSON.stringify(home).replace('{{DRAFT_PAGE_ID}}', examplePageID))
const { id: homePageID } = await payload.create({ const { id: homePageID } = await payload.create({
collection: 'pages', collection: 'pages',
context: {
skipRevalidate: true,
},
data: homepageJSON, data: homepageJSON,
}) })
await payload.updateGlobal({ await payload.updateGlobal({
slug: 'main-menu', slug: 'main-menu',
context: {
skipRevalidate: true,
},
data: { data: {
navItems: [ navItems: [
{ {
link: { link: {
type: 'reference', type: 'reference',
label: 'Home',
reference: { reference: {
relationTo: 'pages', relationTo: 'pages',
value: homePageID, value: homePageID,
}, },
label: 'Home',
url: '', url: '',
}, },
}, },
{ {
link: { link: {
type: 'reference', type: 'reference',
label: 'Example Page',
reference: { reference: {
relationTo: 'pages', relationTo: 'pages',
value: examplePageID, value: examplePageID,
}, },
label: 'Example Page',
url: '', url: '',
}, },
}, },
{ {
link: { link: {
type: 'custom', type: 'custom',
reference: null,
label: 'Dashboard', label: 'Dashboard',
reference: undefined,
url: 'http://localhost:3000/admin', url: 'http://localhost:3000/admin',
}, },
}, },

View File

@@ -7,95 +7,264 @@
*/ */
export interface Config { export interface Config {
auth: {
users: UserAuthOperations;
};
collections: { collections: {
pages: Page pages: Page;
users: User users: User;
'payload-preferences': PayloadPreference 'payload-locked-documents': PayloadLockedDocument;
'payload-migrations': PayloadMigration '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: { 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 { export interface Page {
id: string id: string;
title: string title: string;
slug?: string slug?: string | null;
richText: { richText: {
[k: string]: unknown [k: string]: unknown;
}[] }[];
updatedAt: string updatedAt: string;
createdAt: string createdAt: string;
_status?: 'draft' | 'published' _status?: ('draft' | 'published') | null;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User { export interface User {
id: string id: string;
updatedAt: string updatedAt: string;
createdAt: string createdAt: string;
email: string email: string;
resetPasswordToken?: string resetPasswordToken?: string | null;
resetPasswordExpiration?: string resetPasswordExpiration?: string | null;
salt?: string salt?: string | null;
hash?: string hash?: string | null;
loginAttempts?: number loginAttempts?: number | null;
lockUntil?: string lockUntil?: string | null;
password?: string 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: { user: {
relationTo: 'users' relationTo: 'users';
value: string | User value: string | User;
} };
key?: string 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?: value?:
| { | {
[k: string]: unknown [k: string]: unknown;
} }
| unknown[] | unknown[]
| string | string
| number | number
| boolean | boolean
| null | null;
updatedAt: string updatedAt: string;
createdAt: string createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration { export interface PayloadMigration {
id: string id: string;
name?: string name?: string | null;
batch?: number batch?: number | null;
updatedAt: string updatedAt: string;
createdAt: 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 { export interface MainMenu {
id: string id: string;
navItems?: { navItems?:
| {
link: { link: {
type?: 'reference' | 'custom' type?: ('reference' | 'custom') | null;
newTab?: boolean newTab?: boolean | null;
reference: { reference?: {
relationTo: 'pages' relationTo: 'pages';
value: string | Page value: string | Page;
} } | null;
url: string url?: string | null;
label: string label: string;
} };
id?: string id?: string | null;
}[] }[]
updatedAt?: string | null;
createdAt?: string 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' { declare module 'payload' {
export interface GeneratedTypes { export interface GeneratedTypes extends Config {}
collections: {
pages: Page
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {
'main-menu': MainMenu
}
}
} }

View File

@@ -1,37 +1,42 @@
import { webpackBundler } from '@payloadcms/bundler-webpack'
import { mongooseAdapter } from '@payloadcms/db-mongodb' import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { slateEditor } from '@payloadcms/richtext-slate' import { slateEditor } from '@payloadcms/richtext-slate'
import { fileURLToPath } from 'node:url'
import path from 'path' import path from 'path'
import { buildConfig } from 'payload/config' import { buildConfig } from 'payload'
import { Pages } from './collections/Pages' import { Pages } from './collections/Pages'
import { Users } from './collections/Users' import { Users } from './collections/Users'
import BeforeLogin from './components/BeforeLogin'
import { MainMenu } from './globals/MainMenu' 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({ export default buildConfig({
collections: [Pages, Users],
admin: { admin: {
bundler: webpackBundler(), importMap: {
components: { baseDir: path.resolve(dirname),
beforeLogin: [BeforeLogin],
}, },
}, },
editor: slateEditor({}), collections: [Pages, Users],
db: mongooseAdapter({
url: process.env.DATABASE_URI,
}),
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
cors: [ cors: [
process.env.PAYLOAD_PUBLIC_SERVER_URL || '', process.env.NEXT_PUBLIC_SERVER_URL || '',
process.env.PAYLOAD_PUBLIC_SITE_URL || '', process.env.PAYLOAD_PUBLIC_SITE_URL || '',
].filter(Boolean), ].filter(Boolean),
csrf: [ csrf: [
process.env.PAYLOAD_PUBLIC_SERVER_URL || '', process.env.NEXT_PUBLIC_SERVER_URL || '',
process.env.PAYLOAD_PUBLIC_SITE_URL || '', process.env.PAYLOAD_PUBLIC_SITE_URL || '',
].filter(Boolean), ].filter(Boolean),
db: mongooseAdapter({
url: process.env.DATABASE_URI || '',
}),
editor: slateEditor({}),
globals: [MainMenu], globals: [MainMenu],
graphQL: {
schemaOutputFile: path.resolve(dirname, 'generated-schema.graphql'),
},
secret: process.env.PAYLOAD_SECRET || '',
typescript: { typescript: {
outputFile: path.resolve(__dirname, 'payload-types.ts'), outputFile: path.resolve(dirname, 'payload-types.ts'),
}, },
}) })

View File

@@ -1,6 +1,8 @@
import type { Page } from '../payload-types' 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', slug: 'home',
_status: 'published', _status: 'published',
richText: [ richText: [
@@ -59,7 +61,7 @@ export const home: Partial<Page> = {
type: 'link', type: 'link',
children: [{ text: 'example page' }], children: [{ text: 'example page' }],
linkType: 'custom', linkType: 'custom',
url: 'http://localhost:3001/example-page', url: 'http://localhost:3000/example-page',
}, },
{ text: ' to see how we control access to draft content. ' }, { text: ' to see how we control access to draft content. ' },
], ],

View File

@@ -1,7 +1,6 @@
import type { Page } from '../payload-types' import type { Page } from '../payload-types'
export const examplePage: Partial<Page> = { export const examplePage: Partial<Page> = {
title: 'Example Page (Published)',
slug: 'example-page', slug: 'example-page',
_status: 'published', _status: 'published',
richText: [ 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: 'This is an example page with two versions, draft and published. You are currently seeing ',
}, },
{ {
text: 'published',
bold: true, bold: true,
text: 'published',
}, },
{ {
text: ' content because you are not in preview mode. ', text: ' content because you are not in preview mode. ',
}, },
{ {
type: 'link', type: 'link',
linkType: 'custom',
url: 'http://localhost:3000/admin',
newTab: true,
children: [{ text: 'Log in to the admin panel' }], 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.', 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)',
} }

View File

@@ -1,7 +1,6 @@
import type { Page } from '../payload-types' import type { Page } from '../payload-types'
export const examplePageDraft: Partial<Page> = { export const examplePageDraft: Partial<Page> = {
title: 'Example Page (Draft)',
richText: [ richText: [
{ {
children: [ 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: 'This page is an example page with two versions, draft and published. You are currently seeing ',
}, },
{ {
text: 'draft',
bold: true, bold: true,
text: 'draft',
}, },
{ {
text: ' content because you in preview mode. ', text: ' content because you in preview mode. ',
}, },
{ {
type: 'link', type: 'link',
linkType: 'custom',
url: 'http://localhost:3000/admin/logout',
newTab: true,
children: [{ text: 'Log out' }], 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.', 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)',
} }

View File

@@ -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()

View File

@@ -14,6 +14,7 @@ export function isObject(item: unknown): boolean {
* @param target * @param target
* @param ...sources * @param ...sources
*/ */
// eslint-disable-next-line no-restricted-exports
export default function deepMerge<T, R>(target: T, source: R): T { export default function deepMerge<T, R>(target: T, source: R): T {
const output = { ...target } const output = { ...target }
if (isObject(target) && isObject(source)) { if (isObject(target) && isObject(source)) {

View File

@@ -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()}`
}

View 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}`],
})

View File

@@ -1,5 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"esModuleInterop": true,
"target": "ES2022", "target": "ES2022",
"lib": [ "lib": [
"DOM", "DOM",
@@ -7,27 +9,43 @@
"ES2022" "ES2022"
], ],
"allowJs": true, "allowJs": true,
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"outDir": "./dist", "strict": false,
"rootDir": "./src", "strictNullChecks": true,
"jsx": "react", "forceConsistentCasingInFileNames": true,
"sourceMap": true, "noEmit": true,
"incremental": true,
"jsx": "preserve",
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"sourceMap": true,
"isolatedModules": true,
"plugins": [
{
"name": "next"
}
],
"paths": { "paths": {
"node_modules/*": ["./node_modules/*"] "@payload-config": [
}, "./src/payload.config.ts"
],
"react": [
"./node_modules/@types/react"
],
"@/*": [
"./src/*"
],
}
}, },
"include": [ "include": [
"src" "**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"redirects.js",
"next.config.mjs"
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules"
"dist", ]
"build",
],
"ts-node": {
"transpileOnly": true
}
} }

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