chore: moves live preview test suite into main app folder

This commit is contained in:
James
2024-03-18 14:27:56 -04:00
parent 70f29785ca
commit 5eaea1c7f1
93 changed files with 276 additions and 1279 deletions

View File

@@ -1,35 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [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 Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -1,39 +0,0 @@
'use client'
import type { Page as PageType } from '@/payload-types'
import { PAYLOAD_SERVER_URL } from '@/app/_api/serverURL'
import { Blocks } from '@/app/_components/Blocks'
import { Hero } from '@/app/_components/Hero'
import React from 'react'
import { useLivePreview } from '../../../../../../packages/live-preview-react/src'
export const PageClient: React.FC<{
page: PageType
}> = ({ page: initialPage }) => {
const { data } = useLivePreview<PageType>({
depth: 2,
initialData: initialPage,
serverURL: PAYLOAD_SERVER_URL,
})
return (
<React.Fragment>
<Hero {...data?.hero} />
<Blocks
blocks={[
...(data?.layout ?? []),
{
blockName: 'Relationships',
blockType: 'relationships',
data,
},
]}
disableTopPadding={
!data?.hero || data?.hero?.type === 'none' || data?.hero?.type === 'lowImpact'
}
/>
</React.Fragment>
)
}

View File

@@ -1,36 +0,0 @@
import { fetchDoc } from '@/app/_api/fetchDoc'
import { fetchDocs } from '@/app/_api/fetchDocs'
import { notFound } from 'next/navigation'
import React from 'react'
import type { Page } from '../../../payload-types'
import { PageClient } from './page.client'
export default async function Page({ params: { slug = 'home' } }) {
let page: Page | null = null
try {
page = await fetchDoc<Page>({
slug,
collection: 'pages',
})
} catch (error) {
console.error(error)
}
if (!page) {
return notFound()
}
return <PageClient page={page} />
}
export async function generateStaticParams() {
try {
const pages = await fetchDocs<Page>('pages')
return pages?.map(({ slug }) => slug)
} catch (error) {
return []
}
}

View File

@@ -1,68 +0,0 @@
'use client'
import type { Post as PostType } from '@/payload-types'
import { PAYLOAD_SERVER_URL } from '@/app/_api/serverURL'
import { Blocks } from '@/app/_components/Blocks'
import { PostHero } from '@/app/_heros/PostHero'
import React from 'react'
import { useLivePreview } from '../../../../../../../packages/live-preview-react/src'
export const PostClient: React.FC<{
post: PostType
}> = ({ post: initialPost }) => {
const { data } = useLivePreview<PostType>({
depth: 2,
initialData: initialPost,
serverURL: PAYLOAD_SERVER_URL,
})
return (
<React.Fragment>
<PostHero post={data} />
<Blocks blocks={data?.layout} />
<Blocks
blocks={[
{
blockName: 'Related Posts',
blockType: 'relatedPosts',
docs: data?.relatedPosts,
introContent: [
{
type: 'h4',
children: [
{
text: 'Related posts',
},
],
},
{
type: 'p',
children: [
{
text: 'The posts displayed here are individually selected for this page. Admins can select any number of related posts to display here and the layout will adjust accordingly. Alternatively, you could swap this out for the "Archive" block to automatically populate posts by category complete with pagination. To manage related posts, ',
},
{
type: 'link',
children: [
{
text: 'navigate to the admin dashboard',
},
],
url: `/admin/collections/posts/${data?.id}`,
},
{
text: '.',
},
],
},
],
relationTo: 'posts',
},
]}
disableTopPadding
/>
</React.Fragment>
)
}

View File

@@ -1,36 +0,0 @@
import { notFound } from 'next/navigation'
import React from 'react'
import type { Post } from '../../../../payload-types'
import { fetchDoc } from '../../../_api/fetchDoc'
import { fetchDocs } from '../../../_api/fetchDocs'
import { PostClient } from './page.client'
export default async function Post({ params: { slug = '' } }) {
let post: Post | null = null
try {
post = await fetchDoc<Post>({
slug,
collection: 'posts',
})
} catch (error) {
console.error(error) // eslint-disable-line no-console
}
if (!post) {
notFound()
}
return <PostClient post={post} />
}
export async function generateStaticParams() {
try {
const posts = await fetchDocs<Post>('posts')
return posts?.map(({ slug }) => slug)
} catch (error) {
return []
}
}

View File

@@ -1,37 +0,0 @@
import QueryString from 'qs'
import type { Config } from '../../payload-types'
import { PAYLOAD_SERVER_URL } from './serverURL'
export const fetchDoc = async <T>(args: {
collection: keyof Config['collections']
depth?: number
id?: string
slug?: string
}): Promise<T> => {
const { collection, slug, id, depth = 2 } = args || {}
const queryString = QueryString.stringify(
{
...(slug ? { 'where[slug][equals]': slug } : {}),
...(depth ? { depth } : {}),
},
{ addQueryPrefix: true },
)
const doc: T = await fetch(`${PAYLOAD_SERVER_URL}/api/${collection}${queryString}`, {
method: 'GET',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
})
?.then((res) => res.json())
?.then((res) => {
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching doc')
return res?.docs?.[0]
})
return doc
}

View File

@@ -1,21 +0,0 @@
import type { Config } from '../../payload-types'
import { PAYLOAD_SERVER_URL } from './serverURL'
export const fetchDocs = async <T>(collection: keyof Config['collections']): Promise<T[]> => {
const docs: T[] = await fetch(`${PAYLOAD_SERVER_URL}/api/${collection}?depth=0&limit=100`, {
method: 'GET',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
})
?.then((res) => res.json())
?.then((res) => {
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching docs')
return res?.docs
})
return docs
}

View File

@@ -1,25 +0,0 @@
import type { Footer } from '../../payload-types'
import { PAYLOAD_SERVER_URL } from './serverURL'
export async function fetchFooter(): Promise<Footer> {
if (!PAYLOAD_SERVER_URL) throw new Error('PAYLOAD_SERVER_URL not found')
const footer = await fetch(`${PAYLOAD_SERVER_URL}/api/globals/footer`, {
method: 'GET',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
})
.then((res) => {
if (!res.ok) throw new Error('Error fetching doc')
return res.json()
})
?.then((res) => {
if (res?.errors) throw new Error(res?.errors[0]?.message || 'Error fetching footer')
return res
})
return footer
}

View File

@@ -1,25 +0,0 @@
import type { Header } from '../../payload-types'
import { PAYLOAD_SERVER_URL } from './serverURL'
export async function fetchHeader(): Promise<Header> {
if (!PAYLOAD_SERVER_URL) throw new Error('PAYLOAD_SERVER_URL not found')
const header = await fetch(`${PAYLOAD_SERVER_URL}/api/globals/header`, {
method: 'GET',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
})
?.then((res) => {
if (!res.ok) throw new Error('Error fetching doc')
return res.json()
})
?.then((res) => {
if (res?.errors) throw new Error(res?.errors[0]?.message || 'Error fetching header')
return res
})
return header
}

View File

@@ -1 +0,0 @@
export const PAYLOAD_SERVER_URL = 'http://localhost:3000'

View File

@@ -1,13 +0,0 @@
@import '../../_css/common';
.archiveBlock {
position: relative;
}
.introContent {
margin-bottom: calc(var(--base) * 2);
@include mid-break {
margin-bottom: calc(var(--base) * 2);
}
}

View File

@@ -1,46 +0,0 @@
import React from 'react'
import type { ArchiveBlockProps } from './types'
import { CollectionArchive } from '../../_components/CollectionArchive'
import { Gutter } from '../../_components/Gutter'
import RichText from '../../_components/RichText'
import classes from './index.module.scss'
export const ArchiveBlock: React.FC<
ArchiveBlockProps & {
id?: string
}
> = (props) => {
const {
id,
categories,
introContent,
limit,
populateBy,
populatedDocs,
populatedDocsTotal,
relationTo,
selectedDocs,
} = props
return (
<div className={classes.archiveBlock} id={`block-${id}`}>
{introContent && (
<Gutter className={classes.introContent}>
<RichText content={introContent} />
</Gutter>
)}
<CollectionArchive
categories={categories}
limit={limit}
populateBy={populateBy}
populatedDocs={populatedDocs}
populatedDocsTotal={populatedDocsTotal}
relationTo={relationTo}
selectedDocs={selectedDocs}
sort="-publishedDate"
/>
</div>
)
}

View File

@@ -1,6 +0,0 @@
import type { Page } from '../../../payload-types'
export type ArchiveBlockProps = Extract<
Exclude<Page['layout'], undefined>[0],
{ blockType: 'archive' }
>

View File

@@ -1,47 +0,0 @@
@use '../../_css/queries.scss' as *;
$spacer-h: calc(var(--block-padding) / 2);
.callToAction {
padding-left: $spacer-h;
padding-right: $spacer-h;
position: relative;
background-color: var(--color-base-100);
color: var(--color-base-1000);
}
.invert {
background-color: var(--color-base-1000);
color: var(--color-base-0);
}
.wrap {
display: flex;
gap: $spacer-h;
align-items: center;
@include mid-break {
flex-direction: column;
align-items: flex-start;
}
}
.content {
flex-grow: 1;
}
.linkGroup {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
flex-shrink: 0;
> * {
margin-bottom: calc(var(--base) / 2);
&:last-child {
margin-bottom: 0;
}
}
}

View File

@@ -1,38 +0,0 @@
import React from 'react'
import type { Page } from '../../../payload-types'
import { Gutter } from '../../_components/Gutter'
import { CMSLink } from '../../_components/Link'
import RichText from '../../_components/RichText'
import { VerticalPadding } from '../../_components/VerticalPadding'
import classes from './index.module.scss'
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'cta' }>
export const CallToActionBlock: React.FC<
Props & {
id?: string
}
> = ({ invertBackground, links, richText }) => {
return (
<Gutter>
<VerticalPadding
className={[classes.callToAction, invertBackground && classes.invert]
.filter(Boolean)
.join(' ')}
>
<div className={classes.wrap}>
<div className={classes.content}>
<RichText className={classes.richText} content={richText} />
</div>
<div className={classes.linkGroup}>
{(links || []).map(({ link }, i) => {
return <CMSLink key={i} {...link} invert={invertBackground} />
})}
</div>
</div>
</VerticalPadding>
</Gutter>
)
}

View File

@@ -1,42 +0,0 @@
@import '../../_css/common';
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: var(--base) calc(var(--base) * 2);
@include mid-break {
grid-template-columns: repeat(6, 1fr);
gap: var(--base) var(--base);
}
}
.column--oneThird {
grid-column-end: span 4;
}
.column--half {
grid-column-end: span 6;
}
.column--twoThirds {
grid-column-end: span 8;
}
.column--full {
grid-column-end: span 12;
}
.column {
@include mid-break {
grid-column-end: span 6;
}
@include small-break {
grid-column-end: span 6;
}
}
.link {
margin-top: var(--base);
}

