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
1. Clone this repo
2. `cd` into this directory and run `yarn` or `npm install`
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
5. `open http://localhost:3001` to see the result
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload) for full details.

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,19 +5,20 @@ import { NextResponse } from 'next/server'
// this endpoint will revalidate a page by tag or path
// this is to achieve on-demand revalidation of pages that use this data
// send either `collection` and `slug` or `revalidatePath` as query params
export async function GET(request: NextRequest): Promise<unknown> {
/* eslint-disable @typescript-eslint/require-await */
export async function GET(request: NextRequest): Promise<Response> {
const collection = request.nextUrl.searchParams.get('collection')
const slug = request.nextUrl.searchParams.get('slug')
const path = request.nextUrl.searchParams.get('path')
const secret = request.nextUrl.searchParams.get('secret')
if (secret !== process.env.NEXT_PRIVATE_REVALIDATION_KEY) {
return NextResponse.json({ revalidated: false, now: Date.now() })
return NextResponse.json({ now: Date.now(), revalidated: false })
}
if (typeof collection === 'string' && typeof slug === 'string') {
revalidateTag(`${collection}_${slug}`)
return NextResponse.json({ revalidated: true, now: Date.now() })
return NextResponse.json({ now: Date.now(), revalidated: true })
}
// there is a known limitation with `revalidatePath` where it will not revalidate exact paths of dynamic routes
@@ -27,8 +28,8 @@ export async function GET(request: NextRequest): Promise<unknown> {
// - https://github.com/vercel/next.js/issues/49778#issuecomment-1547028830
if (typeof path === 'string') {
revalidatePath(path)
return NextResponse.json({ revalidated: true, now: Date.now() })
return NextResponse.json({ now: Date.now(), revalidated: true })
}
return NextResponse.json({ revalidated: false, now: Date.now() })
return NextResponse.json({ now: Date.now(), revalidated: false })
}

View File

@@ -4,10 +4,11 @@ import { Header } from './_components/Header'
import './app.scss'
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
title: 'Create Next App',
}
// eslint-disable-next-line @typescript-eslint/require-await
export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props
@@ -23,7 +24,6 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
Update: this is fixed in `@types/react` v18.2.14 but still requires `@ts-expect-error` to build :shrug:
See my comment here: https://github.com/vercel/next.js/issues/42292#issuecomment-1622979777
*/}
{/* @ts-expect-error */}
<Header />
{children}
</body>

View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

View File

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

View File

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

View File

