chore: reviews preview example
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3000
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:8000
|
||||
MONGODB_URI=mongodb://localhost/payload-preview-example
|
||||
PAYLOAD_SECRET=ENTER-STRING-HERE
|
||||
MONGODB_URI=mongodb://localhost/payload-example-preview
|
||||
PAYLOAD_SECRET=PAYLOAD_PREVIEW_EXAMPLE_SECRET_KEY
|
||||
COOKIE_DOMAIN=
|
||||
REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
|
||||
|
||||
4
examples/preview/cms/.eslintrc.js
Normal file
4
examples/preview/cms/.eslintrc.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['./eslint-config'],
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"quote-props": "disabled"
|
||||
}
|
||||
8
examples/preview/cms/.prettierrc.js
Normal file
8
examples/preview/cms/.prettierrc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: "typescript",
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
arrowParens: "avoid",
|
||||
};
|
||||
@@ -1,24 +1,47 @@
|
||||
# Preview Example for Payload CMS
|
||||
|
||||
This is an example repo that showcases how to implement the `preview` feature into Payload CMS.
|
||||
This example demonstrates how to implement preview into Payload CMS using [Versions](https://payloadcms.com/docs/versions/overview) and [Drafts](https://payloadcms.com/docs/versions/drafts).
|
||||
|
||||
There is a fully working Next.js app tailored specifically for this example which can be found [here](../nextjs). Follow the instructions there to get started. If you are setting up `preview` for another front-end, please consider contributing to this repo with your own example!
|
||||
There is a fully working Next.js app tailored specifically for this example which can be found [here](../nextjs). Follow the instructions there to get started. If you are setting up preview for another front-end, please consider contributing to this repo with your own example!
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into the directory and run `yarn` or `npm install`
|
||||
3. Copy (`cp`) the `.env.example` file to an `.env` file
|
||||
4. Run `yarn dev` or `npm run dev` to start the development server
|
||||
5. Visit `http://localhost:8000/admin` to access the admin panel
|
||||
6. Login with the following credentials:
|
||||
- Email: `dev@payloadcms.com`
|
||||
- Password: `test`
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:8000/admin` to access the admin panel
|
||||
6. Login with email `dev@payloadcms.com` and password `test`
|
||||
|
||||
## How it works
|
||||
|
||||
On boot, a seed script is included to create a `user`, a `Home` page, and a `Draft` page for you to test with:
|
||||
A `pages` collection is created with `versions: { drafts: true }` and access control that restricts access to only logged-in users and `published` pages. On your front-end, a query similar to this can be used to fetch data and bypass access control in preview mode:
|
||||
|
||||
- The `Home` page has been set to `published` on start up, however the `Draft` page is only set to draft (not published yet). You can edit these pages - save them - and then preview them to view your saved changes without having these changes published and accessible to the public.
|
||||
- Upon previewing, you will notice an `admin bar` above the header of the site. This admin bar gives you freedom to exit preview mode, which will then return the page to it's most recent published version.
|
||||
- Note: the admin bar will only ever be seen by users logged into the cms. The admin bar stays hidden to public viewers.
|
||||
```ts
|
||||
const preview = true; // set this based on your own front-end environment (see `Preview Mode` below)
|
||||
const pageSlug = 'example-page'; // same here
|
||||
const searchParams = `?where[slug][equals]=${pageSlug}&depth=1${preview ? `&draft=true` : ''}`
|
||||
|
||||
// when previewing, send the payload token to bypass draft access control
|
||||
const pageReq = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/pages${searchParams}`, {
|
||||
headers: {
|
||||
...preview ? {
|
||||
Authorization: `JWT ${payloadToken}`,
|
||||
} : {},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
[CORS](https://payloadcms.com/docs/production/preventing-abuse#cross-origin-resource-sharing-cors), [CSRF](https://payloadcms.com/docs/production/preventing-abuse#cross-site-request-forgery-csrf), and [Cookies](https://payloadcms.com/docs/authentication/config#options) are all configured to ensure that the admin panel and front-end can communicate with each other securely.
|
||||
|
||||
### Preview Mode
|
||||
|
||||
To enter preview mode we format a custom URL using a [preview function](https://payloadcms.com/docs/configuration/collections#preview) in the collection config. When a user clicks the "Preview" button, they are routed to this URL along with their http-only cookies and revalidation key. Your front-end can then use the `payload-token` and revalidation key to verify the request and enter into its own preview mode.
|
||||
|
||||
### Instant Static Regeneration (ISR)
|
||||
|
||||
If your front-end is statically generated then you may also want to regenerate the HTML for each page as they are published. To do this, we add an `afterChange` hook to the collection that fires a request to your front-end in the background each time the document is updated. You can handle this request on your front-end and regenerate the HTML for your page however needed.
|
||||
|
||||
### Seed
|
||||
|
||||
On boot, a seed script is included to create a user, a home page, and an example page with two versions, one published and one draft.
|
||||
|
||||
15
examples/preview/cms/eslint-config/index.js
Normal file
15
examples/preview/cms/eslint-config/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
require.resolve('./rules/style.js'),
|
||||
require.resolve('./rules/import.js'),
|
||||
require.resolve('./rules/typescript.js'),
|
||||
require.resolve('./rules/prettier.js'),
|
||||
],
|
||||
env: {
|
||||
es6: true,
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
}
|
||||
32
examples/preview/cms/eslint-config/rules/import.js
Normal file
32
examples/preview/cms/eslint-config/rules/import.js
Normal file
@@ -0,0 +1,32 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
},
|
||||
extends: ['plugin:import/errors', 'plugin:import/warnings', 'plugin:import/typescript'],
|
||||
plugins: ['import'],
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts'],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'import/no-unresolved': ['error', { commonjs: true, caseSensitive: true }],
|
||||
'import/no-default-export': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{
|
||||
ts: 'never',
|
||||
tsx: 'never',
|
||||
js: 'never',
|
||||
jsx: 'never',
|
||||
},
|
||||
],
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'import/named': 'error',
|
||||
'import/no-relative-packages': 'warn',
|
||||
'import/no-import-module-exports': 'warn',
|
||||
'import/no-cycle': 'warn',
|
||||
},
|
||||
}
|
||||
7
examples/preview/cms/eslint-config/rules/prettier.js
Normal file
7
examples/preview/cms/eslint-config/rules/prettier.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: ['prettier'],
|
||||
extends: ['plugin:prettier/recommended'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
}
|
||||
13
examples/preview/cms/eslint-config/rules/style.js
Normal file
13
examples/preview/cms/eslint-config/rules/style.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
'prefer-named-exports': 'off',
|
||||
'prefer-destructuring': 'off',
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
'class-methods-use-this': 'off',
|
||||
'function-paren-newline': ['error', 'consistent'],
|
||||
'eol-last': ['error', 'always'],
|
||||
'no-restricted-syntax': 'off',
|
||||
'no-await-in-loop': 'off',
|
||||
'no-console': 'error',
|
||||
},
|
||||
}
|
||||
322
examples/preview/cms/eslint-config/rules/typescript.js
Normal file
322
examples/preview/cms/eslint-config/rules/typescript.js
Normal file
@@ -0,0 +1,322 @@
|
||||
module.exports = {
|
||||
plugins: ['@typescript-eslint'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/**.ts', '**/**.d.ts'],
|
||||
rules: {
|
||||
'no-undef': 'off',
|
||||
camelcase: 'off',
|
||||
'@typescript-eslint/adjacent-overload-signatures': 'error',
|
||||
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
|
||||
'@typescript-eslint/await-thenable': 'off',
|
||||
'@typescript-eslint/consistent-type-assertions': [
|
||||
'error',
|
||||
{ assertionStyle: 'as', objectLiteralTypeAssertions: 'allow-as-parameter' },
|
||||
],
|
||||
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
||||
'@typescript-eslint/consistent-type-imports': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'error',
|
||||
{
|
||||
allowExpressions: true,
|
||||
allowTypedFunctionExpressions: true,
|
||||
allowHigherOrderFunctions: true,
|
||||
allowConciseArrowFunctionExpressionsStartingWithVoid: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-member-accessibility': [
|
||||
'error',
|
||||
{ accessibility: 'no-public' },
|
||||
],
|
||||
'@typescript-eslint/member-delimiter-style': [
|
||||
'error',
|
||||
{
|
||||
multiline: {
|
||||
delimiter: 'none',
|
||||
requireLast: true,
|
||||
},
|
||||
singleline: {
|
||||
delimiter: 'semi',
|
||||
requireLast: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/method-signature-style': 'off',
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'off',
|
||||
{
|
||||
selector: 'default',
|
||||
format: ['camelCase'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
format: ['camelCase', 'UPPER_CASE'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: 'typeParameter',
|
||||
format: ['PascalCase'],
|
||||
prefix: ['T', 'U'],
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
types: ['boolean'],
|
||||
format: ['PascalCase'],
|
||||
prefix: ['is', 'should', 'has', 'can', 'did', 'will'],
|
||||
},
|
||||
{
|
||||
selector: 'interface',
|
||||
format: ['PascalCase'],
|
||||
custom: {
|
||||
regex: '^I[A-Z]',
|
||||
match: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: [
|
||||
'function',
|
||||
'parameter',
|
||||
'property',
|
||||
'parameterProperty',
|
||||
'method',
|
||||
'accessor',
|
||||
],
|
||||
format: ['camelCase'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: ['class', 'interface', 'typeAlias', 'enum', 'typeParameter'],
|
||||
format: ['PascalCase'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-base-to-string': 'off',
|
||||
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
|
||||
'@typescript-eslint/no-dynamic-delete': 'error',
|
||||
'@typescript-eslint/no-empty-interface': 'off',
|
||||
'@typescript-eslint/no-explicit-any': [
|
||||
'warn',
|
||||
{
|
||||
ignoreRestArgs: true,
|
||||
// enable later
|
||||
fixToUnknown: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-extra-non-null-assertion': 'error',
|
||||
'@typescript-eslint/no-extraneous-class': [
|
||||
'error',
|
||||
{
|
||||
allowConstructorOnly: false,
|
||||
allowEmpty: false,
|
||||
allowStaticOnly: false,
|
||||
allowWithDecorator: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-for-in-array': 'off',
|
||||
'@typescript-eslint/no-implicit-any-catch': [
|
||||
'error',
|
||||
{
|
||||
allowExplicitAny: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-implied-eval': 'off',
|
||||
'@typescript-eslint/no-inferrable-types': [
|
||||
'error',
|
||||
{
|
||||
ignoreParameters: false,
|
||||
ignoreProperties: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-invalid-void-type': [
|
||||
'off',
|
||||
{
|
||||
allowInGenericTypeArguments: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-misused-new': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'off',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
'@typescript-eslint/no-parameter-properties': 'error',
|
||||
'@typescript-eslint/no-require-imports': 'error',
|
||||
'@typescript-eslint/no-this-alias': 'error',
|
||||
'@typescript-eslint/no-throw-literal': 'off',
|
||||
'@typescript-eslint/no-type-alias': [
|
||||
'off',
|
||||
{
|
||||
allowAliases: 'always',
|
||||
allowCallbacks: 'always',
|
||||
allowConditionalTypes: 'always',
|
||||
allowConstructors: 'never',
|
||||
allowLiterals: 'in-unions-and-intersections',
|
||||
allowMappedTypes: 'always',
|
||||
allowTupleTypes: 'always',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||
'@typescript-eslint/no-unnecessary-qualifier': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-arguments': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'error',
|
||||
'@typescript-eslint/prefer-as-const': 'error',
|
||||
'@typescript-eslint/prefer-enum-initializers': 'off',
|
||||
'@typescript-eslint/prefer-for-of': 'error',
|
||||
'@typescript-eslint/prefer-includes': 'off',
|
||||
'@typescript-eslint/prefer-literal-enum-member': 'error',
|
||||
'@typescript-eslint/prefer-namespace-keyword': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/prefer-readonly': 'off',
|
||||
'@typescript-eslint/prefer-readonly-parameter-types': 'off',
|
||||
'@typescript-eslint/prefer-reduce-type-parameter': 'off',
|
||||
'@typescript-eslint/prefer-regexp-exec': 'off',
|
||||
'@typescript-eslint/prefer-string-starts-ends-with': 'off',
|
||||
'@typescript-eslint/prefer-ts-expect-error': 'warn',
|
||||
'@typescript-eslint/promise-function-async': 'off',
|
||||
'@typescript-eslint/require-array-sort-compare': 'off',
|
||||
'@typescript-eslint/restrict-plus-operands': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||
'@typescript-eslint/switch-exhaustiveness-check': 'off',
|
||||
'@typescript-eslint/triple-slash-reference': 'error',
|
||||
'@typescript-eslint/type-annotation-spacing': [
|
||||
'error',
|
||||
{
|
||||
before: false,
|
||||
after: true,
|
||||
overrides: {
|
||||
arrow: {
|
||||
before: true,
|
||||
after: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/typedef': [
|
||||
'error',
|
||||
{
|
||||
arrayDestructuring: false,
|
||||
arrowParameter: false,
|
||||
memberVariableDeclaration: false,
|
||||
objectDestructuring: false,
|
||||
parameter: false,
|
||||
propertyDeclaration: true,
|
||||
variableDeclaration: false,
|
||||
variableDeclarationIgnoreFunction: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'@typescript-eslint/unified-signatures': 'off',
|
||||
'brace-style': 'off',
|
||||
'@typescript-eslint/brace-style': 'error',
|
||||
'comma-spacing': 'off',
|
||||
'@typescript-eslint/comma-spacing': 'error',
|
||||
'default-param-last': 'off',
|
||||
'@typescript-eslint/default-param-last': 'error',
|
||||
'dot-notation': 'error',
|
||||
'@typescript-eslint/dot-notation': 'off',
|
||||
'func-call-spacing': 'off',
|
||||
'@typescript-eslint/func-call-spacing': 'error',
|
||||
indent: 'off',
|
||||
'@typescript-eslint/indent': 'off',
|
||||
'@typescript-eslint/init-declarations': 'off',
|
||||
'keyword-spacing': 'off',
|
||||
'@typescript-eslint/keyword-spacing': 'error',
|
||||
'lines-between-class-members': 'off',
|
||||
'@typescript-eslint/lines-between-class-members': [
|
||||
'error',
|
||||
'always',
|
||||
{
|
||||
exceptAfterSingleLine: true,
|
||||
exceptAfterOverload: true,
|
||||
},
|
||||
],
|
||||
'no-array-constructor': 'off',
|
||||
'@typescript-eslint/no-array-constructor': 'error',
|
||||
'no-dupe-class-members': 'off',
|
||||
'@typescript-eslint/no-dupe-class-members': 'error',
|
||||
'no-extra-parens': 'off',
|
||||
'@typescript-eslint/no-extra-parens': 'off',
|
||||
'no-extra-semi': 'off',
|
||||
'@typescript-eslint/no-extra-semi': 'error',
|
||||
'no-invalid-this': 'off',
|
||||
'@typescript-eslint/no-invalid-this': 'error',
|
||||
'no-loss-of-precision': 'off',
|
||||
'@typescript-eslint/no-loss-of-precision': 'error',
|
||||
'no-magic-numbers': 'off',
|
||||
'@typescript-eslint/no-magic-numbers': [
|
||||
'off',
|
||||
{
|
||||
ignoreArrayIndexes: true,
|
||||
ignoreDefaultValues: true,
|
||||
enforceConst: true,
|
||||
ignoreEnums: true,
|
||||
ignoreNumericLiteralTypes: true,
|
||||
ignoreReadonlyClassProperties: true,
|
||||
},
|
||||
],
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': [
|
||||
'error',
|
||||
{
|
||||
builtinGlobals: true,
|
||||
},
|
||||
],
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': [
|
||||
'error',
|
||||
{
|
||||
ignoreTypeValueShadow: false,
|
||||
},
|
||||
],
|
||||
'no-unused-expressions': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'no-useless-constructor': 'off',
|
||||
'@typescript-eslint/no-useless-constructor': 'error',
|
||||
quotes: 'off',
|
||||
'@typescript-eslint/quotes': [
|
||||
'error',
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true,
|
||||
allowTemplateLiterals: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/require-await': 'off',
|
||||
'@typescript-eslint/return-await': 'off',
|
||||
semi: 'off',
|
||||
'@typescript-eslint/semi': ['error', 'never'],
|
||||
'space-before-function-paren': 'off',
|
||||
'@typescript-eslint/space-before-function-paren': [
|
||||
'error',
|
||||
{
|
||||
anonymous: 'never',
|
||||
named: 'never',
|
||||
asyncArrow: 'always',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -12,22 +12,29 @@
|
||||
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema"
|
||||
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faceless-ui/modal": "^2.0.1",
|
||||
"@payloadcms/plugin-nested-docs": "^1.0.4",
|
||||
"@payloadcms/plugin-seo": "^1.0.8",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "^1.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"nodemon": "^2.0.6",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.1.3"
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { Access } from 'payload/config'
|
||||
|
||||
export const loggedIn: Access = ({ req: { user } }) => {
|
||||
return Boolean(user)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Access } from 'payload/config'
|
||||
|
||||
export const publishedOrLoggedIn: Access = ({ req: { user } }) => {
|
||||
if (user) {
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export const formatAppURL = (breadcrumbs): string => {
|
||||
let url: string;
|
||||
|
||||
if (breadcrumbs && Array.isArray(breadcrumbs) && breadcrumbs.length > 0) {
|
||||
let pathToUse = breadcrumbs[breadcrumbs.length - 1].url;
|
||||
if (pathToUse === 'home') pathToUse = '/';
|
||||
url = `${process.env.PAYLOAD_PUBLIC_SITE_URL}${pathToUse}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { FieldHook } from 'payload/types'
|
||||
|
||||
const format = (val: string): string =>
|
||||
val
|
||||
.replace(/ /g, '-')
|
||||
.replace(/[^\w-]+/g, '')
|
||||
.toLowerCase()
|
||||
|
||||
const formatSlug =
|
||||
(fallback: string): FieldHook =>
|
||||
({ operation, value, originalDoc, data }) => {
|
||||
if (typeof value === 'string') {
|
||||
return format(value)
|
||||
}
|
||||
|
||||
if (operation === 'create') {
|
||||
const fallbackData = data?.[fallback] || originalDoc?.[fallback]
|
||||
|
||||
if (fallbackData && typeof fallbackData === 'string') {
|
||||
return format(fallbackData)
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export default formatSlug
|
||||
@@ -1,15 +1,34 @@
|
||||
import { AfterChangeHook } from 'payload/dist/collections/config/types';
|
||||
import { revalidatePath } from '../../../utilities/revalidatePath';
|
||||
import { formatAppURL } from '../formatAppURL';
|
||||
import type { AfterChangeHook } from 'payload/dist/collections/config/types'
|
||||
|
||||
export const revalidatePage: AfterChangeHook = ({ doc }) => {
|
||||
const url = new URL(formatAppURL(doc.breadcrumbs));
|
||||
// ensure that the home page is revalidated at '/' instead of '/home'
|
||||
export const formatAppURL = ({ doc }): string => {
|
||||
const pathToUse = doc.slug === 'home' ? '' : doc.slug
|
||||
const { pathname } = new URL(`${process.env.PAYLOAD_PUBLIC_SITE_URL}/${pathToUse}`)
|
||||
return pathname
|
||||
}
|
||||
|
||||
revalidatePath(url.pathname);
|
||||
// Revalidate the page in the background, so the user doesn't have to wait
|
||||
// Notice that the hook itself is not async and we are not awaiting `revalidate`
|
||||
export const revalidatePage: AfterChangeHook = ({ doc, req }) => {
|
||||
const url = formatAppURL({ doc })
|
||||
|
||||
if (url.pathname === '/home') {
|
||||
revalidatePath('/')
|
||||
const revalidate = async (): Promise<void> => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/revalidate?secret=${process.env.REVALIDATION_KEY}&revalidatePath=${url}`,
|
||||
)
|
||||
|
||||
if (res.ok) {
|
||||
req.payload.logger.info(`Revalidated path ${url}`)
|
||||
} else {
|
||||
req.payload.logger.error(`Error revalidating path ${url}`)
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error(`Error hitting revalidate route for ${url}`)
|
||||
}
|
||||
}
|
||||
|
||||
return doc;
|
||||
};
|
||||
revalidate()
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
import richText from '../../fields/richText';
|
||||
import { slugField } from '../../fields/slug';
|
||||
import { formatAppURL } from './formatAppURL';
|
||||
import { revalidatePage } from './hooks/revalidatePage';
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
import richText from '../../fields/richText'
|
||||
import { loggedIn } from './access/loggedIn'
|
||||
import { publishedOrLoggedIn } from './access/publishedOrLoggedIn'
|
||||
import formatSlug from './hooks/formatSlug'
|
||||
import { formatAppURL, revalidatePage } from './hooks/revalidatePage'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'slug', 'updatedAt'],
|
||||
preview: ({ breadcrumbs }) => `${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/preview?url=${formatAppURL(breadcrumbs)}`,
|
||||
preview: doc =>
|
||||
`${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/preview?url=${formatAppURL({ doc })}`,
|
||||
},
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
read: publishedOrLoggedIn,
|
||||
create: loggedIn,
|
||||
update: loggedIn,
|
||||
delete: loggedIn,
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [
|
||||
revalidatePage,
|
||||
],
|
||||
afterChange: [revalidatePage],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
@@ -28,7 +31,18 @@ export const Pages: CollectionConfig = {
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
label: 'Slug',
|
||||
type: 'text',
|
||||
index: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate: [formatSlug('title')],
|
||||
},
|
||||
},
|
||||
richText(),
|
||||
slugField(),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
@@ -14,4 +14,4 @@ export const Users: CollectionConfig = {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
fields: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Field } from 'payload/types';
|
||||
import deepMerge from '../utilities/deepMerge';
|
||||
import type { Field } from 'payload/types'
|
||||
import deepMerge from '../utilities/deepMerge'
|
||||
|
||||
export const appearanceOptions = {
|
||||
primary: {
|
||||
@@ -14,23 +14,17 @@ export const appearanceOptions = {
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type LinkAppearances = 'primary' | 'secondary' | 'default'
|
||||
|
||||
type LinkType = (
|
||||
options?: {
|
||||
type LinkType = (options?: {
|
||||
appearances?: LinkAppearances[] | false
|
||||
disableLabel?: boolean
|
||||
overrides?: Record<string, unknown>
|
||||
}
|
||||
) => Field;
|
||||
}) => Field
|
||||
|
||||
const link: LinkType = ({
|
||||
appearances,
|
||||
disableLabel = false,
|
||||
overrides = {},
|
||||
} = {}) => {
|
||||
const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } = {}) => {
|
||||
const linkResult: Field = {
|
||||
name: 'link',
|
||||
type: 'group',
|
||||
@@ -74,7 +68,7 @@ const link: LinkType = ({
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const linkTypes: Field[] = [
|
||||
{
|
||||
@@ -97,11 +91,11 @@ const link: LinkType = ({
|
||||
condition: (_, siblingData) => siblingData?.type === 'custom',
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
if (!disableLabel) {
|
||||
linkTypes[0].admin.width = '50%';
|
||||
linkTypes[1].admin.width = '50%';
|
||||
linkTypes[0].admin.width = '50%'
|
||||
linkTypes[1].admin.width = '50%'
|
||||
|
||||
linkResult.fields.push({
|
||||
type: 'row',
|
||||
@@ -117,21 +111,20 @@ const link: LinkType = ({
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
} else {
|
||||
linkResult.fields = [...linkResult.fields, ...linkTypes];
|
||||
linkResult.fields = [...linkResult.fields, ...linkTypes]
|
||||
}
|
||||
|
||||
|
||||
if (appearances !== false) {
|
||||
let appearanceOptionsToUse = [
|
||||
appearanceOptions.default,
|
||||
appearanceOptions.primary,
|
||||
appearanceOptions.secondary,
|
||||
];
|
||||
]
|
||||
|
||||
if (appearances) {
|
||||
appearanceOptionsToUse = appearances.map((appearance) => appearanceOptions[appearance]);
|
||||
appearanceOptionsToUse = appearances.map(appearance => appearanceOptions[appearance])
|
||||
}
|
||||
|
||||
linkResult.fields.push({
|
||||
@@ -142,10 +135,10 @@ const link: LinkType = ({
|
||||
admin: {
|
||||
description: 'Choose how the link should be rendered.',
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return deepMerge(linkResult, overrides);
|
||||
};
|
||||
return deepMerge(linkResult, overrides)
|
||||
}
|
||||
|
||||
export default link;
|
||||
export default link
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { RichTextElement } from 'payload/dist/fields/config/types';
|
||||
import type { RichTextElement } from 'payload/dist/fields/config/types'
|
||||
|
||||
const elements: RichTextElement[] = [
|
||||
'blockquote',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'link',
|
||||
];
|
||||
const elements: RichTextElement[] = ['blockquote', 'h2', 'h3', 'h4', 'h5', 'h6', 'link']
|
||||
|
||||
export default elements;
|
||||
export default elements
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { RichTextElement, RichTextField, RichTextLeaf } from 'payload/dist/fields/config/types';
|
||||
import deepMerge from '../../utilities/deepMerge';
|
||||
import elements from './elements';
|
||||
import leaves from './leaves';
|
||||
import link from '../link';
|
||||
import type { RichTextElement, RichTextField, RichTextLeaf } from 'payload/dist/fields/config/types'
|
||||
import deepMerge from '../../utilities/deepMerge'
|
||||
import elements from './elements'
|
||||
import leaves from './leaves'
|
||||
import link from '../link'
|
||||
|
||||
type RichText = (
|
||||
overrides?: Partial<RichTextField>,
|
||||
additions?: {
|
||||
elements?: RichTextElement[]
|
||||
leaves?: RichTextLeaf[]
|
||||
}
|
||||
},
|
||||
) => RichTextField
|
||||
|
||||
const richText: RichText = (
|
||||
@@ -18,7 +18,8 @@ const richText: RichText = (
|
||||
elements: [],
|
||||
leaves: [],
|
||||
},
|
||||
) => deepMerge<RichTextField, Partial<RichTextField>>(
|
||||
) =>
|
||||
deepMerge<RichTextField, Partial<RichTextField>>(
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
@@ -33,12 +34,8 @@ const richText: RichText = (
|
||||
name: 'caption',
|
||||
label: 'Caption',
|
||||
admin: {
|
||||
elements: [
|
||||
...elements,
|
||||
],
|
||||
leaves: [
|
||||
...leaves,
|
||||
],
|
||||
elements: [...elements],
|
||||
leaves: [...leaves],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -78,17 +75,11 @@ const richText: RichText = (
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: [
|
||||
...elements,
|
||||
...additions.elements || [],
|
||||
],
|
||||
leaves: [
|
||||
...leaves,
|
||||
...additions.leaves || [],
|
||||
],
|
||||
elements: [...elements, ...(additions.elements || [])],
|
||||
leaves: [...leaves, ...(additions.leaves || [])],
|
||||
},
|
||||
},
|
||||
overrides,
|
||||
);
|
||||
)
|
||||
|
||||
export default richText;
|
||||
export default richText
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { RichTextLeaf } from 'payload/dist/fields/config/types';
|
||||
import type { RichTextLeaf } from 'payload/dist/fields/config/types'
|
||||
|
||||
const defaultLeaves: RichTextLeaf[] = [
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
];
|
||||
const defaultLeaves: RichTextLeaf[] = ['bold', 'italic', 'underline']
|
||||
|
||||
export default defaultLeaves;
|
||||
export default defaultLeaves
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Field } from 'payload/types';
|
||||
import formatSlug from '../utilities/formatSlug';
|
||||
import deepMerge from '../utilities/deepMerge';
|
||||
|
||||
type Slug = (fieldToUse?: string, overrides?: Partial<Field>) => Field
|
||||
|
||||
export const slugField: Slug = (fieldToUse = 'title', overrides) => deepMerge<Field, Partial<Field>>(
|
||||
{
|
||||
name: 'slug',
|
||||
label: 'Slug',
|
||||
type: 'text',
|
||||
index: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate: [
|
||||
formatSlug(fieldToUse),
|
||||
],
|
||||
},
|
||||
},
|
||||
overrides,
|
||||
);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GlobalConfig } from 'payload/types';
|
||||
import link from '../fields/link';
|
||||
import type { GlobalConfig } from 'payload/types'
|
||||
import link from '../fields/link'
|
||||
|
||||
export const MainMenu: GlobalConfig = {
|
||||
slug: 'main-menu',
|
||||
@@ -18,4 +18,4 @@ export const MainMenu: GlobalConfig = {
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,54 +7,49 @@
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
pages: Page;
|
||||
users: User;
|
||||
};
|
||||
pages: Page
|
||||
users: User
|
||||
}
|
||||
globals: {
|
||||
'main-menu': MainMenu;
|
||||
};
|
||||
'main-menu': MainMenu
|
||||
}
|
||||
}
|
||||
export interface Page {
|
||||
id: string;
|
||||
title: string;
|
||||
richText: {
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
slug?: string;
|
||||
parent?: string | Page;
|
||||
breadcrumbs: {
|
||||
doc?: string | Page;
|
||||
url?: string;
|
||||
label?: string;
|
||||
id?: string;
|
||||
}[];
|
||||
_status?: 'draft' | 'published';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
id: string
|
||||
title: string
|
||||
slug?: string
|
||||
richText: Array<{
|
||||
[k: string]: unknown
|
||||
}>
|
||||
_status?: 'draft' | 'published'
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
password?: string
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
id: string
|
||||
email?: string
|
||||
resetPasswordToken?: string
|
||||
resetPasswordExpiration?: string
|
||||
loginAttempts?: number
|
||||
lockUntil?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
password?: string
|
||||
}
|
||||
export interface MainMenu {
|
||||
id: string;
|
||||
navItems: {
|
||||
id: string
|
||||
navItems: Array<{
|
||||
link: {
|
||||
type?: 'reference' | 'custom';
|
||||
newTab?: boolean;
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
value: string | Page;
|
||||
relationTo: 'pages';
|
||||
};
|
||||
url: string;
|
||||
label: string;
|
||||
};
|
||||
id?: string;
|
||||
}[];
|
||||
value: string | Page
|
||||
relationTo: 'pages'
|
||||
}
|
||||
url: string
|
||||
label: string
|
||||
}
|
||||
id?: string
|
||||
}>
|
||||
}
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
import { buildConfig } from 'payload/config';
|
||||
import nestedDocs from '@payloadcms/plugin-nested-docs';
|
||||
import path from 'path';
|
||||
import { Users } from './collections/Users';
|
||||
import { Pages } from './collections/Pages';
|
||||
import { MainMenu } from './globals/MainMenu';
|
||||
import { buildConfig } from 'payload/config'
|
||||
import path from 'path'
|
||||
import { Users } from './collections/Users'
|
||||
import { Pages } from './collections/Pages'
|
||||
import { MainMenu } from './globals/MainMenu'
|
||||
|
||||
// eslint-disable-next-line
|
||||
require('dotenv').config({
|
||||
path: path.resolve(__dirname, '../.env'),
|
||||
})
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
Pages,
|
||||
Users,
|
||||
],
|
||||
cors: [
|
||||
'http://localhost:3000',
|
||||
process.env.PAYLOAD_PUBLIC_SITE_URL,
|
||||
],
|
||||
globals: [
|
||||
MainMenu,
|
||||
],
|
||||
collections: [Pages, Users],
|
||||
cors: [process.env.PAYLOAD_PUBLIC_SERVER_URL, process.env.PAYLOAD_PUBLIC_SITE_URL],
|
||||
csrf: [process.env.PAYLOAD_PUBLIC_SERVER_URL, process.env.PAYLOAD_PUBLIC_SITE_URL],
|
||||
globals: [MainMenu],
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, 'payload-types.ts'),
|
||||
},
|
||||
plugins: [
|
||||
nestedDocs({
|
||||
collections: ['pages'],
|
||||
generateLabel: (_, doc) => doc.title as string,
|
||||
generateURL: (docs) => docs.reduce((url, doc) => `${url}/${doc.slug}`, ''),
|
||||
}),
|
||||
],
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
export const draftPage = {
|
||||
"title": "Draft Page",
|
||||
"richText": [
|
||||
{
|
||||
"type": "blockquote",
|
||||
"children": [
|
||||
{
|
||||
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"slug": "draft-page",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"doc": "63e172c8269c1a369bf3c539",
|
||||
"url": "/draft-page",
|
||||
"label": "Draft Page",
|
||||
"id": "63e172c82e29d8a92f23ce32"
|
||||
}
|
||||
],
|
||||
"_status": "draft",
|
||||
};
|
||||
@@ -1,94 +1,77 @@
|
||||
export const home = {
|
||||
"title": "Home Page",
|
||||
"slug": "home",
|
||||
"richText": [
|
||||
import type { Page } from '../payload-types'
|
||||
|
||||
export const home: Partial<Page> = {
|
||||
title: 'Home Page',
|
||||
slug: 'home',
|
||||
_status: 'published',
|
||||
richText: [
|
||||
{
|
||||
"children": [
|
||||
children: [
|
||||
{ text: 'This is a ' },
|
||||
{ type: 'link', newTab: true, url: 'https://nextjs.org/', children: [{ text: '' }] },
|
||||
{ text: '' },
|
||||
{
|
||||
"text": "This is a "
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
url: 'https://nextjs.org/',
|
||||
newTab: true,
|
||||
children: [{ text: 'Next.js' }],
|
||||
},
|
||||
{ text: " app made explicitly for Payload's " },
|
||||
{
|
||||
"type": "link",
|
||||
"newTab": true,
|
||||
"url": "https://nextjs.org/",
|
||||
"children": [
|
||||
{
|
||||
"text": ""
|
||||
}
|
||||
]
|
||||
type: 'link',
|
||||
newTab: true,
|
||||
url: 'https://github.com/payloadcms/payload/tree/master/examples/redirects/cms',
|
||||
children: [{ text: '' }],
|
||||
},
|
||||
{ text: '' },
|
||||
{
|
||||
"text": ""
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
newTab: true,
|
||||
url: 'https://github.com/payloadcms/payload/tree/master/examples/preview/cms',
|
||||
children: [{ text: 'Preview Example' }],
|
||||
},
|
||||
{ text: '. This example demonstrates how to implement preview into Payload CMS using ' },
|
||||
{
|
||||
"type": "link",
|
||||
"linkType": "custom",
|
||||
"url": "https://nextjs.org/",
|
||||
"children": [
|
||||
{
|
||||
"text": "Next.js"
|
||||
}
|
||||
]
|
||||
type: 'link',
|
||||
newTab: true,
|
||||
url: 'https://payloadcms.com/docs/versions/drafts#drafts',
|
||||
children: [{ text: 'Drafts' }],
|
||||
},
|
||||
{
|
||||
"text": " app made explicitly for Payload's "
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"newTab": true,
|
||||
"url": "https://github.com/payloadcms/payload/tree/master/examples/redirects/cms",
|
||||
"children": [
|
||||
{
|
||||
"text": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"linkType": "custom",
|
||||
"url": "https://github.com/payloadcms/payload/tree/master/examples/preview/cms",
|
||||
"children": [
|
||||
{
|
||||
"text": "Preview"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": " Example. This example demonstrates how to implement the "
|
||||
},
|
||||
{
|
||||
"text": "preview",
|
||||
"italic": true
|
||||
},
|
||||
{
|
||||
"text": " feature into Payload CMS."
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"newTab": true,
|
||||
"url": "https://github.com/payloadcms/plugin-redirects",
|
||||
"children": [
|
||||
{
|
||||
"text": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
{ text: '.' },
|
||||
],
|
||||
"breadcrumbs": [
|
||||
},
|
||||
{ children: [{ text: '' }] },
|
||||
{
|
||||
"doc": "63e172221b8268abd16a6a91",
|
||||
"url": "/home",
|
||||
"label": "Home Page",
|
||||
"id": "63e17222c3943ca73a4a0ff9"
|
||||
}
|
||||
children: [
|
||||
{ text: 'Visit the ' },
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
url: 'http://localhost:3000/example-page',
|
||||
children: [{ text: 'example page' }],
|
||||
},
|
||||
{ text: ' to see how access to draft content is controlled. ' },
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
url: 'http://localhost:8000/admin',
|
||||
newTab: true,
|
||||
children: [{ text: 'Log in' }],
|
||||
},
|
||||
{ text: ' to the admin panel and refresh this page to see the ' },
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
newTab: true,
|
||||
url: 'https://github.com/payloadcms/payload-admin-bar',
|
||||
children: [{ text: 'Payload Admin Bar' }],
|
||||
},
|
||||
{
|
||||
text: ' appear at the top of the viewport so you can seamlessly navigate between the two apps.',
|
||||
},
|
||||
],
|
||||
"_status": "published",
|
||||
};
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,45 +1,60 @@
|
||||
import { Payload } from 'payload';
|
||||
import { draftPage } from './draftPage';
|
||||
import { home } from './home';
|
||||
import type { Payload } from 'payload'
|
||||
import { examplePage } from './page'
|
||||
import { examplePageDraft } from './pageDraft'
|
||||
import { home } from './home'
|
||||
|
||||
export const seed = async (payload: Payload) => {
|
||||
export const seed = async (payload: Payload): Promise<void> => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'test',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const homepageJSON = JSON.parse(JSON.stringify(home));
|
||||
const { id: examplePageID } = await payload.create({
|
||||
collection: 'pages',
|
||||
data: examplePage as any, // eslint-disable-line
|
||||
})
|
||||
|
||||
const draftPageJSON = JSON.parse(JSON.stringify(draftPage));
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: examplePageID,
|
||||
draft: true,
|
||||
data: examplePageDraft as any, // eslint-disable-line
|
||||
})
|
||||
|
||||
const homepageJSON = JSON.parse(JSON.stringify(home).replace('{{DRAFT_PAGE_ID}}', examplePageID))
|
||||
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: homepageJSON,
|
||||
});
|
||||
|
||||
const { id: draftPageID } = await payload.create({
|
||||
collection: 'pages',
|
||||
data: draftPageJSON,
|
||||
});
|
||||
})
|
||||
|
||||
await payload.updateGlobal({
|
||||
slug: 'main-menu',
|
||||
data: {
|
||||
navItems: [
|
||||
{
|
||||
link: {
|
||||
type: 'custom',
|
||||
reference: null,
|
||||
label: 'Dashboard',
|
||||
url: 'http://localhost:8000/admin',
|
||||
},
|
||||
},
|
||||
{
|
||||
link: {
|
||||
type: 'reference',
|
||||
reference: {
|
||||
relationTo: 'pages',
|
||||
value: draftPageID
|
||||
value: examplePageID,
|
||||
},
|
||||
label: 'Draft Page',
|
||||
}
|
||||
label: 'Example Page',
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
33
examples/preview/cms/src/seed/page.ts
Normal file
33
examples/preview/cms/src/seed/page.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Page } from '../payload-types'
|
||||
|
||||
export const examplePage: Partial<Page> = {
|
||||
title: 'Example Page',
|
||||
slug: 'example-page',
|
||||
_status: 'published',
|
||||
richText: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
text: 'This is an example page with two versions, draft and published. You are currently seeing ',
|
||||
},
|
||||
{
|
||||
text: 'published',
|
||||
bold: true,
|
||||
},
|
||||
{
|
||||
text: ' content because you are not in preview mode. ',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
url: 'http://localhost:8000/admin',
|
||||
newTab: true,
|
||||
children: [{ text: 'Log in' }],
|
||||
},
|
||||
{
|
||||
text: ' to the admin panel and click "preview" to return to this page and view the latest draft content in Next.js preview mode.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
30
examples/preview/cms/src/seed/pageDraft.ts
Normal file
30
examples/preview/cms/src/seed/pageDraft.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Page } from '../payload-types'
|
||||
|
||||
export const examplePageDraft: Partial<Page> = {
|
||||
richText: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
text: 'This page is an example page with two versions, draft and published. You are currently seeing ',
|
||||
},
|
||||
{
|
||||
text: 'draft',
|
||||
bold: true,
|
||||
},
|
||||
{
|
||||
text: ' content because you in preview mode. ',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
url: 'http://localhost:8000/admin/logout',
|
||||
newTab: true,
|
||||
children: [{ text: 'Log out' }],
|
||||
},
|
||||
{
|
||||
text: ' or exit Next.js preview mode to see the latest published content.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,35 +1,36 @@
|
||||
import path from 'path';
|
||||
import express from 'express';
|
||||
import payload from 'payload';
|
||||
import { seed } from './seed';
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import payload from 'payload'
|
||||
import { seed } from './seed'
|
||||
|
||||
// eslint-disable-next-line
|
||||
require('dotenv').config({
|
||||
path: path.resolve(__dirname, '../.env'),
|
||||
});
|
||||
})
|
||||
|
||||
const app = express();
|
||||
const app = express()
|
||||
|
||||
// Redirect root to Admin panel
|
||||
app.get('/', (_, res) => {
|
||||
res.redirect('/admin');
|
||||
});
|
||||
res.redirect('/admin')
|
||||
})
|
||||
|
||||
const start = async () => {
|
||||
const start = async (): Promise<void> => {
|
||||
await payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
mongoURL: process.env.MONGODB_URI,
|
||||
express: app,
|
||||
onInit: () => {
|
||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
|
||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (process.env.PAYLOAD_SEED === 'true') {
|
||||
payload.logger.info('---- SEEDING DATABASE ----');
|
||||
await seed(payload);
|
||||
payload.logger.info('---- SEEDING DATABASE ----')
|
||||
await seed(payload)
|
||||
}
|
||||
|
||||
app.listen(8000);
|
||||
};
|
||||
app.listen(8000)
|
||||
}
|
||||
|
||||
start();
|
||||
start()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isObject(item: unknown): boolean {
|
||||
return (item && typeof item === 'object' && !Array.isArray(item));
|
||||
return item && typeof item === 'object' && !Array.isArray(item)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -15,20 +15,20 @@ export function isObject(item: unknown): boolean {
|
||||
* @param ...sources
|
||||
*/
|
||||
export default function deepMerge<T, R>(target: T, source: R): T {
|
||||
const output = { ...target };
|
||||
const output = { ...target }
|
||||
if (isObject(target) && isObject(source)) {
|
||||
Object.keys(source).forEach((key) => {
|
||||
Object.keys(source).forEach(key => {
|
||||
if (isObject(source[key])) {
|
||||
if (!(key in target)) {
|
||||
Object.assign(output, { [key]: source[key] });
|
||||
Object.assign(output, { [key]: source[key] })
|
||||
} else {
|
||||
output[key] = deepMerge(target[key], source[key]);
|
||||
output[key] = deepMerge(target[key], source[key])
|
||||
}
|
||||
} else {
|
||||
Object.assign(output, { [key]: source[key] });
|
||||
Object.assign(output, { [key]: source[key] })
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return output;
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { FieldHook } from 'payload/types';
|
||||
|
||||
const format = (val: string): string => val.replace(/ /g, '-').replace(/[^\w-]+/g, '').toLowerCase();
|
||||
|
||||
const formatSlug = (fallback: string): FieldHook => ({ operation, value, originalDoc, data }) => {
|
||||
if (typeof value === 'string') {
|
||||
return format(value);
|
||||
}
|
||||
|
||||
if (operation === 'create') {
|
||||
const fallbackData = (data && data[fallback]) || (originalDoc && originalDoc[fallback]);
|
||||
|
||||
if (fallbackData && typeof fallbackData === 'string') {
|
||||
return format(fallbackData);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export default formatSlug;
|
||||
@@ -1,15 +0,0 @@
|
||||
type RevalidatePath = (path: string) => void
|
||||
export const revalidatePath: RevalidatePath = async (path) => {
|
||||
try {
|
||||
const res = await fetch(`${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/revalidate?secret=${process.env.NEXT_PRIVATE_REVALIDATION_KEY}&revalidatePath=${path}`);
|
||||
|
||||
if (res.ok) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Revalidated path ${path}`);
|
||||
} else {
|
||||
console.error(`Error revalidating path ${path}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error hitting revalidate route for ${path}`);
|
||||
}
|
||||
};
|
||||
@@ -16,7 +16,8 @@
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"payload/generated-types": ["./src/payload-types.ts"]
|
||||
"payload/generated-types": ["./src/payload-types.ts"],
|
||||
"node_modules/*": ["./node_modules/*"]
|
||||
},
|
||||
},
|
||||
"include": [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,2 @@
|
||||
NEXT_PUBLIC_CMS_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_OFFLINE_MODE=true
|
||||
NEXT_PRIVATE_REVALIDATION_KEY=some_key
|
||||
NEXT_PRIVATE_REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
|
||||
|
||||
4
examples/preview/nextjs/.eslintrc.js
Normal file
4
examples/preview/nextjs/.eslintrc.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['./eslint-config'],
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
8
examples/preview/nextjs/.prettierrc.js
Normal file
8
examples/preview/nextjs/.prettierrc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: "typescript",
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
arrowParens: "avoid",
|
||||
};
|
||||
@@ -1,9 +1,17 @@
|
||||
@import '../../css/type.scss';
|
||||
|
||||
.button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: space-around;
|
||||
|
||||
svg {
|
||||
margin-right: calc(var(--base) / 2);
|
||||
@@ -14,34 +22,37 @@
|
||||
|
||||
.label {
|
||||
@extend %label;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
padding: 12px 18px;
|
||||
margin-bottom: var(--base);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: inline-flex;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
.appearance--primary {
|
||||
background-color: var(--color-black);
|
||||
color: var(--color-white);
|
||||
.primary--white {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.appearance--secondary {
|
||||
background-color: var(--color-white);
|
||||
box-shadow: inset 0 0 0 1px var(--color-black);
|
||||
.primary--black {
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.secondary--white {
|
||||
background-color: white;
|
||||
box-shadow: inset 0 0 0 1px black;
|
||||
}
|
||||
|
||||
.secondary--black {
|
||||
background-color: black;
|
||||
box-shadow: inset 0 0 0 1px white;
|
||||
}
|
||||
|
||||
.appearance--default {
|
||||
padding: 0;
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import classes from './index.module.scss';
|
||||
import React, { ElementType } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export type Props = {
|
||||
label: string
|
||||
@@ -8,59 +9,63 @@ export type Props = {
|
||||
el?: 'button' | 'link' | 'a'
|
||||
onClick?: () => void
|
||||
href?: string
|
||||
form?: string
|
||||
newTab?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const elements = {
|
||||
a: 'a',
|
||||
link: Link,
|
||||
button: 'button',
|
||||
type?: 'submit' | 'button'
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const Button: React.FC<Props> = ({
|
||||
el = 'button',
|
||||
el: elFromProps = 'link',
|
||||
label,
|
||||
newTab,
|
||||
href,
|
||||
form,
|
||||
appearance,
|
||||
className: classNameFromProps
|
||||
className: classNameFromProps,
|
||||
onClick,
|
||||
type = 'button',
|
||||
disabled,
|
||||
}) => {
|
||||
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {};
|
||||
const Element = elements[el];
|
||||
const className = [classNameFromProps, classes[`appearance--${appearance}`], classes.button].filter(Boolean).join(' ');
|
||||
|
||||
const elementProps = {
|
||||
...newTabProps,
|
||||
href,
|
||||
className,
|
||||
form,
|
||||
}
|
||||
let el = elFromProps
|
||||
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
|
||||
const className = [
|
||||
classes.button,
|
||||
classNameFromProps,
|
||||
classes[`appearance--${appearance}`],
|
||||
classes.button,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
const content = (
|
||||
<div className={classes.content}>
|
||||
<span className={classes.label}>
|
||||
{label}
|
||||
</span>
|
||||
{/* <Chevron /> */}
|
||||
<span className={classes.label}>{label}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (onClick || type === 'submit') el = 'button'
|
||||
|
||||
if (el === 'link') {
|
||||
return (
|
||||
<Element {...elementProps}>
|
||||
<React.Fragment>
|
||||
{el === 'link' && (
|
||||
<a {...newTabProps} href={href} className={elementProps.className}>
|
||||
<Link href={href} className={className} {...newTabProps} onClick={onClick}>
|
||||
{content}
|
||||
</a>
|
||||
)}
|
||||
{el !== 'link' && (
|
||||
<React.Fragment>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const Element: ElementType = el
|
||||
|
||||
return (
|
||||
<Element
|
||||
href={href}
|
||||
className={className}
|
||||
type={type}
|
||||
{...newTabProps}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{content}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</Element>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import { Page } from '../../payload-types';
|
||||
import { Button } from '../Button';
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
type CMSLinkType = {
|
||||
import { Page } from '../../payload-types'
|
||||
import { Button } from '../Button'
|
||||
|
||||
export type CMSLinkType = {
|
||||
type?: 'custom' | 'reference'
|
||||
url?: string
|
||||
newTab?: boolean
|
||||
@@ -27,10 +28,13 @@ export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
const href = (type === 'reference' && typeof reference?.value === 'object' && reference.value.slug) ? `/${reference.value.slug}` : url;
|
||||
const href =
|
||||
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
|
||||
? `/${reference.value.slug}`
|
||||
: url
|
||||
|
||||
if (!appearance) {
|
||||
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {};
|
||||
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
|
||||
|
||||
if (type === 'custom') {
|
||||
return (
|
||||
@@ -43,11 +47,9 @@ export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<a {...newTabProps} className={className}>
|
||||
<Link href={href} {...newTabProps} className={className}>
|
||||
{label && label}
|
||||
{children && children}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -60,7 +62,5 @@ export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
label,
|
||||
}
|
||||
|
||||
return (
|
||||
<Button className={className} {...buttonProps} el="link" />
|
||||
)
|
||||
return <Button className={className} {...buttonProps} el="link" />
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { forwardRef, Ref } from 'react';
|
||||
import classes from './index.module.scss';
|
||||
import React, { forwardRef, Ref } from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
left?: boolean
|
||||
@@ -10,26 +11,18 @@ type Props = {
|
||||
}
|
||||
|
||||
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const {
|
||||
left = true,
|
||||
right = true,
|
||||
className,
|
||||
children
|
||||
} = props;
|
||||
const { left = true, right = true, className, children } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={[
|
||||
left && classes.gutterLeft,
|
||||
right && classes.gutterRight,
|
||||
className
|
||||
].filter(Boolean).join(' ')}
|
||||
className={[left && classes.gutterLeft, right && classes.gutterRight, className]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
Gutter.displayName = 'Gutter';
|
||||
Gutter.displayName = 'Gutter'
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import React from 'react';
|
||||
import { PayloadMeUser, PayloadAdminBarProps, PayloadAdminBar } from 'payload-admin-bar';
|
||||
import { Gutter } from '../../Gutter';
|
||||
import React from 'react'
|
||||
import { PayloadAdminBar, PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
|
||||
|
||||
import classes from './index.module.scss';
|
||||
import { Gutter } from '../../Gutter'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const AdminBar: React.FC<{
|
||||
adminBarProps?: PayloadAdminBarProps
|
||||
user?: PayloadMeUser
|
||||
setUser?: (user: PayloadMeUser) => void
|
||||
}> = (props) => {
|
||||
const {
|
||||
adminBarProps,
|
||||
user,
|
||||
setUser
|
||||
} = props;
|
||||
setUser?: (user: PayloadMeUser) => void // eslint-disable-line no-unused-vars
|
||||
}> = props => {
|
||||
const { adminBarProps, user, setUser } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
classes.adminBar,
|
||||
user && classes.show
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
<div className={[classes.adminBar, user && classes.show].filter(Boolean).join(' ')}>
|
||||
<Gutter className={classes.container}>
|
||||
<PayloadAdminBar
|
||||
{...adminBarProps}
|
||||
@@ -37,10 +29,10 @@ export const AdminBar: React.FC<{
|
||||
position: 'relative',
|
||||
zIndex: 'unset',
|
||||
padding: 0,
|
||||
backgroundColor: 'transparent'
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
/>
|
||||
</Gutter>
|
||||
</div >
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import { useGlobals } from '../../providers/Globals';
|
||||
import { Gutter } from '../Gutter';
|
||||
import { Logo } from '../Logo';
|
||||
import { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar';
|
||||
import React, { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
|
||||
|
||||
import classes from './index.module.scss';
|
||||
import { AdminBar } from './AdminBar';
|
||||
import { CMSLink } from '../CMSLink';
|
||||
import { MainMenu } from '../../payload-types'
|
||||
import { CMSLink } from '../CMSLink'
|
||||
import { Gutter } from '../Gutter'
|
||||
import { Logo } from '../Logo'
|
||||
import { AdminBar } from './AdminBar'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type HeaderBarProps = {
|
||||
children?: React.ReactNode;
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const HeaderBar: React.FC<HeaderBarProps> = ({ children }) => {
|
||||
@@ -18,45 +19,38 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({ children }) => {
|
||||
<header className={classes.header}>
|
||||
<Gutter className={classes.wrap}>
|
||||
<Link href="/">
|
||||
<a>
|
||||
<Logo />
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
{children}
|
||||
|
||||
</Gutter>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export const Header: React.FC<{
|
||||
globals: {
|
||||
mainMenu: MainMenu
|
||||
}
|
||||
adminBarProps: PayloadAdminBarProps
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const { globals, adminBarProps } = props
|
||||
|
||||
const [user, setUser] = useState<PayloadMeUser>()
|
||||
|
||||
const {
|
||||
adminBarProps,
|
||||
} = props;
|
||||
mainMenu: { navItems },
|
||||
} = globals
|
||||
|
||||
const [user, setUser] = useState<PayloadMeUser>();
|
||||
|
||||
const { mainMenu: { navItems } } = useGlobals();
|
||||
|
||||
const hasNavItems = navItems && Array.isArray(navItems) && navItems.length > 0;
|
||||
const hasNavItems = navItems && Array.isArray(navItems) && navItems.length > 0
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminBar
|
||||
adminBarProps={adminBarProps}
|
||||
user={user}
|
||||
setUser={setUser}
|
||||
/>
|
||||
<AdminBar adminBarProps={adminBarProps} user={user} setUser={setUser} />
|
||||
<HeaderBar>
|
||||
{hasNavItems && (
|
||||
<nav className={classes.nav}>
|
||||
{navItems.map(({ link }, i) => {
|
||||
return (
|
||||
<CMSLink key={i} {...link} />
|
||||
)
|
||||
return <CMSLink key={i} {...link} />
|
||||
})}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
@@ -1,18 +1,48 @@
|
||||
import React from 'react';
|
||||
import React from 'react'
|
||||
|
||||
export const Logo: React.FC = () => {
|
||||
return (
|
||||
<svg width="123" height="29" viewBox="0 0 123 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M34.7441 22.9997H37.2741V16.3297H41.5981C44.7031 16.3297 46.9801 14.9037 46.9801 11.4537C46.9801 8.00369 44.7031 6.55469 41.5981 6.55469H34.7441V22.9997ZM37.2741 14.1447V8.73969H41.4831C43.3921 8.73969 44.3581 9.59069 44.3581 11.4537C44.3581 13.2937 43.3921 14.1447 41.4831 14.1447H37.2741Z" fill="black" />
|
||||
<path d="M51.3652 23.3217C53.2742 23.3217 54.6082 22.5627 55.3672 21.3437H55.4132C55.5512 22.6777 56.1492 23.1147 57.2762 23.1147C57.6442 23.1147 58.0352 23.0687 58.4262 22.9767V21.5967C58.2882 21.6197 58.2192 21.6197 58.1502 21.6197C57.7132 21.6197 57.5982 21.1827 57.5982 20.3317V14.9497C57.5982 11.9137 55.6662 10.9017 53.2512 10.9017C49.6632 10.9017 48.1912 12.6727 48.0762 14.9267H50.3762C50.4912 13.3627 51.1122 12.7187 53.1592 12.7187C54.8842 12.7187 55.3902 13.4317 55.3902 14.2827C55.3902 15.4327 54.2632 15.6627 52.4232 16.0077C49.5022 16.5597 47.5242 17.3417 47.5242 19.9637C47.5242 21.9647 49.0192 23.3217 51.3652 23.3217ZM49.8702 19.8027C49.8702 18.5837 50.7442 18.0087 52.8142 17.5947C54.0102 17.3417 55.0222 17.0887 55.3902 16.7437V18.4227C55.3902 20.4697 53.8952 21.5047 51.8712 21.5047C50.4682 21.5047 49.8702 20.9067 49.8702 19.8027Z" fill="black" />
|
||||
<path d="M61.4996 27.1167C63.3166 27.1167 64.4436 26.1737 65.5706 23.2757L70.2166 11.2697H67.8476L64.6276 20.2397H64.5816L61.1546 11.2697H58.6936L63.4316 22.8847C62.9716 24.7247 61.9136 25.1847 61.0166 25.1847C60.6486 25.1847 60.4416 25.1617 60.0506 25.1157V26.9557C60.6486 27.0707 60.9936 27.1167 61.4996 27.1167Z" fill="black" />
|
||||
<svg
|
||||
width="123"
|
||||
height="29"
|
||||
viewBox="0 0 123 29"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M34.7441 22.9997H37.2741V16.3297H41.5981C44.7031 16.3297 46.9801 14.9037 46.9801 11.4537C46.9801 8.00369 44.7031 6.55469 41.5981 6.55469H34.7441V22.9997ZM37.2741 14.1447V8.73969H41.4831C43.3921 8.73969 44.3581 9.59069 44.3581 11.4537C44.3581 13.2937 43.3921 14.1447 41.4831 14.1447H37.2741Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M51.3652 23.3217C53.2742 23.3217 54.6082 22.5627 55.3672 21.3437H55.4132C55.5512 22.6777 56.1492 23.1147 57.2762 23.1147C57.6442 23.1147 58.0352 23.0687 58.4262 22.9767V21.5967C58.2882 21.6197 58.2192 21.6197 58.1502 21.6197C57.7132 21.6197 57.5982 21.1827 57.5982 20.3317V14.9497C57.5982 11.9137 55.6662 10.9017 53.2512 10.9017C49.6632 10.9017 48.1912 12.6727 48.0762 14.9267H50.3762C50.4912 13.3627 51.1122 12.7187 53.1592 12.7187C54.8842 12.7187 55.3902 13.4317 55.3902 14.2827C55.3902 15.4327 54.2632 15.6627 52.4232 16.0077C49.5022 16.5597 47.5242 17.3417 47.5242 19.9637C47.5242 21.9647 49.0192 23.3217 51.3652 23.3217ZM49.8702 19.8027C49.8702 18.5837 50.7442 18.0087 52.8142 17.5947C54.0102 17.3417 55.0222 17.0887 55.3902 16.7437V18.4227C55.3902 20.4697 53.8952 21.5047 51.8712 21.5047C50.4682 21.5047 49.8702 20.9067 49.8702 19.8027Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M61.4996 27.1167C63.3166 27.1167 64.4436 26.1737 65.5706 23.2757L70.2166 11.2697H67.8476L64.6276 20.2397H64.5816L61.1546 11.2697H58.6936L63.4316 22.8847C62.9716 24.7247 61.9136 25.1847 61.0166 25.1847C60.6486 25.1847 60.4416 25.1617 60.0506 25.1157V26.9557C60.6486 27.0707 60.9936 27.1167 61.4996 27.1167Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path d="M71.5939 22.9997H73.8479V6.55469H71.5939V22.9997Z" fill="black" />
|
||||
<path d="M81.6221 23.3447C85.2791 23.3447 87.4871 20.7917 87.4871 17.1117C87.4871 13.4547 85.2791 10.9017 81.6451 10.9017C77.9651 10.9017 75.7571 13.4777 75.7571 17.1347C75.7571 20.8147 77.9651 23.3447 81.6221 23.3447ZM78.1031 17.1347C78.1031 14.6737 79.2071 12.7877 81.6451 12.7877C84.0371 12.7877 85.1411 14.6737 85.1411 17.1347C85.1411 19.5727 84.0371 21.4817 81.6451 21.4817C79.2071 21.4817 78.1031 19.5727 78.1031 17.1347Z" fill="black" />
|
||||
<path d="M92.6484 23.3217C94.5574 23.3217 95.8914 22.5627 96.6504 21.3437H96.6964C96.8344 22.6777 97.4324 23.1147 98.5594 23.1147C98.9274 23.1147 99.3184 23.0687 99.7094 22.9767V21.5967C99.5714 21.6197 99.5024 21.6197 99.4334 21.6197C98.9964 21.6197 98.8814 21.1827 98.8814 20.3317V14.9497C98.8814 11.9137 96.9494 10.9017 94.5344 10.9017C90.9464 10.9017 89.4744 12.6727 89.3594 14.9267H91.6594C91.7744 13.3627 92.3954 12.7187 94.4424 12.7187C96.1674 12.7187 96.6734 13.4317 96.6734 14.2827C96.6734 15.4327 95.5464 15.6627 93.7064 16.0077C90.7854 16.5597 88.8074 17.3417 88.8074 19.9637C88.8074 21.9647 90.3024 23.3217 92.6484 23.3217ZM91.1534 19.8027C91.1534 18.5837 92.0274 18.0087 94.0974 17.5947C95.2934 17.3417 96.3054 17.0887 96.6734 16.7437V18.4227C96.6734 20.4697 95.1784 21.5047 93.1544 21.5047C91.7514 21.5047 91.1534 20.9067 91.1534 19.8027Z" fill="black" />
|
||||
<path d="M106.181 23.3217C108.021 23.3217 109.148 22.4477 109.792 21.6197H109.838V22.9997H112.092V6.55469H109.838V12.6957H109.792C109.148 11.7757 108.021 10.9247 106.181 10.9247C103.191 10.9247 100.914 13.2707 100.914 17.1347C100.914 20.9987 103.191 23.3217 106.181 23.3217ZM103.26 17.1347C103.26 14.8347 104.341 12.8107 106.549 12.8107C108.573 12.8107 109.815 14.4667 109.815 17.1347C109.815 19.7797 108.573 21.4587 106.549 21.4587C104.341 21.4587 103.26 19.4347 103.26 17.1347Z" fill="black" />
|
||||
<path d="M12.2464 2.33838L22.2871 8.83812V21.1752L14.7265 25.8854V13.5484L4.67383 7.05725L12.2464 2.33838Z" fill="black" />
|
||||
<path
|
||||
d="M81.6221 23.3447C85.2791 23.3447 87.4871 20.7917 87.4871 17.1117C87.4871 13.4547 85.2791 10.9017 81.6451 10.9017C77.9651 10.9017 75.7571 13.4777 75.7571 17.1347C75.7571 20.8147 77.9651 23.3447 81.6221 23.3447ZM78.1031 17.1347C78.1031 14.6737 79.2071 12.7877 81.6451 12.7877C84.0371 12.7877 85.1411 14.6737 85.1411 17.1347C85.1411 19.5727 84.0371 21.4817 81.6451 21.4817C79.2071 21.4817 78.1031 19.5727 78.1031 17.1347Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M92.6484 23.3217C94.5574 23.3217 95.8914 22.5627 96.6504 21.3437H96.6964C96.8344 22.6777 97.4324 23.1147 98.5594 23.1147C98.9274 23.1147 99.3184 23.0687 99.7094 22.9767V21.5967C99.5714 21.6197 99.5024 21.6197 99.4334 21.6197C98.9964 21.6197 98.8814 21.1827 98.8814 20.3317V14.9497C98.8814 11.9137 96.9494 10.9017 94.5344 10.9017C90.9464 10.9017 89.4744 12.6727 89.3594 14.9267H91.6594C91.7744 13.3627 92.3954 12.7187 94.4424 12.7187C96.1674 12.7187 96.6734 13.4317 96.6734 14.2827C96.6734 15.4327 95.5464 15.6627 93.7064 16.0077C90.7854 16.5597 88.8074 17.3417 88.8074 19.9637C88.8074 21.9647 90.3024 23.3217 92.6484 23.3217ZM91.1534 19.8027C91.1534 18.5837 92.0274 18.0087 94.0974 17.5947C95.2934 17.3417 96.3054 17.0887 96.6734 16.7437V18.4227C96.6734 20.4697 95.1784 21.5047 93.1544 21.5047C91.7514 21.5047 91.1534 20.9067 91.1534 19.8027Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M106.181 23.3217C108.021 23.3217 109.148 22.4477 109.792 21.6197H109.838V22.9997H112.092V6.55469H109.838V12.6957H109.792C109.148 11.7757 108.021 10.9247 106.181 10.9247C103.191 10.9247 100.914 13.2707 100.914 17.1347C100.914 20.9987 103.191 23.3217 106.181 23.3217ZM103.26 17.1347C103.26 14.8347 104.341 12.8107 106.549 12.8107C108.573 12.8107 109.815 14.4667 109.815 17.1347C109.815 19.7797 108.573 21.4587 106.549 21.4587C104.341 21.4587 103.26 19.4347 103.26 17.1347Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M12.2464 2.33838L22.2871 8.83812V21.1752L14.7265 25.8854V13.5484L4.67383 7.05725L12.2464 2.33838Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path d="M11.477 25.2017V15.5747L3.90039 20.2936L11.477 25.2017Z" fill="black" />
|
||||
<path d="M120.442 6.30273C119.086 6.30273 117.998 7.29978 117.998 8.75952C117.998 10.2062 119.086 11.1968 120.442 11.1968C121.791 11.1968 122.879 10.2062 122.879 8.75952C122.879 7.29978 121.791 6.30273 120.442 6.30273ZM120.442 10.7601C119.34 10.7601 118.48 9.95207 118.48 8.75952C118.48 7.54742 119.34 6.73935 120.442 6.73935C121.563 6.73935 122.397 7.54742 122.397 8.75952C122.397 9.95207 121.563 10.7601 120.442 10.7601ZM120.52 8.97457L121.048 9.9651H121.641L121.041 8.86378C121.367 8.72042 121.511 8.45975 121.511 8.17302C121.511 7.49528 121.054 7.36495 120.285 7.36495H119.49V9.9651H120.025V8.97457H120.52ZM120.37 7.78853C120.729 7.78853 120.976 7.86673 120.976 8.17953C120.976 8.43368 120.807 8.56402 120.403 8.56402H120.025V7.78853H120.37Z" fill="black" />
|
||||
<path
|
||||
d="M120.442 6.30273C119.086 6.30273 117.998 7.29978 117.998 8.75952C117.998 10.2062 119.086 11.1968 120.442 11.1968C121.791 11.1968 122.879 10.2062 122.879 8.75952C122.879 7.29978 121.791 6.30273 120.442 6.30273ZM120.442 10.7601C119.34 10.7601 118.48 9.95207 118.48 8.75952C118.48 7.54742 119.34 6.73935 120.442 6.73935C121.563 6.73935 122.397 7.54742 122.397 8.75952C122.397 9.95207 121.563 10.7601 120.442 10.7601ZM120.52 8.97457L121.048 9.9651H121.641L121.041 8.86378C121.367 8.72042 121.511 8.45975 121.511 8.17302C121.511 7.49528 121.054 7.36495 120.285 7.36495H119.49V9.9651H120.025V8.97457H120.52ZM120.37 7.78853C120.729 7.78853 120.976 7.86673 120.976 8.17953C120.976 8.43368 120.807 8.56402 120.403 8.56402H120.025V7.78853H120.37Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import React from 'react';
|
||||
import serialize from './serialize';
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss';
|
||||
import serialize from './serialize'
|
||||
|
||||
const RichText: React.FC<{ className?: string, content: any }> = ({ className, content }) => {
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const RichText: React.FC<{ className?: string; content: any }> = ({ className, content }) => {
|
||||
if (!content) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
|
||||
{serialize(content)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default RichText;
|
||||
export default RichText
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import escapeHTML from 'escape-html';
|
||||
import { Text } from 'slate';
|
||||
import React, { Fragment } from 'react'
|
||||
import escapeHTML from 'escape-html'
|
||||
import { Text } from 'slate'
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
type Children = Leaf[]
|
||||
@@ -16,145 +16,77 @@ type Leaf = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const serialize = (children: Children): React.ReactElement[] => children.map((node, i) => {
|
||||
const serialize = (children: Children): React.ReactElement[] =>
|
||||
children.map((node, i) => {
|
||||
if (Text.isText(node)) {
|
||||
let text = <span dangerouslySetInnerHTML={{ __html: escapeHTML(node.text) }} />;
|
||||
let text = <span dangerouslySetInnerHTML={{ __html: escapeHTML(node.text) }} />
|
||||
|
||||
if (node.bold) {
|
||||
text = (
|
||||
<strong key={i}>
|
||||
{text}
|
||||
</strong>
|
||||
);
|
||||
text = <strong key={i}>{text}</strong>
|
||||
}
|
||||
|
||||
if (node.code) {
|
||||
text = (
|
||||
<code key={i}>
|
||||
{text}
|
||||
</code>
|
||||
);
|
||||
text = <code key={i}>{text}</code>
|
||||
}
|
||||
|
||||
if (node.italic) {
|
||||
text = (
|
||||
<em key={i}>
|
||||
{text}
|
||||
</em>
|
||||
);
|
||||
text = <em key={i}>{text}</em>
|
||||
}
|
||||
|
||||
if (node.underline) {
|
||||
text = (
|
||||
<span
|
||||
style={{ textDecoration: 'underline' }}
|
||||
key={i}
|
||||
>
|
||||
<span style={{ textDecoration: 'underline' }} key={i}>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (node.strikethrough) {
|
||||
text = (
|
||||
<span
|
||||
style={{ textDecoration: 'line-through' }}
|
||||
key={i}
|
||||
>
|
||||
<span style={{ textDecoration: 'line-through' }} key={i}>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
{text}
|
||||
</Fragment>
|
||||
);
|
||||
return <Fragment key={i}>{text}</Fragment>
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case 'h1':
|
||||
return (
|
||||
<h1 key={i}>
|
||||
{serialize(node.children)}
|
||||
</h1>
|
||||
);
|
||||
return <h1 key={i}>{serialize(node.children)}</h1>
|
||||
case 'h2':
|
||||
return (
|
||||
<h2 key={i}>
|
||||
{serialize(node.children)}
|
||||
</h2>
|
||||
);
|
||||
return <h2 key={i}>{serialize(node.children)}</h2>
|
||||
case 'h3':
|
||||
return (
|
||||
<h3 key={i}>
|
||||
{serialize(node.children)}
|
||||
</h3>
|
||||
);
|
||||
return <h3 key={i}>{serialize(node.children)}</h3>
|
||||
case 'h4':
|
||||
return (
|
||||
<h4 key={i}>
|
||||
{serialize(node.children)}
|
||||
</h4>
|
||||
);
|
||||
return <h4 key={i}>{serialize(node.children)}</h4>
|
||||
case 'h5':
|
||||
return (
|
||||
<h5 key={i}>
|
||||
{serialize(node.children)}
|
||||
</h5>
|
||||
);
|
||||
return <h5 key={i}>{serialize(node.children)}</h5>
|
||||
case 'h6':
|
||||
return (
|
||||
<h6 key={i}>
|
||||
{serialize(node.children)}
|
||||
</h6>
|
||||
);
|
||||
return <h6 key={i}>{serialize(node.children)}</h6>
|
||||
case 'quote':
|
||||
return (
|
||||
<blockquote key={i}>
|
||||
{serialize(node.children)}
|
||||
</blockquote>
|
||||
);
|
||||
return <blockquote key={i}>{serialize(node.children)}</blockquote>
|
||||
case 'ul':
|
||||
return (
|
||||
<ul key={i}>
|
||||
{serialize(node.children)}
|
||||
</ul>
|
||||
);
|
||||
return <ul key={i}>{serialize(node.children)}</ul>
|
||||
case 'ol':
|
||||
return (
|
||||
<ol key={i}>
|
||||
{serialize(node.children)}
|
||||
</ol>
|
||||
);
|
||||
return <ol key={i}>{serialize(node.children)}</ol>
|
||||
case 'li':
|
||||
return (
|
||||
<li key={i}>
|
||||
{serialize(node.children)}
|
||||
</li>
|
||||
);
|
||||
return <li key={i}>{serialize(node.children)}</li>
|
||||
case 'link':
|
||||
return (
|
||||
<a
|
||||
href={escapeHTML(node.url)}
|
||||
key={i}
|
||||
>
|
||||
<a href={escapeHTML(node.url)} key={i}>
|
||||
{serialize(node.children)}
|
||||
</a>
|
||||
);
|
||||
)
|
||||
|
||||
default:
|
||||
return (
|
||||
<p key={i}>
|
||||
{serialize(node.children)}
|
||||
</p>
|
||||
);
|
||||
return <p key={i}>{serialize(node.children)}</p>
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
export default serialize;
|
||||
export default serialize
|
||||
|
||||
14
examples/preview/nextjs/eslint-config/constants.js
Normal file
14
examples/preview/nextjs/eslint-config/constants.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const TEST_FILE_PATTERNS = [
|
||||
'**/**.it-test.ts',
|
||||
'**/**.test.ts',
|
||||
'**/**.spec.ts',
|
||||
'**/__mocks__/**.ts',
|
||||
'**/test/**.ts',
|
||||
]
|
||||
|
||||
const CODE_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.scss', '.json']
|
||||
|
||||
module.exports = {
|
||||
TEST_FILE_PATTERNS,
|
||||
CODE_FILE_EXTENSIONS,
|
||||
}
|
||||
20
examples/preview/nextjs/eslint-config/index.js
Normal file
20
examples/preview/nextjs/eslint-config/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
'plugin:@next/next/recommended',
|
||||
require.resolve('./rules/typescript.js'),
|
||||
require.resolve('./rules/import.js'),
|
||||
require.resolve('./rules/prettier.js'),
|
||||
require.resolve('./rules/style.js'),
|
||||
require.resolve('./rules/react.js'),
|
||||
],
|
||||
env: {
|
||||
es6: true,
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
globals: {
|
||||
NodeJS: true,
|
||||
},
|
||||
}
|
||||
6
examples/preview/nextjs/eslint-config/rules/filenames.js
Normal file
6
examples/preview/nextjs/eslint-config/rules/filenames.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ['filenames'],
|
||||
rules: {
|
||||
'filenames/match-regex': ['error', '^[a-z0-9-.]+$', true],
|
||||
},
|
||||
}
|
||||
52
examples/preview/nextjs/eslint-config/rules/import.js
Normal file
52
examples/preview/nextjs/eslint-config/rules/import.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const { CODE_FILE_EXTENSIONS } = require('../constants')
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
},
|
||||
extends: ['plugin:import/errors', 'plugin:import/warnings', 'plugin:import/typescript'],
|
||||
plugins: ['import', 'simple-import-sort'],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
node: {
|
||||
extensions: CODE_FILE_EXTENSIONS,
|
||||
},
|
||||
},
|
||||
'import/extensions': CODE_FILE_EXTENSIONS,
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts'],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'import/no-unresolved': 'off',
|
||||
'import/no-default-export': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{
|
||||
ts: 'never',
|
||||
tsx: 'never',
|
||||
js: 'never',
|
||||
jsx: 'never',
|
||||
},
|
||||
],
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
['^react', '^@?\\w'],
|
||||
['^(@components|@root|@utils|@scss|@hooks)(/.*|$)', '^\\.((?!.scss).)*$'],
|
||||
['^[^.]'],
|
||||
],
|
||||
},
|
||||
],
|
||||
'simple-import-sort/exports': 'error',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'import/named': 'error',
|
||||
'import/no-relative-packages': 'warn',
|
||||
'import/no-import-module-exports': 'warn',
|
||||
'import/no-cycle': 'warn',
|
||||
'import/no-duplicates': 'error',
|
||||
},
|
||||
}
|
||||
7
examples/preview/nextjs/eslint-config/rules/prettier.js
Normal file
7
examples/preview/nextjs/eslint-config/rules/prettier.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: ['prettier'],
|
||||
extends: ['plugin:prettier/recommended'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
}
|
||||
3
examples/preview/nextjs/eslint-config/rules/react.js
vendored
Normal file
3
examples/preview/nextjs/eslint-config/rules/react.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: ['plugin:react-hooks/recommended'],
|
||||
}
|
||||
15
examples/preview/nextjs/eslint-config/rules/style.js
Normal file
15
examples/preview/nextjs/eslint-config/rules/style.js
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
'prefer-named-exports': 'off',
|
||||
'prefer-destructuring': 'off',
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
'class-methods-use-this': 'off',
|
||||
'function-paren-newline': ['error', 'consistent'],
|
||||
'eol-last': ['error', 'always'],
|
||||
'no-restricted-syntax': 'off',
|
||||
'no-await-in-loop': 'off',
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
'space-infix-ops': 'off',
|
||||
'@typescript-eslint/space-infix-ops': 'warn',
|
||||
},
|
||||
}
|
||||
322
examples/preview/nextjs/eslint-config/rules/typescript.js
Normal file
322
examples/preview/nextjs/eslint-config/rules/typescript.js
Normal file
@@ -0,0 +1,322 @@
|
||||
module.exports = {
|
||||
plugins: ['@typescript-eslint'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/**.ts', '**/**.d.ts'],
|
||||
rules: {
|
||||
'no-undef': 'off',
|
||||
camelcase: 'off',
|
||||
'@typescript-eslint/adjacent-overload-signatures': 'error',
|
||||
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
|
||||
'@typescript-eslint/await-thenable': 'off',
|
||||
'@typescript-eslint/consistent-type-assertions': [
|
||||
'error',
|
||||
{ assertionStyle: 'as', objectLiteralTypeAssertions: 'allow-as-parameter' },
|
||||
],
|
||||
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
||||
'@typescript-eslint/consistent-type-imports': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'error',
|
||||
{
|
||||
allowExpressions: true,
|
||||
allowTypedFunctionExpressions: true,
|
||||
allowHigherOrderFunctions: true,
|
||||
allowConciseArrowFunctionExpressionsStartingWithVoid: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-member-accessibility': [
|
||||
'error',
|
||||
{ accessibility: 'no-public' },
|
||||
],
|
||||
'@typescript-eslint/member-delimiter-style': [
|
||||
'error',
|
||||
{
|
||||
multiline: {
|
||||
delimiter: 'none',
|
||||
requireLast: true,
|
||||
},
|
||||
singleline: {
|
||||
delimiter: 'semi',
|
||||
requireLast: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/method-signature-style': 'off',
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'off',
|
||||
{
|
||||
selector: 'default',
|
||||
format: ['camelCase'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
format: ['camelCase', 'UPPER_CASE'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: 'typeParameter',
|
||||
format: ['PascalCase'],
|
||||
prefix: ['T', 'U'],
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
types: ['boolean'],
|
||||
format: ['PascalCase'],
|
||||
prefix: ['is', 'should', 'has', 'can', 'did', 'will'],
|
||||
},
|
||||
{
|
||||
selector: 'interface',
|
||||
format: ['PascalCase'],
|
||||
custom: {
|
||||
regex: '^I[A-Z]',
|
||||
match: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: [
|
||||
'function',
|
||||
'parameter',
|
||||
'property',
|
||||
'parameterProperty',
|
||||
'method',
|
||||
'accessor',
|
||||
],
|
||||
format: ['camelCase'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: ['class', 'interface', 'typeAlias', 'enum', 'typeParameter'],
|
||||
format: ['PascalCase'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-base-to-string': 'off',
|
||||
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
|
||||
'@typescript-eslint/no-dynamic-delete': 'error',
|
||||
'@typescript-eslint/no-empty-interface': 'off',
|
||||
'@typescript-eslint/no-explicit-any': [
|
||||
'warn',
|
||||
{
|
||||
ignoreRestArgs: true,
|
||||
// enable later
|
||||
fixToUnknown: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-extra-non-null-assertion': 'error',
|
||||
'@typescript-eslint/no-extraneous-class': [
|
||||
'error',
|
||||
{
|
||||
allowConstructorOnly: false,
|
||||
allowEmpty: false,
|
||||
allowStaticOnly: false,
|
||||
allowWithDecorator: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-for-in-array': 'off',
|
||||
'@typescript-eslint/no-implicit-any-catch': [
|
||||
'error',
|
||||
{
|
||||
allowExplicitAny: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-implied-eval': 'off',
|
||||
'@typescript-eslint/no-inferrable-types': [
|
||||
'error',
|
||||
{
|
||||
ignoreParameters: false,
|
||||
ignoreProperties: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-invalid-void-type': [
|
||||
'off',
|
||||
{
|
||||
allowInGenericTypeArguments: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-misused-new': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'off',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
'@typescript-eslint/no-parameter-properties': 'error',
|
||||
'@typescript-eslint/no-require-imports': 'error',
|
||||
'@typescript-eslint/no-this-alias': 'error',
|
||||
'@typescript-eslint/no-throw-literal': 'off',
|
||||
'@typescript-eslint/no-type-alias': [
|
||||
'off',
|
||||
{
|
||||
allowAliases: 'always',
|
||||
allowCallbacks: 'always',
|
||||
allowConditionalTypes: 'always',
|
||||
allowConstructors: 'never',
|
||||
allowLiterals: 'in-unions-and-intersections',
|
||||
allowMappedTypes: 'always',
|
||||
allowTupleTypes: 'always',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||
'@typescript-eslint/no-unnecessary-qualifier': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-arguments': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'error',
|
||||
'@typescript-eslint/prefer-as-const': 'error',
|
||||
'@typescript-eslint/prefer-enum-initializers': 'off',
|
||||
'@typescript-eslint/prefer-for-of': 'error',
|
||||
'@typescript-eslint/prefer-includes': 'off',
|
||||
'@typescript-eslint/prefer-literal-enum-member': 'error',
|
||||
'@typescript-eslint/prefer-namespace-keyword': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/prefer-readonly': 'off',
|
||||
'@typescript-eslint/prefer-readonly-parameter-types': 'off',
|
||||
'@typescript-eslint/prefer-reduce-type-parameter': 'off',
|
||||
'@typescript-eslint/prefer-regexp-exec': 'off',
|
||||
'@typescript-eslint/prefer-string-starts-ends-with': 'off',
|
||||
'@typescript-eslint/prefer-ts-expect-error': 'warn',
|
||||
'@typescript-eslint/promise-function-async': 'off',
|
||||
'@typescript-eslint/require-array-sort-compare': 'off',
|
||||
'@typescript-eslint/restrict-plus-operands': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||
'@typescript-eslint/switch-exhaustiveness-check': 'off',
|
||||
'@typescript-eslint/triple-slash-reference': 'error',
|
||||
'@typescript-eslint/type-annotation-spacing': [
|
||||
'error',
|
||||
{
|
||||
before: false,
|
||||
after: true,
|
||||
overrides: {
|
||||
arrow: {
|
||||
before: true,
|
||||
after: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/typedef': [
|
||||
'error',
|
||||
{
|
||||
arrayDestructuring: false,
|
||||
arrowParameter: false,
|
||||
memberVariableDeclaration: false,
|
||||
objectDestructuring: false,
|
||||
parameter: false,
|
||||
propertyDeclaration: true,
|
||||
variableDeclaration: false,
|
||||
variableDeclarationIgnoreFunction: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'@typescript-eslint/unified-signatures': 'off',
|
||||
'brace-style': 'off',
|
||||
'@typescript-eslint/brace-style': 'error',
|
||||
'comma-spacing': 'off',
|
||||
'@typescript-eslint/comma-spacing': 'error',
|
||||
'default-param-last': 'off',
|
||||
'@typescript-eslint/default-param-last': 'error',
|
||||
'dot-notation': 'error',
|
||||
'@typescript-eslint/dot-notation': 'off',
|
||||
'func-call-spacing': 'off',
|
||||
'@typescript-eslint/func-call-spacing': 'error',
|
||||
indent: 'off',
|
||||
'@typescript-eslint/indent': 'off',
|
||||
'@typescript-eslint/init-declarations': 'off',
|
||||
'keyword-spacing': 'off',
|
||||
'@typescript-eslint/keyword-spacing': 'error',
|
||||
'lines-between-class-members': 'off',
|
||||
'@typescript-eslint/lines-between-class-members': [
|
||||
'error',
|
||||
'always',
|
||||
{
|
||||
exceptAfterSingleLine: true,
|
||||
exceptAfterOverload: true,
|
||||
},
|
||||
],
|
||||
'no-array-constructor': 'off',
|
||||
'@typescript-eslint/no-array-constructor': 'error',
|
||||
'no-dupe-class-members': 'off',
|
||||
'@typescript-eslint/no-dupe-class-members': 'error',
|
||||
'no-extra-parens': 'off',
|
||||
'@typescript-eslint/no-extra-parens': 'off',
|
||||
'no-extra-semi': 'off',
|
||||
'@typescript-eslint/no-extra-semi': 'error',
|
||||
'no-invalid-this': 'off',
|
||||
'@typescript-eslint/no-invalid-this': 'error',
|
||||
'no-loss-of-precision': 'off',
|
||||
'@typescript-eslint/no-loss-of-precision': 'error',
|
||||
'no-magic-numbers': 'off',
|
||||
'@typescript-eslint/no-magic-numbers': [
|
||||
'off',
|
||||
{
|
||||
ignoreArrayIndexes: true,
|
||||
ignoreDefaultValues: true,
|
||||
enforceConst: true,
|
||||
ignoreEnums: true,
|
||||
ignoreNumericLiteralTypes: true,
|
||||
ignoreReadonlyClassProperties: true,
|
||||
},
|
||||
],
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': [
|
||||
'error',
|
||||
{
|
||||
builtinGlobals: true,
|
||||
},
|
||||
],
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': [
|
||||
'error',
|
||||
{
|
||||
ignoreTypeValueShadow: false,
|
||||
},
|
||||
],
|
||||
'no-unused-expressions': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'no-useless-constructor': 'off',
|
||||
'@typescript-eslint/no-useless-constructor': 'error',
|
||||
quotes: 'off',
|
||||
'@typescript-eslint/quotes': [
|
||||
'error',
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true,
|
||||
allowTemplateLiterals: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/require-await': 'off',
|
||||
'@typescript-eslint/return-await': 'off',
|
||||
semi: 'off',
|
||||
'@typescript-eslint/semi': ['error', 'never'],
|
||||
'space-before-function-paren': 'off',
|
||||
'@typescript-eslint/space-before-function-paren': [
|
||||
'error',
|
||||
{
|
||||
anonymous: 'never',
|
||||
named: 'never',
|
||||
asyncArrow: 'always',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -3,11 +3,8 @@ const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
images: {
|
||||
domains: [
|
||||
'localhost',
|
||||
process.env.NEXT_PUBLIC_CMS_URL
|
||||
],
|
||||
}
|
||||
domains: ['localhost', process.env.NEXT_PUBLIC_CMS_URL],
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-html": "^1.0.3",
|
||||
"next": "12.3.1",
|
||||
"next": "^13.1.6",
|
||||
"payload-admin-bar": "^1.0.5",
|
||||
"payload-plugin-nested-pages": "^0.0.4",
|
||||
"qs": "^6.11.0",
|
||||
"react": "18.2.0",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "18.2.0",
|
||||
@@ -20,10 +20,20 @@
|
||||
"slate": "^0.84.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^13.1.6",
|
||||
"@types/node": "18.11.3",
|
||||
"@types/react": "18.0.21",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"eslint": "8.25.0",
|
||||
"eslint-config-next": "12.3.1",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-filenames": "^1.3.2",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
GetStaticProps,
|
||||
GetStaticPropsContext,
|
||||
GetStaticPaths
|
||||
} from 'next';
|
||||
import qs from 'qs';
|
||||
import { ParsedUrlQuery } from 'querystring';
|
||||
import type { Page, MainMenu } from '../payload-types';
|
||||
import { revalidationRate } from '../revalidationRate';
|
||||
import { Gutter } from '../components/Gutter';
|
||||
import RichText from '../components/RichText';
|
||||
|
||||
import classes from './index.module.scss';
|
||||
|
||||
const Page: React.FC<Page & {
|
||||
mainMenu: MainMenu
|
||||
preview?: boolean
|
||||
}> = (props) => {
|
||||
const {
|
||||
title,
|
||||
richText,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<main>
|
||||
<Gutter>
|
||||
<h1 className={classes.hero}>{title}</h1>
|
||||
<RichText content={richText} />
|
||||
</Gutter>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page;
|
||||
|
||||
interface IParams extends ParsedUrlQuery {
|
||||
slug: string[]
|
||||
}
|
||||
|
||||
// when 'preview' cookies are set in the browser, getStaticProps runs on every request :)
|
||||
// NOTE: 'slug' is an array (i.e. [...slug].tsx)
|
||||
export const getStaticProps: GetStaticProps = async (
|
||||
context: GetStaticPropsContext,
|
||||
) => {
|
||||
const {
|
||||
preview,
|
||||
previewData,
|
||||
params
|
||||
} = context;
|
||||
|
||||
const {
|
||||
payloadToken
|
||||
} = previewData as {
|
||||
payloadToken: string
|
||||
} || {};
|
||||
|
||||
let { slug } = params as IParams || {};
|
||||
if (!slug) slug = ['home'];
|
||||
|
||||
let doc = {};
|
||||
let notFound = false;
|
||||
|
||||
const lastSlug = slug[slug.length - 1];
|
||||
|
||||
// when previewing, send the payload token to bypass draft access control
|
||||
const lowerCaseSlug = lastSlug.toLowerCase(); // NOTE: let the url be case insensitive
|
||||
|
||||
let pageReq;
|
||||
let pageData;
|
||||
|
||||
const query = qs.stringify({
|
||||
draft: preview && true
|
||||
})
|
||||
|
||||
pageReq = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/pages?where[slug][equals]=${lowerCaseSlug}&depth=2&${query}`, {
|
||||
headers: {
|
||||
...preview ? {
|
||||
Authorization: `JWT ${payloadToken}`
|
||||
} : {}
|
||||
}
|
||||
});
|
||||
if (pageReq.ok) {
|
||||
pageData = await pageReq.json();
|
||||
}
|
||||
|
||||
if (pageData) {
|
||||
const { docs } = pageData;
|
||||
|
||||
if (docs.length > 0) {
|
||||
const slugChain = `/${slug.join('/')}`;
|
||||
// 'slug' is not unique, need to match the correct result to its last-most breadcrumb
|
||||
const foundDoc = docs.find((doc) => {
|
||||
const { breadcrumbs } = doc;
|
||||
const hasBreadcrumbs = breadcrumbs && Array.isArray(breadcrumbs) && breadcrumbs.length > 0;
|
||||
if (hasBreadcrumbs) {
|
||||
const lastCrumb = breadcrumbs[breadcrumbs.length - 1];
|
||||
return lastCrumb.url === slugChain;
|
||||
}
|
||||
})
|
||||
|
||||
if (foundDoc) {
|
||||
doc = foundDoc
|
||||
} else notFound = true
|
||||
} else notFound = true;
|
||||
} else notFound = true;
|
||||
|
||||
return ({
|
||||
props: {
|
||||
...doc,
|
||||
preview: preview || null,
|
||||
collection: 'pages'
|
||||
},
|
||||
notFound,
|
||||
revalidate: revalidationRate
|
||||
})
|
||||
}
|
||||
|
||||
type Path = {
|
||||
params: {
|
||||
slug: string[]
|
||||
}
|
||||
};
|
||||
|
||||
type Paths = Path[];
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
let paths: Paths = [];
|
||||
let pagesReq;
|
||||
let pagesData;
|
||||
|
||||
pagesReq = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/pages?where[_status][equals]=published&depth=0&limit=300`);
|
||||
pagesData = await pagesReq.json();
|
||||
|
||||
if (pagesReq?.ok) {
|
||||
const { docs: pages } = pagesData;
|
||||
|
||||
if (pages && Array.isArray(pages) && pages.length > 0) {
|
||||
paths = pages.map((page) => {
|
||||
const {
|
||||
slug,
|
||||
breadcrumbs,
|
||||
} = page;
|
||||
|
||||
let slugs = [slug];
|
||||
|
||||
const hasBreadcrumbs = breadcrumbs && Array.isArray(breadcrumbs) && breadcrumbs.length > 0;
|
||||
|
||||
if (hasBreadcrumbs) {
|
||||
slugs = breadcrumbs.map((crumb: any) => {
|
||||
const { url } = crumb;
|
||||
let slug;
|
||||
if (url) {
|
||||
const split = url.split('/');
|
||||
slug = split[split.length - 1];
|
||||
}
|
||||
return slug;
|
||||
})
|
||||
}
|
||||
|
||||
return ({ params: { slug: slugs } })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
paths,
|
||||
fallback: true
|
||||
}
|
||||
}
|
||||
130
examples/preview/nextjs/pages/[slug].tsx
Normal file
130
examples/preview/nextjs/pages/[slug].tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React from 'react'
|
||||
import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next'
|
||||
import QueryString from 'qs'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
|
||||
import { Gutter } from '../components/Gutter'
|
||||
import RichText from '../components/RichText'
|
||||
import type { MainMenu, Page as PageType } from '../payload-types'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const Page: React.FC<
|
||||
PageType & {
|
||||
mainMenu: MainMenu
|
||||
preview?: boolean
|
||||
}
|
||||
> = props => {
|
||||
const { title, richText } = props
|
||||
|
||||
return (
|
||||
<main>
|
||||
<Gutter>
|
||||
<h1 className={classes.hero}>{title}</h1>
|
||||
<RichText content={richText} />
|
||||
</Gutter>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
|
||||
interface IParams extends ParsedUrlQuery {
|
||||
slug: string
|
||||
}
|
||||
|
||||
// when 'preview' cookies are set in the browser, getStaticProps runs on every request :)
|
||||
export const getStaticProps: GetStaticProps = async (context: GetStaticPropsContext) => {
|
||||
const { preview, previewData, params } = context
|
||||
|
||||
const { payloadToken } =
|
||||
(previewData as {
|
||||
payloadToken: string
|
||||
}) || {}
|
||||
|
||||
let { slug } = (params as IParams) || {}
|
||||
if (!slug) slug = 'home'
|
||||
|
||||
let doc = {}
|
||||
const notFound = false
|
||||
|
||||
const lowerCaseSlug = slug.toLowerCase() // NOTE: let the url be case insensitive
|
||||
|
||||
const searchParams = QueryString.stringify(
|
||||
{
|
||||
where: {
|
||||
slug: {
|
||||
equals: lowerCaseSlug,
|
||||
},
|
||||
},
|
||||
depth: 1,
|
||||
draft: preview ? true : undefined,
|
||||
},
|
||||
{
|
||||
encode: false,
|
||||
addQueryPrefix: true,
|
||||
},
|
||||
)
|
||||
|
||||
// when previewing, send the payload token to bypass draft access control
|
||||
const pageReq = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/pages${searchParams}`, {
|
||||
headers: {
|
||||
...(preview
|
||||
? {
|
||||
Authorization: `JWT ${payloadToken}`,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
})
|
||||
|
||||
if (pageReq.ok) {
|
||||
const pageData = await pageReq.json()
|
||||
doc = pageData.docs[0]
|
||||
if (!doc) {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
...doc,
|
||||
preview: preview || null,
|
||||
collection: 'pages',
|
||||
},
|
||||
notFound,
|
||||
revalidate: 3600, // in seconds
|
||||
}
|
||||
}
|
||||
|
||||
type Path = {
|
||||
params: {
|
||||
slug: string
|
||||
}
|
||||
}
|
||||
|
||||
type Paths = Path[]
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
let paths: Paths = []
|
||||
|
||||
const pagesReq = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_CMS_URL}/api/pages?where[_status][equals]=published&depth=0&limit=300`,
|
||||
)
|
||||
|
||||
const pagesData = await pagesReq.json()
|
||||
|
||||
if (pagesReq?.ok) {
|
||||
const { docs: pages } = pagesData
|
||||
|
||||
if (pages && Array.isArray(pages) && pages.length > 0) {
|
||||
paths = pages.map(page => ({ params: { slug: page.slug } }))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
paths,
|
||||
fallback: true,
|
||||
}
|
||||
}
|
||||
@@ -1,93 +1,76 @@
|
||||
import App, { AppContext, AppProps as NextAppProps } from 'next/app';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { MainMenu } from "../payload-types";
|
||||
import { Header } from '../components/Header';
|
||||
import { GlobalsProvider } from '../providers/Globals';
|
||||
import { useNavigationScrollTo } from '../utilities/useNavigationScrollTo';
|
||||
import { CookiesProvider } from 'react-cookie';
|
||||
import React, { useCallback } from 'react'
|
||||
import { CookiesProvider } from 'react-cookie'
|
||||
import App, { AppContext, AppProps as NextAppProps } from 'next/app'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Header } from '../components/Header'
|
||||
import { MainMenu } from '../payload-types'
|
||||
|
||||
import '../css/app.scss'
|
||||
|
||||
import '../css/app.scss';
|
||||
export interface IGlobals {
|
||||
mainMenu: MainMenu,
|
||||
mainMenu: MainMenu
|
||||
}
|
||||
|
||||
export const getAllGlobals = async (): Promise<IGlobals> => {
|
||||
const [
|
||||
mainMenu,
|
||||
] = await Promise.all([
|
||||
fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/globals/main-menu?depth=1`).then((res) => res.json()),
|
||||
]);
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/globals/main-menu?depth=1`)
|
||||
const mainMenu = await res.json()
|
||||
|
||||
return {
|
||||
mainMenu,
|
||||
}
|
||||
}
|
||||
|
||||
const transitionTime = 500;
|
||||
|
||||
type AppProps<P = any> = {
|
||||
pageProps: P;
|
||||
} & Omit<NextAppProps<P>, "pageProps">;
|
||||
pageProps: P
|
||||
} & Omit<NextAppProps<P>, 'pageProps'>
|
||||
|
||||
const PayloadApp = (appProps: AppProps & {
|
||||
globals: IGlobals,
|
||||
}): React.ReactElement => {
|
||||
const {
|
||||
Component,
|
||||
pageProps,
|
||||
globals,
|
||||
} = appProps;
|
||||
const PayloadApp = (
|
||||
appProps: AppProps & {
|
||||
globals: IGlobals
|
||||
},
|
||||
): React.ReactElement => {
|
||||
const { Component, pageProps, globals } = appProps
|
||||
|
||||
const {
|
||||
breadcrumbs,
|
||||
collection,
|
||||
id,
|
||||
preview,
|
||||
} = pageProps;
|
||||
const { collection, id, preview } = pageProps
|
||||
|
||||
const router = useRouter();
|
||||
useNavigationScrollTo({
|
||||
router,
|
||||
navigationTime: transitionTime
|
||||
});
|
||||
const router = useRouter()
|
||||
|
||||
const onPreviewExit = useCallback(() => {
|
||||
const exit = async () => {
|
||||
const exitReq = await fetch('/api/exit-preview');
|
||||
const exitReq = await fetch('/api/exit-preview')
|
||||
if (exitReq.status === 200) {
|
||||
router.reload();
|
||||
router.reload()
|
||||
}
|
||||
}
|
||||
exit();
|
||||
exit()
|
||||
}, [router])
|
||||
|
||||
return (
|
||||
<CookiesProvider>
|
||||
<GlobalsProvider {...globals}>
|
||||
<Header
|
||||
globals={globals}
|
||||
adminBarProps={{
|
||||
collection,
|
||||
id: id,
|
||||
id,
|
||||
preview,
|
||||
onPreviewExit
|
||||
onPreviewExit,
|
||||
}}
|
||||
/>
|
||||
<Component {...pageProps} />
|
||||
</GlobalsProvider>
|
||||
</CookiesProvider>
|
||||
)
|
||||
}
|
||||
|
||||
PayloadApp.getInitialProps = async (appContext: AppContext) => {
|
||||
const appProps = await App.getInitialProps(appContext);
|
||||
const appProps = await App.getInitialProps(appContext)
|
||||
|
||||
const globals = await getAllGlobals();
|
||||
const globals = await getAllGlobals()
|
||||
|
||||
return {
|
||||
...appProps,
|
||||
globals
|
||||
};
|
||||
};
|
||||
globals,
|
||||
}
|
||||
}
|
||||
|
||||
export default PayloadApp
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
const exitPreview = (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const exitPreview = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||
res.clearPreviewData()
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
res.writeHead(200)
|
||||
res.end()
|
||||
}
|
||||
|
||||
export default exitPreview
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
const preview = (req: NextApiRequest, res: NextApiResponse) => {
|
||||
// eslint-disable-next-line consistent-return
|
||||
const preview = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||
const {
|
||||
cookies: {
|
||||
'payload-token': payloadToken
|
||||
},
|
||||
query: {
|
||||
url,
|
||||
}
|
||||
cookies: { 'payload-token': payloadToken },
|
||||
query: { url },
|
||||
} = req
|
||||
|
||||
if (!url) {
|
||||
return res.status(404).json({
|
||||
message: 'No URL provided'
|
||||
message: 'No URL provided',
|
||||
})
|
||||
}
|
||||
|
||||
if (!payloadToken) {
|
||||
return res.status(403).json({
|
||||
message: 'You are not allowed to preview this page'
|
||||
message: 'You are not allowed to preview this page',
|
||||
})
|
||||
}
|
||||
|
||||
res.setPreviewData({
|
||||
payloadToken
|
||||
});
|
||||
payloadToken,
|
||||
})
|
||||
|
||||
res.redirect(url as string);
|
||||
res.redirect(url as string)
|
||||
}
|
||||
|
||||
export default preview;
|
||||
export default preview
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
const revalidate = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const revalidate = async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
|
||||
// Check for secret to confirm this is a valid request
|
||||
if (req.query.secret !== process.env.NEXT_PRIVATE_REVALIDATION_KEY) {
|
||||
return res.status(401).json({ message: 'Invalid token' });
|
||||
return res.status(401).json({ message: 'Invalid token' })
|
||||
}
|
||||
|
||||
if (typeof req.query.revalidatePath === 'string') {
|
||||
try {
|
||||
await res.revalidate(req.query.revalidatePath);
|
||||
return res.json({ revalidated: true });
|
||||
} catch (err) {
|
||||
await res.revalidate(req.query.revalidatePath)
|
||||
return res.json({ revalidated: true })
|
||||
} catch (err: unknown) {
|
||||
// If there was an error, Next.js will continue
|
||||
// to show the last successfully generated page
|
||||
return res.status(500).send('Error revalidating');
|
||||
return res.status(500).send('Error revalidating')
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).send('No path to revalidate');
|
||||
return res.status(400).send('No path to revalidate')
|
||||
}
|
||||
|
||||
export default revalidate;
|
||||
export default revalidate
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { GetStaticProps } from 'next';
|
||||
import Page, { getStaticProps as sharedGetStaticProps } from './[...slug]';
|
||||
import { GetStaticProps } from 'next'
|
||||
|
||||
export default Page;
|
||||
import Page, { getStaticProps as sharedGetStaticProps } from './[slug]'
|
||||
|
||||
export const getStaticProps: GetStaticProps = async (ctx) => {
|
||||
const func = sharedGetStaticProps.bind(this);
|
||||
return func(ctx);
|
||||
};
|
||||
export default Page
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ctx => {
|
||||
const func = sharedGetStaticProps.bind(this)
|
||||
return func(ctx)
|
||||
}
|
||||
|
||||
@@ -17,20 +17,14 @@ export interface Config {
|
||||
export interface Page {
|
||||
id: string;
|
||||
title: string;
|
||||
slug?: string;
|
||||
richText: {
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
slug?: string;
|
||||
parent?: string | Page;
|
||||
breadcrumbs: {
|
||||
doc?: string | Page;
|
||||
url?: string;
|
||||
label?: string;
|
||||
id?: string;
|
||||
}[];
|
||||
_status?: 'draft' | 'published';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
password?: string;
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
@@ -41,6 +35,7 @@ export interface User {
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
password?: string;
|
||||
}
|
||||
export interface MainMenu {
|
||||
id: string;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { MainMenu } from '../../payload-types';
|
||||
|
||||
export type MainMenuType = MainMenu
|
||||
|
||||
export interface IGlobals {
|
||||
mainMenu: MainMenuType,
|
||||
}
|
||||
|
||||
export const GlobalsContext = createContext<IGlobals>({} as IGlobals);
|
||||
export const useGlobals = (): IGlobals => useContext(GlobalsContext);
|
||||
|
||||
export const GlobalsProvider: React.FC<IGlobals & {
|
||||
children: React.ReactNode
|
||||
}> = (props) => {
|
||||
const {
|
||||
mainMenu,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<GlobalsContext.Provider
|
||||
value={{
|
||||
mainMenu,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</GlobalsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
// https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration
|
||||
export const revalidationRate = 3600; // in seconds
|
||||
@@ -1,4 +0,0 @@
|
||||
export default !!(
|
||||
(typeof window !== 'undefined'
|
||||
&& window.document && window.document.createElement)
|
||||
);
|
||||
@@ -1,2 +0,0 @@
|
||||
export const toKebabCase = (string) => string?.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import React from 'react';
|
||||
import Router, { NextRouter } from 'next/router';
|
||||
|
||||
function saveScrollPos(asPath: string) {
|
||||
sessionStorage.setItem(
|
||||
`scrollPos:${asPath}`,
|
||||
JSON.stringify({
|
||||
x: window.scrollX,
|
||||
y: window.scrollY
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function restoreScrollPos(asPath: string) {
|
||||
const json = sessionStorage.getItem(`scrollPos:${asPath}`);
|
||||
const scrollPos = json ? JSON.parse(json) : undefined;
|
||||
if (scrollPos) {
|
||||
window.scrollTo(scrollPos.x, scrollPos.y);
|
||||
}
|
||||
}
|
||||
|
||||
type NavigationScrollToProps = {
|
||||
router: NextRouter
|
||||
navigationTime: number
|
||||
}
|
||||
|
||||
export const useNavigationScrollTo: React.FC<NavigationScrollToProps> = (props) => {
|
||||
const { router, navigationTime } = props;
|
||||
const deviceNavigated = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (('scrollRestoration' in window.history)) {
|
||||
window.history.scrollRestoration = 'manual';
|
||||
}
|
||||
|
||||
const onBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
saveScrollPos(router.asPath);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
delete event.returnValue;
|
||||
};
|
||||
|
||||
const onRouteChangeStart = () => {
|
||||
saveScrollPos(router.asPath);
|
||||
|
||||
// @ts-ignore
|
||||
document.documentElement.style['scroll-behavior'] = 'initial';
|
||||
};
|
||||
|
||||
const onRouteChangeComplete = (url: string) => {
|
||||
// scroll and transition (timeout must be equal to css-transition duration)
|
||||
setTimeout(() => {
|
||||
if (url && deviceNavigated.current) {
|
||||
restoreScrollPos(url);
|
||||
deviceNavigated.current = false;
|
||||
} else {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
});
|
||||
}
|
||||
// @ts-ignore
|
||||
document.documentElement.style['scroll-behavior'] = 'smooth';
|
||||
}, navigationTime);
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', onBeforeUnload);
|
||||
Router.events.on('routeChangeStart', onRouteChangeStart);
|
||||
Router.events.on('routeChangeComplete', onRouteChangeComplete);
|
||||
Router.beforePopState(() => {
|
||||
deviceNavigated.current = true;
|
||||
return true;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', onBeforeUnload);
|
||||
Router.events.off('routeChangeStart', onRouteChangeStart);
|
||||
Router.events.off('routeChangeComplete', onRouteChangeComplete);
|
||||
Router.beforePopState(() => true);
|
||||
};
|
||||
}, [router, navigationTime]);
|
||||
|
||||
return null;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,11 +15,16 @@ There is a fully working Next.js app tailored specifically for this example whic
|
||||
|
||||
## How it works
|
||||
|
||||
A `redirects` collection with `from` and `to` fields. Your front-end can fetch these redirects as needed and inject them into your own router. `from` is a simple string, while `to` is a conditional field that allows you to select between related documents or a custom url.
|
||||
The [Redirects Plugin](https://github.com/payloadcms/plugin-redirects) automatically adds a `redirects` collection to your config which your front-end can fetch and inject them into its own router. The redirect fields are:
|
||||
|
||||
- `from` This is a URL string that will be matched against the request path.
|
||||
- `to` This is a conditional field that allows you to select between related documents or a custom URL.
|
||||
|
||||
See the official [Redirects Plugin](https://github.com/payloadcms/plugin-redirects) for full details.
|
||||
|
||||
### Seed
|
||||
|
||||
On boot, a seed script is included to create a user, a home page, and a th following redirects for you to test with:
|
||||
On boot, a seed script is included to create a user, a home page, and a the following redirects for you to test with:
|
||||
|
||||
- From `/redirect-to-external` to `https://payloadcms.com`
|
||||
- From `/redirect-to-internal` to `/redirected`
|
||||
|
||||
Reference in New Issue
Block a user