View File

@@ -1,39 +0,0 @@
import React, { Fragment } from 'react'
import type { Page } from '../../../payload-types'
import { Gutter } from '../../_components/Gutter'
import { CMSLink } from '../../_components/Link'
import RichText from '../../_components/RichText'
import classes from './index.module.scss'
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'content' }>
export const ContentBlock: React.FC<
Props & {
id?: string
}
> = (props) => {
const { columns } = props
return (
<Gutter className={classes.content}>
<div className={classes.grid}>
{columns && columns.length > 0 ? (
<Fragment>
{columns.map((col, index) => {
const { enableLink, link, richText, size } = col
return (
<div className={[classes.column, classes[`column--${size}`]].join(' ')} key={index}>
<RichText content={richText} />
{enableLink && <CMSLink className={classes.link} {...link} />}
</div>
)
})}
</Fragment>
) : null}
</div>
</Gutter>
)
}

View File

@@ -1,8 +0,0 @@
.mediaBlock {
position: relative;
}
.caption {
color: var(--color-base-500);
margin-top: var(--base);
}

View File

@@ -1,42 +0,0 @@
import type { StaticImageData } from 'next/image'
import React from 'react'
import type { Page } from '../../../payload-types'
import { Gutter } from '../../_components/Gutter'
import { Media } from '../../_components/Media'
import RichText from '../../_components/RichText'
import classes from './index.module.scss'
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'mediaBlock' }> & {
id?: string
staticImage?: StaticImageData
}
export const MediaBlock: React.FC<Props> = (props) => {
const { media, position = 'default', staticImage } = props
let caption
if (media && typeof media === 'object') caption = media.caption
return (
<div className={classes.mediaBlock}>
{position === 'fullscreen' && (
<div className={classes.fullscreen}>
<Media resource={media} src={staticImage} />
</div>
)}
{position === 'default' && (
<Gutter>
<Media resource={media} src={staticImage} />
</Gutter>
)}
{caption && (
<Gutter className={classes.caption}>
<RichText content={caption} />
</Gutter>
)}
</div>
)
}

View File

@@ -1,58 +0,0 @@
@import '../../_css/common';
.introContent {
position: relative;
margin-bottom: calc(var(--base) * 2);
@include mid-break {
margin-bottom: var(--base);
}
}
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
width: 100%;
gap: var(--base) 40px;
@include mid-break {
grid-template-columns: repeat(6, 1fr);
gap: calc(var(--base) / 2) var(--base);
}
}
.column {
grid-column-end: span 12;
@include mid-break {
grid-column-end: span 6;
}
@include small-break {
grid-column-end: span 6;
}
}
.cols-half {
grid-column-end: span 6;
@include mid-break {
grid-column-end: span 6;
}
@include small-break {
grid-column-end: span 6;
}
}
.cols-thirds {
grid-column-end: span 3;
@include mid-break {
grid-column-end: span 6;
}
@include small-break {
grid-column-end: span 6;
}
}

View File

@@ -1,52 +0,0 @@
import React from 'react'
import type { Post } from '../../../payload-types'
import { Card } from '../../_components/Card'
import { Gutter } from '../../_components/Gutter'
import RichText from '../../_components/RichText'
import classes from './index.module.scss'
export type RelatedPostsProps = {
blockName: string
blockType: 'relatedPosts'
docs?: (Post | string)[] | null
introContent?: any
relationTo: 'posts'
}
export const RelatedPosts: React.FC<RelatedPostsProps> = (props) => {
const { docs, introContent, relationTo } = props
return (
<div className={classes.relatedPosts}>
{introContent && (
<Gutter className={classes.introContent}>
<RichText content={introContent} />
</Gutter>
)}
<Gutter>
<div className={classes.grid}>
{docs?.map((doc, index) => {
if (typeof doc === 'string') return null
return (
<div
className={[
classes.column,
docs.length === 2 && classes['cols-half'],
docs.length >= 3 && classes['cols-thirds'],
]
.filter(Boolean)
.join(' ')}
key={index}
>
<Card doc={doc} relationTo={relationTo} showCategories />
</div>
)
})}
</div>
</Gutter>
</div>
)
}

View File

@@ -1,17 +0,0 @@
@import '../../_css/common';
.relationshipsBlock {
}
.array {
border: 1px solid var(--color-base-100);
padding: var(--base);
& > *:first-child {
margin-top: 0;
}
& > *:last-child {
margin-bottom: 0;
}
}

View File

@@ -1,196 +0,0 @@
import { Gutter } from '@/app/_components/Gutter'
import RichText from '@/app/_components/RichText'
import React, { Fragment } from 'react'
import type { Page } from '../../../payload-types'
import classes from './index.module.scss'
export type RelationshipsBlockProps = {
blockName: string
blockType: 'relationships'
data: Page
}
export const RelationshipsBlock: React.FC<RelationshipsBlockProps> = (props) => {
const { data } = props
return (
<div className={classes.relationshipsBlock}>
<Gutter>
<p>
This block is for testing purposes only. It renders every possible type of relationship.
</p>
<p>
<b>Rich Text Slate:</b>
</p>
{data?.richTextSlate && <RichText content={data.richTextSlate} renderUploadFilenameOnly />}
<p>
<b>Rich Text Lexical:</b>
</p>
{data?.richTextLexical && (
<RichText content={data.richTextLexical} renderUploadFilenameOnly serializer="lexical" />
)}
<p>
<b>Upload:</b>
</p>
{data?.relationshipAsUpload ? (
<div>
{typeof data?.relationshipAsUpload === 'string'
? data?.relationshipAsUpload
: data?.relationshipAsUpload.filename}
</div>
) : (
<div>None</div>
)}
<p>
<b>Monomorphic Has One:</b>
</p>
{data?.relationshipMonoHasOne ? (
<div>
{typeof data?.relationshipMonoHasOne === 'string'
? data?.relationshipMonoHasOne
: data?.relationshipMonoHasOne.title}
</div>
) : (
<div>None</div>
)}
<p>
<b>Monomorphic Has Many:</b>
</p>
{data?.relationshipMonoHasMany ? (
<Fragment>
{data?.relationshipMonoHasMany.length
? data?.relationshipMonoHasMany?.map((item, index) =>
item ? (
<div key={index}>{typeof item === 'string' ? item : item.title}</div>
) : (
'null'
),
)
: 'None'}
</Fragment>
) : (
<div>None</div>
)}
<p>
<b>Polymorphic Has One:</b>
</p>
{data?.relationshipPolyHasOne ? (
<div>
{typeof data?.relationshipPolyHasOne.value === 'string'
? data?.relationshipPolyHasOne.value
: data?.relationshipPolyHasOne.value.title}
</div>
) : (
<div>None</div>
)}
<p>
<b>Polymorphic Has Many:</b>
</p>
{data?.relationshipPolyHasMany ? (
<Fragment>
{data?.relationshipPolyHasMany.length
? data?.relationshipPolyHasMany?.map((item, index) =>
item.value ? (
<div key={index}>
{typeof item.value === 'string' ? item.value : item.value.title}
</div>
) : (
'null'
),
)
: 'None'}
</Fragment>
) : (
<div>None</div>
)}
<p>
<b>Array of Relationships:</b>
</p>
{data?.arrayOfRelationships?.map((item, index) => (
<div className={classes.array} key={index}>
<p>
<b>Rich Text:</b>
</p>
{item?.richTextInArray && <RichText content={item.richTextInArray} />}
<p>
<b>Upload:</b>
</p>
{item?.uploadInArray ? (
<div>
{typeof item?.uploadInArray === 'string'
? item?.uploadInArray
: item?.uploadInArray.filename}
</div>
) : (
<div>None</div>
)}
<p>
<b>Monomorphic Has One:</b>
</p>
{item?.relationshipInArrayMonoHasOne ? (
<div>
{typeof item?.relationshipInArrayMonoHasOne === 'string'
? item?.relationshipInArrayMonoHasOne
: item?.relationshipInArrayMonoHasOne.title}
</div>
) : (
<div>None</div>
)}
<p>
<b>Monomorphic Has Many:</b>
</p>
{item?.relationshipInArrayMonoHasMany ? (
<Fragment>
{item?.relationshipInArrayMonoHasMany.length
? item?.relationshipInArrayMonoHasMany?.map((rel, relIndex) =>
rel ? (
<div key={relIndex}>{typeof rel === 'string' ? rel : rel.title}</div>
) : (
'null'
),
)
: 'None'}
</Fragment>
) : (
<div>None</div>
)}
<p>
<b>Polymorphic Has One:</b>
</p>
{item?.relationshipInArrayPolyHasOne ? (
<div>
{typeof item?.relationshipInArrayPolyHasOne.value === 'string'
? item?.relationshipInArrayPolyHasOne.value
: item?.relationshipInArrayPolyHasOne.value.title}
</div>
) : (
<div>None</div>
)}
<p>
<b>Polymorphic Has Many:</b>
</p>
{item?.relationshipInArrayPolyHasMany ? (
<Fragment>
{item?.relationshipInArrayPolyHasMany.length
? item?.relationshipInArrayPolyHasMany?.map((rel, relIndex) =>
rel.value ? (
<div key={relIndex}>
{typeof rel.value === 'string' ? rel.value : rel.value.title}
</div>
) : (
'null'
),
)
: 'None'}
</Fragment>
) : (
<div>None</div>
)}
</div>
))}
</Gutter>
</div>
)
}

View File

@@ -1,3 +0,0 @@
.invert {
background-color: var(--color-base-750);
}

View File

@@ -1,20 +0,0 @@
import React from 'react'
import classes from './index.module.scss'
type Props = {
children?: React.ReactNode
className?: string
id?: string
invert?: boolean | null
}
export const BackgroundColor: React.FC<Props> = (props) => {
const { id, children, className, invert } = props
return (
<div className={[invert && classes.invert, className].filter(Boolean).join(' ')} id={id}>
{children}
</div>
)
}

View File

