chore: fix live-preview tests against prod (#9122)

Live preview e2e tests had no CSS when tested against prod.

For all our other tests, we have a separate test/app directory that
imports CSS. Otherwise, the root-level /app directory is used.

For live-preview, we currently always run against test/live-preview/app,
that has no CSS import.

This PR adds a new test/live-preview/prod/app directory that imports CSS
and is used when we run tests against prod.

In order for this to work, I had to make import map generation smarter
This commit is contained in:
Alessio Gravili
2024-11-11 19:28:55 -07:00
committed by GitHub
parent d8391389ab
commit 9c559d7304
99 changed files with 3873 additions and 14 deletions

View File

@@ -59,6 +59,7 @@
"dev:generate-types": "pnpm runts ./test/generateTypes.ts", "dev:generate-types": "pnpm runts ./test/generateTypes.ts",
"dev:postgres": "cross-env PAYLOAD_DATABASE=postgres pnpm runts ./test/dev.ts", "dev:postgres": "cross-env PAYLOAD_DATABASE=postgres pnpm runts ./test/dev.ts",
"dev:prod": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts --prod", "dev:prod": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts --prod",
"dev:prod:memorydb": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts --prod --start-memory-db",
"dev:vercel-postgres": "cross-env PAYLOAD_DATABASE=vercel-postgres pnpm runts ./test/dev.ts", "dev:vercel-postgres": "cross-env PAYLOAD_DATABASE=vercel-postgres pnpm runts ./test/dev.ts",
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev", "devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start", "docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",

View File

@@ -64,13 +64,24 @@ export function addPayloadComponentToImportMap({
// then path needs to be /test/fields/components/Field.tsx NOT /users/username/project/test/fields/components/Field.tsx // then path needs to be /test/fields/components/Field.tsx NOT /users/username/project/test/fields/components/Field.tsx
// so we need to append baseDir to componentPath // so we need to append baseDir to componentPath
if (componentPath.startsWith('.') || componentPath.startsWith('/')) {
const normalizedBaseDir = baseDir.replace(/\\/g, '/')
const finalPath = normalizedBaseDir.startsWith('/../')
? `${normalizedBaseDir}${componentPath.slice(1)}`
: path.posix.join(normalizedBaseDir, componentPath.slice(1))
imports[importIdentifier] = { imports[importIdentifier] = {
path: path:
componentPath.startsWith('.') || componentPath.startsWith('/') componentPath.startsWith('.') || componentPath.startsWith('/') ? finalPath : componentPath,
? path.posix.join(baseDir.replace(/\\/g, '/'), componentPath.slice(1))
: componentPath,
specifier: exportName, specifier: exportName,
} }
} else {
imports[importIdentifier] = {
path: componentPath,
specifier: exportName,
}
}
importMap[componentPath + '#' + exportName] = importIdentifier importMap[componentPath + '#' + exportName] = importIdentifier
} }
@@ -109,7 +120,29 @@ export async function generateImportMap(
// rootDir: / // rootDir: /
// componentsBaseDir = / // componentsBaseDir = /
const componentsBaseDir = path.relative(rootDir, config.admin.importMap.baseDir) // E.g.:
// config.admin.importMap.baseDir = /test/fields/
// rootDir: /test/fields/prod
// componentsBaseDir = ../
// Check if rootDir is a subdirectory of baseDir
const baseDir = config.admin.importMap.baseDir
const isSubdirectory = path.relative(baseDir, rootDir).startsWith('..')
let componentsBaseDir
if (isSubdirectory) {
// Get the relative path from rootDir to baseDir
componentsBaseDir = path.relative(rootDir, baseDir)
} else {
// If rootDir is not a subdirectory, just return baseDir relative to rootDir
componentsBaseDir = `/${path.relative(rootDir, baseDir)}`
}
// Ensure result has a trailing slash
if (!componentsBaseDir.endsWith('/')) {
componentsBaseDir += '/'
}
const addToImportMap: AddToImportMap = (payloadComponent) => { const addToImportMap: AddToImportMap = (payloadComponent) => {
if (!payloadComponent) { if (!payloadComponent) {

View File

@@ -4,6 +4,7 @@ const [testConfigDir] = process.argv.slice(2)
import type { SanitizedConfig } from 'payload' import type { SanitizedConfig } from 'payload'
import fs from 'fs'
import { generateImportMap } from 'payload' import { generateImportMap } from 'payload'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
@@ -21,11 +22,24 @@ async function run() {
const config: SanitizedConfig = await (await import(pathWithConfig)).default const config: SanitizedConfig = await (await import(pathWithConfig)).default
process.env.ROOT_DIR = let rootDir = ''
testConfigDir === 'live-preview' || testConfigDir === 'admin-root' if (testConfigDir === 'live-preview' || testConfigDir === 'admin-root') {
? testDir rootDir = testDir
: path.resolve(dirname, '..') if (process.env.PAYLOAD_TEST_PROD === 'true') {
// If in prod mode, there may be a testSuite/prod folder. If so, use that as the rootDir
const prodDir = path.resolve(testDir, 'prod')
try {
fs.accessSync(prodDir, fs.constants.F_OK)
rootDir = prodDir
} catch (err) {
// Swallow err - no prod folder
}
}
} else {
rootDir = path.resolve(dirname, '..')
}
process.env.ROOT_DIR = rootDir
await generateImportMap(config, { log: true, force: true }) await generateImportMap(config, { log: true, force: true })
} }
} }

View File

@@ -348,7 +348,8 @@ export function initPageConsoleErrorCatch(page: Page) {
!msg.text().includes('Error: NEXT_REDIRECT') && !msg.text().includes('Error: NEXT_REDIRECT') &&
!msg.text().includes('Error getting document data') && !msg.text().includes('Error getting document data') &&
!msg.text().includes('Failed trying to load default language strings') && !msg.text().includes('Failed trying to load default language strings') &&
!msg.text().includes('TypeError: Failed to fetch') // This happens when server actions are aborted !msg.text().includes('TypeError: Failed to fetch') && // This happens when server actions are aborted
!msg.text().includes('der-radius: 2px Server Error: Error getting do') // This is a weird error that happens in the console
) { ) {
// "Failed to fetch RSC payload for" happens seemingly randomly. There are lots of issues in the next.js repository for this. Causes e2e tests to fail and flake. Will ignore for now // "Failed to fetch RSC payload for" happens seemingly randomly. There are lots of issues in the next.js repository for this. Causes e2e tests to fail and flake. Will ignore for now
// the the server responded with a status of error happens frequently. Will ignore it for now. // the the server responded with a status of error happens frequently. Will ignore it for now.

View File

@@ -33,8 +33,19 @@ export function getNextRootDir(testSuite?: string) {
} }
if (hasNextConfig) { if (hasNextConfig) {
let rootDir = testSuiteDir
if (process.env.PAYLOAD_TEST_PROD === 'true') {
// If in prod mode, there may be a testSuite/prod folder. If so, use that as the rootDir
const prodDir = resolve(testSuiteDir, 'prod')
try {
fs.accessSync(prodDir, fs.constants.F_OK)
rootDir = prodDir
} catch (err) {
// Swallow err - no prod folder
}
}
return { return {
rootDir: testSuiteDir, rootDir,
adminRoute, adminRoute,
} }
} }

View File

@@ -32,6 +32,7 @@ import {
ssrAutosavePagesSlug, ssrAutosavePagesSlug,
ssrPagesSlug, ssrPagesSlug,
} from './shared.js' } from './shared.js'
import { wait } from 'payload/shared'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
@@ -178,8 +179,12 @@ describe('Live Preview', () => {
await expect(frame.locator(renderedPageTitleLocator)).toHaveText('For Testing: SSR Home') await expect(frame.locator(renderedPageTitleLocator)).toHaveText('For Testing: SSR Home')
const newTitleValue = 'SSR Home (Edited)' const newTitleValue = 'SSR Home (Edited)'
await wait(1000)
await titleField.fill(newTitleValue) await titleField.clear()
await titleField.pressSequentially(newTitleValue)
await wait(1000)
await waitForAutoSaveToRunAndComplete(page) await waitForAutoSaveToRunAndComplete(page)

View File

@@ -0,0 +1,25 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, importMap, params, searchParams })
export default NotFound

View File

@@ -0,0 +1,25 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { generatePageMetadata, RootPage } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, importMap, params, searchParams })
export default Page

