chore(examples): removes external draft-preview examples (#9545)

In an effort to keep the Examples Directory as easy to navigate as
possible, and to keep the Payload Monorepo only as verbose as it needs
to be, we need to remove all alternatives from the Examples Directory.
This includes setups that interact with Payload from a standalone
server, keeping only the Payload recommended "combined" Next.js +
Payload setups.
This commit is contained in:
Patrik
2024-11-26 16:59:16 -05:00
committed by GitHub
parent 1a8ac74df7
commit f8b7e3eb14
129 changed files with 74 additions and 9594 deletions

View File

@@ -0,0 +1,8 @@
# 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

View File

@@ -1,11 +1,6 @@
# Payload Draft Preview Example # Payload Draft Preview Example
The [Payload Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload) demonstrates how to implement draft preview in [Payload](https://github.com/payloadcms/payload) using [Versions](https://payloadcms.com/docs/versions/overview) and [Drafts](https://payloadcms.com/docs/versions/drafts). Draft preview allows you to see content on your front-end before it is published. There are various fully working front-ends made explicitly for this example, including: The [Payload Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload) demonstrates how to implement draft preview in [Payload](https://github.com/payloadcms/payload) using [Versions](https://payloadcms.com/docs/versions/overview) and [Drafts](https://payloadcms.com/docs/versions/drafts). Draft preview allows you to see content on your front-end before it is published.
- [Next.js App Router](../next-app)
- [Next.js Pages Router](../next-pages)
Follow the instructions in each respective README to get started. If you are setting up draft preview for another front-end, please consider contributing to this repo with your own example!
## Quick Start ## Quick Start
@@ -17,9 +12,6 @@ To spin up this example locally, follow these steps:
> \*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. > \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
3. `cp .env.example .env` to copy the example environment variables 3. `cp .env.example .env` to copy the example environment variables
> 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 4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
5. `open http://localhost:3000/admin` to access the admin panel 5. `open http://localhost:3000/admin` to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo` 6. Login with email `demo@payloadcms.com` and password `demo`
@@ -30,33 +22,6 @@ That's it! Changes made in `./src` will be reflected in your app. See the [Devel
Draft preview works by sending the user to your front-end with a `secret` along with their http-only cookies. Your front-end catches the request, verifies the authenticity, then enters into it's own preview mode. Once in preview mode, your front-end can begin securely requesting draft documents from Payload. See [Preview Mode](#preview-mode) for more details. Draft preview works by sending the user to your front-end with a `secret` along with their http-only cookies. Your front-end catches the request, verifies the authenticity, then enters into it's own preview mode. Once in preview mode, your front-end can begin securely requesting draft documents from Payload. See [Preview Mode](#preview-mode) for more details.
### Environment Variables
Depending on how you run this example, you need different environment variables:
- #### Running Payload and the Front-End Together
When the Payload server and front-end run on the same domain and port:
```ts
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
PAYLOAD_SECRET=YOUR_SECRET_HERE
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
```
- #### Running Payload and the Front-End Separately
When running Payload on one domain (e.g., `localhost:3000`) and the front-end on another (e.g., `localhost:3001`):
```ts
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
PAYLOAD_SECRET=YOUR_SECRET_HERE
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3001
REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
```
### Collections ### Collections
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend any of this functionality. See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend any of this functionality.
@@ -94,13 +59,13 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
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. To preview draft documents, the user first needs to have at least one draft document saved. When they click the "preview" button from the Payload admin panel, a custom [preview function](https://payloadcms.com/docs/admin/collections#preview) routes them to your front-end with a `secret` along with their http-only cookies. An API route on your front-end will verify the secret and token before entering into it's own preview mode. Once in preview mode, it can begin requesting drafts from Payload using the `Authorization` header. See [Pages](#pages) for more details.
> "Preview mode" looks differently for every front-end framework. For instance, check out the differences between Next.js [Preview Mode](https://nextjs.org/docs/pages/building-your-application/configuring/preview-mode) in the Pages Router and [Draft Mode](https://nextjs.org/docs/pages/building-your-application/configuring/draft-mode) in the App Router. In Next.js, methods are provided that set cookies in your browser, but this may not be the case for all frameworks. > "Preview mode" can vary between frameworks. In the Next.js App Router, [Draft Mode](https://nextjs.org/docs/pages/building-your-application/configuring/draft-mode) enables you to work with previewable content. It provides methods to set cookies in your browser, ensuring content is displayed as a draft, but this behavior might differ in other frameworks.
### On-demand Revalidation ### On-demand Revalidation
If your front-end is statically generated then you may also want to regenerate the HTML for each page individually as they are published, referred to as On-demand Revalidation. This will prevent your static site from having to fully rebuild every page in order to deploy content changes. To do this, we add an `afterChange` hook to the collection that fires a request to your front-end in the background each time the document is updated. You can handle this request on your front-end to revalidate the HTML for your page. If your front-end is statically generated then you may also want to regenerate the HTML for each page individually as they are published, referred to as On-demand Revalidation. This will prevent your static site from having to fully rebuild every page in order to deploy content changes. To do this, we add an `afterChange` hook to the collection that fires a request to your front-end in the background each time the document is updated. You can handle this request on your front-end to revalidate the HTML for your page.
> On-demand revalidation looks differently for every front-end framework. For instance, check out the differences between Next.js on-demand revalidation in the [Pages Router](https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration) and the [App Router](https://nextjs.org/docs/app/building-your-application/data-fetching/revalidating#on-demand-revalidation). In Next.js, methods are provided that regenerate the HTML for each page, but this may not be the case for all frameworks. > On-demand revalidation can vary between frameworks. In the Next.js [App Router](https://nextjs.org/docs/app/building-your-application/data-fetching/revalidating#on-demand-revalidation), on-demand revalidation allows you to regenerate the HTML for specific pages as needed. However, this behavior may differ in other frameworks.
### Admin Bar ### Admin Bar

View File

@@ -1,3 +0,0 @@
NEXT_PUBLIC_PAYLOAD_URL=http://localhost:3000
NEXT_PRIVATE_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
NEXT_PRIVATE_REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY

View File

@@ -1,7 +0,0 @@
module.exports = {
root: true,
extends: ['plugin:@next/next/recommended', '@payloadcms'],
rules: {
'import/extensions': 'off',
},
}

View File

@@ -1,6 +0,0 @@
.next
dist
build
node_modules
.env
package-lock.json

View File

@@ -1,43 +0,0 @@
# Payload Draft Preview Example Front-End
This is a [Next.js](https://nextjs.org) app using the [App Router](https://nextjs.org/docs/app). It was made explicitly for Payload's [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload).
> This example uses the App Router, the latest API of Next.js. If your app is using the legacy [Pages Router](https://nextjs.org/docs/pages), check out the official [Pages Router Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/next-pages).
## Getting Started
### Payload
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload). If you have not done so already, clone it down and follow the setup instructions there. This will provide all the necessary APIs that your Next.js app requires for authentication.
### Next.js
1. Clone this repo
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
> 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.
## Learn More
To learn more about Payload and Next.js, take a look at the following resources:
- [Payload Documentation](https://payloadcms.com/docs) - learn about Payload features and API.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Payload GitHub repository](https://github.com/payloadcms/payload) as well as [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deployment
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. You could also combine this app into a [single Express server](https://github.com/payloadcms/payload/tree/main/examples/custom-server) and deploy in to [Payload Cloud](https://payloadcms.com/new/import).
Check out our [Payload deployment documentation](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -1,51 +0,0 @@
import { draftMode } from 'next/headers'
import { notFound } from 'next/navigation'
import type { Page as PageType } from '../../payload-types'
import { fetchPage } from '../_api/fetchPage'
import { fetchPages } from '../_api/fetchPages'
import { Gutter } from '../_components/Gutter'
import RichText from '../_components/RichText'
import classes from './index.module.scss'
interface PageParams {
params: Promise<{
slug?: string
}>
}
export const PageTemplate: React.FC<{ page: null | PageType | undefined }> = ({ page }) => (
<main className={classes.page}>
<Gutter>
<h1>{page?.title}</h1>
<RichText content={page?.richText} />
</Gutter>
</main>
)
export default async function Page({ params }: PageParams) {
const { slug = 'home' } = await params
const { isEnabled: isDraftMode } = await draftMode()
const page = await fetchPage(slug, isDraftMode)
if (page === null) {
return notFound()
}
return <PageTemplate page={page} />
}
export async function generateStaticParams() {
const pages = await fetchPages()
return pages.map(({ slug }) =>
slug !== 'home'
? {
slug,
}
: {},
) // eslint-disable-line function-paren-newline
}

View File

@@ -1,39 +0,0 @@
import type { RequestCookie } from 'next/dist/compiled/@edge-runtime/cookies'
import type { Page } from '../../payload-types'
export const fetchPage = async (
slug: string,
draft?: boolean,
): Promise<null | Page | undefined> => {
let payloadToken: RequestCookie | undefined
if (draft) {
const { cookies } = await import('next/headers')
payloadToken = (await cookies()).get('payload-token')
}
const pageRes: {
docs: Page[]
} = await fetch(
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/pages?where[slug][equals]=${slug}${
draft && payloadToken ? '&draft=true' : ''
}`,
{
method: 'GET',
// this is the key we'll use to on-demand revalidate pages that use this data
// we do this by calling `revalidateTag()` using the same key
// see `app/api/revalidate.ts` for more info
next: { tags: [`pages_${slug}`] },
...(draft && payloadToken
? {
headers: {
Authorization: `JWT ${payloadToken?.value}`,
},
}
: {}),
},
).then((res) => res.json())
return pageRes?.docs?.[0] ?? null
}