@@ -1,88 +0,0 @@
import React, { Fragment } from 'react'
import type { Page } from '../../../payload-types.js'
import type { RelationshipsBlockProps } from '../../_blocks/Relationships'
import type { VerticalPaddingOptions } from '../VerticalPadding'
import { ArchiveBlock } from '../../_blocks/ArchiveBlock'
import { CallToActionBlock } from '../../_blocks/CallToAction'
import { ContentBlock } from '../../_blocks/Content'
import { MediaBlock } from '../../_blocks/MediaBlock'
import { RelatedPosts, type RelatedPostsProps } from '../../_blocks/RelatedPosts'
import { RelationshipsBlock } from '../../_blocks/Relationships'
import { toKebabCase } from '../../_utilities/toKebabCase'
import { BackgroundColor } from '../BackgroundColor'
import { VerticalPadding } from '../VerticalPadding'
const blockComponents = {
archive: ArchiveBlock,
content: ContentBlock,
cta: CallToActionBlock,
mediaBlock: MediaBlock,
relatedPosts: RelatedPosts,
relationships: RelationshipsBlock,
}
type Block = NonNullable<Page['layout']>[number]
export const Blocks: React.FC<{
blocks?: (Block | RelatedPostsProps | RelationshipsBlockProps)[] | null
disableTopPadding?: boolean
}> = (props) => {
const { blocks, disableTopPadding } = props
const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0
if (hasBlocks) {
return (
<Fragment>
{blocks.map((block, index) => {
const { blockName, blockType } = block
if (blockType && blockType in blockComponents) {
const Block = blockComponents[blockType]
// the cta block is containerized, so we don't consider it to be inverted at the block-level
const blockIsInverted =
'invertBackground' in block && blockType !== 'cta' ? block.invertBackground : false
const prevBlock = blocks[index - 1]
const prevBlockInverted =
prevBlock && 'invertBackground' in prevBlock && prevBlock?.invertBackground
const isPrevSame = Boolean(blockIsInverted) === Boolean(prevBlockInverted)
let paddingTop: VerticalPaddingOptions = 'large'
let paddingBottom: VerticalPaddingOptions = 'large'
if (prevBlock && isPrevSame) {
paddingTop = 'none'
}
if (index === blocks.length - 1) {
paddingBottom = 'large'
}
if (disableTopPadding && index === 0) {
paddingTop = 'none'
}
if (Block) {
return (
<BackgroundColor invert={blockIsInverted} key={index}>
<VerticalPadding bottom={paddingBottom} top={paddingTop}>
{/* @ts-expect-error */}
<Block id={toKebabCase(blockName)} {...block} />
</VerticalPadding>
</BackgroundColor>
)
}
}
return null
})}
</Fragment>
)
}
return null
}

View File

@@ -1,72 +0,0 @@
@import '../../_css/type.scss';
.button {
border: none;
cursor: pointer;
display: inline-flex;
justify-content: center;
background-color: transparent;
text-decoration: none;
display: inline-flex;
padding: 12px 24px;
font-family: inherit;
line-height: inherit;
font-size: inherit;
}
.content {
display: flex;
align-items: center;
justify-content: space-around;
svg {
margin-right: calc(var(--base) / 2);
width: var(--base);
height: var(--base);
}
}
.label {
@extend %label;
text-align: center;
display: flex;
align-items: center;
}
.appearance--primary {
background-color: var(--color-base-1000);
color: var(--color-base-0);
}
.appearance--secondary {
background-color: transparent;
box-shadow: inset 0 0 0 1px var(--color-base-1000);
}
.primary--invert {
background-color: var(--color-base-0);
color: var(--color-base-1000);
}
.secondary--invert {
background-color: var(--color-base-1000);
box-shadow: inset 0 0 0 1px var(--color-base-0);
}
.appearance--default {
padding: 0;
color: var(--theme-text);
}
.appearance--none {
padding: 0;
color: var(--theme-text);
&:local() {
.label {
text-transform: none;
line-height: inherit;
font-size: inherit;
}
}
}

View File