View File

@@ -0,0 +1,141 @@
import { RscEntrySlateCell as RscEntrySlateCell_0e78253914a550fdacd75626f1dabe17 } from '@payloadcms/richtext-slate/rsc'
import { RscEntrySlateField as RscEntrySlateField_0e78253914a550fdacd75626f1dabe17 } from '@payloadcms/richtext-slate/rsc'
import { BoldLeafButton as BoldLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { BoldLeaf as BoldLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { CodeLeafButton as CodeLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { CodeLeaf as CodeLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { ItalicLeafButton as ItalicLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { ItalicLeaf as ItalicLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { StrikethroughLeafButton as StrikethroughLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { StrikethroughLeaf as StrikethroughLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { UnderlineLeafButton as UnderlineLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { UnderlineLeaf as UnderlineLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { BlockquoteElementButton as BlockquoteElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { BlockquoteElement as BlockquoteElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { H1ElementButton as H1ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { Heading1Element as Heading1Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { H2ElementButton as H2ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { Heading2Element as Heading2Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { H3ElementButton as H3ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { Heading3Element as Heading3Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { H4ElementButton as H4ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { Heading4Element as Heading4Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { H5ElementButton as H5ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { Heading5Element as Heading5Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { H6ElementButton as H6ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { Heading6Element as Heading6Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { IndentButton as IndentButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { IndentElement as IndentElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { ListItemElement as ListItemElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { LinkButton as LinkButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { LinkElement as LinkElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { WithLinks as WithLinks_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { OLElementButton as OLElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { OrderedListElement as OrderedListElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { RelationshipButton as RelationshipButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { RelationshipElement as RelationshipElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { WithRelationship as WithRelationship_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { TextAlignElementButton as TextAlignElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { ULElementButton as ULElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { UnorderedListElement as UnorderedListElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { UploadElementButton as UploadElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { UploadElement as UploadElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { WithUpload as WithUpload_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { TreeViewFeatureClient as TreeViewFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { CollectionLivePreviewButton as CollectionLivePreviewButton_cdf8c2f401206aec7d2609afbb1e2f40 } from '/../components/CollectionLivePreviewButton/index.js'
import { GlobalLivePreviewButton as GlobalLivePreviewButton_cef560d649f896ba95825fe38ff5f91f } from '/../components/GlobalLivePreviewButton/index.js'
export const importMap = {
"@payloadcms/richtext-slate/rsc#RscEntrySlateCell": RscEntrySlateCell_0e78253914a550fdacd75626f1dabe17,
"@payloadcms/richtext-slate/rsc#RscEntrySlateField": RscEntrySlateField_0e78253914a550fdacd75626f1dabe17,
"@payloadcms/richtext-slate/client#BoldLeafButton": BoldLeafButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#BoldLeaf": BoldLeaf_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#CodeLeafButton": CodeLeafButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#CodeLeaf": CodeLeaf_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#ItalicLeafButton": ItalicLeafButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#ItalicLeaf": ItalicLeaf_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#StrikethroughLeafButton": StrikethroughLeafButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#StrikethroughLeaf": StrikethroughLeaf_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#UnderlineLeafButton": UnderlineLeafButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#UnderlineLeaf": UnderlineLeaf_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#BlockquoteElementButton": BlockquoteElementButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#BlockquoteElement": BlockquoteElement_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#H1ElementButton": H1ElementButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#Heading1Element": Heading1Element_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#H2ElementButton": H2ElementButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#Heading2Element": Heading2Element_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#H3ElementButton": H3ElementButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#Heading3Element": Heading3Element_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#H4ElementButton": H4ElementButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#Heading4Element": Heading4Element_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#H5ElementButton": H5ElementButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#Heading5Element": Heading5Element_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#H6ElementButton": H6ElementButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#Heading6Element": Heading6Element_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#IndentButton": IndentButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#IndentElement": IndentElement_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#ListItemElement": ListItemElement_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#LinkButton": LinkButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#LinkElement": LinkElement_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#WithLinks": WithLinks_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#OLElementButton": OLElementButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#OrderedListElement": OrderedListElement_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#RelationshipButton": RelationshipButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#RelationshipElement": RelationshipElement_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#WithRelationship": WithRelationship_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#TextAlignElementButton": TextAlignElementButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#ULElementButton": ULElementButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#UnorderedListElement": UnorderedListElement_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#UploadElementButton": UploadElementButton_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#UploadElement": UploadElement_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-slate/client#WithUpload": WithUpload_0b388c087d9de8c4f011dd323a130cfb,
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#TreeViewFeatureClient": TreeViewFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"/components/CollectionLivePreviewButton/index.js#CollectionLivePreviewButton": CollectionLivePreviewButton_cdf8c2f401206aec7d2609afbb1e2f40,
"/components/GlobalLivePreviewButton/index.js#GlobalLivePreviewButton": GlobalLivePreviewButton_cef560d649f896ba95825fe38ff5f91f
}

View File

@@ -0,0 +1,10 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -0,0 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes/index.js'
export const GET = GRAPHQL_PLAYGROUND_GET(config)

View File

@@ -0,0 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -0,0 +1,7 @@
#custom-css {
font-family: monospace;
}
#custom-css::after {
content: 'custom-css';
}

View File

@@ -0,0 +1,32 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { ServerFunctionClient } from 'payload'
import config from '@payload-config'
import '@payloadcms/next/css'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'
import { importMap } from './admin/importMap.js'
import './custom.scss'
type Args = {
children: React.ReactNode
}
const serverFunction: ServerFunctionClient = async function (args) {
'use server'
return handleServerFunctions({
...args,
config,
importMap,
})
}
const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
)
export default Layout

View File

@@ -0,0 +1,44 @@
'use client'
import { useLivePreview } from '@payloadcms/live-preview-react'
import React from 'react'
import type { Page as PageType } from '../../../../../payload-types.js'
import { renderedPageTitleID } from '../../../../../shared.js'
import { PAYLOAD_SERVER_URL } from '../../_api/serverURL.js'
import { Blocks } from '../../_components/Blocks/index.js'
import { Gutter } from '../../_components/Gutter/index.js'
import { Hero } from '../../_components/Hero/index.js'
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'
}
/>
<Gutter>
<div id={renderedPageTitleID}>{`For Testing: ${data.title}`}</div>
</Gutter>
</React.Fragment>
)
}

View File

@@ -0,0 +1,41 @@
import { notFound } from 'next/navigation.js'
import React from 'react'
import type { Page } from '../../../../../payload-types.js'
import { getDoc } from '../../_api/getDoc.js'
import { getDocs } from '../../_api/getDocs.js'
import { PageClient } from './page.client.js'
type Args = {
params: Promise<{ slug?: string }>
}
export default async function Page({ params: paramsPromise }: Args) {
const { slug = 'home' } = await paramsPromise
let page: null | Page = null
try {
page = await getDoc<Page>({
slug,
collection: 'pages',
})
} catch (error) {
console.error(error) // eslint-disable-line no-console
}
if (!page) {
return notFound()
}
return <PageClient page={page} />
}
export async function generateStaticParams() {
process.env.PAYLOAD_DROP_DATABASE = 'false'
try {
const pages = await getDocs<Page>('pages')
return pages?.map(({ slug }) => slug)
} catch (error) {
return []
}
}

View File

@@ -0,0 +1,73 @@
'use client'
import { useLivePreview } from '@payloadcms/live-preview-react'
import { Gutter } from '@payloadcms/ui'
import React from 'react'
import type { Post as PostType } from '../../../../../../payload-types.js'
import { postsSlug, renderedPageTitleID } from '../../../../../../shared.js'
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
import { Blocks } from '../../../_components/Blocks/index.js'
import { PostHero } from '../../../_heros/PostHero/index.js'
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: postsSlug,
},
]}
disableTopPadding
/>
<Gutter>
<div id={renderedPageTitleID}>{`For Testing: ${data.title}`}</div>
</Gutter>
</React.Fragment>
)
}

View File

@@ -0,0 +1,45 @@
import { notFound } from 'next/navigation.js'
import React from 'react'
import type { Post } from '../../../../../../payload-types.js'
import { postsSlug } from '../../../../../../shared.js'
import { getDoc } from '../../../_api/getDoc.js'
import { getDocs } from '../../../_api/getDocs.js'
import { PostClient } from './page.client.js'
type Args = {
params: Promise<{
slug?: string
}>
}
export default async function Post({ params: paramsPromise }: Args) {
const { slug = '' } = await paramsPromise
let post: null | Post = null
try {
post = await getDoc<Post>({
slug,
collection: postsSlug,
})
} catch (error) {
console.error(error) // eslint-disable-line no-console
}
if (!post) {
notFound()
}
return <PostClient post={post} />
}
export async function generateStaticParams() {
process.env.PAYLOAD_DROP_DATABASE = 'false'
try {
const ssrPosts = await getDocs<Post>(postsSlug)
return ssrPosts?.map(({ slug }) => slug)
} catch (error) {
return []
}
}

View File

@@ -0,0 +1,12 @@
'use client'
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
import { useRouter } from 'next/navigation.js'
import React from 'react'
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
export const RefreshRouteOnSave: React.FC = () => {
const router = useRouter()
return <PayloadLivePreview refresh={() => router.refresh()} serverURL={PAYLOAD_SERVER_URL} />
}

View File

@@ -0,0 +1,52 @@
import { Gutter } from '@payloadcms/ui'
import { notFound } from 'next/navigation.js'
import React, { Fragment } from 'react'
import type { Page } from '../../../../../../payload-types.js'
import { renderedPageTitleID, ssrAutosavePagesSlug } from '../../../../../../shared.js'
import { getDoc } from '../../../_api/getDoc.js'
import { getDocs } from '../../../_api/getDocs.js'
import { Blocks } from '../../../_components/Blocks/index.js'
import { Hero } from '../../../_components/Hero/index.js'
import { RefreshRouteOnSave } from './RefreshRouteOnSave.js'
type Args = {
params: Promise<{
slug?: string
}>
}
export default async function SSRAutosavePage({ params: paramsPromise }: Args) {
const { slug = '' } = await paramsPromise
const data = await getDoc<Page>({
slug,
collection: ssrAutosavePagesSlug,
draft: true,
})
if (!data) {
notFound()
}
return (
<Fragment>
<RefreshRouteOnSave />
<Hero {...data?.hero} />
<Blocks blocks={data?.layout} />
<Gutter>
<div id={renderedPageTitleID}>{`For Testing: ${data.title}`}</div>
</Gutter>
</Fragment>
)
}
export async function generateStaticParams() {
process.env.PAYLOAD_DROP_DATABASE = 'false'
try {
const ssrPages = await getDocs<Page>(ssrAutosavePagesSlug)
return ssrPages?.map(({ slug }) => slug)
} catch (error) {
return []
}
}

View File

@@ -0,0 +1,12 @@
'use client'
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
import { useRouter } from 'next/navigation.js'
import React from 'react'
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
export const RefreshRouteOnSave: React.FC = () => {
const router = useRouter()
return <PayloadLivePreview refresh={() => router.refresh()} serverURL={PAYLOAD_SERVER_URL} />
}

View File

@@ -0,0 +1,52 @@
import { Gutter } from '@payloadcms/ui'
import { notFound } from 'next/navigation.js'
import React, { Fragment } from 'react'
import type { Page } from '../../../../../../payload-types.js'
import { renderedPageTitleID, ssrPagesSlug } from '../../../../../../shared.js'
import { getDoc } from '../../../_api/getDoc.js'
import { getDocs } from '../../../_api/getDocs.js'
import { Blocks } from '../../../_components/Blocks/index.js'
import { Hero } from '../../../_components/Hero/index.js'
import { RefreshRouteOnSave } from './RefreshRouteOnSave.js'
type Args = {
params: Promise<{
slug?: string
}>
}
export default async function SSRPage({ params: paramsPromise }: Args) {
const { slug = ' ' } = await paramsPromise
const data = await getDoc<Page>({
slug,
collection: ssrPagesSlug,
draft: true,
})
if (!data) {
notFound()
}
return (
<Fragment>
<RefreshRouteOnSave />
<Hero {...data?.hero} />
<Blocks blocks={data?.layout} />
<Gutter>
<div id={renderedPageTitleID}>{`For Testing: ${data.title}`}</div>
</Gutter>
</Fragment>
)
}
export async function generateStaticParams() {
process.env.PAYLOAD_DROP_DATABASE = 'false'
try {
const ssrPages = await getDocs<Page>(ssrPagesSlug)
return ssrPages?.map(({ slug }) => slug)
} catch (error) {
return []
}
}

View File

@@ -0,0 +1,37 @@
import type { CollectionSlug, Where } from 'payload'
import config from '@payload-config'
import { getPayloadHMR } from '@payloadcms/next/utilities/getPayloadHMR.js'
export const getDoc = async <T>(args: {
collection: CollectionSlug
depth?: number
draft?: boolean
slug?: string
}): Promise<T> => {
const payload = await getPayloadHMR({ config })
const { slug, collection, depth = 2, draft } = args || {}
const where: Where = {}
if (slug) {
where.slug = {
equals: slug,
}
}
try {
const { docs } = await payload.find({
collection,
depth,
where,
draft,
})
if (docs[0]) return docs[0] as T
} catch (err) {
console.log('Error getting doc', err)
}
throw new Error('Error getting doc')
}

View File

@@ -0,0 +1,20 @@
import config from '@payload-config'
import { getPayloadHMR } from '@payloadcms/next/utilities/getPayloadHMR.js'
export const getDocs = async <T>(collection: string): Promise<T[]> => {
const payload = await getPayloadHMR({ config })
try {
const { docs } = await payload.find({
collection,
depth: 0,
limit: 100,
})
return docs as T[]
} catch (err) {
console.error(err)
}
throw new Error('Error getting docs')
}

View File

@@ -0,0 +1,20 @@
import config from '@payload-config'
import { getPayloadHMR } from '@payloadcms/next/utilities/getPayloadHMR.js'
import type { Footer } from '../../../../payload-types.js'
export async function getFooter(): Promise<Footer> {
const payload = await getPayloadHMR({ config })
try {
const footer = await payload.findGlobal({
slug: 'footer',
})
return footer
} catch (err) {
console.error(err)
}
throw new Error('Error getting footer.')
}

View File

@@ -0,0 +1,20 @@
import config from '@payload-config'
import { getPayloadHMR } from '@payloadcms/next/utilities/getPayloadHMR.js'
import type { Header } from '../../../../payload-types.js'
export async function getHeader(): Promise<Header> {
const payload = await getPayloadHMR({ config })
try {
const header = await payload.findGlobal({
slug: 'header',
})
return header
} catch (err) {
console.error(err)
}
throw new Error('Error getting header.')
}

View File

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

View File

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

View File

@@ -0,0 +1,46 @@
import React from 'react'
import type { ArchiveBlockProps } from './types.js'
import { CollectionArchive } from '../../_components/CollectionArchive/index.js'
import { Gutter } from '../../_components/Gutter/index.js'
import RichText from '../../_components/RichText/index.js'
import classes from './index.module.scss'
export const ArchiveBlock: React.FC<
{
id?: string
} & ArchiveBlockProps
> = (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

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

View File

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

@@ -0,0 +1,38 @@
import React from 'react'
import type { Page } from '../../../../../payload-types.js'
import { Gutter } from '../../_components/Gutter/index.js'
import { CMSLink } from '../../_components/Link/index.js'
import RichText from '../../_components/RichText/index.js'
import { VerticalPadding } from '../../_components/VerticalPadding/index.js'
import classes from './index.module.scss'
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'cta' }>
export const CallToActionBlock: React.FC<
{
id?: string
} & Props
> = ({ 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

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

@@ -0,0 +1,39 @@
import React, { Fragment } from 'react'
import type { Page } from '../../../../../payload-types.js'
import { Gutter } from '../../_components/Gutter/index.js'
import { CMSLink } from '../../_components/Link/index.js'
import RichText from '../../_components/RichText/index.js'
import classes from './index.module.scss'
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'content' }>
export const ContentBlock: React.FC<
{
id?: string
} & Props
> = (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

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

View File

@@ -0,0 +1,42 @@
import type { StaticImageData } from 'next/image.js'
import React from 'react'
import type { Page } from '../../../../../payload-types.js'
import { Gutter } from '../../_components/Gutter/index.js'
import { Media } from '../../_components/Media/index.js'
import RichText from '../../_components/RichText/index.js'
import classes from './index.module.scss'
type Props = {
id?: string
staticImage?: StaticImageData
} & Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'mediaBlock' }>
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

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

@@ -0,0 +1,52 @@
import React from 'react'
import type { Post } from '../../../../../payload-types.js'
import { Card } from '../../_components/Card/index.js'
import { Gutter } from '../../_components/Gutter/index.js'
import RichText from '../../_components/RichText/index.js'
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

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

@@ -0,0 +1,196 @@
import React, { Fragment } from 'react'
import type { Page } from '../../../../../payload-types.js'
import { Gutter } from '../../_components/Gutter/index.js'
import RichText from '../../_components/RichText/index.js'
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

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

View File

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

@@ -0,0 +1,88 @@
import React, { Fragment } from 'react'
import type { Page } from '../../../../../payload-types.js'
import type { RelationshipsBlockProps } from '../../_blocks/Relationships/index.js'
import type { VerticalPaddingOptions } from '../VerticalPadding/index.js'
import { ArchiveBlock } from '../../_blocks/ArchiveBlock/index.js'
import { CallToActionBlock } from '../../_blocks/CallToAction/index.js'
import { ContentBlock } from '../../_blocks/Content/index.js'
import { MediaBlock } from '../../_blocks/MediaBlock/index.js'
import { RelatedPosts, type RelatedPostsProps } from '../../_blocks/RelatedPosts/index.js'
import { RelationshipsBlock } from '../../_blocks/Relationships/index.js'
import { toKebabCase } from '../../_utilities/toKebabCase.js'
import { BackgroundColor } from '../BackgroundColor/index.js'
import { VerticalPadding } from '../VerticalPadding/index.js'
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

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

@@ -0,0 +1,80 @@
'use client'
import type { ElementType } from 'react'
import LinkWithDefault from 'next/link.js'
import React from 'react'
import classes from './index.module.scss'
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
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

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

@@ -0,0 +1,88 @@
import LinkWithDefault from 'next/link.js'
import React, { Fragment } from 'react'
import type { Post } from '../../../../../payload-types.js'
import { Media } from '../Media/index.js'
import classes from './index.module.scss'
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
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 = `/live-preview/${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

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

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

@@ -0,0 +1,186 @@
'use client'
import * as qs from 'qs-esm'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import type { Post } from '../../../../../../payload-types.js'
import type { ArchiveBlockProps } from '../../../_blocks/ArchiveBlock/types.js'
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
import { Card } from '../../Card/index.js'
import { Gutter } from '../../Gutter/index.js'
import { PageRange } from '../../PageRange/index.js'
import { Pagination } from '../../Pagination/index.js'
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 = {
className?: string
onResultChange?: (result: Result) => void
showPageRange?: boolean
sort?: string
} & Omit<ArchiveBlockProps, 'blockType'>
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(() => {
// 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
const 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.`)
}
}
void 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

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

@@ -0,0 +1,42 @@
'use client'
import React, { Fragment } from 'react'
import type { ArchiveBlockProps } from '../../../_blocks/ArchiveBlock/types.js'
import { Card } from '../../Card/index.js'
import { Gutter } from '../../Gutter/index.js'
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

@@ -0,0 +1,25 @@
import React from 'react'
import type { ArchiveBlockProps } from '../../_blocks/ArchiveBlock/types.js'
import { CollectionArchiveByCollection } from './PopulateByCollection/index.js'
import { CollectionArchiveBySelection } from './PopulateBySelection/index.js'
export type Props = {
className?: string
sort?: string
} & Omit<ArchiveBlockProps, 'blockType'>
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

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

@@ -0,0 +1,41 @@
import LinkWithDefault from 'next/link.js'
import React from 'react'
import { getFooter } from '../../_api/getFooter.js'
import { Gutter } from '../Gutter/index.js'
import { CMSLink } from '../Link/index.js'
import classes from './index.module.scss'
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
export async function Footer() {
const footer = await getFooter()
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

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

View File

@@ -0,0 +1,35 @@
import type { Ref } from 'react'
import React, { forwardRef } from 'react'
import classes from './index.module.scss'
type Props = {
children: React.ReactNode
className?: string
left?: boolean
ref?: Ref<HTMLDivElement>
right?: boolean
}
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
const { children, className, left = true, right = true } = props
return (
<div
className={[
classes.gutter,
left && classes.gutterLeft,
right && classes.gutterRight,
className,
]
.filter(Boolean)
.join(' ')}
ref={ref}
>
{children}
</div>
)
})
Gutter.displayName = 'Gutter'

View File

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

@@ -0,0 +1,20 @@
'use client'
import React from 'react'
import type { Header as HeaderType } from '../../../../../../payload-types.js'
import { CMSLink } from '../../Link/index.js'
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

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

@@ -0,0 +1,28 @@
import LinkWithDefault from 'next/link.js'
import React from 'react'
import { getHeader } from '../../_api/getHeader.js'
import { Gutter } from '../Gutter/index.js'
import { HeaderNav } from './Nav/index.js'
import classes from './index.module.scss'
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
export async function Header() {
const header = await getHeader()
return (
<header className={classes.header}>
<Gutter className={classes.wrap}>
<Link href="/live-preview">
<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

@@ -0,0 +1,23 @@
import React from 'react'
import type { Page } from '../../../../../payload-types.js'
import { HighImpactHero } from '../../_heros/HighImpact/index.js'
import { LowImpactHero } from '../../_heros/LowImpact/index.js'
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

@@ -0,0 +1,67 @@
import LinkWithDefault from 'next/link.js'
import React from 'react'
import type { Page, Post } from '../../../../../payload-types.js'
import type { Props as ButtonProps } from '../Button/index.js'
import { Button } from '../Button/index.js'
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
type CMSLinkType = {
appearance?: ButtonProps['appearance']
children?: React.ReactNode
className?: string
invert?: ButtonProps['invert']
label?: string
newTab?: boolean
reference?: {
relationTo: 'pages' | 'posts'
value: Page | Post | 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

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

View File

@@ -0,0 +1,81 @@
'use client'
import type { StaticImageData } from 'next/image.js'
import NextImageWithDefault from 'next/image.js'
import React from 'react'
import type { Props as MediaProps } from '../types.js'
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
import cssVariables from '../../../cssVariables.js'
import classes from './index.module.scss'
const { breakpoints } = cssVariables
const NextImage = (NextImageWithDefault.default ||
NextImageWithDefault) as typeof NextImageWithDefault.default
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}/api/media/file/${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

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

View File

@@ -0,0 +1,46 @@
'use client'
import React, { useEffect, useRef } from 'react'
import type { Props as MediaProps } from '../types.js'
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
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

@@ -0,0 +1,25 @@
import React, { Fragment } from 'react'
import type { Props } from './types.js'
import { Image } from './Image/index.js'
import { Video } from './Video/index.js'
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 || Fragment
return (
<Tag
{...(htmlElement !== null
? {
className,
}
: {})}
>
{isVideo ? <Video {...props} /> : <Image {...props} />}
</Tag>
)
}

View File

@@ -0,0 +1,20 @@
import type { StaticImageData } from 'next/image.js'
import type { ElementType, Ref } from 'react'
import type { Media as MediaType } from '../../../../../payload-types.js'
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

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

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

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

@@ -0,0 +1,45 @@
import React from 'react'
import { Chevron } from '../Chevron/index.js'
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

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

View File

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

@@ -0,0 +1,92 @@
import type { SerializedEditorState } from 'lexical'
import React from 'react'
import { CMSLink } from '../Link/index.js'
import { Media } from '../Media/index.js'
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

@@ -0,0 +1,130 @@
import escapeHTML from 'escape-html'
import React, { Fragment } from 'react'
import { Text } from 'slate'
import { CMSLink } from '../Link/index.js'
import { Media } from '../Media/index.js'
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

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

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

@@ -0,0 +1,121 @@
@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;
}
}
#page-title {
@extend %h6;
}
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

@@ -0,0 +1,105 @@
// 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(237, 245, 249);
--color-success-100: rgb(218, 237, 248);
--color-success-150: rgb(188, 225, 248);
--color-success-200: rgb(156, 216, 253);
--color-success-250: rgb(125, 204, 248);
--color-success-300: rgb(97, 190, 241);
--color-success-350: rgb(65, 178, 236);
--color-success-400: rgb(36, 164, 223);
--color-success-450: rgb(18, 148, 204);
--color-success-500: rgb(21, 135, 186);
--color-success-550: rgb(12, 121, 168);
--color-success-600: rgb(11, 110, 153);
--color-success-650: rgb(11, 97, 135);
--color-success-700: rgb(17, 88, 121);
--color-success-750: rgb(17, 76, 105);
--color-success-800: rgb(18, 66, 90);
--color-success-850: rgb(18, 56, 76);
--color-success-900: rgb(19, 44, 58);
--color-success-950: rgb(22, 33, 39);
--color-error-50: rgb(250, 241, 240);
--color-error-100: rgb(252, 229, 227);
--color-error-150: rgb(247, 208, 204);
--color-error-200: rgb(254, 193, 188);
--color-error-250: rgb(253, 177, 170);
--color-error-300: rgb(253, 154, 146);
--color-error-350: rgb(253, 131, 123);
--color-error-400: rgb(246, 109, 103);
--color-error-450: rgb(234, 90, 86);
--color-error-500: rgb(218, 75, 72);
--color-error-550: rgb(200, 62, 61);
--color-error-600: rgb(182, 54, 54);
--color-error-650: rgb(161, 47, 47);
--color-error-700: rgb(144, 44, 43);
--color-error-750: rgb(123, 41, 39);
--color-error-800: rgb(105, 39, 37);
--color-error-850: rgb(86, 36, 33);
--color-error-900: rgb(64, 32, 29);
--color-error-950: rgb(44, 26, 24);
--color-warning-50: rgb(249, 242, 237);
--color-warning-100: rgb(248, 232, 219);
--color-warning-150: rgb(243, 212, 186);
--color-warning-200: rgb(243, 200, 162);
--color-warning-250: rgb(240, 185, 136);
--color-warning-300: rgb(238, 166, 98);
--color-warning-350: rgb(234, 148, 58);
--color-warning-400: rgb(223, 132, 17);
--color-warning-450: rgb(204, 120, 15);
--color-warning-500: rgb(185, 108, 13);
--color-warning-550: rgb(167, 97, 10);
--color-warning-600: rgb(150, 87, 11);
--color-warning-650: rgb(134, 78, 11);
--color-warning-700: rgb(120, 70, 13);
--color-warning-750: rgb(105, 61, 13);
--color-warning-800: rgb(90, 55, 19);
--color-warning-850: rgb(73, 47, 21);
--color-warning-900: rgb(56, 38, 20);
--color-warning-950: rgb(38, 29, 21);
--color-blue-50: rgb(237, 245, 249);
--color-blue-100: rgb(218, 237, 248);
--color-blue-150: rgb(188, 225, 248);
--color-blue-200: rgb(156, 216, 253);
--color-blue-250: rgb(125, 204, 248);
--color-blue-300: rgb(97, 190, 241);
--color-blue-350: rgb(65, 178, 236);
--color-blue-400: rgb(36, 164, 223);
--color-blue-450: rgb(18, 148, 204);
--color-blue-500: rgb(21, 135, 186);
--color-blue-550: rgb(12, 121, 168);
--color-blue-600: rgb(11, 110, 153);
--color-blue-650: rgb(11, 97, 135);
--color-blue-700: rgb(17, 88, 121);
--color-blue-750: rgb(17, 76, 105);
--color-blue-800: rgb(18, 66, 90);
--color-blue-850: rgb(18, 56, 76);
--color-blue-900: rgb(19, 44, 58);
--color-blue-950: rgb(22, 33, 39);
}

View File

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

View File

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

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

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

@@ -0,0 +1,31 @@
import React, { Fragment } from 'react'
import type { Page } from '../../../../../payload-types.js'
import { Gutter } from '../../_components/Gutter/index.js'
import { Media } from '../../_components/Media/index.js'
import RichText from '../../_components/RichText/index.js'
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

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

View File

@@ -0,0 +1,20 @@
import React from 'react'
import type { Page } from '../../../../../payload-types.js'
import { Gutter } from '../../_components/Gutter/index.js'
import RichText from '../../_components/RichText/index.js'
import { VerticalPadding } from '../../_components/VerticalPadding/index.js'
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

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

@@ -0,0 +1,57 @@
import LinkWithDefault from 'next/link.js'
import React, { Fragment } from 'react'
import type { Post } from '../../../../../payload-types.js'
import { PAYLOAD_SERVER_URL } from '../../_api/serverURL.js'
import { Gutter } from '../../_components/Gutter/index.js'
import { Media } from '../../_components/Media/index.js'
import RichText from '../../_components/RichText/index.js'
import { formatDateTime } from '../../_utilities/formatDateTime.js'
import classes from './index.module.scss'
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
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

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

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

View File

@@ -0,0 +1,16 @@
// Keep these in sync with the CSS variables in the `_css` directory
export default {
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

@@ -0,0 +1,24 @@
import type { Metadata } from 'next'
import React from 'react'
import { Footer } from './_components/Footer/index.js'
import { Header } from './_components/Header/index.js'
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

@@ -0,0 +1,19 @@
'use client'
import React from 'react'
import { Gutter } from './_components/Gutter/index.js'
import { VerticalPadding } from './_components/VerticalPadding/index.js'
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

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

5
test/live-preview/prod/next-env.d.ts vendored Normal file
View File

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

View File

@@ -0,0 +1,15 @@
import nextConfig from '../../../next.config.mjs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(__filename)
export default {
...nextConfig,
env: {
PAYLOAD_CORE_DEV: 'true',
ROOT_DIR: path.resolve(dirname),
},
}

View File

@@ -0,0 +1,26 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@payload-config": ["../config.ts"],
"@payloadcms/ui/assets": ["../../../packages/ui/src/assets/index.ts"],
"@payloadcms/ui/elements/*": ["../../../packages/ui/src/elements/*/index.tsx"],
"@payloadcms/ui/fields/*": ["../../../packages/ui/src/fields/*/index.tsx"],
"@payloadcms/ui/forms/*": ["../../../packages/ui/src/forms/*/index.tsx"],
"@payloadcms/ui/graphics/*": ["../../../packages/ui/src/graphics/*/index.tsx"],
"@payloadcms/ui/hooks/*": ["../../../packages/ui/src/hooks/*.ts"],
"@payloadcms/ui/icons/*": ["../../../packages/ui/src/icons/*/index.tsx"],
"@payloadcms/ui/providers/*": ["../../../packages/ui/src/providers/*/index.tsx"],
"@payloadcms/ui/templates/*": ["../../../packages/ui/src/templates/*/index.tsx"],
"@payloadcms/ui/utilities/*": ["../../../packages/ui/src/utilities/*.ts"],
"@payloadcms/ui/scss": ["../../../packages/ui/src/scss.scss"],
"@payloadcms/ui/scss/app.scss": ["../../../packages/ui/src/scss/app.scss"],
"payload/types": ["../../../packages/payload/src/exports/types.ts"],
"@payloadcms/next/*": ["../../../packages/next/src/*"],
"@payloadcms/next": ["../../../packages/next/src/exports/*"]
}
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}