@@ -7,95 +7,259 @@
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
pages: Page
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
pages: Page;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
pages: PagesSelect<false> | PagesSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
};
globals: {
'main-menu': MainMenu
}
'main-menu': MainMenu;
};
globalsSelect: {
'main-menu': MainMenuSelect<false> | MainMenuSelect<true>;
};
locale: null;
user: User & {
collection: 'users';
};
jobs: {
tasks: unknown;
workflows: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages".
*/
export interface Page {
id: string
title: string
slug?: string
id: string;
title: string;
slug?: string | null;
richText: {
[k: string]: unknown
}[]
updatedAt: string
createdAt: string
_status?: 'draft' | 'published'
[k: string]: unknown;
}[];
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string
updatedAt: string
createdAt: string
email: string
resetPasswordToken?: string
resetPasswordExpiration?: string
salt?: string
hash?: string
loginAttempts?: number
lockUntil?: string
password?: string
id: string;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
export interface PayloadPreference {
id: string
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
document?:
| ({
relationTo: 'pages';
value: string | Page;
} | null)
| ({
relationTo: 'users';
value: string | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users'
value: string | User
}
key?: string
relationTo: 'users';
value: string | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
user: {
relationTo: 'users';
value: string | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null
updatedAt: string
createdAt: string
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string
name?: string
batch?: number
updatedAt: string
createdAt: string
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages_select".
*/
export interface PagesSelect<T extends boolean = true> {
title?: T;
slug?: T;
richText?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu".
*/
export interface MainMenu {
id: string
navItems?: {
id: string;
navItems?:
| {
link: {
type?: 'reference' | 'custom'
newTab?: boolean
reference: {
relationTo: 'pages'
value: string | Page
}
url: string
label: string
}
id?: string
type?: ('reference' | 'custom') | null;
newTab?: boolean | null;
reference?: {
relationTo: 'pages';
value: string | Page;
} | null;
url?: string | null;
label: string;
};
id?: string | null;
}[]
updatedAt?: string
createdAt?: string
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
declare module 'payload' {
export interface GeneratedTypes {
collections: {
pages: Page
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {
'main-menu': MainMenu
}
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu_select".
*/
export interface MainMenuSelect<T extends boolean = true> {
navItems?:
| T
| {
link?:
| T
| {
type?: T;
newTab?: T;
reference?: T;
url?: T;
label?: T;
};
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,6 @@
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next.config.js"],
"exclude": ["node_modules"]
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,15 @@ First you'll need a running Payload app. There is one made explicitly for this e
### Next.js
1. Clone this repo
2. `cd` into this directory and run `yarn` or `npm install`
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
5. `open http://localhost:3001` to see the result
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload) for full details.

View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,95 +7,259 @@
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
pages: Page
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
pages: Page;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
pages: PagesSelect<false> | PagesSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
};
globals: {
'main-menu': MainMenu
}
'main-menu': MainMenu;
};
globalsSelect: {
'main-menu': MainMenuSelect<false> | MainMenuSelect<true>;
};
locale: null;
user: User & {
collection: 'users';
};
jobs: {
tasks: unknown;
workflows: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages".
*/
export interface Page {
id: string
title: string
slug?: string
id: string;
title: string;
slug?: string | null;
richText: {
[k: string]: unknown
}[]
updatedAt: string
createdAt: string
_status?: 'draft' | 'published'
[k: string]: unknown;
}[];
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string
updatedAt: string
createdAt: string
email: string
resetPasswordToken?: string
resetPasswordExpiration?: string
salt?: string
hash?: string
loginAttempts?: number
lockUntil?: string
password?: string
id: string;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
export interface PayloadPreference {
id: string
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
document?:
| ({
relationTo: 'pages';
value: string | Page;
} | null)
| ({
relationTo: 'users';
value: string | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users'
value: string | User
}
key?: string
relationTo: 'users';
value: string | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
user: {
relationTo: 'users';
value: string | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null
updatedAt: string
createdAt: string
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string
name?: string
batch?: number
updatedAt: string
createdAt: string
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages_select".
*/
export interface PagesSelect<T extends boolean = true> {
title?: T;
slug?: T;
richText?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu".
*/
export interface MainMenu {
id: string
navItems?: {
id: string;
navItems?:
| {
link: {
type?: 'reference' | 'custom'
newTab?: boolean
reference: {
relationTo: 'pages'
value: string | Page
}
url: string
label: string
}
id?: string
type?: ('reference' | 'custom') | null;
newTab?: boolean | null;
reference?: {
relationTo: 'pages';
value: string | Page;
} | null;
url?: string | null;
label: string;
};
id?: string | null;
}[]
updatedAt?: string
createdAt?: string
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
declare module 'payload' {
export interface GeneratedTypes {
collections: {
pages: Page
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {
'main-menu': MainMenu
}
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu_select".
*/
export interface MainMenuSelect<T extends boolean = true> {
navItems?:
| T
| {
link?:
| T
| {
type?: T;
newTab?: T;
reference?: T;
url?: T;
label?: T;
};
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,16 @@
DATABASE_URI=mongodb://127.0.0.1/payload-example-draft-preview
PAYLOAD_SECRET=ENTER-STRING-HERE
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
# Database connection string
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
# Used to encrypt JWT tokens
PAYLOAD_SECRET=YOUR_SECRET_HERE
# Used to configure CORS, format links and more. No trailing slash
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
# Add the following environment variables when running your payload server & app separately
# i.e. next-app || next-pages on localhost:3001 and payload server on localhost:3000
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3001
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
COOKIE_DOMAIN=localhost
REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
PAYLOAD_PUBLIC_SEED=true
PAYLOAD_DROP_DATABASE=true
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET

View File

@@ -1,4 +1,8 @@
module.exports = {
extends: 'next',
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
To spin up this example locally, follow these steps:
1. Clone this repo
2. `cd` into this directory and run `yarn` or `npm install`
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
5. `open http://localhost:3000/admin` to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo`
@@ -22,6 +30,33 @@ That's it! Changes made in `./src` will be reflected in your app. See the [Devel
Draft preview works by sending the user to your front-end with a `secret` along with their http-only cookies. Your front-end catches the request, verifies the authenticity, then enters into it's own preview mode. Once in preview mode, your front-end can begin securely requesting draft documents from Payload. See [Preview Mode](#preview-mode) for more details.
### Environment Variables
Depending on how you run this example, you need different environment variables:
- #### Running Payload and the Front-End Together
When the Payload server and front-end run on the same domain and port:
```ts
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
PAYLOAD_SECRET=YOUR_SECRET_HERE
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
```
- #### Running Payload and the Front-End Separately
When running Payload on one domain (e.g., `localhost:3000`) and the front-end on another (e.g., `localhost:3001`):
```ts
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
PAYLOAD_SECRET=YOUR_SECRET_HERE
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3001
REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
```
### Collections
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend any of this functionality.
@@ -53,11 +88,11 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
})
```
For more details on how to extend this functionality, see the [Authentication](https://payloadcms.com/docs/authentication) docs.
For more details on how to extend this functionality, see the [Authentication](https://payloadcms.com/docs/authentication/overview) docs.
### Preview Mode
To preview draft documents, the user first needs to have at least one draft document saved. When they click the "preview" button from the Payload admin panel, a custom [preview function](https://payloadcms.com/docs/configuration/collections#preview) routes them to your front-end with a `secret` along with their http-only cookies. An API route on your front-end will verify the secret and token before entering into it's own preview mode. Once in preview mode, it can begin requesting drafts from Payload using the `Authorization` header. See [Pages](#pages) for more details.
To preview draft documents, the user first needs to have at least one draft document saved. When they click the "preview" button from the Payload admin panel, a custom [preview function](https://payloadcms.com/docs/admin/collections#preview) routes them to your front-end with a `secret` along with their http-only cookies. An API route on your front-end will verify the secret and token before entering into it's own preview mode. Once in preview mode, it can begin requesting drafts from Payload using the `Authorization` header. See [Pages](#pages) for more details.
> "Preview mode" looks differently for every front-end framework. For instance, check out the differences between Next.js [Preview Mode](https://nextjs.org/docs/pages/building-your-application/configuring/preview-mode) in the Pages Router and [Draft Mode](https://nextjs.org/docs/pages/building-your-application/configuring/draft-mode) in the App Router. In Next.js, methods are provided that set cookies in your browser, but this may not be the case for all frameworks.
@@ -83,16 +118,16 @@ To spin up this example locally, follow the [Quick Start](#quick-start).
### Seed
On boot, a seed script is included to scaffold a basic database for you to use as an example. This is done by setting the `PAYLOAD_DROP_DATABASE` and `PAYLOAD_PUBLIC_SEED` environment variables which are included in the `.env.example` by default. You can remove these from your `.env` to prevent this behavior. You can also freshly seed your project at any time by running `yarn seed`. This seed creates a user with email `demo@payloadcms.com` and password `demo` along with a home page and an example page with two versions, one published and the other draft.
On boot, a seed script is included to scaffold a basic database for you to use as an example. You can remove `pnpm seed` from the `dev` script in the `package.json` to prevent this behavior. You can also freshly seed your project at any time by running `pnpm seed`. This seed creates a user with email `demo@payloadcms.com` and password `demo` along with a home page and an example page with two versions, one published and the other draft.
> NOTICE: seeding the database is destructive because it drops your current database to populate a fresh one from the seed template. Only run this command if you are starting a new project or can afford to lose your current data.
## Production
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
To run Payload in production, you need to build and start the Admin panel. To do so, follow these steps:
1. First invoke the `payload build` script by running `yarn build` or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
1. Then run `yarn serve` or `npm run serve` to run Node in production and serve Payload from the `./build` directory.
1. Invoke the `next build` script by running `pnpm build` or `npm run build` in your project root. This creates a `.next` directory with a production-ready admin bundle.
1. Finally run `pnpm start` or `npm run start` to run Node in production and serve Payload from the `.build` directory.
### Deployment

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

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 } }) => {
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 } }) => {
if (user) {

View File

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

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'
export const formatAppURL = ({ doc }): string => {
@@ -7,12 +11,14 @@ export const formatAppURL = ({ doc }): string => {
return pathname
}
// revalidate the page in the background, so the user doesn't have to wait
// notice that the hook itself is not async and we are not awaiting `revalidate`
// only revalidate existing docs that are published (not drafts)
// send `revalidatePath`, `collection`, and `slug` to the frontend to use in its revalidate route
// frameworks may have different ways of doing this, but the idea is the same
export const revalidatePage: AfterChangeHook = ({ doc, req, operation }) => {
export const revalidatePage: CollectionAfterChangeHook<Page> = ({
doc,
operation,
previousDoc,
req,
}) => {
if (process.env.PAYLOAD_PUBLIC_SITE_URL && process.env.REVALIDATION_KEY) {
// Revalidate externally if payload is configured separately from the next app
if (operation === 'update' && doc._status === 'published') {
const url = formatAppURL({ doc })
@@ -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

View File

@@ -1,35 +1,43 @@
import type { CollectionConfig } from 'payload/types'
import type { CollectionConfig } from 'payload'
import richText from '../../fields/richText'
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
import { loggedIn } from './access/loggedIn'
import { publishedOrLoggedIn } from './access/publishedOrLoggedIn'
import formatSlug from './hooks/formatSlug'
import { formatSlug } from './hooks/formatSlug'
import { formatAppURL, revalidatePage } from './hooks/revalidatePage'
export const Pages: CollectionConfig = {
slug: 'pages',
access: {
create: loggedIn,
delete: loggedIn,
read: publishedOrLoggedIn,
update: loggedIn,
},
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'slug', 'updatedAt'],
preview: (doc) => {
if (process.env.PAYLOAD_PUBLIC_SITE_URL && process.env.PAYLOAD_PUBLIC_DRAFT_SECRET) {
// Separate Payload and front-end setup
return `${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/preview?url=${encodeURIComponent(
formatAppURL({
doc,
}),
formatAppURL({ doc }),
)}&collection=pages&slug=${doc.slug}&secret=${process.env.PAYLOAD_PUBLIC_DRAFT_SECRET}`
} else if (process.env.NEXT_PUBLIC_SERVER_URL) {
// Unified Payload and front-end setup
const path = generatePreviewPath({
slug: typeof doc?.slug === 'string' ? doc.slug : '',
collection: 'pages',
})
return `${process.env.NEXT_PUBLIC_SERVER_URL}${path}`
}
// Fallback for missing environment variables
throw new Error(
'Environment variables for preview functionality are not set. Ensure that either PAYLOAD_PUBLIC_SITE_URL and PAYLOAD_PUBLIC_DRAFT_SECRET, or NEXT_PUBLIC_SERVER_URL are defined.',
)
},
},
versions: {
drafts: true,
},
access: {
read: publishedOrLoggedIn,
create: loggedIn,
update: loggedIn,
delete: loggedIn,
},
hooks: {
afterChange: [revalidatePage],
useAsTitle: 'title',
},
fields: [
{
@@ -39,16 +47,22 @@ export const Pages: CollectionConfig = {
},
{
name: 'slug',
label: 'Slug',
type: 'text',
index: true,
admin: {
position: 'sidebar',
},
hooks: {
beforeValidate: [formatSlug('title')],
},
index: true,
label: 'Slug',
},
richText(),
],
hooks: {
afterChange: [revalidatePage],
},
versions: {
drafts: true,
},
}

View File

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

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

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

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

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

View File

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

View File

@@ -7,95 +7,264 @@
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
pages: Page
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
pages: Page;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
pages: PagesSelect<false> | PagesSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
};
globals: {
'main-menu': MainMenu
}
'main-menu': MainMenu;
};
globalsSelect: {
'main-menu': MainMenuSelect<false> | MainMenuSelect<true>;
};
locale: null;
user: User & {
collection: 'users';
};
jobs: {
tasks: unknown;
workflows: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages".
*/
export interface Page {
id: string
title: string
slug?: string
id: string;
title: string;
slug?: string | null;
richText: {
[k: string]: unknown
}[]
updatedAt: string
createdAt: string
_status?: 'draft' | 'published'
[k: string]: unknown;
}[];
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string
updatedAt: string
createdAt: string
email: string
resetPasswordToken?: string
resetPasswordExpiration?: string
salt?: string
hash?: string
loginAttempts?: number
lockUntil?: string
password?: string
id: string;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
export interface PayloadPreference {
id: string
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
document?:
| ({
relationTo: 'pages';
value: string | Page;
} | null)
| ({
relationTo: 'users';
value: string | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users'
value: string | User
}
key?: string
relationTo: 'users';
value: string | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
user: {
relationTo: 'users';
value: string | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null
updatedAt: string
createdAt: string
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string
name?: string
batch?: number
updatedAt: string
createdAt: string
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages_select".
*/
export interface PagesSelect<T extends boolean = true> {
title?: T;
slug?: T;
richText?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu".
*/
export interface MainMenu {
id: string
navItems?: {
id: string;
navItems?:
| {
link: {
type?: 'reference' | 'custom'
newTab?: boolean
reference: {
relationTo: 'pages'
value: string | Page
}
url: string
label: string
}
id?: string
type?: ('reference' | 'custom') | null;
newTab?: boolean | null;
reference?: {
relationTo: 'pages';
value: string | Page;
} | null;
url?: string | null;
label: string;
};
id?: string | null;
}[]
updatedAt?: string
createdAt?: string
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu_select".
*/
export interface MainMenuSelect<T extends boolean = true> {
navItems?:
| T
| {
link?:
| T
| {
type?: T;
newTab?: T;
reference?: T;
url?: T;
label?: T;
};
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}
declare module 'payload' {
export interface GeneratedTypes {
collections: {
pages: Page
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {
'main-menu': MainMenu
}
}
export interface GeneratedTypes extends Config {}
}

View File

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

View File

@@ -1,6 +1,8 @@
import type { Page } from '../payload-types'
export const home: Partial<Page> = {
// Used for pre-seeded content so that the homepage is not empty
// @ts-expect-error: Page type is not fully compatible with the provided object structure
export const home: Page = {
slug: 'home',
_status: 'published',
richText: [
@@ -59,7 +61,7 @@ export const home: Partial<Page> = {
type: 'link',
children: [{ text: 'example page' }],
linkType: 'custom',
url: 'http://localhost:3001/example-page',
url: 'http://localhost:3000/example-page',
},
{ text: ' to see how we control access to draft content. ' },
],

View File

@@ -1,7 +1,6 @@
import type { Page } from '../payload-types'
export const examplePage: Partial<Page> = {
title: 'Example Page (Published)',
slug: 'example-page',
_status: 'published',
richText: [
@@ -11,18 +10,18 @@ export const examplePage: Partial<Page> = {
text: 'This is an example page with two versions, draft and published. You are currently seeing ',
},
{
text: 'published',
bold: true,
text: 'published',
},
{
text: ' content because you are not in preview mode. ',
},
{
type: 'link',
linkType: 'custom',
url: 'http://localhost:3000/admin',
newTab: true,
children: [{ text: 'Log in to the admin panel' }],
linkType: 'custom',
newTab: true,
url: 'http://localhost:3000/admin',
},
{
text: ' and click "preview" to return to this page and view the latest draft content in Next.js preview mode. To make additional changes to the draft, click "save draft" before returning to the preview.',
@@ -30,4 +29,5 @@ export const examplePage: Partial<Page> = {
],
},
],
title: 'Example Page (Published)',
}

View File

@@ -1,7 +1,6 @@
import type { Page } from '../payload-types'
export const examplePageDraft: Partial<Page> = {
title: 'Example Page (Draft)',
richText: [
{
children: [
@@ -9,18 +8,18 @@ export const examplePageDraft: Partial<Page> = {
text: 'This page is an example page with two versions, draft and published. You are currently seeing ',
},
{
text: 'draft',
bold: true,
text: 'draft',
},
{
text: ' content because you in preview mode. ',
},
{
type: 'link',
linkType: 'custom',
url: 'http://localhost:3000/admin/logout',
newTab: true,
children: [{ text: 'Log out' }],
linkType: 'custom',
newTab: true,
url: 'http://localhost:3000/admin/logout',
},
{
text: ' or click "exit preview mode" from the Payload Admin Bar to see the latest published content. To make additional changes to the draft, click "save draft" before returning to the preview.',
@@ -28,4 +27,5 @@ export const examplePageDraft: Partial<Page> = {
],
},
],
title: 'Example Page (Draft)',
}

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 ...sources
*/
// eslint-disable-next-line no-restricted-exports
export default function deepMerge<T, R>(target: T, source: R): T {
const output = { ...target }
if (isObject(target) && isObject(source)) {

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

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More