chore: reviews preview example

This commit is contained in:
Jacob Fletcher
2023-02-14 09:04:25 -05:00
parent 0a87f106ec
commit 7a99b2544a
75 changed files with 2981 additions and 1655 deletions

View File

@@ -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

View File

@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['./eslint-config'],
}

View File

@@ -1,3 +0,0 @@
{
"quote-props": "disabled"
}

View File

@@ -0,0 +1,8 @@
module.exports = {
printWidth: 100,
parser: "typescript",
semi: false,
singleQuote: true,
trailingComma: "all",
arrowParens: "avoid",
};

View File

@@ -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.

View 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,
},
}

View 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',
},
}

View File

@@ -0,0 +1,7 @@
module.exports = {
plugins: ['prettier'],
extends: ['plugin:prettier/recommended'],
rules: {
'prettier/prettier': 'error',
},
}

View 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',
},
}

View 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',
},
],
},
},
],
}

View File

@@ -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"
}
}

View File

@@ -0,0 +1,5 @@
import type { Access } from 'payload/config'
export const loggedIn: Access = ({ req: { user } }) => {
return Boolean(user)
}

View File

@@ -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',
},
},
],
}
}

View File

@@ -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;
};

View File

@@ -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

View File

@@ -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));
revalidatePath(url.pathname);
if (url.pathname === '/home') {
revalidatePath('/')
// 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
}
return doc;
};
// 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 })
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}`)
}
}
revalidate()
return doc
}

View File

@@ -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(),
],
};
}

View File

@@ -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: [],
};
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,
);

View File

@@ -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 = {
],
},
],
};
}

View File

@@ -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
}>
}

View File

@@ -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}`, ''),
}),
],
});
})

View File

@@ -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",
};

View File

@@ -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",
};
},
],
}

View File

@@ -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: '',
},
},
],
},
]
}
})
};
}

View 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.',
},
],
},
],
}

View 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.',
},
],
},
],
}

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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}`);
}
};

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['./eslint-config'],
}

View File

@@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

View File

@@ -0,0 +1,8 @@
module.exports = {
printWidth: 100,
parser: "typescript",
semi: false,
singleQuote: true,
trailingComma: "all",
arrowParens: "avoid",
};

View File

@@ -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;
}

View File

@@ -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>
)
}

View File

@@ -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" />
}

View File

@@ -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'

View File

@@ -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,7 +29,7 @@ export const AdminBar: React.FC<{
position: 'relative',
zIndex: 'unset',
padding: 0,
backgroundColor: 'transparent'
backgroundColor: 'transparent',
}}
/>
</Gutter>

View File

@@ -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>
)}

View File

@@ -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>
)
}

View File

@@ -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

View File

@@ -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

View 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,
}

View 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,
},
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: ['filenames'],
rules: {
'filenames/match-regex': ['error', '^[a-z0-9-.]+$', true],
},
}

View 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',
},
}

View File

@@ -0,0 +1,7 @@
module.exports = {
plugins: ['prettier'],
extends: ['plugin:prettier/recommended'],
rules: {
'prettier/prettier': 'error',
},
}

View File

@@ -0,0 +1,3 @@
module.exports = {
extends: ['plugin:react-hooks/recommended'],
}

View 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',
},
}

View 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',
},
],
},
},
],
}

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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
}
}

View 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,
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -1,2 +0,0 @@
// https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration
export const revalidationRate = 3600; // in seconds

View File

@@ -1,4 +0,0 @@
export default !!(
(typeof window !== 'undefined'
&& window.document && window.document.createElement)
);

View File

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

View File

@@ -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

View File

@@ -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`