View File

@@ -1,11 +0,0 @@
import type { Page } from '../../payload-types'
export const fetchPages = async (): Promise<Page[]> => {
const pageRes: {
docs: Page[]
} = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/pages?depth=0&limit=100`).then(
(res) => res.json(),
) // eslint-disable-line function-paren-newline
return pageRes?.docs ?? []
}

View File

@@ -1,44 +0,0 @@
'use client'
import React, { useState } from 'react'
import type { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
import { PayloadAdminBar } from 'payload-admin-bar'
import { Gutter } from '../Gutter'
import classes from './index.module.scss'
const Title: React.FC = () => <span>Dashboard</span>
export const AdminBarClient: React.FC<PayloadAdminBarProps> = (props) => {
const [user, setUser] = useState<PayloadMeUser>()
return (
<div className={[classes.adminBar, user && classes.show].filter(Boolean).join(' ')}>
<Gutter className={classes.container}>
<PayloadAdminBar
{...props}
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()
}}
style={{
backgroundColor: 'transparent',
padding: 0,
position: 'relative',
zIndex: 'unset',
}}
/>
</Gutter>
</div>
)
}

View File

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

View File

@@ -1,74 +0,0 @@
import type { ElementType } from 'react'
import React from 'react'
import Link from 'next/link'
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

@@ -1,72 +0,0 @@
import React from 'react'
import Link from 'next/link'
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

@@ -1,34 +0,0 @@
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

@@ -1,49 +0,0 @@
import React from 'react'
import Image from 'next/image'
import Link from 'next/link'
import type { MainMenu } from '../../../payload-types'
import { CMSLink } from '../CMSLink'
import { Gutter } from '../Gutter'
import classes from './index.module.scss'
export async function Header() {
const mainMenu: MainMenu = await fetch(
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/globals/main-menu`,
).then((res) => res.json())
const { navItems } = mainMenu
const hasNavItems = navItems && Array.isArray(navItems) && navItems.length > 0
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>
{hasNavItems && (
<nav className={classes.nav}>
{navItems.map(({ link }, i) => {
return <CMSLink key={i} {...link} />
})}
</nav>
)}
</Gutter>
</header>
)
}
export default Header