@@ -1,78 +0,0 @@
'use client'
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' | 'none' | 'primary' | 'secondary'
className?: string
disabled?: boolean
el?: 'a' | 'button' | 'link'
href?: string
invert?: boolean
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,
invert,
label,
newTab,
onClick,
}) => {
let el = elFromProps
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
const className = [
classes.button,
classNameFromProps,
classes[`appearance--${appearance}`],
invert && classes[`${appearance}--invert`],
]
.filter(Boolean)
.join(' ')
const content = (
<div className={classes.content}>
<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,106 +0,0 @@
@import '../../_css/common';
.card {
border: 1px var(--color-base-200) solid;
border-radius: 4px;
height: 100%;
display: flex;
flex-direction: column;
}
.vertical {
flex-direction: column;
}
.horizontal {
flex-direction: row;
&:local() {
.mediaWrapper {
width: 150px;
@include mid-break {
width: 100%;
}
}
}
@include mid-break {
flex-direction: column;
}
}
.content {
padding: var(--base);
flex-grow: 1;
display: flex;
flex-direction: column;
gap: calc(var(--base) / 2);
@include small-break {
padding: calc(var(--base) / 2);
gap: calc(var(--base) / 4);
}
}
.title {
margin: 0;
}
.titleLink {
text-decoration: none;
}
.centerAlign {
align-items: center;
}
.body {
flex-grow: 1;
}
.leader {
@extend %label;
display: flex;
gap: var(--base);
}
.description {
margin: 0;
}
.hideImageOnMobile {
@include mid-break {
display: none;
}
}
.mediaWrapper {
text-decoration: none;
display: block;
position: relative;
aspect-ratio: 16 / 9;
}
.image {
object-fit: cover;
}
.placeholder {
background-color: var(--color-base-50);
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.actions {
display: flex;
align-items: center;
@include mid-break {
flex-direction: column;
align-items: flex-start;
}
}

View File

@@ -1,86 +0,0 @@
import Link from 'next/link'
import React, { Fragment } from 'react'
import type { Post } from '../../../payload-types'
import { Media } from '../Media'
import classes from './index.module.scss'
export const Card: React.FC<{
alignItems?: 'center'
className?: string
doc?: Post
hideImagesOnMobile?: boolean
orientation?: 'horizontal' | 'vertical'
relationTo?: 'posts'
showCategories?: boolean
title?: string
}> = (props) => {
const {
className,
doc,
orientation = 'vertical',
relationTo,
showCategories,
title: titleFromProps,
} = props
const { slug, categories, meta, title } = doc || {}
const { description, image: metaImage } = meta || {}
const hasCategories = categories && Array.isArray(categories) && categories.length > 0
const titleToUse = titleFromProps || title
const sanitizedDescription = description?.replace(/\s/g, ' ') // replace non-breaking space with white space
const href = `/${relationTo}/${slug}`
return (
<div
className={[classes.card, className, orientation && classes[orientation]]
.filter(Boolean)
.join(' ')}
>
<Link className={classes.mediaWrapper} href={href}>
{!metaImage && <div className={classes.placeholder}>No image</div>}
{metaImage && typeof metaImage !== 'string' && (
<Media fill imgClassName={classes.image} resource={metaImage} />
)}
</Link>
<div className={classes.content}>
{showCategories && hasCategories && (
<div className={classes.leader}>
{showCategories && hasCategories && (
<div>
{categories?.map((category, index) => {
const titleFromCategory = typeof category === 'string' ? category : category.title
const categoryTitle = titleFromCategory || 'Untitled category'
const isLast = index === categories.length - 1
return (
<Fragment key={index}>
{categoryTitle}
{!isLast && <Fragment>, &nbsp;</Fragment>}
</Fragment>
)
})}
</div>
)}
</div>
)}
{titleToUse && (
<h4 className={classes.title}>
<Link className={classes.titleLink} href={href}>
{titleToUse}
</Link>
</h4>
)}
{description && (
<div className={classes.body}>
{description && <p className={classes.description}>{sanitizedDescription}</p>}
</div>
)}
</div>
</div>
)
}

View File

@@ -1,26 +0,0 @@
import React from 'react'
export const Chevron: React.FC<{
className?: string
rotate?: number
}> = ({ className, rotate }) => {
return (
<svg
className={className}
height="100%"
style={{
transform: typeof rotate === 'number' ? `rotate(${rotate || 0}deg)` : undefined,
}}
viewBox="0 0 24 24"
width="100%"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M23.245 4l-11.245 14.374-11.219-14.374-.781.619 12 15.381 12-15.391-.755-.609z"
fill="none"
stroke="currentColor"
vectorEffect="non-scaling-stroke"
/>
</svg>
)
}

View File

@@ -1,73 +0,0 @@
@import '../../../_css/common';
// this is to make up for the space taken by the fixed header, since the scroll method does not accept an offset parameter
.scrollRef {
position: absolute;
left: 0;
top: calc(var(--base) * -5);
@include mid-break {
top: calc(var(--base) * -2);
}
}
.introContent {
position: relative;
margin-bottom: calc(var(--base) * 2);
@include mid-break {
margin-bottom: var(--base);
}
}
.resultCountWrapper {
display: flex;
margin-bottom: calc(var(--base) * 2);
@include mid-break {
margin-bottom: var(--base);
}
}
.pageRange {
margin-bottom: var(--base);
@include mid-break {
margin-bottom: var(--base);
}
}
.list {
position: relative;
}
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
width: 100%;
gap: var(--base) 40px;
@include mid-break {
grid-template-columns: repeat(6, 1fr);
gap: calc(var(--base) / 2) var(--base);
}
}
.column {
grid-column-end: span 4;
@include mid-break {
grid-column-end: span 6;
}
@include small-break {
grid-column-end: span 6;
}
}
.pagination {
margin-top: calc(var(--base) * 2);
@include mid-break {
margin-top: var(--base);
}
}

View File

@@ -1,188 +0,0 @@
'use client'
import { PAYLOAD_SERVER_URL } from '@/app/_api/serverURL'
import qs from 'qs'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import type { Post } from '../../../../payload-types'
import type { ArchiveBlockProps } from '../../../_blocks/ArchiveBlock/types'
import { Card } from '../../Card'
import { Gutter } from '../../Gutter'
import { PageRange } from '../../PageRange'
import { Pagination } from '../../Pagination'
import classes from './index.module.scss'
type Result = {
docs: (Post | string)[]
hasNextPage: boolean
hasPrevPage: boolean
nextPage: number
page: number
prevPage: number
totalDocs: number
totalPages: number
}
export type Props = Omit<ArchiveBlockProps, 'blockType'> & {
className?: string
onResultChange?: (result: Result) => void // eslint-disable-line no-unused-vars
showPageRange?: boolean
sort?: string
}
export const CollectionArchiveByCollection: React.FC<Props> = (props) => {
const {
categories: catsFromProps,
className,
limit = 10,
onResultChange,
populatedDocs,
populatedDocsTotal,
relationTo,
showPageRange,
sort = '-createdAt',
} = props
const [results, setResults] = useState<Result>({
docs: populatedDocs?.map((doc) => doc.value) || [],
hasNextPage: false,
hasPrevPage: false,
nextPage: 1,
page: 1,
prevPage: 1,
totalDocs: typeof populatedDocsTotal === 'number' ? populatedDocsTotal : 0,
totalPages: 1,
})
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | undefined>(undefined)
const scrollRef = useRef<HTMLDivElement>(null)
const hasHydrated = useRef(false)
const [page, setPage] = useState(1)
const scrollToRef = useCallback(() => {
const { current } = scrollRef
if (current) {
// current.scrollIntoView({
// behavior: 'smooth',
// })
}
}, [])
useEffect(() => {
if (!isLoading && typeof results.page !== 'undefined') {
// scrollToRef()
}
}, [isLoading, scrollToRef, results])
useEffect(() => {
let timer: NodeJS.Timeout
// hydrate the block with fresh content after first render
// don't show loader unless the request takes longer than x ms
// and don't show it during initial hydration
timer = setTimeout(() => {
if (hasHydrated) {
setIsLoading(true)
}
}, 500)
const searchQuery = qs.stringify(
{
depth: 1,
limit,
page,
sort,
where: {
...(catsFromProps && catsFromProps?.length > 0
? {
categories: {
in:
typeof catsFromProps === 'string'
? [catsFromProps]
: catsFromProps
.map((cat) => (typeof cat === 'object' && cat !== null ? cat.id : cat))
.join(','),
},
}
: {}),
},
},
{ encode: false },
)
const makeRequest = async () => {
try {
const req = await fetch(`${PAYLOAD_SERVER_URL}/api/${relationTo}?${searchQuery}`)
const json = await req.json()
clearTimeout(timer)
hasHydrated.current = true
const { docs } = json as { docs: Post[] }
if (docs && Array.isArray(docs)) {
setResults(json)
setIsLoading(false)
if (typeof onResultChange === 'function') {
onResultChange(json)
}
}
} catch (err) {
console.warn(err) // eslint-disable-line no-console
setIsLoading(false)
setError(`Unable to load "${relationTo} archive" data at this time.`)
}
}
makeRequest()
return () => {
if (timer) clearTimeout(timer)
}
}, [page, catsFromProps, relationTo, onResultChange, sort, limit])
return (
<div className={[classes.collectionArchive, className].filter(Boolean).join(' ')}>
<div className={classes.scrollRef} ref={scrollRef} />
{!isLoading && error && <Gutter>{error}</Gutter>}
<Fragment>
{showPageRange !== false && (
<Gutter>
<div className={classes.pageRange}>
<PageRange
collection={relationTo}
currentPage={results.page}
limit={limit}
totalDocs={results.totalDocs}
/>
</div>
</Gutter>
)}
<Gutter>
<div className={classes.grid}>
{results.docs?.map((result, index) => {
if (typeof result === 'string') {
return null
}
return (
<div className={classes.column} key={index}>
<Card doc={result} relationTo="posts" showCategories />
</div>
)
})}
</div>
{results.totalPages > 1 && (
<Pagination
className={classes.pagination}
onClick={setPage}
page={results.page}
totalPages={results.totalPages}
/>
)}
</Gutter>
</Fragment>
</div>
)
}

View File

@@ -1,25 +0,0 @@
@import '../../../_css/common';
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
width: 100%;
gap: var(--base) 40px;
@include mid-break {
grid-template-columns: repeat(6, 1fr);
gap: calc(var(--base) / 2) var(--base);
}
}
.column {
grid-column-end: span 4;
@include mid-break {
grid-column-end: span 6;
}
@include small-break {
grid-column-end: span 6;
}
}

View File

@@ -1,42 +0,0 @@
'use client'
import React, { Fragment } from 'react'
import type { ArchiveBlockProps } from '../../../_blocks/ArchiveBlock/types'
import { Card } from '../../Card'
import { Gutter } from '../../Gutter'
import classes from './index.module.scss'
export type Props = {
className?: string
selectedDocs?: ArchiveBlockProps['selectedDocs']
}
export const CollectionArchiveBySelection: React.FC<Props> = (props) => {
const { className, selectedDocs } = props
const result = selectedDocs?.map((doc) => doc.value)
return (
<div className={[classes.collectionArchive, className].filter(Boolean).join(' ')}>
<Fragment>
<Gutter>
<div className={classes.grid}>
{result?.map((result, index) => {
if (typeof result === 'string') {
return null
}
return (
<div className={classes.column} key={index}>
<Card doc={result} relationTo="posts" showCategories />
</div>
)
})}
</div>
</Gutter>
</Fragment>
</div>
)
}

View File

@@ -1,25 +0,0 @@
import React from 'react'
import type { ArchiveBlockProps } from '../../_blocks/ArchiveBlock/types'
import { CollectionArchiveByCollection } from './PopulateByCollection'
import { CollectionArchiveBySelection } from './PopulateBySelection'
export type Props = Omit<ArchiveBlockProps, 'blockType'> & {
className?: string
sort?: string
}
export const CollectionArchive: React.FC<Props> = (props) => {
const { className, populateBy, selectedDocs } = props
if (populateBy === 'selection') {
return <CollectionArchiveBySelection className={className} selectedDocs={selectedDocs} />
}
if (populateBy === 'collection') {
return <CollectionArchiveByCollection {...props} className={className} />
}
return null
}

View File

@@ -1,36 +0,0 @@
@use '../../_css/queries.scss' as *;
.footer {
padding: calc(var(--base) * 4) 0;
background-color: var(--color-base-1000);
color: var(--color-base-0);
@include small-break {
padding: calc(var(--base) * 2) 0;
}
}
.wrap {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: calc(var(--base) / 2) var(--base);
}
.logo {
width: 150px;
}
.nav {
display: flex;
gap: calc(var(--base) / 4) var(--base);
align-items: center;
flex-wrap: wrap;
opacity: 1;
transition: opacity 100ms linear;
visibility: visible;
> * {
text-decoration: none;
}
}

View File

@@ -1,39 +0,0 @@
import Link from 'next/link'
import React from 'react'
import { fetchFooter } from '../../_api/fetchFooter'
import { Gutter } from '../Gutter'
import { CMSLink } from '../Link'
import classes from './index.module.scss'
export async function Footer() {
const footer = await fetchFooter()
const navItems = footer?.navItems || []
return (
<footer className={classes.footer}>
<Gutter className={classes.wrap}>
<Link href="/">
<picture>
<img
alt="Payload Logo"
className={classes.logo}
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-light.svg"
/>
</picture>
</Link>
<nav className={classes.nav}>
{navItems.map(({ link }, i) => {
return <CMSLink key={i} {...link} />
})}
<Link href="/admin">Admin</Link>
<Link href="https://github.com/payloadcms/payload/tree/main/templates/ecommerce">
Source Code
</Link>
<Link href="https://github.com/payloadcms/payload">Payload</Link>
</nav>
</Gutter>
</footer>
)
}

View File

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

View File

@@ -1,35 +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,12 +0,0 @@
@use '../../../_css/queries.scss' as *;
.nav {
display: flex;
gap: calc(var(--base) / 4) var(--base);
align-items: center;
flex-wrap: wrap;
> * {
text-decoration: none;
}
}

View File

@@ -1,20 +0,0 @@
'use client'
import React from 'react'
import type { Header as HeaderType } from '../../../../payload-types'
import { CMSLink } from '../../Link'
import classes from './index.module.scss'
export const HeaderNav: React.FC<{ header: HeaderType }> = ({ header }) => {
const navItems = header?.navItems || []
return (
<nav className={classes.nav}>
{navItems.map(({ link }, i) => {
return <CMSLink key={i} {...link} />
})}
</nav>
)
}

View File

@@ -1,16 +0,0 @@
@use '../../_css/queries.scss' as *;
.header {
padding: var(--base) 0;
}
.wrap {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: calc(var(--base) / 2) var(--base);
}
.logo {
width: 150px;
}

View File

@@ -1,30 +0,0 @@
{
/* eslint-disable @next/next/no-img-element */
}
import { fetchHeader } from '@/app/_api/fetchHeader'
import Link from 'next/link'
import React from 'react'
import { Gutter } from '../Gutter'
import { HeaderNav } from './Nav'
import classes from './index.module.scss'
export async function Header() {
const header = await fetchHeader()
return (
<header className={classes.header}>
<Gutter className={classes.wrap}>
<Link href="/">
<img
alt="Payload Logo"
className={classes.logo}
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-dark.svg"
/>
</Link>
<HeaderNav header={header} />
</Gutter>
</header>
)
}

View File

@@ -1,23 +0,0 @@
import React from 'react'
import type { Page } from '../../../payload-types'
import { HighImpactHero } from '../../_heros/HighImpact'
import { LowImpactHero } from '../../_heros/LowImpact'
const heroes = {
highImpact: HighImpactHero,
lowImpact: LowImpactHero,
}
export const Hero: React.FC<Page['hero']> = (props) => {
const { type } = props || {}
if (!type || type === 'none') return null
const HeroToRender = heroes[type]
if (!HeroToRender) return null
return <HeroToRender {...props} />
}

View File

@@ -1,65 +0,0 @@
import Link from 'next/link'
import React from 'react'
import type { Page } from '../../../payload-types'
import type { Props as ButtonProps } from '../Button'
import { Button } from '../Button'
type CMSLinkType = {
appearance?: ButtonProps['appearance']
children?: React.ReactNode
className?: string
invert?: ButtonProps['invert']
label?: string
newTab?: boolean
reference?: {
relationTo: 'pages' | 'posts'
value: Page | string
}
type?: 'custom' | 'reference'
url?: string
}
export const CMSLink: React.FC<CMSLinkType> = ({
type,
appearance,
children,
className,
invert,
label,
newTab,
reference,
url,
}) => {
const href =
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
? `/${reference.value.slug}`
: url
if (!href) return null
if (!appearance) {
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
if (href || url) {
return (
<Link {...newTabProps} className={className} href={href || url || ''}>
{label && label}
{children && children}
</Link>
)
}
}
return (
<Button
appearance={appearance}
className={className}
href={href}
invert={invert}
label={label}
newTab={newTab}
/>
)
}

View File

@@ -1,7 +0,0 @@
.placeholder-color-light {
background-color: rgba(0, 0, 0, 0.05);
}
.placeholder {
background-color: var(--color-base-50);
}

View File

@@ -1,78 +0,0 @@
'use client'
import type { StaticImageData } from 'next/image'
import { PAYLOAD_SERVER_URL } from '@/app/_api/serverURL'
import NextImage from 'next/image'
import React from 'react'
import type { Props as MediaProps } from '../types'
import cssVariables from '../../../cssVariables'
import classes from './index.module.scss'
const { breakpoints } = cssVariables
export const Image: React.FC<MediaProps> = (props) => {
const {
alt: altFromProps,
fill,
imgClassName,
onClick,
onLoad: onLoadFromProps,
priority,
resource,
src: srcFromProps,
} = props
const [isLoading, setIsLoading] = React.useState(true)
let width: number | undefined
let height: number | undefined
let alt = altFromProps
let src: StaticImageData | string = srcFromProps || ''
if (!src && resource && typeof resource !== 'string') {
const {
alt: altFromResource,
filename: fullFilename,
height: fullHeight,
width: fullWidth,
} = resource
width = fullWidth || undefined
height = fullHeight || undefined
alt = altFromResource
const filename = fullFilename
src = `${PAYLOAD_SERVER_URL}/media/${filename}`
}
// NOTE: this is used by the browser to determine which image to download at different screen sizes
const sizes = Object.entries(breakpoints)
.map(([, value]) => `(max-width: ${value}px) ${value}px`)
.join(', ')
return (
<NextImage
alt={alt || ''}
className={[isLoading && classes.placeholder, classes.image, imgClassName]
.filter(Boolean)
.join(' ')}
fill={fill}
height={!fill ? height : undefined}
onClick={onClick}
onLoad={() => {
setIsLoading(false)
if (typeof onLoadFromProps === 'function') {
onLoadFromProps()
}
}}
priority={priority}
sizes={sizes}
src={src}
width={!fill ? width : undefined}
/>
)
}

View File

@@ -1,11 +0,0 @@
.video {
max-width: 100%;
width: 100%;
background-color: var(--color-base-50);
}
.cover {
object-fit: cover;
width: 100%;
height: 100%;
}

View File

@@ -1,46 +0,0 @@
'use client'
import { PAYLOAD_SERVER_URL } from '@/app/_api/serverURL'
import React, { useEffect, useRef } from 'react'
import type { Props as MediaProps } from '../types'
import classes from './index.module.scss'
export const Video: React.FC<MediaProps> = (props) => {
const { onClick, resource, videoClassName } = props
const videoRef = useRef<HTMLVideoElement>(null)
// const [showFallback] = useState<boolean>()
useEffect(() => {
const { current: video } = videoRef
if (video) {
video.addEventListener('suspend', () => {
// setShowFallback(true);
// console.warn('Video was suspended, rendering fallback image.')
})
}
}, [])
if (resource && typeof resource !== 'string') {
const { filename } = resource
return (
<video
autoPlay
className={[classes.video, videoClassName].filter(Boolean).join(' ')}
controls={false}
loop
muted
onClick={onClick}
playsInline
ref={videoRef}
>
<source src={`${PAYLOAD_SERVER_URL}/media/${filename}`} />
</video>
)
}
return null
}

View File

@@ -1,31 +0,0 @@
import type { ElementType } from 'react'
import React, { Fragment } from 'react'
import type { Props } from './types'
import { Image } from './Image'
import { Video } from './Video'
export const Media: React.FC<Props> = (props) => {
const { className, htmlElement = 'div', resource } = props
const isVideo = typeof resource !== 'string' && resource?.mimeType?.includes('video')
const Tag = (htmlElement as ElementType) || Fragment
return (
<Tag
{...(htmlElement !== null
? {
className,
}
: {})}
>
{isVideo ? (
<Video {...props} />
) : (
<Image {...props} /> // eslint-disable-line
)}
</Tag>
)
}

View File

@@ -1,20 +0,0 @@
import type { StaticImageData } from 'next/image'
import type { ElementType, Ref } from 'react'
import type { Media as MediaType } from '../../../payload-types'
export interface Props {
alt?: string
className?: string
fill?: boolean // for NextImage only
htmlElement?: ElementType | null
imgClassName?: string
onClick?: () => void
onLoad?: () => void
priority?: boolean // for NextImage only
ref?: Ref<HTMLImageElement | HTMLVideoElement | null>
resource?: MediaType | string // for Payload media
size?: string // for NextImage only
src?: StaticImageData // for static media
videoClassName?: string
}

View File

@@ -1,21 +0,0 @@
@import '../../_css/common';
.pageRange {
display: flex;
align-items: center;
font-weight: 600;
}
.content {
display: flex;
align-items: center;
margin: 0 var(--base(0.5));
}
.divider {
margin: 0 2px;
}
.hyperlink {
display: flex;
}

View File

@@ -1,52 +0,0 @@
import React from 'react'
import classes from './index.module.scss'
const defaultLabels = {
plural: 'Docs',
singular: 'Doc',
}
const defaultCollectionLabels = {
products: {
plural: 'Products',
singular: 'Product',
},
}
export const PageRange: React.FC<{
className?: string
collection?: string
collectionLabels?: {
plural?: string
singular?: string
}
currentPage?: number
limit?: number
totalDocs?: number
}> = (props) => {
const {
className,
collection,
collectionLabels: collectionLabelsFromProps,
currentPage,
limit,
totalDocs,
} = props
const indexStart = (currentPage ? currentPage - 1 : 1) * (limit || 1) + 1
let indexEnd = (currentPage || 1) * (limit || 1)
if (totalDocs && indexEnd > totalDocs) indexEnd = totalDocs
const { plural, singular } =
collectionLabelsFromProps || defaultCollectionLabels[collection || ''] || defaultLabels || {}
return (
<div className={[className, classes.pageRange].filter(Boolean).join(' ')}>
{(typeof totalDocs === 'undefined' || totalDocs === 0) && 'Search produced no results.'}
{typeof totalDocs !== 'undefined' &&
totalDocs > 0 &&
`Showing ${indexStart} - ${indexEnd} of ${totalDocs} ${totalDocs > 1 ? plural : singular}`}
</div>
)
}

View File

@@ -1,29 +0,0 @@
@import '../../_css/type.scss';
.pagination {
@extend %label;
display: flex;
align-items: center;
gap: calc(var(--base) / 2);
}
.button {
all: unset;
cursor: pointer;
position: relative;
display: flex;
padding: calc(var(--base) / 2);
color: var(--color-base-500);
border: 1px solid var(--color-base-200);
&:disabled {
cursor: not-allowed;
color: var(--color-base-200);
border-color: var(--color-base-150);
}
}
.icon {
width: calc(var(--base) / 2);
height: calc(var(--base) / 2);
}

View File

@@ -1,45 +0,0 @@
import React from 'react'
import { Chevron } from '../Chevron'
import classes from './index.module.scss'
export const Pagination: React.FC<{
className?: string
onClick: (page: number) => void
page: number
totalPages: number
}> = (props) => {
const { className, onClick, page, totalPages } = props
const hasNextPage = page < totalPages
const hasPrevPage = page > 1
return (
<div className={[classes.pagination, className].filter(Boolean).join(' ')}>
<button
className={classes.button}
disabled={!hasPrevPage}
onClick={() => {
onClick(page - 1)
}}
type="button"
>
<Chevron className={classes.icon} rotate={90} />
</button>
<div className={classes.pageRange}>
<span className={classes.pageRangeLabel}>
Page {page} of {totalPages}
</span>
</div>
<button
className={classes.button}
disabled={!hasNextPage}
onClick={() => {
onClick(page + 1)
}}
type="button"
>
<Chevron className={classes.icon} rotate={-90} />
</button>
</div>
)
}

View File

@@ -1,9 +0,0 @@
.richText {
> *:first-child {
margin-top: 0;
}
> *:last-child {
margin-bottom: 0;
}
}

View File

@@ -1,26 +0,0 @@
import React from 'react'
import classes from './index.module.scss'
import serializeLexical from './serializeLexical'
import serializeSlate from './serializeSlate'
const RichText: React.FC<{
className?: string
content: any
renderUploadFilenameOnly?: boolean
serializer?: 'lexical' | 'slate'
}> = ({ className, content, renderUploadFilenameOnly, serializer = 'slate' }) => {
if (!content) {
return null
}
return (
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
{serializer === 'slate'
? serializeSlate(content, renderUploadFilenameOnly)
: serializeLexical(content, renderUploadFilenameOnly)}
</div>
)
}
export default RichText

View File

@@ -1,90 +0,0 @@
import type { SerializedEditorState } from 'lexical'
import { CMSLink } from '../Link'
import { Media } from '../Media'
const serializer = (
content?: SerializedEditorState['root']['children'],
renderUploadFilenameOnly?: boolean,
): React.ReactNode | React.ReactNode[] =>
content?.map((node, i) => {
switch (node.type) {
case 'h1':
return <h1 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h1>
case 'h2':
return <h2 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h2>
case 'h3':
return <h3 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h3>
case 'h4':
return <h4 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h4>
case 'h5':
return <h5 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h5>
case 'h6':
return <h6 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h6>
case 'quote':
return (
<blockquote key={i}>
{serializeLexical(node?.children, renderUploadFilenameOnly)}
</blockquote>
)
case 'ul':
return <ul key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</ul>
case 'ol':
return <ol key={i}>{serializeLexical(node.children, renderUploadFilenameOnly)}</ol>
case 'li':
return <li key={i}>{serializeLexical(node.children, renderUploadFilenameOnly)}</li>
case 'relationship':
return (
<span key={i}>
{node.value && typeof node.value === 'object'
? node.value.title || node.value.id
: node.value}
</span>
)
case 'link':
return (
<CMSLink
key={i}
newTab={Boolean(node?.newTab)}
reference={node.doc as any}
type={node.linkType === 'internal' ? 'reference' : 'custom'}
url={node.url}
>
{serializer(node?.children, renderUploadFilenameOnly)}
</CMSLink>
)
case 'upload':
if (renderUploadFilenameOnly) {
return <span key={i}>{node.value.filename}</span>
}
return <Media key={i} resource={node?.value} />
case 'paragraph':
return <p key={i}>{serializer(node?.children, renderUploadFilenameOnly)}</p>
case 'text':
return <span key={i}>{node.text}</span>
}
})
const serializeLexical = (
content?: SerializedEditorState,
renderUploadFilenameOnly?: boolean,
): React.ReactNode | React.ReactNode[] => {
return serializer(content?.root?.children, renderUploadFilenameOnly)
}
export default serializeLexical

View File

@@ -1,131 +0,0 @@
import escapeHTML from 'escape-html'
import React, { Fragment } from 'react'
import { Text } from 'slate'
import { CMSLink } from '../Link'
import { Media } from '../Media'
// eslint-disable-next-line no-use-before-define
type Children = Leaf[]
type Leaf = {
[key: string]: unknown
children?: Children
type: string
url?: string
value?: any
}
const serializeSlate = (
children?: Children,
renderUploadFilenameOnly?: boolean,
): 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 'h1':
return <h1 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h1>
case 'h2':
return <h2 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h2>
case 'h3':
return <h3 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h3>
case 'h4':
return <h4 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h4>
case 'h5':
return <h5 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h5>
case 'h6':
return <h6 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h6>
case 'quote':
return (
<blockquote key={i}>
{serializeSlate(node?.children, renderUploadFilenameOnly)}
</blockquote>
)
case 'ul':
return <ul key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</ul>
case 'ol':
return <ol key={i}>{serializeSlate(node.children, renderUploadFilenameOnly)}</ol>
case 'li':
return <li key={i}>{serializeSlate(node.children, renderUploadFilenameOnly)}</li>
case 'relationship':
return (
<span key={i}>
{node.value && typeof node.value === 'object'
? node.value.title || node.value.id
: node.value}
</span>
)
case 'link':
return (
<CMSLink
key={i}
newTab={Boolean(node?.newTab)}
reference={node.doc as any}
type={node.linkType === 'internal' ? 'reference' : 'custom'}
url={node.url}
>
{serializeSlate(node?.children, renderUploadFilenameOnly)}
</CMSLink>
)
case 'upload':
if (renderUploadFilenameOnly) {
return <span key={i}>{node.value.filename}</span>
}
return <Media key={i} resource={node?.value} />
default:
return <p key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</p>
}
}) || []
export default serializeSlate

