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:
@@ -59,6 +59,7 @@
|
||||
"dev:generate-types": "pnpm runts ./test/generateTypes.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: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",
|
||||
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
|
||||
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
|
||||
|
||||
@@ -64,12 +64,23 @@ export function addPayloadComponentToImportMap({
|
||||
// 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
|
||||
|
||||
imports[importIdentifier] = {
|
||||
path:
|
||||
componentPath.startsWith('.') || componentPath.startsWith('/')
|
||||
? path.posix.join(baseDir.replace(/\\/g, '/'), componentPath.slice(1))
|
||||
: componentPath,
|
||||
specifier: exportName,
|
||||
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] = {
|
||||
path:
|
||||
componentPath.startsWith('.') || componentPath.startsWith('/') ? finalPath : componentPath,
|
||||
specifier: exportName,
|
||||
}
|
||||
} else {
|
||||
imports[importIdentifier] = {
|
||||
path: componentPath,
|
||||
specifier: exportName,
|
||||
}
|
||||
}
|
||||
importMap[componentPath + '#' + exportName] = importIdentifier
|
||||
}
|
||||
@@ -109,7 +120,29 @@ export async function generateImportMap(
|
||||
// rootDir: /
|
||||
// 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) => {
|
||||
if (!payloadComponent) {
|
||||
|
||||
@@ -4,6 +4,7 @@ const [testConfigDir] = process.argv.slice(2)
|
||||
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import fs from 'fs'
|
||||
import { generateImportMap } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -21,11 +22,24 @@ async function run() {
|
||||
|
||||
const config: SanitizedConfig = await (await import(pathWithConfig)).default
|
||||
|
||||
process.env.ROOT_DIR =
|
||||
testConfigDir === 'live-preview' || testConfigDir === 'admin-root'
|
||||
? testDir
|
||||
: path.resolve(dirname, '..')
|
||||
let rootDir = ''
|
||||
if (testConfigDir === 'live-preview' || testConfigDir === 'admin-root') {
|
||||
rootDir = testDir
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,7 +348,8 @@ export function initPageConsoleErrorCatch(page: Page) {
|
||||
!msg.text().includes('Error: NEXT_REDIRECT') &&
|
||||
!msg.text().includes('Error getting document data') &&
|
||||
!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
|
||||
// the the server responded with a status of error happens frequently. Will ignore it for now.
|
||||
|
||||
@@ -33,8 +33,19 @@ export function getNextRootDir(testSuite?: string) {
|
||||
}
|
||||
|
||||
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 {
|
||||
rootDir: testSuiteDir,
|
||||
rootDir,
|
||||
adminRoute,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
ssrAutosavePagesSlug,
|
||||
ssrPagesSlug,
|
||||
} from './shared.js'
|
||||
import { wait } from 'payload/shared'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@@ -178,8 +179,12 @@ describe('Live Preview', () => {
|
||||
await expect(frame.locator(renderedPageTitleLocator)).toHaveText('For Testing: SSR Home')
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
141
test/live-preview/prod/app/(payload)/admin/importMap.js
Normal file
141
test/live-preview/prod/app/(payload)/admin/importMap.js
Normal 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
|
||||
}
|
||||
10
test/live-preview/prod/app/(payload)/api/[...slug]/route.ts
Normal file
10
test/live-preview/prod/app/(payload)/api/[...slug]/route.ts
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
7
test/live-preview/prod/app/(payload)/custom.scss
Normal file
7
test/live-preview/prod/app/(payload)/custom.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
#custom-css {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#custom-css::after {
|
||||
content: 'custom-css';
|
||||
}
|
||||
32
test/live-preview/prod/app/(payload)/layout.tsx
Normal file
32
test/live-preview/prod/app/(payload)/layout.tsx
Normal 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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 []
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 []
|
||||
}
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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 []
|
||||
}
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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 []
|
||||
}
|
||||
}
|
||||
37
test/live-preview/prod/app/live-preview/_api/getDoc.ts
Normal file
37
test/live-preview/prod/app/live-preview/_api/getDoc.ts
Normal 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')
|
||||
}
|
||||
20
test/live-preview/prod/app/live-preview/_api/getDocs.ts
Normal file
20
test/live-preview/prod/app/live-preview/_api/getDocs.ts
Normal 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')
|
||||
}
|
||||
20
test/live-preview/prod/app/live-preview/_api/getFooter.ts
Normal file
20
test/live-preview/prod/app/live-preview/_api/getFooter.ts
Normal 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.')
|
||||
}
|
||||
20
test/live-preview/prod/app/live-preview/_api/getHeader.ts
Normal file
20
test/live-preview/prod/app/live-preview/_api/getHeader.ts
Normal 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.')
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const PAYLOAD_SERVER_URL = 'http://localhost:3000'
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Page } from '../../../../payload-types.js'
|
||||
|
||||
export type ArchiveBlockProps = Extract<
|
||||
Exclude<Page['layout'], undefined>[0],
|
||||
{ blockType: 'archive' }
|
||||
>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.mediaBlock {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.caption {
|
||||
color: var(--color-base-500);
|
||||
margin-top: var(--base);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.invert {
|
||||
background-color: var(--color-base-750);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>, </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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.placeholder-color-light {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
background-color: var(--color-base-50);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.video {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--color-base-50);
|
||||
}
|
||||
|
||||
.cover {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.richText {
|
||||
> *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
121
test/live-preview/prod/app/live-preview/_css/app.scss
Normal file
121
test/live-preview/prod/app/live-preview/_css/app.scss
Normal 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;
|
||||
}
|
||||
105
test/live-preview/prod/app/live-preview/_css/colors.scss
Normal file
105
test/live-preview/prod/app/live-preview/_css/colors.scss
Normal 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);
|
||||
}
|
||||
2
test/live-preview/prod/app/live-preview/_css/common.scss
Normal file
2
test/live-preview/prod/app/live-preview/_css/common.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@forward './queries.scss';
|
||||
@forward './type.scss';
|
||||
30
test/live-preview/prod/app/live-preview/_css/queries.scss
Normal file
30
test/live-preview/prod/app/live-preview/_css/queries.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
110
test/live-preview/prod/app/live-preview/_css/type.scss
Normal file
110
test/live-preview/prod/app/live-preview/_css/type.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@use '../../_css/type.scss' as *;
|
||||
|
||||
.lowImpactHero {
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}`
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const toKebabCase = (string: string): string =>
|
||||
string
|
||||
?.replace(/([a-z])([A-Z])/g, '$1-$2')
|
||||
.replace(/\s+/g, '-')
|
||||
.toLowerCase()
|
||||
16
test/live-preview/prod/app/live-preview/cssVariables.js
Normal file
16
test/live-preview/prod/app/live-preview/cssVariables.js
Normal 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)',
|
||||
},
|
||||
}
|
||||
24
test/live-preview/prod/app/live-preview/layout.tsx
Normal file
24
test/live-preview/prod/app/live-preview/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
19
test/live-preview/prod/app/live-preview/not-found.tsx
Normal file
19
test/live-preview/prod/app/live-preview/not-found.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
3
test/live-preview/prod/app/live-preview/page.tsx
Normal file
3
test/live-preview/prod/app/live-preview/page.tsx
Normal 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
5
test/live-preview/prod/next-env.d.ts
vendored
Normal 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.
|
||||
15
test/live-preview/prod/next.config.mjs
Normal file
15
test/live-preview/prod/next.config.mjs
Normal 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),
|
||||
},
|
||||
}
|
||||
26
test/live-preview/prod/tsconfig.json
Normal file
26
test/live-preview/prod/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user