View File

@@ -1,19 +0,0 @@
import React from 'react'
import serialize from './serialize'
import classes from './index.module.scss'
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

@@ -1,92 +0,0 @@
import React, { Fragment } from 'react'
import escapeHTML from 'escape-html'
import { Text } from 'slate'
// eslint-disable-next-line no-use-before-define
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,49 +0,0 @@
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET(
req: {
cookies: {
get: (name: string) => {
value: string
}
}
} & Request,
): Promise<Response> {
const payloadToken = req.cookies.get('payload-token')?.value
const { searchParams } = new URL(req.url)
const url = searchParams.get('url')
const secret = searchParams.get('secret')
if (!url) {
return new Response('No URL provided', { status: 404 })
}
if (!payloadToken) {
new Response('You are not allowed to preview this page', { status: 403 })
}
// validate the Payload token
const userReq = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/me`, {
headers: {
Authorization: `JWT ${payloadToken}`,
},
})
const userRes = await userReq.json()
const draft = await draftMode()
if (!userReq.ok || !userRes?.user) {
draft.disable()
return new Response('You are not allowed to preview this page', { status: 403 })
}
if (secret !== process.env.NEXT_PRIVATE_DRAFT_SECRET) {
return new Response('Invalid token', { status: 401 })
}
draft.enable()
redirect(url)
}

View File

@@ -1,35 +0,0 @@
import { revalidatePath, revalidateTag } from 'next/cache'
import type { NextRequest } from 'next/server'
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
/* 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({ now: Date.now(), revalidated: false })
}
if (typeof collection === 'string' && typeof slug === 'string') {
revalidateTag(`${collection}_${slug}`)
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
// instead, Next.js expects us to revalidate entire directories, i.e. `revalidatePath('/[slug]')` instead of `/example-page`
// for this reason, it is preferred to use `revalidateTag` instead of `revalidatePath`
// - https://github.com/vercel/next.js/issues/49387
// - https://github.com/vercel/next.js/issues/49778#issuecomment-1547028830
if (typeof path === 'string') {
revalidatePath(path)
return NextResponse.json({ now: Date.now(), revalidated: true })
}
return NextResponse.json({ now: Date.now(), revalidated: false })
}

View File

@@ -1,32 +0,0 @@
import { AdminBar } from './_components/AdminBar'
import { Header } from './_components/Header'
import './app.scss'
export const metadata = {
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
return (
<html lang="en">
<body>
<AdminBar />
{/* The error ignored here is related to `@types/react` and `typescript` not
aligning with their implementations of Promise-based server components.
This can be removed once these dependencies are resolved in their respective modules.
- https://github.com/vercel/next.js/issues/42292
- https://github.com/vercel/next.js/issues/43537
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
*/}
<Header />
{children}
</body>
</html>
)
}

View File

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

View File

@@ -1,40 +0,0 @@
{
"name": "payload-draft-preview-next-app",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev -p 3001",
"lint": "next lint",
"start": "next start -p 3001"
},
"dependencies": {
"escape-html": "^1.0.3",
"next": "^15.0.0",
"payload-admin-bar": "^1.0.6",
"react": "19.0.0-rc-65a56d0e-20241020",
"react-dom": "19.0.0-rc-65a56d0e-20241020"
},
"devDependencies": {
"@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": "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": "15.0.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",
"prettier": "^2.7.1",
"sass": "^1.81.0",
"slate": "^0.82.0",
"typescript": "5.5.2"
}
}

View File

@@ -1,265 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
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;
};
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 | null;
richText: {
[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 | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
/**
* 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;
};
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;
}
| unknown[]
| string
| number
| boolean
| 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 | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages_select".
*/
export interface PagesSelect<T extends boolean = true> {
title?: T;
slug?: T;
richText?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu".
*/
export interface MainMenu {
id: string;
navItems?:
| {
link: {
type?: ('reference' | 'custom') | null;
newTab?: boolean | null;
reference?: {
relationTo: 'pages';
value: string | Page;
} | null;
url?: string | null;
label: string;
};
id?: string | null;
}[]
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu_select".
*/
export interface MainMenuSelect<T extends boolean = true> {
navItems?:
| T
| {
link?:
| T
| {
type?: T;
newTab?: T;
reference?: T;
url?: T;
label?: T;
};
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,15 +0,0 @@
<svg width="260" height="260" viewBox="0 0 260 260" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
path {
fill: #0F0F0F;
}
@media (prefers-color-scheme: dark) {
path {
fill: white;
}
}
</style>
<path d="M120.59 8.5824L231.788 75.6142V202.829L148.039 251.418V124.203L36.7866 57.2249L120.59 8.5824Z" />
<path d="M112.123 244.353V145.073L28.2114 193.769L112.123 244.353Z" />
</svg>

Before

Width:  |  Height:  |  Size: 437 B

View File

@@ -1,32 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [
"DOM",
"DOM.Iterable",
"ES2022"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next.config.js"],
"exclude": ["node_modules"]
}

View File

@@ -1,10 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
max_line_length = null

View File

@@ -1,2 +0,0 @@
NEXT_PUBLIC_PAYLOAD_URL=http://localhost:3000
NEXT_PRIVATE_REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY

View File

@@ -1,4 +0,0 @@
module.exports = {
root: true,
extends: ['plugin:@next/next/recommended', '@payloadcms'],
}

View File

@@ -1,6 +0,0 @@
.next
dist
build
node_modules
.env
package-lock.json

View File

@@ -1,43 +0,0 @@
# Payload Draft Preview Example Front-End
This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages). It was made explicitly for Payload's [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload).
> This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/app), check out the official [App Router Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/next-app).
## Getting Started
### Payload
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload). If you have not done so already, clone it down and follow the setup instructions there. This will provide all the necessary APIs that your Next.js app requires for authentication.
### Next.js
1. Clone this repo
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
> 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.
## Learn More
To learn more about Payload and Next.js, take a look at the following resources:
- [Payload Documentation](https://payloadcms.com/docs) - learn about Payload features and API.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Payload GitHub repository](https://github.com/payloadcms/payload) as well as [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deployment
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. You could also combine this app into a [single Express server](https://github.com/payloadcms/payload/tree/main/examples/custom-server) and deploy in to [Payload Cloud](https://payloadcms.com/new/import).
Check out our [Payload deployment documentation](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

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

View File

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

View File

@@ -1,40 +0,0 @@
{
"name": "payload-draft-preview-next-pages",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev -p 3001",
"lint": "next lint",
"start": "next start -p 3001"
},
"dependencies": {
"escape-html": "^1.0.3",
"next": "^15.0.0",
"payload-admin-bar": "^1.0.6",
"qs": "^6.11.0",
"react": "19.0.0-rc-65a56d0e-20241020",
"react-cookie": "^4.1.1",
"react-dom": "19.0.0-rc-65a56d0e-20241020"
},
"devDependencies": {
"@next/eslint-plugin-next": "^15.0.0",
"@payloadcms/eslint-config": "^0.0.2",
"@types/node": "18.11.3",
"@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-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",
"prettier": "^2.7.1",
"sass": "^1.81.0",
"slate": "^0.82.0",
"typescript": "5.5.2"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,15 +0,0 @@
<svg width="260" height="260" viewBox="0 0 260 260" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
path {
fill: #0F0F0F;
}
@media (prefers-color-scheme: dark) {
path {
fill: white;
}
}
</style>
<path d="M120.59 8.5824L231.788 75.6142V202.829L148.039 251.418V124.203L36.7866 57.2249L120.59 8.5824Z" />
<path d="M112.123 244.353V145.073L28.2114 193.769L112.123 244.353Z" />
</svg>

Before

Width:  |  Height:  |  Size: 437 B

View File

@@ -1,51 +0,0 @@
.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

@@ -1,42 +0,0 @@
import React from 'react'
import type { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
import { PayloadAdminBar } from 'payload-admin-bar'
import { Gutter } from '../Gutter'
import classes from './index.module.scss'
const Title: React.FC = () => <span>Dashboard</span>
export const AdminBar: React.FC<{
adminBarProps?: PayloadAdminBarProps
setUser?: (user: PayloadMeUser) => void
user?: PayloadMeUser
}> = (props) => {
const { adminBarProps, setUser, user } = props
return (
<div className={[classes.adminBar, user && 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_PAYLOAD_URL}
logo={<Title />}
onAuthChange={setUser}
style={{
backgroundColor: 'transparent',
padding: 0,
position: 'relative',
zIndex: 'unset',
}}
/>
</Gutter>
</div>
)
}

View File

@@ -1,55 +0,0 @@
.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

@@ -1,74 +0,0 @@
import type { ElementType } from 'react'
import React from 'react'
import Link from 'next/link'
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
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

@@ -1,66 +0,0 @@
import React from 'react'
import Link from 'next/link'
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
reference?: {
relationTo: 'pages'
value: Page | string
}
type?: 'custom' | 'reference'
url?: 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.value.slug === 'home' ? '' : reference.value.slug}`
: url
if (!appearance) {
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
if (type === 'custom') {
return (
<a href={url} {...newTabProps} className={className}>
{label && label}
{children && children}
</a>
)
}
if (href) {
return (
<Link href={href} {...newTabProps} className={className} prefetch={false}>
{label && label}
{children && children}
</Link>
)
}
}
const buttonProps = {
appearance,
href,
label,
newTab,
}
return <Button className={className} {...buttonProps} el="link" />
}