View File

@@ -1,15 +0,0 @@
.top-large {
padding-top: var(--block-padding);
}
.top-medium {
padding-top: calc(var(--block-padding) / 2);
}
.bottom-large {
padding-bottom: var(--block-padding);
}
.bottom-medium {
padding-bottom: calc(var(--block-padding) / 2);
}

View File

@@ -1,29 +0,0 @@
import React from 'react'
import classes from './index.module.scss'
export type VerticalPaddingOptions = 'large' | 'medium' | 'none'
type Props = {
bottom?: VerticalPaddingOptions
children: React.ReactNode
className?: string
top?: VerticalPaddingOptions
}
export const VerticalPadding: React.FC<Props> = ({
bottom = 'medium',
children,
className,
top = 'medium',
}) => {
return (
<div
className={[className, classes[`top-${top}`], classes[`bottom-${bottom}`]]
.filter(Boolean)
.join(' ')}
>
{children}
</div>
)
}

View File

@@ -1,117 +0,0 @@
@use './queries.scss' as *;
@use './colors.scss' as *;
@use './type.scss' as *;
:root {
--base: 24px;
--font-body: system-ui;
--font-mono: 'Roboto Mono', monospace;
--gutter-h: 180px;
--block-padding: 120px;
--theme-text: var(--color-base-750);
@include large-break {
--gutter-h: 144px;
--block-padding: 96px;
}
@include mid-break {
--gutter-h: 24px;
--block-padding: 60px;
}
}
* {
box-sizing: border-box;
}
html {
@extend %body;
-webkit-font-smoothing: antialiased;
}
html,
body,
#app {
height: 100%;
}
body {
font-family: var(--font-body);
margin: 0;
color: var(--theme-text);
}
::selection {
background: var(--color-success-500);
color: var(--color-base-800);
}
::-moz-selection {
background: var(--color-success-500);
color: var(--color-base-800);
}
img {
max-width: 100%;
height: auto;
display: block;
}
h1 {
@extend %h1;
}
h2 {
@extend %h2;
}
h3 {
@extend %h3;
}
h4 {
@extend %h4;
}
h5 {
@extend %h5;
}
h6 {
@extend %h6;
}
p {
margin: var(--base) 0;
@include mid-break {
margin: calc(var(--base) * 0.75) 0;
}
}
ul,
ol {
padding-left: var(--base);
margin: 0 0 var(--base);
}
a {
color: currentColor;
&:focus {
opacity: 0.8;
outline: none;
}
&:active {
opacity: 0.7;
outline: none;
}
}
svg {
vertical-align: middle;
}