View File

@@ -1,13 +0,0 @@
.gutter {
max-width: var(--max-width);
width: 100%;
margin: auto;
}
.gutterLeft {
padding-left: var(--gutter-h);
}
.gutterRight {
padding-right: var(--gutter-h);
}

View File

@@ -1,34 +0,0 @@
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

@@ -1,32 +0,0 @@
.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

@@ -1,71 +0,0 @@
import React, { useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
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 = {
children?: React.ReactNode
}
export const HeaderBar: React.FC<HeaderBarProps> = ({ children }) => {
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>
{children}
</Gutter>
</header>
)
}
export const Header: React.FC<{
adminBarProps: PayloadAdminBarProps
globals: {
mainMenu: MainMenu
}
}> = (props) => {
const { adminBarProps, globals } = props
const [user, setUser] = useState<PayloadMeUser>()
const {
mainMenu: { navItems },
} = globals
const hasNavItems = navItems && Array.isArray(navItems) && navItems.length > 0
return (
<div>
<AdminBar adminBarProps={adminBarProps} setUser={setUser} user={user} />
<HeaderBar>
{hasNavItems && (
<nav className={classes.nav}>
{navItems.map(({ link }, i) => {
return <CMSLink key={i} {...link} />
})}
</nav>
)}
</HeaderBar>
</div>
)
}

View File

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

View File

@@ -1,19 +0,0 @@
import React from 'react'
import serialize from './serialize'
import classes from './index.module.scss'
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

@@ -1,92 +0,0 @@
import React, { Fragment } from 'react'
import escapeHTML from 'escape-html'
import { Text } from 'slate'
// eslint-disable-next-line no-use-before-define
type Children = Leaf[]
type Leaf = {
[key: string]: unknown
children?: Children
type: string
url?: string
value?: {
alt: string
url: string
}
}
const serialize = (children: Children): React.ReactElement[] =>
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,3 +0,0 @@
.page {
margin-top: calc(var(--base) * 2);
}

View File

@@ -1,132 +0,0 @@
import React from 'react'
import type { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next'
import QueryString from 'qs'
import type { ParsedUrlQuery } from 'querystring'
import { Gutter } from '../components/Gutter'
import RichText from '../components/RichText'
import type { MainMenu, Page as PageType } from '../payload-types'
import classes from './[slug].module.scss'
const Page: React.FC<
{
mainMenu: MainMenu
preview?: boolean
} & PageType
> = (props) => {
const { richText, title } = props
return (
<main className={classes.page}>
<Gutter>
<h1>{title}</h1>
<RichText content={richText} />
</Gutter>
</main>
)
}
export default Page
interface IParams extends ParsedUrlQuery {
slug: string
}
// when 'preview' cookies are set in the browser, getStaticProps runs on every request :)
export const getStaticProps: GetStaticProps = async (context: GetStaticPropsContext) => {
const { params, preview, previewData } = context
const { payloadToken } =
(previewData as {
payloadToken: string
}) || {}
let { slug } = (params as IParams) || {}
if (!slug) {
slug = 'home'
}
let doc = {}
const notFound = false
const lowerCaseSlug = slug.toLowerCase() // NOTE: let the url be case insensitive
const searchParams = QueryString.stringify(
{
depth: 1,
draft: preview ? true : undefined,
where: {
slug: {
equals: lowerCaseSlug,
},
},
},
{
addQueryPrefix: true,
encode: false,
},
)
// when previewing, send the payload token to bypass draft access control
const pageReq = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/pages${searchParams}`, {
headers: {
...(preview
? {
Authorization: `JWT ${payloadToken}`,
}
: {}),
},
})
if (pageReq.ok) {
const pageData = await pageReq.json()
doc = pageData.docs[0]
if (!doc) {
return {
notFound: true,
}
}
}
return {
notFound,
props: {
...doc,
collection: 'pages',
preview: preview || null,
},
revalidate: 3600, // in seconds
}
}
type Path = {
params: {
slug: string
}
}
type Paths = Path[]
export const getStaticPaths: GetStaticPaths = async () => {
let paths: Paths = []
const pagesReq = await fetch(
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/pages?where[_status][equals]=published&depth=0&limit=300`,
)
const pagesData = await pagesReq.json()
if (pagesReq?.ok) {
const { docs: pages } = pagesData
if (pages && Array.isArray(pages) && pages.length > 0) {
paths = pages.map((page) => ({ params: { slug: page.slug } }))
}
}
return {
fallback: true,
paths,
}
}

View File