View File

@@ -1,85 +0,0 @@
// Keep these in sync with the colors exported in '../cssVariables.js'
:root {
--color-base-0: rgb(255, 255, 255);
--color-base-50: rgb(245, 245, 245);
--color-base-100: rgb(235, 235, 235);
--color-base-150: rgb(221, 221, 221);
--color-base-200: rgb(208, 208, 208);
--color-base-250: rgb(195, 195, 195);
--color-base-300: rgb(181, 181, 181);
--color-base-350: rgb(168, 168, 168);
--color-base-400: rgb(154, 154, 154);
--color-base-450: rgb(141, 141, 141);
--color-base-500: rgb(128, 128, 128);
--color-base-550: rgb(114, 114, 114);
--color-base-600: rgb(101, 101, 101);
--color-base-650: rgb(87, 87, 87);
--color-base-700: rgb(74, 74, 74);
--color-base-750: rgb(60, 60, 60);
--color-base-800: rgb(47, 47, 47);
--color-base-850: rgb(34, 34, 34);
--color-base-900: rgb(20, 20, 20);
--color-base-950: rgb(7, 7, 7);
--color-base-1000: rgb(0, 0, 0);
--color-success-50: rgb(247, 255, 251);
--color-success-100: rgb(240, 255, 247);
--color-success-150: rgb(232, 255, 243);
--color-success-200: rgb(224, 255, 239);
--color-success-250: rgb(217, 255, 235);
--color-success-300: rgb(209, 255, 230);
--color-success-350: rgb(201, 255, 226);
--color-success-400: rgb(193, 255, 222);
--color-success-450: rgb(186, 255, 218);
--color-success-500: rgb(178, 255, 214);
--color-success-550: rgb(160, 230, 193);
--color-success-600: rgb(142, 204, 171);
--color-success-650: rgb(125, 179, 150);
--color-success-700: rgb(107, 153, 128);
--color-success-750: rgb(89, 128, 107);
--color-success-800: rgb(71, 102, 86);
--color-success-850: rgb(53, 77, 64);
--color-success-900: rgb(36, 51, 43);
--color-success-950: rgb(18, 25, 21);
--color-warning-50: rgb(255, 255, 246);
--color-warning-100: rgb(255, 255, 237);
--color-warning-150: rgb(254, 255, 228);
--color-warning-200: rgb(254, 255, 219);
--color-warning-250: rgb(254, 255, 210);
--color-warning-300: rgb(254, 255, 200);
--color-warning-350: rgb(254, 255, 191);
--color-warning-400: rgb(253, 255, 182);
--color-warning-450: rgb(253, 255, 173);
--color-warning-500: rgb(253, 255, 164);
--color-warning-550: rgb(228, 230, 148);
--color-warning-600: rgb(202, 204, 131);
--color-warning-650: rgb(177, 179, 115);
--color-warning-700: rgb(152, 153, 98);
--color-warning-750: rgb(127, 128, 82);
--color-warning-800: rgb(101, 102, 66);
--color-warning-850: rgb(76, 77, 49);
--color-warning-900: rgb(51, 51, 33);
--color-warning-950: rgb(25, 25, 16);
--color-error-50: rgb(255, 241, 241);
--color-error-100: rgb(255, 226, 228);
--color-error-150: rgb(255, 212, 214);
--color-error-200: rgb(255, 197, 200);
--color-error-250: rgb(255, 183, 187);
--color-error-300: rgb(255, 169, 173);
--color-error-350: rgb(255, 154, 159);
--color-error-400: rgb(255, 140, 145);
--color-error-450: rgb(255, 125, 132);
--color-error-500: rgb(255, 111, 118);
--color-error-550: rgb(230, 100, 106);
--color-error-600: rgb(204, 89, 94);
--color-error-650: rgb(179, 78, 83);
--color-error-700: rgb(153, 67, 71);
--color-error-750: rgb(128, 56, 59);
--color-error-800: rgb(102, 44, 47);
--color-error-850: rgb(77, 33, 35);
--color-error-900: rgb(51, 22, 24);
--color-error-950: rgb(25, 11, 12);
}

View File

@@ -1,2 +0,0 @@
@forward './queries.scss';
@forward './type.scss';

View File

@@ -1,30 +0,0 @@
// Keep these in sync with the breakpoints exported in '../cssVariables.js'
$breakpoint-xs-width: 400px;
$breakpoint-s-width: 768px;
$breakpoint-m-width: 1024px;
$breakpoint-l-width: 1440px;
@mixin extra-small-break {
@media (max-width: #{$breakpoint-xs-width}) {
@content;
}
}
@mixin small-break {
@media (max-width: #{$breakpoint-s-width}) {
@content;
}
}
@mixin mid-break {
@media (max-width: #{$breakpoint-m-width}) {
@content;
}
}
@mixin large-break {
@media (max-width: #{$breakpoint-l-width}) {
@content;
}
}

View File

@@ -1,110 +0,0 @@
@use 'queries' as *;
%h1,
%h2,
%h3,
%h4,
%h5,
%h6 {
font-weight: 700;
}
%h1 {
margin: 40px 0;
font-size: 64px;
line-height: 70px;
font-weight: bold;
@include mid-break {
margin: 24px 0;
font-size: 42px;
line-height: 42px;
}
}
%h2 {
margin: 28px 0;
font-size: 48px;
line-height: 54px;
font-weight: bold;
@include mid-break {
margin: 22px 0;
font-size: 32px;
line-height: 40px;
}
}
%h3 {
margin: 24px 0;
font-size: 32px;
line-height: 40px;
font-weight: bold;
@include mid-break {
margin: 20px 0;
font-size: 26px;
line-height: 32px;
}
}
%h4 {
margin: 20px 0;
font-size: 26px;
line-height: 32px;
font-weight: bold;
@include mid-break {
font-size: 22px;
line-height: 30px;
}
}
%h5 {
margin: 20px 0;
font-size: 22px;
line-height: 30px;
font-weight: bold;
@include mid-break {
font-size: 18px;
line-height: 24px;
}
}
%h6 {
margin: 20px 0;
font-size: inherit;
line-height: inherit;
font-weight: bold;
}
%body {
font-size: 18px;
line-height: 32px;
@include mid-break {
font-size: 15px;
line-height: 24px;
}
}
%large-body {
font-size: 25px;
line-height: 32px;
@include mid-break {
font-size: 22px;
line-height: 30px;
}
}
%label {
font-size: 16px;
line-height: 24px;
text-transform: uppercase;
@include mid-break {
font-size: 13px;
}
}

View File

@@ -1,55 +0,0 @@
@import '../../_css/queries.scss';
.hero {
padding-top: calc(var(--base) * 2);
position: relative;
overflow: hidden;
@include large-break {
padding-top: var(--base);
}
}
.media {
width: calc(100% + var(--gutter-h));
left: calc(var(--gutter-h) / -2);
margin-top: calc(var(--base) * 3);
position: relative;
@include mid-break {
left: 0;
margin-top: var(--base);
margin-left: calc(var(--gutter-h) * -1);
width: calc(100% + var(--gutter-h) * 2);
}
}
.links {
list-style: none;
margin: 0;
padding: 0;
display: flex;
padding-top: var(--base);
flex-wrap: wrap;
margin: calc(var(--base) * -0.5);
& > * {
margin: calc(var(--base) / 2);
}
}
.caption {
margin-top: var(--base);
color: var(--color-base-500);
left: calc(var(--gutter-h) / 2);
width: calc(100% - var(--gutter-h));
position: relative;
@include mid-break {
left: var(--gutter-h);
}
}
.content {
position: relative;
}

View File