@@ -1,84 +0,0 @@
import React, { useCallback } from 'react'
import { CookiesProvider } from 'react-cookie'
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 type { MainMenu } from '../payload-types'
import './app.scss'
export interface IGlobals {
mainMenu: MainMenu
}
export const getAllGlobals = async (): Promise<IGlobals> => {
const res = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/globals/main-menu?depth=1`)
const mainMenu = await res.json()
return {
mainMenu,
}
}
type AppProps<P = any> = {
pageProps: P
} & Omit<NextAppProps<P>, 'pageProps'>
const PayloadApp = (
appProps: {
globals: IGlobals
} & AppProps,
): React.ReactElement => {
const { Component, globals, pageProps } = appProps
const { id, collection, preview } = pageProps
const router = useRouter()
const onPreviewExit = useCallback(() => {
const exit = async () => {
const exitReq = await fetch('/api/exit-preview')
if (exitReq.status === 200) {
router.reload()
}
}
exit().catch((error) => {
// eslint-disable-next-line no-console
console.error('Failed to exit preview:', error)
})
}, [router])
return (
<CookiesProvider>
<Header
adminBarProps={{
id,
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
*/}
<Component {...pageProps} />
</CookiesProvider>
)
}
PayloadApp.getInitialProps = async (appContext: AppContext) => {
const appProps = await App.getInitialProps(appContext)
const globals = await getAllGlobals()
return {
...appProps,
globals,
}
}
export default PayloadApp

View File

@@ -1,9 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next'
const exitPreview = (req: NextApiRequest, res: NextApiResponse): void => {
res.clearPreviewData()
res.writeHead(200)
res.end()
}
export default exitPreview

View File

@@ -1,44 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next'
// eslint-disable-next-line consistent-return
const preview = async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
const {
cookies: { 'payload-token': payloadToken },
query: { url },
} = req
if (!url) {
return res.status(404).json({
message: 'No URL provided',
})
}
if (!payloadToken) {
return res.status(403).json({
message: 'You are not allowed to preview this page',
})
}
// validate the Payload token
const userReq = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/me`, {
headers: {
Authorization: `JWT ${payloadToken}`,
},
})
const userRes = await userReq.json()
if (!userReq.ok || !userRes?.user) {
return res.status(401).json({
message: 'You are not allowed to preview this page',
})
}
res.setPreviewData({
payloadToken,
})
res.redirect(url as string)
}
export default preview

View File

@@ -1,23 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next'
const revalidate = async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
// Check for secret to confirm this is a valid request
if (req.query.secret !== process.env.NEXT_PRIVATE_REVALIDATION_KEY) {
return res.status(401).json({ message: 'Invalid token' })
}
if (typeof req.query.path === 'string') {
try {
await res.revalidate(req.query.path)
return res.json({ revalidated: true })
} catch (err: unknown) {
// If there was an error, Next.js will continue
// to show the last successfully generated page
return res.status(500).send('Error revalidating')
}
}
return res.status(400).send('No path to revalidate')
}
export default revalidate

View File

@@ -1,236 +0,0 @@
$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;
}
}
$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

@@ -1,11 +0,0 @@
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