@@ -1,31 +0,0 @@
import React, { Fragment } from 'react'
import type { Page } from '../../../payload-types'
import { Gutter } from '../../_components/Gutter'
import { Media } from '../../_components/Media'
import RichText from '../../_components/RichText'
import classes from './index.module.scss'
export const HighImpactHero: React.FC<Page['hero']> = ({ media, richText }) => {
return (
<Gutter className={classes.hero}>
<div className={classes.content}>
<RichText content={richText} />
</div>
<div className={classes.media}>
{typeof media === 'object' && media !== null && (
<Fragment>
<Media
// fill
imgClassName={classes.image}
priority
resource={media}
/>
{media?.caption && <RichText className={classes.caption} content={media.caption} />}
</Fragment>
)}
</div>
</Gutter>
)
}

View File

@@ -1,4 +0,0 @@
@use '../../_css/type.scss' as *;
.lowImpactHero {
}

View File

@@ -1,20 +0,0 @@
import React from 'react'
import type { Page } from '../../../payload-types'
import { Gutter } from '../../_components/Gutter'
import RichText from '../../_components/RichText'
import { VerticalPadding } from '../../_components/VerticalPadding'
import classes from './index.module.scss'
export const LowImpactHero: React.FC<Page['hero']> = ({ richText }) => {
return (
<Gutter className={classes.lowImpactHero}>
<div className={classes.content}>
<VerticalPadding>
<RichText className={classes.richText} content={richText} />
</VerticalPadding>
</div>
</Gutter>
)
}

View File

@@ -1,76 +0,0 @@
@use '../../_css/common.scss' as *;
.postHero {
display: flex;
gap: calc(var(--base) * 2);
@include mid-break {
flex-direction: column;
gap: var(--base);
}
}
.content {
width: 50%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: var(--base);
@include mid-break {
width: 100%;
gap: calc(var(--base) / 2);
}
}
.warning {
margin-bottom: calc(var(--base) * 1.5);
}
.meta {
margin: 0;
}
.description {
margin: 0;
}
.media {
width: 50%;
@include mid-break {
width: 100%;
}
}
.mediaWrapper {
text-decoration: none;
display: block;
position: relative;
aspect-ratio: 5 / 4;
margin-bottom: calc(var(--base) / 2);
width: calc(100% + calc(var(--gutter-h) / 2));
@include mid-break {
margin-left: calc(var(--gutter-h) * -1);
width: calc(100% + var(--gutter-h) * 2);
}
}
.image {
object-fit: cover;
}
.placeholder {
background-color: var(--color-base-50);
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.caption {
color: var(--color-base-500);
}

View File

@@ -1,55 +0,0 @@
import { PAYLOAD_SERVER_URL } from '@/app/_api/serverURL'
import Link from 'next/link'
import React, { Fragment } from 'react'
import type { Post } from '../../../payload-types'
import { Gutter } from '../../_components/Gutter'
import { Media } from '../../_components/Media'
import RichText from '../../_components/RichText'
import { formatDateTime } from '../../_utilities/formatDateTime'
import classes from './index.module.scss'
export const PostHero: React.FC<{
post: Post
}> = ({ post }) => {
const { id, createdAt, meta: { description, image: metaImage } = {} } = post
return (
<Fragment>
<Gutter className={classes.postHero}>
<div className={classes.content}>
<RichText className={classes.richText} content={post?.hero?.richText} />
<p className={classes.meta}>
{createdAt && (
<Fragment>
{'Created on '}
{formatDateTime(createdAt)}
</Fragment>
)}
</p>
<div>
<p className={classes.description}>
{`${description ? `${description} ` : ''}To edit this post, `}
<Link href={`${PAYLOAD_SERVER_URL}/admin/collections/posts/${id}`}>
navigate to the admin dashboard
</Link>
.
</p>
</div>
</div>
<div className={classes.media}>
<div className={classes.mediaWrapper}>
{!metaImage && <div className={classes.placeholder}>No image</div>}
{metaImage && typeof metaImage !== 'string' && (
<Media fill imgClassName={classes.image} resource={metaImage} />
)}
</div>
{metaImage && typeof metaImage !== 'string' && metaImage?.caption && (
<RichText className={classes.caption} content={metaImage.caption} />
)}
</div>
</Gutter>
</Fragment>
)
}

View File

@@ -1,20 +0,0 @@
export const formatDateTime = (timestamp: string): string => {
const now = new Date()
let date = now
if (timestamp) date = new Date(timestamp)
const months = date.getMonth()
const days = date.getDate()
// const hours = date.getHours();
// const minutes = date.getMinutes();
// const seconds = date.getSeconds();
const MM = months + 1 < 10 ? `0${months + 1}` : months + 1
const DD = days < 10 ? `0${days}` : days
const YYYY = date.getFullYear()
// const AMPM = hours < 12 ? 'AM' : 'PM';
// const HH = hours > 12 ? hours - 12 : hours;
// const MinMin = (minutes < 10) ? `0${minutes}` : minutes;
// const SS = (seconds < 10) ? `0${seconds}` : seconds;
return `${MM}/${DD}/${YYYY}`
}

View File

@@ -1,5 +0,0 @@
export const toKebabCase = (string: string): string =>
string
?.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/\s+/g, '-')
.toLowerCase()

View File

@@ -1,17 +0,0 @@
// Keep these in sync with the CSS variables in the `_css` directory
module.exports = {
breakpoints: {
s: 768,
m: 1024,
l: 1440,
},
colors: {
base0: 'rgb(255, 255, 255)',
base100: 'rgb(235, 235, 235)',
base500: 'rgb(128, 128, 128)',
base850: 'rgb(34, 34, 34)',
base1000: 'rgb(0, 0, 0)',
error500: 'rgb(255, 111, 118)',
},
}

View File

@@ -1,22 +0,0 @@
import type { Metadata } from 'next'
import { Footer } from './_components/Footer'
import { Header } from './_components/Header'
import './_css/app.scss'
export const metadata: Metadata = {
description: 'Payload Live Preview',
title: 'Payload Live Preview',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Header />
{children}
<Footer />
</body>
</html>
)
}

View File

@@ -1,19 +0,0 @@
'use client'
import React from 'react'
import { Gutter } from './_components/Gutter'
import { VerticalPadding } from './_components/VerticalPadding'
export default function NotFound() {
return (
<main>
<VerticalPadding bottom="medium" top="none">
<Gutter>
<h1>404</h1>
<p>This page could not be found.</p>
</Gutter>
</VerticalPadding>
</main>
)
}

View File

@@ -1,3 +0,0 @@
import PageTemplate from './(pages)/[slug]/page'
export default PageTemplate

View File

@@ -1,13 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// this is only required for local development of the `useLivePreview` hook
// see `./app/page.client.tsx` for more details
experimental: {
externalDir: true,
},
images: {
domains: ['localhost'],
},
}
module.exports = nextConfig

View File

@@ -1,24 +0,0 @@
{
"name": "payload-live-preview",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@types/escape-html": "^1.0.2",
"escape-html": "^1.0.3",
"next": "^13.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.68.0",
"slate": "^0.94.1"
},
"devDependencies": {
"@types/node": "^20.6.2",
"@types/react": "^18.2.22"
}
}

View File

@@ -1,459 +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 {
collections: {
users: User
pages: Page
posts: Post
tenants: Tenant
categories: Category
media: Media
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {
header: Header
footer: Footer
}
}
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
}
export interface Page {
id: string
slug: string
tenant?: (string | null) | Tenant
title: string
hero: {
type: 'none' | 'highImpact' | 'lowImpact'
richText?:
| {
[k: string]: unknown
}[]
| null
media?: string | Media | null
}
layout?:
| (
| {
invertBackground?: boolean | null
richText?:
| {
[k: string]: unknown
}[]
| null
links?:
| {
link: {
type?: ('reference' | 'custom') | null
newTab?: boolean | null
reference?:
| ({
relationTo: 'posts'
value: string | Post
} | null)
| ({
relationTo: 'pages'
value: string | Page
} | null)
url?: string | null
label: string
appearance?: ('primary' | 'secondary') | null
}
id?: string | null
}[]
| null
id?: string | null
blockName?: string | null
blockType: 'cta'
}
| {
invertBackground?: boolean | null
columns?:
| {
size?: ('oneThird' | 'half' | 'twoThirds' | 'full') | null
richText?:
| {
[k: string]: unknown
}[]
| null
enableLink?: boolean | null
link?: {
type?: ('reference' | 'custom') | null
newTab?: boolean | null
reference?:
| ({
relationTo: 'posts'
value: string | Post
} | null)
| ({
relationTo: 'pages'
value: string | Page
} | null)
url?: string | null
label: string
appearance?: ('default' | 'primary' | 'secondary') | null
}
id?: string | null
}[]
| null
id?: string | null
blockName?: string | null
blockType: 'content'
}
| {
invertBackground?: boolean | null
position?: ('default' | 'fullscreen') | null
media: string | Media
id?: string | null
blockName?: string | null
blockType: 'mediaBlock'
}
| {
introContent?:
| {
[k: string]: unknown
}[]
| null
populateBy?: ('collection' | 'selection') | null
relationTo?: 'posts' | null
categories?: (string | Category)[] | null
limit?: number | null
selectedDocs?:
| {
relationTo: 'posts'
value: string | Post
}[]
| null
populatedDocs?:
| {
relationTo: 'posts'
value: string | Post
}[]
| null
populatedDocsTotal?: number | null
id?: string | null
blockName?: string | null
blockType: 'archive'
}
)[]
| null
relationshipAsUpload?: string | Media | null
relationshipMonoHasOne?: (string | null) | Post
relationshipMonoHasMany?: (string | Post)[] | null
relationshipPolyHasOne?: {
relationTo: 'posts'
value: string | Post
} | null
relationshipPolyHasMany?:
| {
relationTo: 'posts'
value: string | Post
}[]
| null
arrayOfRelationships?:
| {
uploadInArray?: string | Media | null
richTextInArray?:
| {
[k: string]: unknown
}[]
| null
relationshipInArrayMonoHasOne?: (string | null) | Post
relationshipInArrayMonoHasMany?: (string | Post)[] | null
relationshipInArrayPolyHasOne?: {
relationTo: 'posts'
value: string | Post
} | null
relationshipInArrayPolyHasMany?:
| {
relationTo: 'posts'
value: string | Post
}[]
| null
id?: string | null
}[]
| null
richTextSlate?:
| {
[k: string]: unknown
}[]
| null
richTextLexical?: {
root: {
children: {
type: string
version: number
[k: string]: unknown
}[]
direction: ('ltr' | 'rtl') | null
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''
indent: number
type: string
version: number
}
[k: string]: unknown
} | null
tab: {
relationshipInTab?: (string | null) | Post
}
meta?: {
title?: string | null
description?: string | null
image?: string | Media | null
}
updatedAt: string
createdAt: string
}
export interface Tenant {
id: string
title: string
clientURL: string
updatedAt: string
createdAt: string
}
export interface Media {
id: string
alt: string
caption?:
| {
[k: string]: unknown
}[]
| null
updatedAt: string
createdAt: string
url?: string | null
filename?: string | null
mimeType?: string | null
filesize?: number | null
width?: number | null
height?: number | null
}
export interface Post {
id: string
slug: string
tenant?: (string | null) | Tenant
title: string
hero: {
type: 'none' | 'highImpact' | 'lowImpact'
richText?:
| {
[k: string]: unknown
}[]
| null
media?: string | Media | null
}
layout?:
| (
| {
invertBackground?: boolean | null
richText?:
| {
[k: string]: unknown
}[]
| null
links?:
| {
link: {
type?: ('reference' | 'custom') | null
newTab?: boolean | null
reference?:
| ({
relationTo: 'posts'
value: string | Post
} | null)
| ({
relationTo: 'pages'
value: string | Page
} | null)
url?: string | null
label: string
appearance?: ('primary' | 'secondary') | null
}
id?: string | null
}[]
| null
id?: string | null
blockName?: string | null
blockType: 'cta'
}
| {
invertBackground?: boolean | null
columns?:
| {
size?: ('oneThird' | 'half' | 'twoThirds' | 'full') | null
richText?:
| {
[k: string]: unknown
}[]
| null
enableLink?: boolean | null
link?: {
type?: ('reference' | 'custom') | null
newTab?: boolean | null
reference?:
| ({
relationTo: 'posts'
value: string | Post
} | null)
| ({
relationTo: 'pages'
value: string | Page
} | null)
url?: string | null
label: string
appearance?: ('default' | 'primary' | 'secondary') | null
}
id?: string | null
}[]
| null
id?: string | null
blockName?: string | null
blockType: 'content'
}
| {
invertBackground?: boolean | null
position?: ('default' | 'fullscreen') | null
media: string | Media
id?: string | null
blockName?: string | null
blockType: 'mediaBlock'
}
| {
introContent?:
| {
[k: string]: unknown
}[]
| null
populateBy?: ('collection' | 'selection') | null
relationTo?: 'posts' | null
categories?: (string | Category)[] | null
limit?: number | null
selectedDocs?:
| {
relationTo: 'posts'
value: string | Post
}[]
| null
populatedDocs?:
| {
relationTo: 'posts'
value: string | Post
}[]
| null
populatedDocsTotal?: number | null
id?: string | null
blockName?: string | null
blockType: 'archive'
}
)[]
| null
relatedPosts?: (string | Post)[] | null
meta?: {
title?: string | null
description?: string | null
image?: string | Media | null
}
updatedAt: string
createdAt: string
}
export interface Category {
id: string
title?: string | null
updatedAt: string
createdAt: string
}
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
}
export interface PayloadMigration {
id: string
name?: string | null
batch?: number | null
updatedAt: string
createdAt: string
}
export interface Header {
id: string
navItems?:
| {
link: {
type?: ('reference' | 'custom') | null
newTab?: boolean | null
reference?:
| ({
relationTo: 'posts'
value: string | Post
} | null)
| ({
relationTo: 'pages'
value: string | Page
} | null)
url?: string | null
label: string
appearance?: ('default' | 'primary' | 'secondary') | null
}
id?: string | null
}[]
| null
updatedAt?: string | null
createdAt?: string | null
}
export interface Footer {
id: string
navItems?:
| {
link: {
type?: ('reference' | 'custom') | null
newTab?: boolean | null
reference?:
| ({
relationTo: 'posts'
value: string | Post
} | null)
| ({
relationTo: 'pages'
value: string | Page
} | null)
url?: string | null
label: string
appearance?: ('default' | 'primary' | 'secondary') | null
}
id?: string | null
}[]
| null
updatedAt?: string | null
createdAt?: string | null
}

View File

@@ -1,462 +0,0 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@types/escape-html':
specifier: ^1.0.2
version: 1.0.4
escape-html:
specifier: ^1.0.3
version: 1.0.3
next:
specifier: ^13.5.3
version: 13.5.6(react-dom@18.2.0)(react@18.2.0)(sass@1.72.0)
react:
specifier: ^18.2.0
version: 18.2.0
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
sass:
specifier: ^1.68.0
version: 1.72.0
slate:
specifier: ^0.94.1
version: 0.94.1
devDependencies:
'@types/node':
specifier: ^20.6.2
version: 20.11.28
'@types/react':
specifier: ^18.2.22
version: 18.2.66
packages:
/@next/env@13.5.6:
resolution: {integrity: sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==}
dev: false
/@next/swc-darwin-arm64@13.5.6:
resolution: {integrity: sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@next/swc-darwin-x64@13.5.6:
resolution: {integrity: sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-arm64-gnu@13.5.6:
resolution: {integrity: sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-arm64-musl@13.5.6:
resolution: {integrity: sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-x64-gnu@13.5.6:
resolution: {integrity: sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-x64-musl@13.5.6:
resolution: {integrity: sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-win32-arm64-msvc@13.5.6:
resolution: {integrity: sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@next/swc-win32-ia32-msvc@13.5.6:
resolution: {integrity: sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@next/swc-win32-x64-msvc@13.5.6:
resolution: {integrity: sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@swc/helpers@0.5.2:
resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==}
dependencies:
tslib: 2.6.2
dev: false
/@types/escape-html@1.0.4:
resolution: {integrity: sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==}
dev: false
/@types/node@20.11.28:
resolution: {integrity: sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==}
dependencies:
undici-types: 5.26.5
dev: true
/@types/prop-types@15.7.11:
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
dev: true
/@types/react@18.2.66:
resolution: {integrity: sha512-OYTmMI4UigXeFMF/j4uv0lBBEbongSgptPrHBxqME44h9+yNov+oL6Z3ocJKo0WyXR84sQUNeyIp9MRfckvZpg==}
dependencies:
'@types/prop-types': 15.7.11
'@types/scheduler': 0.16.8
csstype: 3.1.3
dev: true
/@types/scheduler@0.16.8:
resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==}
dev: true
/anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
dev: false
/binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
dev: false
/braces@3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
engines: {node: '>=8'}
dependencies:
fill-range: 7.0.1
dev: false
/busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
dependencies:
streamsearch: 1.1.0
dev: false
/caniuse-lite@1.0.30001597:
resolution: {integrity: sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==}
dev: false
/chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
dependencies:
anymatch: 3.1.3
braces: 3.0.2
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
dev: false
/client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
dev: false
/csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
dev: true
/escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
dev: false
/fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
dependencies:
to-regex-range: 5.0.1
dev: false
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
dev: false
optional: true
/glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
dependencies:
is-glob: 4.0.3
dev: false
/glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
dev: false
/graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
dev: false
/immer@9.0.21:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
dev: false
/immutable@4.3.5:
resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==}
dev: false
/is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
dependencies:
binary-extensions: 2.3.0
dev: false
/is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
dev: false
/is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
dependencies:
is-extglob: 2.1.1
dev: false
/is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
dev: false
/is-plain-object@5.0.0:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
dev: false
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: false
/loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
dependencies:
js-tokens: 4.0.0
dev: false
/nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
dev: false
/next@13.5.6(react-dom@18.2.0)(react@18.2.0)(sass@1.72.0):
resolution: {integrity: sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==}
engines: {node: '>=16.14.0'}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
react: ^18.2.0
react-dom: ^18.2.0
sass: ^1.3.0
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
sass:
optional: true
dependencies:
'@next/env': 13.5.6
'@swc/helpers': 0.5.2
busboy: 1.6.0
caniuse-lite: 1.0.30001597
postcss: 8.4.31
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
sass: 1.72.0
styled-jsx: 5.1.1(react@18.2.0)
watchpack: 2.4.0
optionalDependencies:
'@next/swc-darwin-arm64': 13.5.6
'@next/swc-darwin-x64': 13.5.6
'@next/swc-linux-arm64-gnu': 13.5.6
'@next/swc-linux-arm64-musl': 13.5.6
'@next/swc-linux-x64-gnu': 13.5.6
'@next/swc-linux-x64-musl': 13.5.6
'@next/swc-win32-arm64-msvc': 13.5.6
'@next/swc-win32-ia32-msvc': 13.5.6
'@next/swc-win32-x64-msvc': 13.5.6
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
dev: false
/normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
dev: false
/picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
dev: false
/picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
dev: false
/postcss@8.4.31:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.3.7
picocolors: 1.0.0
source-map-js: 1.0.2
dev: false
/react-dom@18.2.0(react@18.2.0):
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
peerDependencies:
react: ^18.2.0
dependencies:
loose-envify: 1.4.0
react: 18.2.0
scheduler: 0.23.0
dev: false
/react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}
dependencies:
loose-envify: 1.4.0
dev: false
/readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
dependencies:
picomatch: 2.3.1
dev: false
/sass@1.72.0:
resolution: {integrity: sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA==}
engines: {node: '>=14.0.0'}
hasBin: true
dependencies:
chokidar: 3.6.0
immutable: 4.3.5
source-map-js: 1.0.2
dev: false
/scheduler@0.23.0:
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
dependencies:
loose-envify: 1.4.0
dev: false
/slate@0.94.1:
resolution: {integrity: sha512-GH/yizXr1ceBoZ9P9uebIaHe3dC/g6Plpf9nlUwnvoyf6V1UOYrRwkabtOCd3ZfIGxomY4P7lfgLr7FPH8/BKA==}
dependencies:
immer: 9.0.21
is-plain-object: 5.0.0
tiny-warning: 1.0.3
dev: false
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
dev: false
/streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
dev: false
/styled-jsx@5.1.1(react@18.2.0):
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
engines: {node: '>= 12.0.0'}
peerDependencies:
'@babel/core': '*'
babel-plugin-macros: '*'
react: '>= 16.8.0 || 17.x.x || ^18.0.0-0'
peerDependenciesMeta:
'@babel/core':
optional: true
babel-plugin-macros:
optional: true
dependencies:
client-only: 0.0.1
react: 18.2.0
dev: false
/tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
dev: false
/to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
dependencies:
is-number: 7.0.0
dev: false
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: false
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
dev: true
/watchpack@2.4.0:
resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
engines: {node: '>=10.13.0'}
dependencies:
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
dev: false

View File

@@ -1,33 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"],
"@payloadcms/live-preview": ["../../../packages/live-preview"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"],
"references": [
{
"path": "../../../packages/live-preview"
}
]
}

View File

@@ -2,7 +2,7 @@ import type { Page } from '../payload-types.js'
export const postsPage: Partial<Page> = {
title: 'Posts',
slug: 'posts',
slug: 'live-preview/posts',
meta: {
title: 'Payload Website Template',
description: 'An open-source website built with Payload and Next.js.',

View File

@@ -1,5 +1,5 @@
export const formatLivePreviewURL = async ({ data, documentInfo }) => {
let baseURL = 'http://localhost:3001'
let baseURL = 'http://localhost:3000/live-preview'
// You can run async requests here, if needed
// For example, multi-tenant apps may need to lookup additional data