@@ -1,265 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
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;
};
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 | null;
richText: {
[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 | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
/**
* 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;
};
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;
}
| unknown[]
| string
| number
| boolean
| 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 | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages_select".
*/
export interface PagesSelect<T extends boolean = true> {
title?: T;
slug?: T;
richText?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu".
*/
export interface MainMenu {
id: string;
navItems?:
| {
link: {
type?: ('reference' | 'custom') | null;
newTab?: boolean | null;
reference?: {
relationTo: 'pages';
value: string | Page;
} | null;
url?: string | null;
label: string;
};
id?: string | null;
}[]
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu_select".
*/
export interface MainMenuSelect<T extends boolean = true> {
navItems?:
| T
| {
link?:
| T
| {
type?: T;
newTab?: T;
reference?: T;
url?: T;
label?: T;
};
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}

View File

@@ -1,31 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [
"DOM",
"DOM.Iterable",
"ES2022"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"next.config.js"
],
"exclude": [
"node_modules"
]
}

View File

@@ -1,16 +0,0 @@
# 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
REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET

View File

@@ -1,5 +0,0 @@
/// <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

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

View File

@@ -1,118 +0,0 @@
$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

@@ -1,7 +0,0 @@
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

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

View File

@@ -1,63 +0,0 @@
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 => {
const pathToUse = doc.slug === 'home' ? '' : doc.slug
const { pathname } = new URL(`${process.env.PAYLOAD_PUBLIC_SITE_URL}/${pathToUse}`)
return pathname
}
export const revalidatePage: CollectionAfterChangeHook<Page> = ({
doc,
operation,
previousDoc,
req,
}) => {
if (process.env.PAYLOAD_PUBLIC_SITE_URL && process.env.REVALIDATION_KEY) {
// Revalidate externally if payload is configured separately from the next app
if (operation === 'update' && doc._status === 'published') {
const url = formatAppURL({ doc })
const revalidate = async (): Promise<void> => {
try {
const res = await fetch(
`${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/revalidate?secret=${process.env.REVALIDATION_KEY}&collection=pages&slug=${doc?.slug}&path=${url}`,
)
if (res.ok) {
req.payload.logger.info(`Revalidated path ${url}`)
} else {
req.payload.logger.error(`Error revalidating path ${url}`)
}
} catch (err: unknown) {
req.payload.logger.error(`Error hitting revalidate route for ${url}`)
}
}
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,51 +0,0 @@
.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

@@ -1,55 +0,0 @@
.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

@@ -1,13 +0,0 @@
.gutter {
max-width: var(--max-width);
width: 100%;
margin: auto;
}
.gutterLeft {
padding-left: var(--gutter-h);
}
.gutterRight {
padding-right: var(--gutter-h);
}

View File

@@ -1,32 +0,0 @@
.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

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

View File

@@ -771,8 +771,8 @@ packages:
'@types/lodash@4.17.13': '@types/lodash@4.17.13':
resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==}
'@types/node@22.9.4': '@types/node@22.10.0':
resolution: {integrity: sha512-d9RWfoR7JC/87vj7n+PVTzGg9hDyuFjir3RxUHbjFSKNd9mpxbxwMEyaCim/ddCmy4IuW7HjTzF3g9p3EtWEOg==} resolution: {integrity: sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==}
'@types/parse-json@4.0.2': '@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
@@ -1197,8 +1197,8 @@ packages:
es-shim-unscopables@1.0.2: es-shim-unscopables@1.0.2:
resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==}
es-to-primitive@1.2.1: es-to-primitive@1.3.0:
resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
esbuild@0.23.1: esbuild@0.23.1:
@@ -1580,8 +1580,8 @@ packages:
is-buffer@1.1.6: is-buffer@1.1.6:
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
is-bun-module@1.2.1: is-bun-module@1.3.0:
resolution: {integrity: sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==} resolution: {integrity: sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==}
is-callable@1.2.7: is-callable@1.2.7:
resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
@@ -1890,8 +1890,8 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nanoid@3.3.7: nanoid@3.3.8:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
@@ -2065,8 +2065,8 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
prettier@3.3.3: prettier@3.4.1:
resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} resolution: {integrity: sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
@@ -2442,8 +2442,8 @@ packages:
truncate-utf8-bytes@1.0.2: truncate-utf8-bytes@1.0.2:
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
ts-api-utils@1.4.1: ts-api-utils@1.4.2:
resolution: {integrity: sha512-5RU2/lxTA3YUZxju61HO2U6EoZLvBLtmV2mbTvqyu4a/7s7RmJPT+1YekhMVsQhznRWk/czIwDUg+V8Q9ZuG4w==} resolution: {integrity: sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw==}
engines: {node: '>=16'} engines: {node: '>=16'}
peerDependencies: peerDependencies:
typescript: '>=4.2.0' typescript: '>=4.2.0'
@@ -2509,8 +2509,8 @@ packages:
unbox-primitive@1.0.2: unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
undici-types@6.19.8: undici-types@6.20.0:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -3252,7 +3252,7 @@ snapshots:
'@types/busboy@1.5.4': '@types/busboy@1.5.4':
dependencies: dependencies:
'@types/node': 22.9.4 '@types/node': 22.10.0
'@types/escape-html@1.0.4': {} '@types/escape-html@1.0.4': {}
@@ -3264,9 +3264,9 @@ snapshots:
'@types/lodash@4.17.13': {} '@types/lodash@4.17.13': {}
'@types/node@22.9.4': '@types/node@22.10.0':
dependencies: dependencies:
undici-types: 6.19.8 undici-types: 6.20.0
'@types/parse-json@4.0.2': {} '@types/parse-json@4.0.2': {}
@@ -3292,7 +3292,7 @@ snapshots:
graphemer: 1.4.0 graphemer: 1.4.0
ignore: 5.3.2 ignore: 5.3.2
natural-compare: 1.4.0 natural-compare: 1.4.0
ts-api-utils: 1.4.1(typescript@5.5.2) ts-api-utils: 1.4.2(typescript@5.5.2)
optionalDependencies: optionalDependencies:
typescript: 5.5.2 typescript: 5.5.2
transitivePeerDependencies: transitivePeerDependencies:
@@ -3322,7 +3322,7 @@ snapshots:
'@typescript-eslint/utils': 8.16.0(eslint@8.57.1)(typescript@5.5.2) '@typescript-eslint/utils': 8.16.0(eslint@8.57.1)(typescript@5.5.2)
debug: 4.3.7 debug: 4.3.7
eslint: 8.57.1 eslint: 8.57.1
ts-api-utils: 1.4.1(typescript@5.5.2) ts-api-utils: 1.4.2(typescript@5.5.2)
optionalDependencies: optionalDependencies:
typescript: 5.5.2 typescript: 5.5.2
transitivePeerDependencies: transitivePeerDependencies:
@@ -3339,7 +3339,7 @@ snapshots:
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 9.0.5 minimatch: 9.0.5
semver: 7.6.3 semver: 7.6.3
ts-api-utils: 1.4.1(typescript@5.5.2) ts-api-utils: 1.4.2(typescript@5.5.2)
optionalDependencies: optionalDependencies:
typescript: 5.5.2 typescript: 5.5.2
transitivePeerDependencies: transitivePeerDependencies:
@@ -3712,7 +3712,7 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
es-object-atoms: 1.0.0 es-object-atoms: 1.0.0
es-set-tostringtag: 2.0.3 es-set-tostringtag: 2.0.3
es-to-primitive: 1.2.1 es-to-primitive: 1.3.0
function.prototype.name: 1.1.6 function.prototype.name: 1.1.6
get-intrinsic: 1.2.4 get-intrinsic: 1.2.4
get-symbol-description: 1.0.2 get-symbol-description: 1.0.2
@@ -3786,7 +3786,7 @@ snapshots:
dependencies: dependencies:
hasown: 2.0.2 hasown: 2.0.2
es-to-primitive@1.2.1: es-to-primitive@1.3.0:
dependencies: dependencies:
is-callable: 1.2.7 is-callable: 1.2.7
is-date-object: 1.0.5 is-date-object: 1.0.5
@@ -3860,7 +3860,7 @@ snapshots:
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.16.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.16.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.16.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.16.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1)
fast-glob: 3.3.2 fast-glob: 3.3.2
get-tsconfig: 4.8.1 get-tsconfig: 4.8.1
is-bun-module: 1.2.1 is-bun-module: 1.3.0
is-glob: 4.0.3 is-glob: 4.0.3
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.16.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.16.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
@@ -4274,7 +4274,7 @@ snapshots:
is-buffer@1.1.6: {} is-buffer@1.1.6: {}
is-bun-module@1.2.1: is-bun-module@1.3.0:
dependencies: dependencies:
semver: 7.6.3 semver: 7.6.3
@@ -4395,7 +4395,7 @@ snapshots:
js-yaml: 4.1.0 js-yaml: 4.1.0
lodash: 4.17.21 lodash: 4.17.21
minimist: 1.2.8 minimist: 1.2.8
prettier: 3.3.3 prettier: 3.4.1
tinyglobby: 0.2.10 tinyglobby: 0.2.10
json-schema-traverse@0.4.1: {} json-schema-traverse@0.4.1: {}
@@ -4559,7 +4559,7 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
nanoid@3.3.7: {} nanoid@3.3.8: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
@@ -4770,13 +4770,13 @@ snapshots:
postcss@8.4.31: postcss@8.4.31:
dependencies: dependencies:
nanoid: 3.3.7 nanoid: 3.3.8
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier@3.3.3: {} prettier@3.4.1: {}
process-warning@4.0.0: {} process-warning@4.0.0: {}
@@ -5211,7 +5211,7 @@ snapshots:
dependencies: dependencies:
utf8-byte-length: 1.0.5 utf8-byte-length: 1.0.5
ts-api-utils@1.4.1(typescript@5.5.2): ts-api-utils@1.4.2(typescript@5.5.2):
dependencies: dependencies:
typescript: 5.5.2 typescript: 5.5.2
@@ -5293,7 +5293,7 @@ snapshots:
has-symbols: 1.0.3 has-symbols: 1.0.3
which-boxed-primitive: 1.0.2 which-boxed-primitive: 1.0.2
undici-types@6.19.8: {} undici-types@6.20.0: {}
uri-js@4.4.1: uri-js@4.4.1:
dependencies: dependencies:

View File

@@ -0,0 +1,25 @@
import type { CollectionAfterChangeHook } from 'payload'
import { revalidatePath } from 'next/cache'
import type { Page } from '../../../payload-types'
export const revalidatePage: CollectionAfterChangeHook<Page> = ({ doc, previousDoc, req }) => {
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

@@ -5,7 +5,7 @@ import { generatePreviewPath } from '../../utilities/generatePreviewPath'
import { loggedIn } from './access/loggedIn' import { loggedIn } from './access/loggedIn'
import { publishedOrLoggedIn } from './access/publishedOrLoggedIn' import { publishedOrLoggedIn } from './access/publishedOrLoggedIn'
import { formatSlug } from './hooks/formatSlug' import { formatSlug } from './hooks/formatSlug'
import { formatAppURL, revalidatePage } from './hooks/revalidatePage' import { revalidatePage } from './hooks/revalidatePage'
export const Pages: CollectionConfig = { export const Pages: CollectionConfig = {
slug: 'pages', slug: 'pages',
@@ -18,24 +18,11 @@ export const Pages: CollectionConfig = {
admin: { admin: {
defaultColumns: ['title', 'slug', 'updatedAt'], defaultColumns: ['title', 'slug', 'updatedAt'],
preview: (doc) => { preview: (doc) => {
if (process.env.PAYLOAD_PUBLIC_SITE_URL && process.env.PAYLOAD_PUBLIC_DRAFT_SECRET) {
// Separate Payload and front-end setup
return `${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/preview?url=${encodeURIComponent(
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({ const path = generatePreviewPath({
slug: typeof doc?.slug === 'string' ? doc.slug : '', slug: typeof doc?.slug === 'string' ? doc.slug : '',
collection: 'pages', collection: 'pages',
}) })
return `${process.env.NEXT_PUBLIC_SERVER_URL}${path}` return `${process.env.NEXT_PUBLIC_SERVER_URL}${path}`
}
// Fallback for missing environment variables
throw new Error(
'Environment variables for preview functionality are not set. Ensure that either PAYLOAD_PUBLIC_SITE_URL and PAYLOAD_PUBLIC_DRAFT_SECRET, or NEXT_PUBLIC_SERVER_URL are defined.',
)
}, },
useAsTitle: 'title', useAsTitle: 'title',
}, },

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