Compare commits

...

32 Commits

Author SHA1 Message Date
Sasha
76d1f00765 remove auth 2024-11-26 16:47:09 +02:00
Sasha
0b05eb8f38 feat: enforce maximum call depth for local api operations 2024-11-26 16:25:25 +02:00
Elliot DeNolf
4c4eb2ae2b chore: some strictNullChecks mitigation (#9528) 2024-11-26 09:04:06 -05:00
Paul
71c2f63722 fix(plugin-form-builder): allow overrides to the payment fields group (#9522)
Co-authored-by: mikecebul <mikecebul@gmail.com>
2024-11-25 22:40:34 -06:00
Said Akhrarov
4e3be4414b docs: fix link for storage-uploadthing (#9527)
### What?
Fixes a link to the `storage-uploadthing` adapter in Github.

### Why?
To link readers to the correct package location.

### How?
Change to `docs/upload/storage-adapters.mdx`.

Credit to rik in Discord for the catch.
2024-11-26 03:46:25 +00:00
Patrik
d0af8e8d06 chore(examples): migrates draft-preview example to 3.0 (#9362) 2024-11-25 22:13:15 -05:00
Paul
82145f7bb0 fix(ui): remove overflow hidden from app-header wrappers since it breaks any popout elements (#9525)
Unblocks https://github.com/payloadcms/payload/pull/9391

It was previously impossible to create popout, dropdown or other menus
from buttons added to the app-header component
2024-11-25 18:26:54 -06:00
Jacob Fletcher
0757e06e71 feat: deprecates react-animate-height in favor of native css (#9456)
Deprecates `react-animate-height` in favor of native CSS, specifically
the `interpolate-size: allow-keywords;` property which can be used to
animate to `height: auto`—the primary reason this package exists. This
is one less dependency in our `node_modules`. Tried to replicate the
current DOM structure, class names, and API of `react-animate-height`
for best compatibility.

Note that this CSS property is experimental BUT this PR includes a patch
for browsers without native support. Once full support is reached, the
patch can be safely removed.
2024-11-25 17:48:16 -05:00
Elliot DeNolf
058bd02ebd chore(release): v3.1.1 [skip ci] 2024-11-25 16:42:21 -05:00
Tylan Davis
aa26312b96 docs: adds custom anchor tags to docs with duplicate headings (#9521)
### What?
Adds custom anchor tags to docs where duplicate headings exist.

### Why?
Anchor links would not correctly navigate to the proper point on the
page if there were multiple headings with the same string.

### How?
The website now supports adding custom `#anchor` to a heading in
markdown that will attach to the headings and table of content list
items. This PR adds custom anchors to the docs that have duplicate
headings.

**Example:**
```md
/docs/upload/storage-adapters.mdx

### Usage#vercel-blob-installation
```
Generates the path:
`/docs/upload/storage-adapters#vercel-blob-installation`
2024-11-25 16:31:08 -05:00
Sasha
b5f89d5199 fix(db-mongodb): sanitizeRelationshipIDs named tabs within tabs (#9400)
Ensures `sanitizeRelationshipIDs` works properly in any case
Updates predefinedMigration to work with new globals
Skips ObjectID creation errors to not fail with outdated data to the
schema.
2024-11-25 16:27:47 -05:00
dependabot[bot]
c8589a640c chore(deps): bump eslint-plugin-playwright from 1.7.0 to 2.1.0 (#9474)
Bumps
[eslint-plugin-playwright](https://github.com/playwright-community/eslint-plugin-playwright)
from 1.7.0 to 2.1.0.
2024-11-25 15:29:52 -05:00
Elliot DeNolf
da788413eb ci: only security PRs from dependabot 2024-11-25 15:20:14 -05:00
Said Akhrarov
0c7e418dbc fix(translations): add sl to acceptedLanguages (#9506)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR adds `'sl'` to list of accepted languages, dedupes languages not
implemented from langs that have been supported, and adjusts the string
match for `'sl'` in importDateFNSLocale to the correct locale.

### Why?
To fix TS errors and runtime errors encountered while adding Slovenian
language to config, and then selecting it in `/account` view.

### How?
- Addition of `'sl'` to `acceptLanguages` array
- Change from `'sl'` to `'sl-SI'` in `importDateFNSLocale.ts`

Fixes #9504

---------

Co-authored-by: Jessica Chowdhury <jessica@trbl.design>
2024-11-25 20:17:05 +00:00
Jacob Fletcher
8383426313 fix(ui): prevents column reset on sort (#9517)
Fixes #9492. Sorting columns would unintentionally clear column
preferences.
2024-11-25 15:15:07 -05:00
Francisco Lourenço
af096a374a fix(ui): z-index on react-select menu (#9512)
The [previous fix](https://github.com/payloadcms/payload/pull/8735)
worked but was a breaking change because it set a `z-index` in the
`.react-select` wrapper instead of the `.rs__menu`, creating a new
stacking-context, therefore making any existing customizations to the
menu's `z-index` not work. This was a way to fix a regression introduced
by the css-layers, in which Payload's custom `z-index: 4` no longer took
precedence over react-select's default `z-index: 1`.

With this PR we remove the default `z-index: 1` applied by react-select,
so that the `z-index: 4` set in the "payload-default" css layer can take
effect. An alternative to this fix would be to use `z-index: 4
!important`, but this has the advantage of allowing the `z-index` to be
easily customized by the consumers of the CMS, as with all the other
styles.

### Screenshots

![2024-11-25 18 21
22](https://github.com/user-attachments/assets/3dc9a067-901a-41ea-b04e-c811788f8415)
2024-11-25 14:10:20 -06:00
Jarrod Flesch
6ffd4c7825 fix: incorrect locale registration in datepicker (#9516)
### What?
This log was appearing when the DatePicker loaded without a registered
locale:
```
A locale object was not found for the provided string ["enUS"].
```
Also fixes css misalignment of icons inside date picker field

### Why?
If i`18n.dateFNS` had not loaded, we were registering the locale with an
undefined value.

### How?
Only register the locale for react-datepicker if i18n.dateFNS is
present.
2024-11-25 15:05:15 -05:00
Elliot DeNolf
08270426ba ci: update release-canary name 2024-11-25 14:08:18 -05:00
Jacob Fletcher
f136a7db2a fix: improper spread of list preferences (#9510)
List preferences were improperly saving their own records onto
themselves when building table state through the server function. This
was happening because the entire preference document was being spread
onto the new preferences, as opposed to just the value itself:

```diff
const mergedPrefs = {
-  ...(preferencesResult || {}),
+  ...(preferencesResult?.value || {}),
   columns,
}
```

This PR also swaps `dequal` out for `dequal/lite`.
2024-11-25 13:56:35 -05:00
Sasha
e176b8b764 fix: correct migrations sorting in getMigrations (#9330)
### What?
We sorted migrations by `-name` in `getMigrations` as by assumption from
generated file names, however, it may be not true as the improved (+
unflaked, previously it failed sometimes) test for `migrate:down` can
reproduce. As in result, `migrateDown` / `migrateRefresh` may execute in
order different from `migrate`.

Unflakes the 'should commit multiple operations async'  test.
We shouldn't pass the same `req` that doesn't contain a transaction to
different operations that execute in parallel (via `Promise.all`)
without either creating a transaction before or using
`isolateObjectProperty(req, 'transactionID')`. It leads to a race
condition because operation can commit a wrong transaction, different
from inited
2024-11-25 13:53:44 -05:00
Elliot DeNolf
3c8f042d1d ci: update release-canary workflow 2024-11-25 13:38:38 -05:00
Sasha
e5cc9153aa fix(db-postgres): allow to clear select fields with hasMany: true (#9464)
### What?
Previously, using Postgres, select fields with `hasMany: true` weren't
clearable.
Meaning, you couldn't pass an empty array:
```ts
const updatedDoc = await payload.update({
  id,
  collection: 'select-fields',
  data: {
    selectHasMany: [],
  },
})
```

### Why?
To achieve the same behavior with MongoDB.

### How?
Modifies logic in `packages/drizzle/src/upsertRow/index.ts` to include
empty arrays.
2024-11-25 11:15:19 -05:00
Sasha
b96475b7b9 fix: run queues via the /payload-jobs/run endpoint without workflows (#9509)
Fixes https://github.com/payloadcms/payload/discussions/9418 (the
`/api/payload-jobs/run` endpoint) when the config doesn't have any
`workflows` but only `tasks`
2024-11-25 17:57:51 +02:00
Sasha
cae300e8e3 perf: flattenedFields collection/global property, remove deep copying in validateQueryPaths (#9299)
### What?
Improves querying performance of the Local API, reduces copying overhead

### How?

Adds `flattenedFields` property for sanitized collection/global config
which contains fields in database schema structure.
For example, It removes rows / collapsible / unnamed tabs and merges
them to the parent `fields`, ignores UI fields, named tabs are added as
`type: 'tab'`.
This simplifies code in places like Drizzle `transform/traverseFields`.
Also, now we can avoid calling `flattenTopLevelFields` which adds some
overhead and use `collection.flattenedFields` / `field.flattenedFields`.

By refactoring `configToJSONSchema.ts` with `flattenedFields` it also
1. Fixes https://github.com/payloadcms/payload/issues/9467
2. Fixes types for UI fields were generated for `select`



Removes this deep copying for each `where` query constraint
58ac784425/packages/payload/src/database/queryValidation/validateQueryPaths.ts (L69-L73)
which potentially can add overhead if you have a large collection/global
config

UPD:
The overhead is even much more than in the benchmark below if you have
Lexical with blocks.

Benchmark in `relationships/int.spec.ts`:
```ts
const now = Date.now()
for (let i = 0; i < 10; i++) {
  const now = Date.now()
  for (let i = 0; i < 300; i++) {
    const query = await payload.find({
      collection: 'chained',
      where: {
        'relation.relation.name': {
          equals: 'third',
        },
        and: [
          {
            'relation.relation.name': {
              equals: 'third',
            },
          },
          {
            'relation.relation.name': {
              equals: 'third',
            },
          },
          {
            'relation.relation.name': {
              equals: 'third',
            },
          },
          {
            'relation.relation.name': {
              equals: 'third',
            },
          },
          {
            'relation.relation.name': {
              equals: 'third',
            },
          },
          {
            'relation.relation.name': {
              equals: 'third',
            },
          },
          {
            'relation.relation.name': {
              equals: 'third',
            },
          },
          {
            'relation.relation.name': {
              equals: 'third',
            },
          },
          {
            'relation.relation.name': {
              equals: 'third',
            },
          },
        ],
      },
    })
  }

  payload.logger.info(`#${i + 1} ${Date.now() - now}`)
}
payload.logger.info(`Total ${Date.now() - now}`)
```

Before:
```
[02:11:48] INFO: #1 3682
[02:11:50] INFO: #2 2199
[02:11:54] INFO: #3 3483
[02:11:56] INFO: #4 2516
[02:11:59] INFO: #5 2467
[02:12:01] INFO: #6 1987
[02:12:03] INFO: #7 1986
[02:12:05] INFO: #8 2375
[02:12:07] INFO: #9 2040
[02:12:09] INFO: #10 1920
    [PASS] Relationships > Querying > Nested Querying > should allow querying two levels deep (24667ms)
[02:12:09] INFO: Total 24657
```

After:
```
[02:12:36] INFO: #1 2113
[02:12:38] INFO: #2 1854
[02:12:40] INFO: #3 1700
[02:12:42] INFO: #4 1797
[02:12:44] INFO: #5 2121
[02:12:46] INFO: #6 1774
[02:12:47] INFO: #7 1670
[02:12:49] INFO: #8 1610
[02:12:50] INFO: #9 1596
[02:12:52] INFO: #10 1576
    [PASS] Relationships > Querying > Nested Querying > should allow querying two levels deep (17828ms)
[02:12:52] INFO: Total 17818
```
2024-11-25 10:28:07 -05:00
Elliot DeNolf
8658945d7b chore(templates): remove unneeded lock files, add hook (#9508)
- Update lock files for blank, website
- Delete unneeded lock files
- Adds git hook to ensure no new lockfiles are added for _other than_
blank and website.
2024-11-25 10:04:41 -05:00
Sasha
aa1d300062 feat(templates): website template performance improvements (#9466)
- Uses `pagination: false` where we don't need `totalDocs`.
- in `preview/route.ts` uses `depth: 0`, select of only ID to improve
performance
- in `search` uses `select` to select only needed properties
- adds type safety best practices to collection configs with
`defaultPopulate`
- uses `payload.count` to resolve SSG `pageNumber`s
2024-11-25 10:02:27 -05:00
Sasha
150c55de79 chore(next): remove deep copying of docPermissions in the Version View (#9491)
Removes unnecessary `deepCopyObject(docPermissions)` in the Version View
which slows down loading speed.
The comment seems to be resolved, I'm not getting this error and here
for example in the same case
3c0e832a9a/packages/next/src/views/Document/index.tsx (L327)
we don't do deep copying.
2024-11-25 09:20:22 -05:00
Jeffrey Arts
4b4cfbeca7 docs: add migration-guide note public dir changes (#9498)
The location of public directory has changed, this addition highlights
this change and how to resolve it
2024-11-25 09:17:34 -05:00
Sasha
7eb388d403 fix: ensure deleteJobOnComplete property for jobs works (#9283)
Ensures that the `deleteJobOnComplete` (which is `true` by default)
property works properly
2024-11-25 09:11:15 -05:00
Harley Salas
07c76aa3b9 chore(translations): improve russian translation for noResults (#9496)
### What?
The "noResults" translation key, for Russian, which is displayed when
searching a collection list and receiving no results.

![image](https://github.com/user-attachments/assets/b83204c9-0467-42e4-9f38-5a38548e5459)


### Why?
Unlike English, Slavic languages like Russian have the concept of
genders and depending on the ending of a particular word, the endings of
adjectives can be different, to correspond with those genders. The
current version only works with feminine words, directly translating to
"No {{label}} found. Either {{label}} doesn't exist yet, or none of them
match the filters you specified above."
The new version translates to "Nothing found. {{label}} may not exist
yet or doesn't match the specified filters.", which is a more loose
translation, but holds the same meaning, while being grammatically
correct in all scenarios, regardless of the gender.
2024-11-25 12:37:34 +02:00
Said Akhrarov
9c59359da6 docs: fix invalid links (#9500)
### What?
This PR fixes a variety of links around the docs.

### Why?
To link readers to the correct location in the docs

### How?
Changes and fixes to a number of doc links.
2024-11-24 19:18:33 -07:00
Indy
3c0e832a9a docs: spelling error in README.md (#9478)
## Title

fix(docs): correct "kayout" typo to "layout" in README.md

### What?
- Corrected a typo in the `README.md` file where "kayout" was
incorrectly written instead of "layout".

### Why?
- To improve the documentation's readability and professionalism.

### How?
- Located the typo in `README.md` and replaced "kayout" with "layout".

Fixes #9472

----


### Before
<img width="1148" alt="before"
src="https://github.com/user-attachments/assets/232d0e01-ea06-4568-b283-afad09719f0c">

### After
<img width="1208" alt="after"
src="https://github.com/user-attachments/assets/f96f3b6c-6ccc-4d2e-8de3-88b9057a6cab">

---------

Co-authored-by: Indy S <billabong@Mac.attlocal.net>
2024-11-24 08:30:18 +00:00
361 changed files with 18448 additions and 38442 deletions

View File

@@ -21,6 +21,7 @@ updates:
- package-ecosystem: npm
directory: /
target-branch: main
open-pull-requests-limit: 0 # Only allow security updates
schedule:
interval: weekly
day: sunday
@@ -38,8 +39,6 @@ updates:
- patch
patterns:
- '*'
exclude-patterns:
- 'drizzle*'
dev-deps:
dependency-type: development
update-types:
@@ -47,13 +46,11 @@ updates:
- patch
patterns:
- '*'
exclude-patterns:
- 'drizzle*'
# Only bump patch versions for 2.x
- package-ecosystem: npm
directory: /
target-branch: 2.x
open-pull-requests-limit: 0 # Only allow security updates
schedule:
interval: weekly
day: sunday
@@ -70,5 +67,3 @@ updates:
- patch
patterns:
- '*'
exclude-patterns:
- 'drizzle*'

View File

@@ -2,8 +2,6 @@ name: release-canary
on:
workflow_dispatch:
branches:
- main
env:
NODE_VERSION: 22.6.0
@@ -13,6 +11,7 @@ env:
jobs:
release:
name: release-canary-${{ github.ref_name }}-${{ github.sha }}
permissions:
id-token: write
runs-on: ubuntu-latest

View File

@@ -69,7 +69,7 @@ We're constantly adding more templates to our [Templates Directory](https://gith
- [Auth out of the box](https://payloadcms.com/docs/authentication/overview)
- [Versions and drafts](https://payloadcms.com/docs/versions/overview)
- [Localization](https://payloadcms.com/docs/configuration/localization)
- [Block-based kayout builder](https://payloadcms.com/docs/fields/blocks)
- [Block-based layout builder](https://payloadcms.com/docs/fields/blocks)
- [Customizable React admin](https://payloadcms.com/docs/admin/overview)
- [Lexical rich text editor](https://payloadcms.com/docs/fields/rich-text)
- [Conditional field logic](https://payloadcms.com/docs/fields/overview#conditional-logic)

View File

@@ -315,7 +315,7 @@ The following arguments are provided to the `unlock` function:
If the Collection has [Versions](../versions/overview) enabled, the `readVersions` Access Control function determines whether or not the currently logged in user can access the version history of a Document.
To add Read Versions Access Control to a Collection, use the `readVersions` property in the [Collection Config](../collections/overview):
To add Read Versions Access Control to a Collection, use the `readVersions` property in the [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'

View File

@@ -47,7 +47,7 @@ Payload automatically creates an internally used `payload-preferences` Collectio
## APIs
Preferences are available to both [GraphQL](/docs/graphql/overview#preferences) and [REST](/docs/rest-api/overview#) APIs.
Preferences are available to both [GraphQL](/docs/graphql/overview#preferences) and [REST](/docs/rest-api/overview#preferences) APIs.
## Adding or reading Preferences in your own components

View File

@@ -273,7 +273,7 @@ const result = await payload.verifyEmail({
If a user locks themselves out and you wish to deliberately unlock them, you can utilize the Unlock operation. The [Admin Panel](../admin/overview) features an Unlock control automatically for all collections that feature max login attempts, but you can programmatically unlock users as well by using the Unlock operation.
To restrict who is allowed to unlock users, you can utilize the [`unlock`](../access-control/overview#unlock) access control function.
To restrict who is allowed to unlock users, you can utilize the [`unlock`](../access-control/collections#unlock) access control function.
**Example REST API unlock**:

View File

@@ -22,7 +22,7 @@ Here are some common use cases of Authentication in your own applications:
When Authentication is enabled on a [Collection](../configuration/collections), Payload injects all necessary functionality to support the entire user flow. This includes all [auth-related operations](./operations) like account creation, logging in and out, and resetting passwords, all [auth-related emails](./email) like email verification and password reset, as well as any necessary UI to manage users from the Admin Panel.
To enable Authentication on a Collection, use the `auth` property in the [Collection Config](../configuration/collection#auth):
To enable Authentication on a Collection, use the `auth` property in the [Collection Config](../configuration/collections#config-options):
```ts
import type { CollectionConfig } from 'payload'

View File

@@ -46,6 +46,6 @@ _Creating a new project from an existing repository._
<Banner type="warning">
<strong>Note:</strong> In order to make use of the features of Payload Cloud in your own codebase,
you will need to add the [Cloud Plugin](https://github.com/payloadcms/plugin-cloud) to your
you will need to add the [Cloud Plugin](https://github.com/payloadcms/payload/tree/main/packages/payload-cloud) to your
Payload app.
</Banner>

View File

@@ -28,7 +28,7 @@ Your Payload Cloud project comes with a MongoDB serverless Atlas DB instance or
Payload Cloud gives you S3 file storage backed by Cloudflare as a CDN, and this plugin extends Payload so that all of your media will be stored in S3 rather than locally.
AWS Cognito is used for authentication to your S3 bucket. The [Payload Cloud Plugin](https://github.com/payloadcms/plugin-cloud) will automatically pick up these values. These values are only if you'd like to access your files directly, outside of Payload Cloud.
AWS Cognito is used for authentication to your S3 bucket. The [Payload Cloud Plugin](https://github.com/payloadcms/payload/tree/main/packages/payload-cloud) will automatically pick up these values. These values are only if you'd like to access your files directly, outside of Payload Cloud.
### Accessing Files Outside of Payload Cloud

View File

@@ -64,7 +64,7 @@ export default buildConfig({
The following options are available:
| Option | Description |
|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
| **`bin`** | Register custom bin scripts for Payload to execute. |
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
@@ -83,6 +83,7 @@ The following options are available:
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
| **`maxCallDepth`** | The maximum allowed call depth for Local API operations. This setting helps prevent against hooks that lead to infinity loops. Can be disabled with passing `false`. Defaults to `30`. |
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). |

View File

@@ -108,7 +108,7 @@ called with an argument object with the following properties:
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation |
| `user` | An object containing the currently authenticated user |
## Example
### Example#filter-options-example
```ts
const uploadField = {

View File

@@ -77,7 +77,7 @@ export const MyFeature = createServerFeature({
This allows you to add i18n translations scoped to your feature. This specific example translation will be available under `lexical:myFeature:label` - `myFeature` being your feature key.
### Markdown Transformers
### Markdown Transformers#server-feature-markdown-transformers
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/lexical/converters#markdown-lexical).
@@ -120,7 +120,7 @@ export const MyFeature = createServerFeature({
In this example, the node will be outputted as `+++` in Markdown, and the markdown `+++` will be converted to a `MyNode` node in the editor.
### Nodes
### Nodes#server-feature-nodes
While nodes added to the server feature do not control how the node is rendered in the editor, they control other aspects of the node:
- HTML conversion
@@ -266,7 +266,7 @@ export const MyClientFeature = createClientFeature({
Explore the APIs available through ClientFeature to add the specific functionality you need. Remember, do not import directly from `'@payloadcms/richtext-lexical'` when working on the client-side, as it will cause errors with webpack or turbopack. Instead, use `'@payloadcms/richtext-lexical/client'` for all client-side imports. Type-imports are excluded from this rule and can always be imported.
### Nodes
### Nodes#client-feature-nodes
Add nodes to the `nodes` array in **both** your client & server feature. On the server side, nodes are utilized for backend operations like HTML conversion in a headless editor. On the client side, these nodes are integral to how content is displayed and managed in the editor, influencing how they are rendered, behave, and saved in the database.
@@ -705,7 +705,7 @@ export const MyClientFeature = createClientFeature({
| **`keywords`** | Keywords are used to match the item for different texts typed after the '/'. E.g. you might want to show a horizontal rule item if you type both /hr, /separator, /horizontal etc. In addition to the keywords, the label and key will be used to find the right slash menu item. |
### Markdown Transformers
### Markdown Transformers#client-feature-markdown-transformers
The Client Feature, just like the Server Feature, allows you to add markdown transformers. Markdown transformers on the client are used to create new nodes when a certain markdown pattern is typed in the editor.

View File

@@ -102,7 +102,7 @@ const post = await payload.find({
The following Collection operations are available through the Local API:
### Create
### Create#collection-create
```js
// The created Post document is returned
@@ -134,7 +134,7 @@ const post = await payload.create({
})
```
### Find
### Find#collection-find
```js
// Result will be a paginated set of Posts.
@@ -155,7 +155,7 @@ const result = await payload.find({
})
```
### Find by ID
### Find by ID#collection-find-by-id
```js
// Result will be a Post document.
@@ -171,7 +171,7 @@ const result = await payload.findByID({
})
```
### Count
### Count#collection-count
```js
// Result will be an object with:
@@ -187,7 +187,7 @@ const result = await payload.count({
})
```
### Update by ID
### Update by ID#collection-update-by-id
```js
// Result will be the updated Post document.
@@ -219,7 +219,7 @@ const result = await payload.update({
})
```
### Update Many
### Update Many#collection-update-many
```js
// Result will be an object with:
@@ -258,7 +258,7 @@ const result = await payload.update({
})
```
### Delete
### Delete#collection-delete
```js
// Result will be the now-deleted Post document.
@@ -275,7 +275,7 @@ const result = await payload.delete({
})
```
### Delete Many
### Delete Many#collection-delete-many
```js
// Result will be an object with:
@@ -394,7 +394,7 @@ const result = await payload.verifyEmail({
The following Global operations are available through the Local API:
### Find
### Find#global-find
```js
// Result will be the Header Global.
@@ -409,7 +409,7 @@ const result = await payload.findGlobal({
})
```
### Update
### Update#global-update
```js
// Result will be the updated Header Global.

View File

@@ -409,6 +409,7 @@ For more details, see the [Documentation](https://payloadcms.com/docs/getting-st
}
})
```
1. The `./src/public` directory is now located directly at root level `./public` [see nextJS docs for details](https://nextjs.org/docs/pages/building-your-application/optimizing/static-assets)
## Custom Components

View File

@@ -103,7 +103,7 @@ formBuilder({
### `beforeEmail`
The `beforeEmail` property is a [beforeChange](<[beforeChange](https://payloadcms.com/docs/hooks/globals#beforechange)>) hook that is called just after emails are prepared, but before they are sent. This is a great place to inject your own HTML template to add custom styles.
The `beforeEmail` property is a [beforeChange](https://payloadcms.com/docs/hooks/globals#beforechange) hook that is called just after emails are prepared, but before they are sent. This is a great place to inject your own HTML template to add custom styles.
```ts
// payload.config.ts
@@ -215,7 +215,7 @@ formBuilder({
### `handlePayment`
The `handlePayment` property is a [beforeChange](<[beforeChange](https://payloadcms.com/docs/hooks/globals#beforechange)>) hook that is called upon form submission. You can integrate into any third-party payment processing API here to accept payment based on form input. You can use the `getPaymentTotal` function to calculate the total cost after all conditions have been applied. This is only applicable if the form has enabled the `payment` field.
The `handlePayment` property is a [beforeChange](https://payloadcms.com/docs/hooks/globals#beforechange) hook that is called upon form submission. You can integrate into any third-party payment processing API here to accept payment based on form input. You can use the `getPaymentTotal` function to calculate the total cost after all conditions have been applied. This is only applicable if the form has enabled the `payment` field.
First import the utility function. This will execute all of the price conditions that you have set in your form's `payment` field and returns the total price.

View File

@@ -102,8 +102,8 @@ level and stores the following fields.
| Field | Description |
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | The label of the breadcrumb. This field is automatically set to either the `collection.admin.useAsTitle` (if defined) or is set to the `ID` of the document. You can also dynamically define the `label` by passing a function to the options property of [`generateLabel`](#generateLabel). |
| `url` | The URL of the breadcrumb. By default, this field is undefined. You can manually define this field by passing a property called function to the plugin options property of [`generateURL`](#generateURL). |
| `label` | The label of the breadcrumb. This field is automatically set to either the `collection.admin.useAsTitle` (if defined) or is set to the `ID` of the document. You can also dynamically define the `label` by passing a function to the options property of [`generateLabel`](#generatelabel). |
| `url` | The URL of the breadcrumb. By default, this field is undefined. You can manually define this field by passing a property called function to the plugin options property of [`generateURL`](#generateurl). |
### Options
@@ -226,7 +226,7 @@ const examplePageConfig: CollectionConfig = {
This plugin supports localization by default. If the `localization` property is set in your Payload Config,
the `breadcrumbs` field is automatically localized. For more details on how localization works in Payload, see
the [Localization](https://payloadcms.com/docs/localization/overview) docs.
the [Localization](https://payloadcms.com/docs/configuration/localization) docs.
## TypeScript

View File

@@ -8,7 +8,7 @@ keywords: plugins, config, configuration, extensions, custom, documentation, Con
Payload Plugins take full advantage of the modularity of the [Payload Config](../configuration/overview), allowing developers developers to easily inject custom—sometimes complex—functionality into Payload apps from a very small touch-point. This is especially useful is sharing your work across multiple projects or with the greater Payload community.
There are many [Official Plugins](#official-plugins) available that solve for some of the most common uses cases, such as the [Form Builder Plugin](./seo) or [SEO Plugin](./seo). There are also [Community Plugins](#community-plugins) available, maintained entirely by contributing members. To extend Payload's functionality in some other way, you can easily [build your own plugin](./build-your-own).
There are many [Official Plugins](#official-plugins) available that solve for some of the most common uses cases, such as the [Form Builder Plugin](./form-builder) or [SEO Plugin](./seo). There are also [Community Plugins](#community-plugins) available, maintained entirely by contributing members. To extend Payload's functionality in some other way, you can easily [build your own plugin](./build-your-own).
To configure Plugins, use the `plugins` property in your [Payload Config](../configuration/overview):
@@ -68,7 +68,7 @@ Plugins are changing every day, so be sure to check back often to see what new p
Community Plugins are those that are maintained entirely by outside contributors. They are a great way to share your work across the ecosystem for others to use. You can discover Community Plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin).
Some plugins have become so widely used that they are adopted as an [Official Plugin](#official-plugin), such as the [Lexical Plugin](https://github.com/AlessioGr/payload-plugin-lexical). If you have a plugin that you think should be an Official Plugin, please feel free to start a new [Discussion](https://github.com/payloadcms/payload/discussions).
Some plugins have become so widely used that they are adopted as an [Official Plugin](#official-plugins), such as the [Lexical Plugin](https://github.com/AlessioGr/payload-plugin-lexical). If you have a plugin that you think should be an Official Plugin, please feel free to start a new [Discussion](https://github.com/payloadcms/payload/discussions).
<Banner type="warning">
For maintainers building plugins for others to use, please add the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin) to help others find it.

View File

@@ -134,7 +134,7 @@ Note that the `fields` property is a function that receives an object with a `de
#### `beforeSync`
Before creating or updating a search record, the `beforeSync` function runs. This is an [afterChange](<[afterChange](https://payloadcms.com/docs/hooks/globals#afterchange)>) hook that allows you to modify the data or provide fallbacks before its search record is created or updated.
Before creating or updating a search record, the `beforeSync` function runs. This is an [afterChange](https://payloadcms.com/docs/hooks/globals#afterchange) hook that allows you to modify the data or provide fallbacks before its search record is created or updated.
```ts
// payload.config.ts

View File

@@ -171,7 +171,7 @@ A function that allows you to return any meta description, including from docume
}
```
For a full list of arguments, see the [`generateTitle`](#generateTitle) function.
For a full list of arguments, see the [`generateTitle`](#generatetitle) function.
##### `generateImage`
@@ -187,7 +187,7 @@ A function that allows you to return any meta image, including from document's c
}
```
For a full list of arguments, see the [`generateTitle`](#generateTitle) function.
For a full list of arguments, see the [`generateTitle`](#generatetitle) function.
##### `generateURL`
@@ -204,7 +204,7 @@ A function called by the search preview component to display the actual URL of y
}
```
For a full list of arguments, see the [`generateTitle`](#generateTitle) function.
For a full list of arguments, see the [`generateTitle`](#generatetitle) function.
#### `interfaceName`

View File

@@ -8,7 +8,7 @@ keywords: plugins, stripe, payments, ecommerce
[![NPM](https://img.shields.io/npm/v/@payloadcms/plugin-stripe)](https://www.npmjs.com/package/@payloadcms/plugin-stripe)
With this plugin you can easily integrate [Stripe](https://stripe.com) into Payload. Simply provide your Stripe credentials and this plugin will open up a two-way communication channel between the two platforms. This enables you to easily sync data back and forth, as well as proxy the Stripe REST API through Payload's [Access Control](../access-control). Use this plugin to completely offload billing to Stripe and retain full control over your application's data.
With this plugin you can easily integrate [Stripe](https://stripe.com) into Payload. Simply provide your Stripe credentials and this plugin will open up a two-way communication channel between the two platforms. This enables you to easily sync data back and forth, as well as proxy the Stripe REST API through Payload's [Access Control](../access-control/overview). Use this plugin to completely offload billing to Stripe and retain full control over your application's data.
For example, you might be building an e-commerce or SaaS application, where you have a `products` or a `plans` collection that requires either a one-time payment or a subscription. You can to tie each of these products to Stripe, then easily subscribe to billing-related events to perform your application's business logic, such as active purchases or subscription cancellations.

View File

@@ -501,7 +501,7 @@ Globals cannot be created or deleted, so there are only two REST endpoints opene
## Preferences
In addition to the dynamically generated endpoints above Payload also has REST endpoints to manage the admin user [preferences](/docs/admin/overview#preferences) for data specific to the authenticated user.
In addition to the dynamically generated endpoints above Payload also has REST endpoints to manage the admin user [preferences](/docs/admin/preferences) for data specific to the authenticated user.
<RestExamples
data={[

View File

@@ -14,18 +14,18 @@ Payload offers additional storage adapters to handle file uploads. These adapter
| AWS S3 | [`@payloadcms/storage-s3`](https://github.com/payloadcms/payload/tree/main/packages/storage-s3) |
| Azure | [`@payloadcms/storage-azure`](https://github.com/payloadcms/payload/tree/main/packages/storage-azure) |
| Google Cloud Storage | [`@payloadcms/storage-gcs`](https://github.com/payloadcms/payload/tree/main/packages/storage-gcs) |
| Uploadthing | [`@payloadcms/storage-uploadthing`](https://github.com/payloadcms/payload/tree/main/packages/uploadthing) |
| Uploadthing | [`@payloadcms/storage-uploadthing`](https://github.com/payloadcms/payload/tree/main/packages/storage-uploadthing) |
## Vercel Blob Storage
[`@payloadcms/storage-vercel-blob`](https://www.npmjs.com/package/@payloadcms/storage-vercel-blob)
### Installation
### Installation#vercel-blob-installation
```sh
pnpm add @payloadcms/storage-vercel-blob
```
### Usage
### Usage#vercel-blob-usage
- Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs.
- Ensure you have `BLOB_READ_WRITE_TOKEN` set in your Vercel environment variables. This is usually set by Vercel automatically after adding blob storage to your project.
@@ -55,7 +55,7 @@ export default buildConfig({
})
```
### Configuration Options
### Configuration Options#vercel-blob-configuration
| Option | Description | Default |
| -------------------- | -------------------------------------------------------------------- | ----------------------------- |
@@ -68,13 +68,13 @@ export default buildConfig({
## S3 Storage
[`@payloadcms/storage-s3`](https://www.npmjs.com/package/@payloadcms/storage-s3)
### Installation
### Installation#s3-installation
```sh
pnpm add @payloadcms/storage-s3
```
### Usage
### Usage#s3-usage
- Configure the `collections` object to specify which collections should use the S3 Storage adapter. The slug _must_ match one of your existing collection slugs.
- The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information.
@@ -109,20 +109,20 @@ export default buildConfig({
})
```
#### Configuration Options
### Configuration Options#s3-configuration
See the the [AWS SDK Package](https://github.com/aws/aws-sdk-js-v3) and [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object for guidance on AWS S3 configuration.
## Azure Blob Storage
[`@payloadcms/storage-azure`](https://www.npmjs.com/package/@payloadcms/storage-azure)
### Installation
### Installation#azure-installation
```sh
pnpm add @payloadcms/storage-azure
```
### Usage
### Usage#azure-usage
- Configure the `collections` object to specify which collections should use the Azure Blob adapter. The slug _must_ match one of your existing collection slugs.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
@@ -151,7 +151,7 @@ export default buildConfig({
})
```
### Configuration Options
### Configuration Options#azure-configuration
| Option | Description | Default |
| ---------------------- | ------------------------------------------------------------------------ | ------- |
@@ -165,13 +165,13 @@ export default buildConfig({
## Google Cloud Storage
[`@payloadcms/storage-gcs`](https://www.npmjs.com/package/@payloadcms/storage-gcs)
### Installation
### Installation#gcs-installation
```sh
pnpm add @payloadcms/storage-gcs
```
### Usage
### Usage#gcs-usage
- Configure the `collections` object to specify which collections should use the Google Cloud Storage adapter. The slug _must_ match one of your existing collection slugs.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
@@ -201,7 +201,7 @@ export default buildConfig({
})
```
### Configuration Options
### Configuration Options#gcs-configuration
| Option | Description | Default |
| ------------- | --------------------------------------------------------------------------------------------------- | --------- |
@@ -215,13 +215,13 @@ export default buildConfig({
## Uploadthing Storage
[`@payloadcms/storage-uploadthing`](https://www.npmjs.com/package/@payloadcms/storage-uploadthing)
### Installation
### Installation#uploadthing-installation
```sh
pnpm add @payloadcms/storage-uploadthing
```
### Usage
### Usage#uploadthing-usage
- Configure the `collections` object to specify which collections should use uploadthing. The slug _must_ match one of your existing collection slugs and be an `upload` type.
- Get a token from Uploadthing and set it as `token` in the `options` object.
@@ -244,7 +244,7 @@ export default buildConfig({
})
```
### Configuration Options
### Configuration Options#uploadthing-configuration
| Option | Description | Default |
| ---------------- | ----------------------------------------------- | ------------- |
@@ -259,11 +259,11 @@ export default buildConfig({
If you need to create a custom storage adapter, you can use the [`@payloadcms/plugin-cloud-storage`](https://www.npmjs.com/package/@payloadcms/plugin-cloud-storage) package. This package is used internally by the storage adapters mentioned above.
### Installation
### Installation#custom-installation
`pnpm add @payloadcms/plugin-cloud-storage`
### Usage
### Usage#custom-usage
Reference any of the existing storage adapters for guidance on how this should be structured. Create an adapter following the `GeneratedAdapter` interface. Then, pass the adapter to the `cloudStorage` plugin.

View File

@@ -13,9 +13,15 @@ First you'll need a running Payload app. There is one made explicitly for this e
### Next.js
1. Clone this repo
2. `cd` into this directory and run `yarn` or `npm install`
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
5. `open http://localhost:3001` to see the result
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload) for full details.

View File

@@ -1,7 +1,7 @@
import { draftMode } from 'next/headers'
import { notFound } from 'next/navigation'
import { Page } from '../../payload-types'
import type { Page as PageType } from '../../payload-types'
import { fetchPage } from '../_api/fetchPage'
import { fetchPages } from '../_api/fetchPages'
import { Gutter } from '../_components/Gutter'
@@ -10,10 +10,12 @@ import RichText from '../_components/RichText'
import classes from './index.module.scss'
interface PageParams {
params: { slug: string }
params: Promise<{
slug?: string
}>
}
export const PageTemplate: React.FC<{ page: Page | null | undefined }> = ({ page }) => (
export const PageTemplate: React.FC<{ page: null | PageType | undefined }> = ({ page }) => (
<main className={classes.page}>
<Gutter>
<h1>{page?.title}</h1>
@@ -22,8 +24,10 @@ export const PageTemplate: React.FC<{ page: Page | null | undefined }> = ({ page
</main>
)
export default async function Page({ params: { slug = 'home' } }: PageParams) {
const { isEnabled: isDraftMode } = draftMode()
export default async function Page({ params }: PageParams) {
const { slug = 'home' } = await params
const { isEnabled: isDraftMode } = await draftMode()
const page = await fetchPage(slug, isDraftMode)

View File

@@ -5,12 +5,12 @@ import type { Page } from '../../payload-types'
export const fetchPage = async (
slug: string,
draft?: boolean,
): Promise<Page | undefined | null> => {
): Promise<null | Page | undefined> => {
let payloadToken: RequestCookie | undefined
if (draft) {
const { cookies } = await import('next/headers')
payloadToken = cookies().get('payload-token')
payloadToken = (await cookies()).get('payload-token')
}
const pageRes: {

View File

@@ -1,7 +1,8 @@
'use client'
import React, { useState } from 'react'
import { PayloadAdminBar, PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
import type { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
import { PayloadAdminBar } from 'payload-admin-bar'
import { Gutter } from '../Gutter'
@@ -17,24 +18,24 @@ export const AdminBarClient: React.FC<PayloadAdminBarProps> = (props) => {
<Gutter className={classes.container}>
<PayloadAdminBar
{...props}
logo={<Title />}
className={classes.payloadAdminBar}
classNames={{
controls: classes.controls,
logo: classes.logo,
user: classes.user,
}}
cmsURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
logo={<Title />}
onAuthChange={setUser}
onPreviewExit={async () => {
await fetch(`/api/exit-preview`)
window.location.reload()
}}
onAuthChange={setUser}
className={classes.payloadAdminBar}
classNames={{
user: classes.user,
logo: classes.logo,
controls: classes.controls,
}}
style={{
backgroundColor: 'transparent',
padding: 0,
position: 'relative',
zIndex: 'unset',
padding: 0,
backgroundColor: 'transparent',
}}
/>
</Gutter>

View File

@@ -3,14 +3,14 @@ import { draftMode } from 'next/headers'
import { AdminBarClient } from './index.client'
export function AdminBar() {
const { isEnabled: isPreviewMode } = draftMode()
export async function AdminBar() {
const { isEnabled: isPreviewMode } = await draftMode()
return (
<AdminBarClient
preview={isPreviewMode}
// id={page?.id} // TODO: is there any way to do this?!
collection="pages"
preview={isPreviewMode}
/>
)
}

View File

@@ -1,33 +1,34 @@
import React, { ElementType } from 'react'
import type { ElementType } from 'react'
import React from 'react'
import Link from 'next/link'
import classes from './index.module.scss'
export type Props = {
label?: string
appearance?: 'default' | 'primary' | 'secondary'
el?: 'button' | 'link' | 'a'
onClick?: () => void
href?: string
newTab?: boolean
className?: string
type?: 'submit' | 'button'
disabled?: boolean
el?: 'a' | 'button' | 'link'
href?: string
label?: string
newTab?: boolean | null
onClick?: () => void
type?: 'button' | 'submit'
}
export const Button: React.FC<Props> = ({
el: elFromProps = 'link',
label,
newTab,
href,
type = 'button',
appearance,
className: classNameFromProps,
onClick,
type = 'button',
disabled,
el: elFromProps = 'link',
href,
label,
newTab,
onClick,
}) => {
let el = elFromProps
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
const className = [
classes.button,
classNameFromProps,
@@ -44,11 +45,13 @@ export const Button: React.FC<Props> = ({
</div>
)
if (onClick || type === 'submit') el = 'button'
if (onClick || type === 'submit') {
el = 'button'
}
if (el === 'link') {
return (
<Link href={href || ''} className={className} {...newTabProps} onClick={onClick}>
<Link className={className} href={href || ''} {...newTabProps} onClick={onClick}>
{content}
</Link>
)
@@ -58,12 +61,12 @@ export const Button: React.FC<Props> = ({
return (
<Element
href={href}
className={className}
href={href}
type={type}
{...newTabProps}
onClick={onClick}
disabled={disabled}
onClick={onClick}
>
{content}
</Element>

View File

@@ -1,46 +1,52 @@
import React from 'react'
import Link from 'next/link'
import { Page } from '../../../payload-types'
import type { Page } from '../../../payload-types'
import { Button } from '../Button'
export type CMSLinkType = {
type?: 'custom' | 'reference'
url?: string
newTab?: boolean
reference?: {
value: string | Page
relationTo: 'pages'
}
label?: string
appearance?: 'default' | 'primary' | 'secondary'
children?: React.ReactNode
className?: string
label?: string
newTab?: boolean | null
reference?: {
relationTo: 'pages'
value: number | Page | string
} | null
type?: 'custom' | 'reference' | null
url?: null | string
}
export const CMSLink: React.FC<CMSLinkType> = ({
type,
url,
newTab,
reference,
label,
appearance,
children,
className,
label,
newTab,
reference,
url,
}) => {
const href =
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
? `/${reference.value.slug === 'home' ? '' : reference.value.slug}`
? `${reference?.relationTo !== 'pages' ? `/${reference?.relationTo}` : ''}/${
reference.value.slug
}`
: url
if (!href) {
return null
}
if (!appearance) {
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
if (type === 'custom') {
return (
<a href={url} {...newTabProps} className={className}>
<a href={url || ''} {...newTabProps} className={className}>
{label && label}
{children && children}
{children ? <>{children}</> : null}
</a>
)
}
@@ -49,17 +55,17 @@ export const CMSLink: React.FC<CMSLinkType> = ({
return (
<Link href={href} {...newTabProps} className={className} prefetch={false}>
{label && label}
{children && children}
{children ? <>{children}</> : null}
</Link>
)
}
}
const buttonProps = {
newTab,
href,
appearance,
href,
label,
newTab,
}
return <Button className={className} {...buttonProps} el="link" />

View File

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

View File

@@ -1,11 +1,11 @@
import React from 'react'
import Image from 'next/image'
import Link from 'next/link'
import React from 'react'
import type { MainMenu } from '../../../payload-types'
import { CMSLink } from '../CMSLink'
import { Gutter } from '../Gutter'
import classes from './index.module.scss'
export async function Header() {

View File

@@ -6,14 +6,14 @@ import { Text } from 'slate'
type Children = Leaf[]
type Leaf = {
type: string
value?: {
url: string
alt: string
}
children: Children
url?: string
[key: string]: unknown
children: Children
type: string
url?: string
value?: {
alt: string
url: string
}
}
const serialize = (children: Children): React.ReactNode[] =>
@@ -35,7 +35,7 @@ const serialize = (children: Children): React.ReactNode[] =>
if (node.underline) {
text = (
<span style={{ textDecoration: 'underline' }} key={i}>
<span key={i} style={{ textDecoration: 'underline' }}>
{text}
</span>
)
@@ -43,7 +43,7 @@ const serialize = (children: Children): React.ReactNode[] =>
if (node.strikethrough) {
text = (
<span style={{ textDecoration: 'line-through' }} key={i}>
<span key={i} style={{ textDecoration: 'line-through' }}>
{text}
</span>
)
@@ -57,6 +57,8 @@ const serialize = (children: Children): React.ReactNode[] =>
}
switch (node.type) {
case 'blockquote':
return <blockquote key={i}>{serialize(node.children)}</blockquote>
case 'h1':
return <h1 key={i}>{serialize(node.children)}</h1>
case 'h2':
@@ -69,12 +71,6 @@ const serialize = (children: Children): React.ReactNode[] =>
return <h5 key={i}>{serialize(node.children)}</h5>
case 'h6':
return <h6 key={i}>{serialize(node.children)}</h6>
case 'blockquote':
return <blockquote key={i}>{serialize(node.children)}</blockquote>
case 'ul':
return <ul key={i}>{serialize(node.children)}</ul>
case 'ol':
return <ol key={i}>{serialize(node.children)}</ol>
case 'li':
return <li key={i}>{serialize(node.children)}</li>
case 'link':
@@ -83,6 +79,10 @@ const serialize = (children: Children): React.ReactNode[] =>
{serialize(node.children)}
</a>
)
case 'ol':
return <ol key={i}>{serialize(node.children)}</ol>
case 'ul':
return <ul key={i}>{serialize(node.children)}</ul>
default:
return <p key={i}>{serialize(node.children)}</p>

View File

@@ -1,6 +1,7 @@
import { draftMode } from 'next/headers'
export async function GET(): Promise<Response> {
draftMode().disable()
const draft = await draftMode()
draft.disable()
return new Response('Draft mode is disabled')
}

View File

@@ -2,13 +2,13 @@ import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET(
req: Request & {
req: {
cookies: {
get: (name: string) => {
value: string
}
}
},
} & Request,
): Promise<Response> {
const payloadToken = req.cookies.get('payload-token')?.value
const { searchParams } = new URL(req.url)
@@ -32,8 +32,10 @@ export async function GET(
const userRes = await userReq.json()
const draft = await draftMode()
if (!userReq.ok || !userRes?.user) {
draftMode().disable()
draft.disable()
return new Response('You are not allowed to preview this page', { status: 403 })
}
@@ -41,7 +43,7 @@ export async function GET(
return new Response('Invalid token', { status: 401 })
}
draftMode().enable()
draft.enable()
redirect(url)
}

View File

@@ -5,19 +5,20 @@ import { NextResponse } from 'next/server'
// this endpoint will revalidate a page by tag or path
// this is to achieve on-demand revalidation of pages that use this data
// send either `collection` and `slug` or `revalidatePath` as query params
export async function GET(request: NextRequest): Promise<unknown> {
/* eslint-disable @typescript-eslint/require-await */
export async function GET(request: NextRequest): Promise<Response> {
const collection = request.nextUrl.searchParams.get('collection')
const slug = request.nextUrl.searchParams.get('slug')
const path = request.nextUrl.searchParams.get('path')
const secret = request.nextUrl.searchParams.get('secret')
if (secret !== process.env.NEXT_PRIVATE_REVALIDATION_KEY) {
return NextResponse.json({ revalidated: false, now: Date.now() })
return NextResponse.json({ now: Date.now(), revalidated: false })
}
if (typeof collection === 'string' && typeof slug === 'string') {
revalidateTag(`${collection}_${slug}`)
return NextResponse.json({ revalidated: true, now: Date.now() })
return NextResponse.json({ now: Date.now(), revalidated: true })
}
// there is a known limitation with `revalidatePath` where it will not revalidate exact paths of dynamic routes
@@ -27,8 +28,8 @@ export async function GET(request: NextRequest): Promise<unknown> {
// - https://github.com/vercel/next.js/issues/49778#issuecomment-1547028830
if (typeof path === 'string') {
revalidatePath(path)
return NextResponse.json({ revalidated: true, now: Date.now() })
return NextResponse.json({ now: Date.now(), revalidated: true })
}
return NextResponse.json({ revalidated: false, now: Date.now() })
return NextResponse.json({ now: Date.now(), revalidated: false })
}

View File

@@ -4,10 +4,11 @@ import { Header } from './_components/Header'
import './app.scss'
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
title: 'Create Next App',
}
// eslint-disable-next-line @typescript-eslint/require-await
export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props
@@ -23,7 +24,6 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
Update: this is fixed in `@types/react` v18.2.14 but still requires `@ts-expect-error` to build :shrug:
See my comment here: https://github.com/vercel/next.js/issues/42292#issuecomment-1622979777
*/}
{/* @ts-expect-error */}
<Header />
{children}
</body>

View File

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

View File

@@ -1,4 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
const nextConfig = {
sassOptions: {
silenceDeprecations: ['legacy-js-api'],
},
}
module.exports = nextConfig

View File

@@ -3,29 +3,29 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start -p 3001",
"lint": "next lint"
"dev": "next dev -p 3001",
"lint": "next lint",
"start": "next start -p 3001"
},
"dependencies": {
"escape-html": "^1.0.3",
"next": "^13.5.1",
"next": "^15.0.0",
"payload-admin-bar": "^1.0.6",
"react": "18.2.0",
"react-dom": "18.2.0"
"react": "19.0.0-rc-65a56d0e-20241020",
"react-dom": "19.0.0-rc-65a56d0e-20241020"
},
"devDependencies": {
"@next/eslint-plugin-next": "^13.4.8",
"@next/eslint-plugin-next": "^15.0.0",
"@payloadcms/eslint-config": "^0.0.2",
"@types/escape-html": "^1.0.2",
"@types/node": "18.11.3",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"eslint": "8.41.0",
"eslint-config-next": "13.4.3",
"eslint-config-next": "15.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-filenames": "^1.3.2",
"eslint-plugin-import": "2.25.4",
@@ -33,8 +33,8 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"prettier": "^2.7.1",
"sass": "^1.62.1",
"sass": "^1.81.0",
"slate": "^0.82.0",
"typescript": "^4.8.4"
"typescript": "5.5.2"
}
}

View File

@@ -7,95 +7,259 @@
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
pages: Page
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
pages: Page;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
pages: PagesSelect<false> | PagesSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
};
globals: {
'main-menu': MainMenu
}
'main-menu': MainMenu;
};
globalsSelect: {
'main-menu': MainMenuSelect<false> | MainMenuSelect<true>;
};
locale: null;
user: User & {
collection: 'users';
};
jobs: {
tasks: unknown;
workflows: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages".
*/
export interface Page {
id: string
title: string
slug?: string
id: string;
title: string;
slug?: string | null;
richText: {
[k: string]: unknown
}[]
updatedAt: string
createdAt: string
_status?: 'draft' | 'published'
[k: string]: unknown;
}[];
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string
updatedAt: string
createdAt: string
email: string
resetPasswordToken?: string
resetPasswordExpiration?: string
salt?: string
hash?: string
loginAttempts?: number
lockUntil?: string
password?: string
id: string;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
export interface PayloadPreference {
id: string
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
document?:
| ({
relationTo: 'pages';
value: string | Page;
} | null)
| ({
relationTo: 'users';
value: string | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users'
value: string | User
}
key?: string
relationTo: 'users';
value: string | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
user: {
relationTo: 'users';
value: string | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null
updatedAt: string
createdAt: string
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string
name?: string
batch?: number
updatedAt: string
createdAt: string
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages_select".
*/
export interface PagesSelect<T extends boolean = true> {
title?: T;
slug?: T;
richText?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu".
*/
export interface MainMenu {
id: string
navItems?: {
link: {
type?: 'reference' | 'custom'
newTab?: boolean
reference: {
relationTo: 'pages'
value: string | Page
}
url: string
label: string
}
id?: string
}[]
updatedAt?: string
createdAt?: string
id: string;
navItems?:
| {
link: {
type?: ('reference' | 'custom') | null;
newTab?: boolean | null;
reference?: {
relationTo: 'pages';
value: string | Page;
} | null;
url?: string | null;
label: string;
};
id?: string | null;
}[]
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
declare module 'payload' {
export interface GeneratedTypes {
collections: {
pages: Page
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {
'main-menu': MainMenu
}
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu_select".
*/
export interface MainMenuSelect<T extends boolean = true> {
navItems?:
| T
| {
link?:
| T
| {
type?: T;
newTab?: T;
reference?: T;
url?: T;
label?: T;
};
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,6 @@
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next.config.js"],
"exclude": ["node_modules"]
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,15 @@ First you'll need a running Payload app. There is one made explicitly for this e
### Next.js
1. Clone this repo
2. `cd` into this directory and run `yarn` or `npm install`
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
5. `open http://localhost:3001` to see the result
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload) for full details.

View File

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

View File

@@ -1,9 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
images: {
domains: ['localhost', process.env.NEXT_PUBLIC_PAYLOAD_URL || ''].filter(Boolean),
sassOptions: {
silenceDeprecations: ['legacy-js-api'],
},
}

View File

@@ -3,29 +3,29 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start -p 3001",
"lint": "next lint"
"dev": "next dev -p 3001",
"lint": "next lint",
"start": "next start -p 3001"
},
"dependencies": {
"escape-html": "^1.0.3",
"next": "^13.5.1",
"next": "^15.0.0",
"payload-admin-bar": "^1.0.6",
"qs": "^6.11.0",
"react": "^18.2.0",
"react": "19.0.0-rc-65a56d0e-20241020",
"react-cookie": "^4.1.1",
"react-dom": "^18.2.0"
"react-dom": "19.0.0-rc-65a56d0e-20241020"
},
"devDependencies": {
"@next/eslint-plugin-next": "^13.4.8",
"@next/eslint-plugin-next": "^15.0.0",
"@payloadcms/eslint-config": "^0.0.2",
"@types/node": "18.11.3",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"eslint": "8.25.0",
"eslint": "8.41.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-filenames": "^1.3.2",
"eslint-plugin-import": "2.25.4",
@@ -33,8 +33,8 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"prettier": "^2.7.1",
"sass": "^1.55.0",
"sass": "^1.81.0",
"slate": "^0.82.0",
"typescript": "4.8.4"
"typescript": "5.5.2"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import React from 'react'
import { PayloadAdminBar, PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
import type { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
import { PayloadAdminBar } from 'payload-admin-bar'
import { Gutter } from '../Gutter'
@@ -9,30 +10,30 @@ const Title: React.FC = () => <span>Dashboard</span>
export const AdminBar: React.FC<{
adminBarProps?: PayloadAdminBarProps
setUser?: (user: PayloadMeUser) => void
user?: PayloadMeUser
setUser?: (user: PayloadMeUser) => void // eslint-disable-line no-unused-vars
}> = (props) => {
const { adminBarProps, user, setUser } = props
const { adminBarProps, setUser, user } = props
return (
<div className={[classes.adminBar, user && classes.show].filter(Boolean).join(' ')}>
<Gutter className={classes.container}>
<PayloadAdminBar
{...adminBarProps}
logo={<Title />}
cmsURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
onAuthChange={setUser}
className={classes.payloadAdminBar}
classNames={{
user: classes.user,
logo: classes.logo,
controls: classes.controls,
logo: classes.logo,
user: classes.user,
}}
cmsURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
logo={<Title />}
onAuthChange={setUser}
style={{
backgroundColor: 'transparent',
padding: 0,
position: 'relative',
zIndex: 'unset',
padding: 0,
backgroundColor: 'transparent',
}}
/>
</Gutter>

View File

@@ -1,33 +1,34 @@
import React, { ElementType } from 'react'
import type { ElementType } from 'react'
import React from 'react'
import Link from 'next/link'
import classes from './index.module.scss'
export type Props = {
label: string
appearance?: 'default' | 'primary' | 'secondary'
el?: 'button' | 'link' | 'a'
onClick?: () => void
href?: string
newTab?: boolean
className?: string
type?: 'submit' | 'button'
disabled?: boolean
el?: 'a' | 'button' | 'link'
href?: string
label: string
newTab?: boolean
onClick?: () => void
type?: 'button' | 'submit'
}
export const Button: React.FC<Props> = ({
el: elFromProps = 'link',
label,
newTab,
href,
type = 'button',
appearance,
className: classNameFromProps,
onClick,
type = 'button',
disabled,
el: elFromProps = 'link',
href,
label,
newTab,
onClick,
}) => {
let el = elFromProps
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
const className = [
classes.button,
classNameFromProps,
@@ -44,11 +45,13 @@ export const Button: React.FC<Props> = ({
</div>
)
if (onClick || type === 'submit') el = 'button'
if (onClick || type === 'submit') {
el = 'button'
}
if (el === 'link') {
return (
<Link href={href} className={className} {...newTabProps} onClick={onClick}>
<Link className={className} href={href} {...newTabProps} onClick={onClick}>
{content}
</Link>
)
@@ -58,12 +61,12 @@ export const Button: React.FC<Props> = ({
return (
<Element
href={href}
className={className}
href={href}
type={type}
{...newTabProps}
onClick={onClick}
disabled={disabled}
onClick={onClick}
>
{content}
</Element>

View File

@@ -1,32 +1,32 @@
import React from 'react'
import Link from 'next/link'
import { Page } from '../../payload-types'
import type { Page } from '../../payload-types'
import { Button } from '../Button'
export type CMSLinkType = {
type?: 'custom' | 'reference'
url?: string
newTab?: boolean
reference?: {
value: string | Page
relationTo: 'pages'
}
label?: string
appearance?: 'default' | 'primary' | 'secondary'
children?: React.ReactNode
className?: string
label?: string
newTab?: boolean
reference?: {
relationTo: 'pages'
value: Page | string
}
type?: 'custom' | 'reference'
url?: string
}
export const CMSLink: React.FC<CMSLinkType> = ({
type,
url,
newTab,
reference,
label,
appearance,
children,
className,
label,
newTab,
reference,
url,
}) => {
const href =
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
@@ -34,7 +34,7 @@ export const CMSLink: React.FC<CMSLinkType> = ({
: url
if (!appearance) {
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
if (type === 'custom') {
return (
@@ -56,10 +56,10 @@ export const CMSLink: React.FC<CMSLinkType> = ({
}
const buttonProps = {
newTab,
href,
appearance,
href,
label,
newTab,
}
return <Button className={className} {...buttonProps} el="link" />

View File

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

View File

@@ -1,14 +1,13 @@
import type { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
import React, { useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import React, { useState } from 'react'
import type { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
import type { MainMenu } from '../../payload-types'
import { AdminBar } from '../AdminBar'
import { CMSLink } from '../CMSLink'
import { Gutter } from '../Gutter'
import classes from './index.module.scss'
type HeaderBarProps = {

View File

@@ -6,14 +6,14 @@ import { Text } from 'slate'
type Children = Leaf[]
type Leaf = {
type: string
value?: {
url: string
alt: string
}
children?: Children
url?: string
[key: string]: unknown
children?: Children
type: string
url?: string
value?: {
alt: string
url: string
}
}
const serialize = (children: Children): React.ReactElement[] =>
@@ -35,7 +35,7 @@ const serialize = (children: Children): React.ReactElement[] =>
if (node.underline) {
text = (
<span style={{ textDecoration: 'underline' }} key={i}>
<span key={i} style={{ textDecoration: 'underline' }}>
{text}
</span>
)
@@ -43,7 +43,7 @@ const serialize = (children: Children): React.ReactElement[] =>
if (node.strikethrough) {
text = (
<span style={{ textDecoration: 'line-through' }} key={i}>
<span key={i} style={{ textDecoration: 'line-through' }}>
{text}
</span>
)
@@ -57,6 +57,8 @@ const serialize = (children: Children): React.ReactElement[] =>
}
switch (node.type) {
case 'blockquote':
return <blockquote key={i}>{serialize(node.children)}</blockquote>
case 'h1':
return <h1 key={i}>{serialize(node.children)}</h1>
case 'h2':
@@ -69,12 +71,6 @@ const serialize = (children: Children): React.ReactElement[] =>
return <h5 key={i}>{serialize(node.children)}</h5>
case 'h6':
return <h6 key={i}>{serialize(node.children)}</h6>
case 'blockquote':
return <blockquote key={i}>{serialize(node.children)}</blockquote>
case 'ul':
return <ul key={i}>{serialize(node.children)}</ul>
case 'ol':
return <ol key={i}>{serialize(node.children)}</ol>
case 'li':
return <li key={i}>{serialize(node.children)}</li>
case 'link':
@@ -83,6 +79,10 @@ const serialize = (children: Children): React.ReactElement[] =>
{serialize(node.children)}
</a>
)
case 'ol':
return <ol key={i}>{serialize(node.children)}</ol>
case 'ul':
return <ul key={i}>{serialize(node.children)}</ul>
default:
return <p key={i}>{serialize(node.children)}</p>

View File

@@ -1,7 +1,7 @@
import React from 'react'
import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next'
import type { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next'
import QueryString from 'qs'
import { ParsedUrlQuery } from 'querystring'
import type { ParsedUrlQuery } from 'querystring'
import { Gutter } from '../components/Gutter'
import RichText from '../components/RichText'
@@ -10,12 +10,12 @@ import type { MainMenu, Page as PageType } from '../payload-types'
import classes from './[slug].module.scss'
const Page: React.FC<
PageType & {
{
mainMenu: MainMenu
preview?: boolean
}
} & PageType
> = (props) => {
const { title, richText } = props
const { richText, title } = props
return (
<main className={classes.page}>
@@ -35,7 +35,7 @@ interface IParams extends ParsedUrlQuery {
// 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 { params, preview, previewData } = context
const { payloadToken } =
(previewData as {
@@ -43,7 +43,9 @@ export const getStaticProps: GetStaticProps = async (context: GetStaticPropsCont
}) || {}
let { slug } = (params as IParams) || {}
if (!slug) slug = 'home'
if (!slug) {
slug = 'home'
}
let doc = {}
const notFound = false
@@ -52,17 +54,17 @@ export const getStaticProps: GetStaticProps = async (context: GetStaticPropsCont
const searchParams = QueryString.stringify(
{
depth: 1,
draft: preview ? true : undefined,
where: {
slug: {
equals: lowerCaseSlug,
},
},
depth: 1,
draft: preview ? true : undefined,
},
{
encode: false,
addQueryPrefix: true,
encode: false,
},
)
@@ -88,12 +90,12 @@ export const getStaticProps: GetStaticProps = async (context: GetStaticPropsCont
}
return {
notFound,
props: {
...doc,
preview: preview || null,
collection: 'pages',
preview: preview || null,
},
notFound,
revalidate: 3600, // in seconds
}
}
@@ -124,7 +126,7 @@ export const getStaticPaths: GetStaticPaths = async () => {
}
return {
paths,
fallback: true,
paths,
}
}

View File

@@ -1,10 +1,11 @@
import React, { useCallback } from 'react'
import { CookiesProvider } from 'react-cookie'
import App, { AppContext, AppProps as NextAppProps } from 'next/app'
import type { AppContext, AppProps as NextAppProps } from 'next/app'
import App from 'next/app'
import { useRouter } from 'next/router'
import { Header } from '../components/Header'
import { MainMenu } from '../payload-types'
import type { MainMenu } from '../payload-types'
import './app.scss'
@@ -26,13 +27,13 @@ type AppProps<P = any> = {
} & Omit<NextAppProps<P>, 'pageProps'>
const PayloadApp = (
appProps: AppProps & {
appProps: {
globals: IGlobals
},
} & AppProps,
): React.ReactElement => {
const { Component, pageProps, globals } = appProps
const { Component, globals, pageProps } = appProps
const { collection, id, preview } = pageProps
const { id, collection, preview } = pageProps
const router = useRouter()
@@ -43,25 +44,27 @@ const PayloadApp = (
router.reload()
}
}
exit()
exit().catch((error) => {
// eslint-disable-next-line no-console
console.error('Failed to exit preview:', error)
})
}, [router])
return (
<CookiesProvider>
<Header
globals={globals}
adminBarProps={{
collection,
id,
preview,
collection,
onPreviewExit,
preview,
}}
globals={globals}
/>
{/* typescript flags this `@ts-expect-error` declaration as unneeded, but types are breaking the build process
Remove these comments when the issue is resolved
See more here: https://github.com/facebook/react/issues/24304
*/}
{/* @ts-expect-error */}
<Component {...pageProps} />
</CookiesProvider>
)

View File

@@ -1,9 +1,10 @@
import { GetStaticProps } from 'next'
import type { GetStaticProps } from 'next'
import Page, { getStaticProps as sharedGetStaticProps } from './[slug]'
export default Page
// eslint-disable-next-line @typescript-eslint/require-await
export const getStaticProps: GetStaticProps = async (ctx) => {
const func = sharedGetStaticProps.bind(this)
return func(ctx)

View File

@@ -7,95 +7,259 @@
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
pages: Page
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
pages: Page;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
pages: PagesSelect<false> | PagesSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
};
globals: {
'main-menu': MainMenu
}
'main-menu': MainMenu;
};
globalsSelect: {
'main-menu': MainMenuSelect<false> | MainMenuSelect<true>;
};
locale: null;
user: User & {
collection: 'users';
};
jobs: {
tasks: unknown;
workflows: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages".
*/
export interface Page {
id: string
title: string
slug?: string
id: string;
title: string;
slug?: string | null;
richText: {
[k: string]: unknown
}[]
updatedAt: string
createdAt: string
_status?: 'draft' | 'published'
[k: string]: unknown;
}[];
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string
updatedAt: string
createdAt: string
email: string
resetPasswordToken?: string
resetPasswordExpiration?: string
salt?: string
hash?: string
loginAttempts?: number
lockUntil?: string
password?: string
id: string;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
export interface PayloadPreference {
id: string
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
document?:
| ({
relationTo: 'pages';
value: string | Page;
} | null)
| ({
relationTo: 'users';
value: string | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users'
value: string | User
}
key?: string
relationTo: 'users';
value: string | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
user: {
relationTo: 'users';
value: string | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null
updatedAt: string
createdAt: string
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string
name?: string
batch?: number
updatedAt: string
createdAt: string
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages_select".
*/
export interface PagesSelect<T extends boolean = true> {
title?: T;
slug?: T;
richText?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu".
*/
export interface MainMenu {
id: string
navItems?: {
link: {
type?: 'reference' | 'custom'
newTab?: boolean
reference: {
relationTo: 'pages'
value: string | Page
}
url: string
label: string
}
id?: string
}[]
updatedAt?: string
createdAt?: string
id: string;
navItems?:
| {
link: {
type?: ('reference' | 'custom') | null;
newTab?: boolean | null;
reference?: {
relationTo: 'pages';
value: string | Page;
} | null;
url?: string | null;
label: string;
};
id?: string | null;
}[]
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
declare module 'payload' {
export interface GeneratedTypes {
collections: {
pages: Page
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {
'main-menu': MainMenu
}
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "main-menu_select".
*/
export interface MainMenuSelect<T extends boolean = true> {
navItems?:
| T
| {
link?:
| T
| {
type?: T;
newTab?: T;
reference?: T;
url?: T;
label?: T;
};
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}

View File

@@ -22,7 +22,8 @@
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
"**/*.tsx",
"next.config.js"
],
"exclude": [
"node_modules"

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,16 @@
DATABASE_URI=mongodb://127.0.0.1/payload-example-draft-preview
PAYLOAD_SECRET=ENTER-STRING-HERE
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
# Database connection string
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
# Used to encrypt JWT tokens
PAYLOAD_SECRET=YOUR_SECRET_HERE
# Used to configure CORS, format links and more. No trailing slash
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
# Add the following environment variables when running your payload server & app separately
# i.e. next-app || next-pages on localhost:3001 and payload server on localhost:3000
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3001
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
COOKIE_DOMAIN=localhost
REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
PAYLOAD_PUBLIC_SEED=true
PAYLOAD_DROP_DATABASE=true
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET

View File

@@ -1,4 +1,8 @@
module.exports = {
extends: 'next',
root: true,
extends: ['@payloadcms'],
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
}

View File

@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
},
"transform": {
"react": {
"runtime": "automatic",
"pragmaFrag": "React.Fragment",
"throwIfNamespace": true,
"development": false,
"useBuiltins": true
}
}
},
"module": {
"type": "es6"
}
}

View File

@@ -1,21 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Preview Example CMS",
"program": "${workspaceFolder}/src/server.ts",
"preLaunchTask": "npm: build:server",
"env": {
"PAYLOAD_CONFIG_PATH": "${workspaceFolder}/src/payload.config.ts"
}
// "outFiles": [
// "${workspaceFolder}/dist/**/*.js"
// ]
}
]
}

View File

@@ -9,10 +9,18 @@ Follow the instructions in each respective README to get started. If you are set
## Quick Start
To spin up this example locally, follow these steps:
1. Clone this repo
2. `cd` into this directory and run `yarn` or `npm install`
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
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
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
5. `open http://localhost:3000/admin` to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo`
@@ -22,6 +30,33 @@ That's it! Changes made in `./src` will be reflected in your app. See the [Devel
Draft preview works by sending the user to your front-end with a `secret` along with their http-only cookies. Your front-end catches the request, verifies the authenticity, then enters into it's own preview mode. Once in preview mode, your front-end can begin securely requesting draft documents from Payload. See [Preview Mode](#preview-mode) for more details.
### Environment Variables
Depending on how you run this example, you need different environment variables:
- #### Running Payload and the Front-End Together
When the Payload server and front-end run on the same domain and port:
```ts
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
PAYLOAD_SECRET=YOUR_SECRET_HERE
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
```
- #### Running Payload and the Front-End Separately
When running Payload on one domain (e.g., `localhost:3000`) and the front-end on another (e.g., `localhost:3001`):
```ts
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
PAYLOAD_SECRET=YOUR_SECRET_HERE
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3001
REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
```
### Collections
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend any of this functionality.
@@ -53,11 +88,11 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
})
```
For more details on how to extend this functionality, see the [Authentication](https://payloadcms.com/docs/authentication) docs.
For more details on how to extend this functionality, see the [Authentication](https://payloadcms.com/docs/authentication/overview) docs.
### Preview Mode
To preview draft documents, the user first needs to have at least one draft document saved. When they click the "preview" button from the Payload admin panel, a custom [preview function](https://payloadcms.com/docs/configuration/collections#preview) routes them to your front-end with a `secret` along with their http-only cookies. An API route on your front-end will verify the secret and token before entering into it's own preview mode. Once in preview mode, it can begin requesting drafts from Payload using the `Authorization` header. See [Pages](#pages) for more details.
To preview draft documents, the user first needs to have at least one draft document saved. When they click the "preview" button from the Payload admin panel, a custom [preview function](https://payloadcms.com/docs/admin/collections#preview) routes them to your front-end with a `secret` along with their http-only cookies. An API route on your front-end will verify the secret and token before entering into it's own preview mode. Once in preview mode, it can begin requesting drafts from Payload using the `Authorization` header. See [Pages](#pages) for more details.
> "Preview mode" looks differently for every front-end framework. For instance, check out the differences between Next.js [Preview Mode](https://nextjs.org/docs/pages/building-your-application/configuring/preview-mode) in the Pages Router and [Draft Mode](https://nextjs.org/docs/pages/building-your-application/configuring/draft-mode) in the App Router. In Next.js, methods are provided that set cookies in your browser, but this may not be the case for all frameworks.
@@ -83,16 +118,16 @@ To spin up this example locally, follow the [Quick Start](#quick-start).
### Seed
On boot, a seed script is included to scaffold a basic database for you to use as an example. This is done by setting the `PAYLOAD_DROP_DATABASE` and `PAYLOAD_PUBLIC_SEED` environment variables which are included in the `.env.example` by default. You can remove these from your `.env` to prevent this behavior. You can also freshly seed your project at any time by running `yarn seed`. This seed creates a user with email `demo@payloadcms.com` and password `demo` along with a home page and an example page with two versions, one published and the other draft.
On boot, a seed script is included to scaffold a basic database for you to use as an example. You can remove `pnpm seed` from the `dev` script in the `package.json` to prevent this behavior. You can also freshly seed your project at any time by running `pnpm seed`. This seed creates a user with email `demo@payloadcms.com` and password `demo` along with a home page and an example page with two versions, one published and the other draft.
> NOTICE: seeding the database is destructive because it drops your current database to populate a fresh one from the seed template. Only run this command if you are starting a new project or can afford to lose your current data.
## Production
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
To run Payload in production, you need to build and start the Admin panel. To do so, follow these steps:
1. First invoke the `payload build` script by running `yarn build` or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
1. Then run `yarn serve` or `npm run serve` to run Node in production and serve Payload from the `./build` directory.
1. Invoke the `next build` script by running `pnpm build` or `npm run build` in your project root. This creates a `.next` directory with a production-ready admin bundle.
1. Finally run `pnpm start` or `npm run start` to run Node in production and serve Payload from the `.build` directory.
### Deployment

View File

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

View File

@@ -0,0 +1,8 @@
import { withPayload } from '@payloadcms/next/withPayload'
/** @type {import('next').NextConfig} */
const nextConfig = {
// Your Next.js config here
}
export default withPayload(nextConfig)

View File

@@ -1,5 +0,0 @@
{
"ext": "ts",
"exec": "ts-node src/server.ts -- -I",
"stdin": false
}

View File

@@ -1,49 +1,57 @@
{
"name": "payload-example-preview",
"description": "Payload preview example.",
"version": "1.0.0",
"main": "dist/server.js",
"description": "Payload preview example.",
"license": "MIT",
"main": "dist/server.js",
"scripts": {
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
"seed": "rm -rf media && cross-env PAYLOAD_PUBLIC_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts ts-node src/server.ts",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"build:server": "tsc",
"build": "yarn copyfiles && yarn build:payload && yarn build:server",
"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",
"lint": "eslint src",
"lint:fix": "eslint --fix --ext .ts,.tsx src"
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
"dev": "cross-env NODE_OPTIONS=--no-deprecation && pnpm seed && next dev",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:schema": "payload-graphql generate:schema",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"seed": "npm run payload migrate:fresh",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
},
"dependencies": {
"@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "latest",
"@payloadcms/next": "latest",
"@payloadcms/richtext-slate": "latest",
"@payloadcms/ui": "latest",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"payload": "latest"
"escape-html": "^1.0.3",
"graphql": "^16.9.0",
"jsonwebtoken": "9.0.2",
"next": "^15.0.0",
"payload": "latest",
"payload-admin-bar": "^1.0.6",
"react": "19.0.0-rc-65a56d0e-20241020",
"react-dom": "19.0.0-rc-65a56d0e-20241020"
},
"devDependencies": {
"@payloadcms/eslint-config": "^0.0.2",
"@types/express": "^4.17.9",
"@types/node": "18.11.3",
"@types/react": "18.0.21",
"@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-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",
"nodemon": "^2.0.6",
"prettier": "^2.7.1",
"ts-node": "^9.1.1",
"typescript": "^4.8.4"
"@payloadcms/graphql": "latest",
"@swc/core": "^1.6.13",
"@types/escape-html": "^1.0.2",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"eslint": "^8.57.0",
"eslint-config-next": "^15.0.0",
"slate": "^0.82.0",
"tsx": "^4.16.2",
"typescript": "5.5.2"
},
"engines": {
"node": "^18.20.2 || >=20.9.0"
},
"pnpm": {
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
}
},
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
.page {
margin-top: calc(var(--base) * 2);
}

View File

@@ -0,0 +1,89 @@
import { draftMode } from 'next/headers'
import { notFound } from 'next/navigation'
import { getPayload } from 'payload'
import React, { cache, Fragment } from 'react'
import type { Page as PageType } from '../../../payload-types'
import { Gutter } from '../../../components/Gutter'
import RichText from '../../../components/RichText'
import config from '../../../payload.config'
import { home as homeStatic } from '../../../seed/home'
import classes from './index.module.scss'
export async function generateStaticParams() {
const payload = await getPayload({ config })
const pages = await payload.find({
collection: 'pages',
draft: false,
limit: 1000,
overrideAccess: false,
})
const params = pages.docs
?.filter((doc) => {
return doc.slug !== 'home'
})
.map(({ slug }) => {
return { slug }
})
return params || []
}
type Args = {
params: Promise<{
slug?: string
}>
}
// eslint-disable-next-line no-restricted-exports
export default async function Page({ params: paramsPromise }: Args) {
const { slug = 'home' } = await paramsPromise
let page: null | PageType
page = await queryPageBySlug({
slug,
})
// Remove this code once your website is seeded
if (!page && slug === 'home') {
page = homeStatic
}
if (page === null) {
return notFound()
}
return (
<Fragment>
<main className={classes.page}>
<Gutter>
<h1>{page?.title}</h1>
<RichText content={page?.richText} />
</Gutter>
</main>
</Fragment>
)
}
const queryPageBySlug = cache(async ({ slug }: { slug: string }) => {
const { isEnabled: draft } = await draftMode()
const payload = await getPayload({ config })
const result = await payload.find({
collection: 'pages',
draft,
limit: 1,
overrideAccess: draft,
where: {
slug: {
equals: slug,
},
},
})
return result.docs?.[0] || null
})

View File

@@ -0,0 +1,118 @@
$breakpoint: 1000px;
:root {
--max-width: 1600px;
--foreground-rgb: 0, 0, 0;
--background-rgb: 255, 255, 255;
--block-spacing: 2rem;
--gutter-h: 4rem;
--base: 1rem;
@media (max-width: $breakpoint) {
--block-spacing: 1rem;
--gutter-h: 2rem;
--base: 0.75rem;
}
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-rgb: 7, 7, 7;
}
}
* {
box-sizing: border-box;
}
html {
font-size: 20px;
line-height: 1.5;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Open Sans',
'Helvetica Neue',
sans-serif;
@media (max-width: $breakpoint) {
font-size: 16px;
}
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
margin: 0;
color: rgb(var(--foreground-rgb));
background: rgb(var(--background-rgb));
}
img {
height: auto;
max-width: 100%;
display: block;
}
h1 {
font-size: 4.5rem;
line-height: 1.2;
margin: 0 0 2.5rem 0;
@media (max-width: $breakpoint) {
font-size: 3rem;
margin: 0 0 1.5rem 0;
}
}
h2 {
font-size: 3.5rem;
line-height: 1.2;
margin: 0 0 2.5rem 0;
}
h3 {
font-size: 2.5rem;
line-height: 1.2;
margin: 0 0 2rem 0;
}
h4 {
font-size: 1.5rem;
line-height: 1.2;
margin: 0 0 1rem 0;
}
h5 {
font-size: 1.25rem;
line-height: 1.2;
margin: 0 0 1rem 0;
}
h6 {
font-size: 1rem;
line-height: 1.2;
margin: 0 0 1rem 0;
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}

View File

@@ -0,0 +1,31 @@
import type { Metadata } from 'next'
import { draftMode } from 'next/headers'
import { AdminBar } from '../../components/AdminBar'
import { Header } from '../../components/Header'
import './app.scss'
export const metadata: Metadata = {
description: 'Generated by create next app',
title: 'Create Next App',
}
// eslint-disable-next-line no-restricted-exports
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const { isEnabled } = await draftMode()
return (
<html lang="en">
<body>
<AdminBar
adminBarProps={{
preview: isEnabled,
}}
/>
<Header />
{children}
</body>
</html>
)
}

View File

@@ -0,0 +1,7 @@
import { draftMode } from 'next/headers'
export async function GET(): Promise<Response> {
const draft = await draftMode()
draft.disable()
return new Response('Draft mode is disabled')
}

View File

@@ -0,0 +1,7 @@
import { draftMode } from 'next/headers'
export async function GET(): Promise<Response> {
const draft = await draftMode()
draft.disable()
return new Response('Draft mode is disabled')
}

View File

@@ -0,0 +1,98 @@
import type { CollectionSlug } from 'payload'
import jwt from 'jsonwebtoken'
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import { getPayload } from 'payload'
import configPromise from '../../../../payload.config'
const payloadToken = 'payload-token'
export async function GET(
req: {
cookies: {
get: (name: string) => {
value: string
}
}
} & Request,
): Promise<Response> {
const payload = await getPayload({ config: configPromise })
const token = req.cookies.get(payloadToken)?.value
const { searchParams } = new URL(req.url)
const path = searchParams.get('path')
const collection = searchParams.get('collection') as CollectionSlug
const slug = searchParams.get('slug')
const previewSecret = searchParams.get('previewSecret')
if (previewSecret) {
return new Response('You are not allowed to preview this page', { status: 403 })
} else {
if (!path) {
return new Response('No path provided', { status: 404 })
}
if (!collection) {
return new Response('No path provided', { status: 404 })
}
if (!slug) {
return new Response('No path provided', { status: 404 })
}
if (!token) {
new Response('You are not allowed to preview this page', { status: 403 })
}
if (!path.startsWith('/')) {
new Response('This endpoint can only be used for internal previews', { status: 500 })
}
let user
try {
user = jwt.verify(token, payload.secret)
} catch (error) {
payload.logger.error({
err: error,
msg: 'Error verifying token for live preview',
})
}
const draft = await draftMode()
// You can add additional checks here to see if the user is allowed to preview this page
if (!user) {
draft.disable()
return new Response('You are not allowed to preview this page', { status: 403 })
}
// Verify the given slug exists
try {
const docs = await payload.find({
collection,
draft: true,
where: {
slug: {
equals: slug,
},
},
})
if (!docs.docs.length) {
return new Response('Document not found', { status: 404 })
}
} catch (error) {
payload.logger.error({
err: error,
msg: 'Error verifying token for live preview:',
})
}
draft.enable()
redirect(path)
}
}

View File

@@ -0,0 +1,3 @@
import Page from './[slug]/page'
export default Page

View File

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

View File

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

View File

@@ -0,0 +1,121 @@
import { RscEntrySlateCell as RscEntrySlateCell_0e78253914a550fdacd75626f1dabe17 } from '@payloadcms/richtext-slate/rsc'
import { RscEntrySlateField as RscEntrySlateField_0e78253914a550fdacd75626f1dabe17 } from '@payloadcms/richtext-slate/rsc'
import { BoldLeafButton as BoldLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { BoldLeaf as BoldLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { CodeLeafButton as CodeLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { CodeLeaf as CodeLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { ItalicLeafButton as ItalicLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { ItalicLeaf as ItalicLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { StrikethroughLeafButton as StrikethroughLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { StrikethroughLeaf as StrikethroughLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { UnderlineLeafButton as UnderlineLeafButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { UnderlineLeaf as UnderlineLeaf_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { BlockquoteElementButton as BlockquoteElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { BlockquoteElement as BlockquoteElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { H1ElementButton as H1ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { Heading1Element as Heading1Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { H2ElementButton as H2ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { Heading2Element as Heading2Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { H3ElementButton as H3ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { Heading3Element as Heading3Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { H4ElementButton as H4ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { Heading4Element as Heading4Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { H5ElementButton as H5ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { Heading5Element as Heading5Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { H6ElementButton as H6ElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { Heading6Element as Heading6Element_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { IndentButton as IndentButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { IndentElement as IndentElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { ListItemElement as ListItemElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { LinkButton as LinkButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { LinkElement as LinkElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { WithLinks as WithLinks_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { OLElementButton as OLElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { OrderedListElement as OrderedListElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { RelationshipButton as RelationshipButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { RelationshipElement as RelationshipElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { WithRelationship as WithRelationship_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { TextAlignElementButton as TextAlignElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { ULElementButton as ULElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { UnorderedListElement as UnorderedListElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { UploadElementButton as UploadElementButton_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { UploadElement as UploadElement_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
import { WithUpload as WithUpload_0b388c087d9de8c4f011dd323a130cfb } from '@payloadcms/richtext-slate/client'
export const importMap = {
'@payloadcms/richtext-slate/rsc#RscEntrySlateCell':
RscEntrySlateCell_0e78253914a550fdacd75626f1dabe17,
'@payloadcms/richtext-slate/rsc#RscEntrySlateField':
RscEntrySlateField_0e78253914a550fdacd75626f1dabe17,
'@payloadcms/richtext-slate/client#BoldLeafButton':
BoldLeafButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#BoldLeaf': BoldLeaf_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#CodeLeafButton':
CodeLeafButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#CodeLeaf': CodeLeaf_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#ItalicLeafButton':
ItalicLeafButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#ItalicLeaf': ItalicLeaf_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#StrikethroughLeafButton':
StrikethroughLeafButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#StrikethroughLeaf':
StrikethroughLeaf_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#UnderlineLeafButton':
UnderlineLeafButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#UnderlineLeaf': UnderlineLeaf_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#BlockquoteElementButton':
BlockquoteElementButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#BlockquoteElement':
BlockquoteElement_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#H1ElementButton':
H1ElementButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#Heading1Element':
Heading1Element_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#H2ElementButton':
H2ElementButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#Heading2Element':
Heading2Element_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#H3ElementButton':
H3ElementButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#Heading3Element':
Heading3Element_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#H4ElementButton':
H4ElementButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#Heading4Element':
Heading4Element_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#H5ElementButton':
H5ElementButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#Heading5Element':
Heading5Element_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#H6ElementButton':
H6ElementButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#Heading6Element':
Heading6Element_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#IndentButton': IndentButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#IndentElement': IndentElement_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#ListItemElement':
ListItemElement_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#LinkButton': LinkButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#LinkElement': LinkElement_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#WithLinks': WithLinks_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#OLElementButton':
OLElementButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#OrderedListElement':
OrderedListElement_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#RelationshipButton':
RelationshipButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#RelationshipElement':
RelationshipElement_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#WithRelationship':
WithRelationship_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#TextAlignElementButton':
TextAlignElementButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#ULElementButton':
ULElementButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#UnorderedListElement':
UnorderedListElement_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#UploadElementButton':
UploadElementButton_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#UploadElement': UploadElement_0b388c087d9de8c4f011dd323a130cfb,
'@payloadcms/richtext-slate/client#WithUpload': WithUpload_0b388c087d9de8c4f011dd323a130cfb,
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import type { Access } from 'payload/config'
import type { Access } from 'payload'
export const publishedOrLoggedIn: Access = ({ req: { user } }) => {
if (user) {

View File

@@ -1,4 +1,4 @@
import type { FieldHook } from 'payload/types'
import type { FieldHook } from 'payload'
const format = (val: string): string =>
val
@@ -6,9 +6,9 @@ const format = (val: string): string =>
.replace(/[^\w-]+/g, '')
.toLowerCase()
const formatSlug =
export const formatSlug =
(fallback: string): FieldHook =>
({ operation, value, originalDoc, data }) => {
({ data, operation, originalDoc, value }) => {
if (typeof value === 'string') {
return format(value)
}
@@ -23,5 +23,3 @@ const formatSlug =
return value
}
export default formatSlug

View File

@@ -1,4 +1,8 @@
import type { AfterChangeHook } from 'payload/dist/collections/config/types'
import type { CollectionAfterChangeHook } from 'payload'
import { revalidatePath } from 'next/cache'
import type { Page } from '../../../payload-types'
// ensure that the home page is revalidated at '/' instead of '/home'
export const formatAppURL = ({ doc }): string => {
@@ -7,32 +11,52 @@ export const formatAppURL = ({ doc }): string => {
return 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`
// only revalidate existing docs that are published (not drafts)
// send `revalidatePath`, `collection`, and `slug` to the frontend to use in its revalidate route
// frameworks may have different ways of doing this, but the idea is the same
export const revalidatePage: AfterChangeHook = ({ doc, req, operation }) => {
if (operation === 'update' && doc._status === 'published') {
const url = formatAppURL({ doc })
export const revalidatePage: CollectionAfterChangeHook<Page> = ({
doc,
operation,
previousDoc,
req,
}) => {
if (process.env.PAYLOAD_PUBLIC_SITE_URL && process.env.REVALIDATION_KEY) {
// Revalidate externally if payload is configured separately from the next app
if (operation === 'update' && doc._status === 'published') {
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}&collection=pages&slug=${doc?.slug}&path=${url}`,
)
const revalidate = async (): Promise<void> => {
try {
const res = await fetch(
`${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/revalidate?secret=${process.env.REVALIDATION_KEY}&collection=pages&slug=${doc?.slug}&path=${url}`,
)
if (res.ok) {
req.payload.logger.info(`Revalidated path ${url}`)
} else {
req.payload.logger.error(`Error revalidating path ${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}`)
}
} catch (err: unknown) {
req.payload.logger.error(`Error hitting revalidate route for ${url}`)
}
void revalidate()
}
} else {
// Revalidate internally with next/cache if your payload app is installed within /app folder
if (req.context.skipRevalidate) {
return doc
}
revalidate()
if (doc._status === 'published') {
const path = doc.slug === 'home' ? '/' : `/${doc.slug}`
req.payload.logger.info(`Revalidating page at path: ${path}`)
revalidatePath(path)
}
if (previousDoc?._status === 'published' && doc._status !== 'published') {
const oldPath = previousDoc.slug === 'home' ? '/' : `/${previousDoc.slug}`
req.payload.logger.info(`Revalidating old page at path: ${oldPath}`)
revalidatePath(oldPath)
}
}
return doc

View File

@@ -1,35 +1,43 @@
import type { CollectionConfig } from 'payload/types'
import type { CollectionConfig } from 'payload'
import richText from '../../fields/richText'
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
import { loggedIn } from './access/loggedIn'
import { publishedOrLoggedIn } from './access/publishedOrLoggedIn'
import formatSlug from './hooks/formatSlug'
import { formatSlug } from './hooks/formatSlug'
import { formatAppURL, revalidatePage } from './hooks/revalidatePage'
export const Pages: CollectionConfig = {
slug: 'pages',
access: {
create: loggedIn,
delete: loggedIn,
read: publishedOrLoggedIn,
update: loggedIn,
},
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'slug', 'updatedAt'],
preview: (doc) => {
return `${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/preview?url=${encodeURIComponent(
formatAppURL({
doc,
}),
)}&collection=pages&slug=${doc.slug}&secret=${process.env.PAYLOAD_PUBLIC_DRAFT_SECRET}`
if (process.env.PAYLOAD_PUBLIC_SITE_URL && process.env.PAYLOAD_PUBLIC_DRAFT_SECRET) {
// Separate Payload and front-end setup
return `${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/preview?url=${encodeURIComponent(
formatAppURL({ doc }),
)}&collection=pages&slug=${doc.slug}&secret=${process.env.PAYLOAD_PUBLIC_DRAFT_SECRET}`
} else if (process.env.NEXT_PUBLIC_SERVER_URL) {
// Unified Payload and front-end setup
const path = generatePreviewPath({
slug: typeof doc?.slug === 'string' ? doc.slug : '',
collection: 'pages',
})
return `${process.env.NEXT_PUBLIC_SERVER_URL}${path}`
}
// Fallback for missing environment variables
throw new Error(
'Environment variables for preview functionality are not set. Ensure that either PAYLOAD_PUBLIC_SITE_URL and PAYLOAD_PUBLIC_DRAFT_SECRET, or NEXT_PUBLIC_SERVER_URL are defined.',
)
},
},
versions: {
drafts: true,
},
access: {
read: publishedOrLoggedIn,
create: loggedIn,
update: loggedIn,
delete: loggedIn,
},
hooks: {
afterChange: [revalidatePage],
useAsTitle: 'title',
},
fields: [
{
@@ -39,16 +47,22 @@ export const Pages: CollectionConfig = {
},
{
name: 'slug',
label: 'Slug',
type: 'text',
index: true,
admin: {
position: 'sidebar',
},
hooks: {
beforeValidate: [formatSlug('title')],
},
index: true,
label: 'Slug',
},
richText(),
],
hooks: {
afterChange: [revalidatePage],
},
versions: {
drafts: true,
},
}

View File

@@ -1,17 +1,10 @@
import type { CollectionConfig } from 'payload/types'
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
auth: {
tokenExpiration: 28800, // 8 hours
cookies: {
sameSite: 'none',
secure: true,
domain: process.env.COOKIE_DOMAIN,
},
},
admin: {
useAsTitle: 'email',
},
auth: true,
fields: [],
}

View File

@@ -0,0 +1,51 @@
.adminBar {
z-index: 10;
width: 100%;
background-color: rgba(var(--foreground-rgb), 0.075);
padding: calc(var(--base) * 0.5) 0;
display: none;
visibility: hidden;
opacity: 0;
transition: opacity 150ms linear;
}
.payloadAdminBar {
color: rgb(var(--foreground-rgb)) !important;
}
.show {
display: block;
visibility: visible;
opacity: 1;
}
.controls {
& > *:not(:last-child) {
margin-right: calc(var(--base) * 0.5) !important;
}
}
.user {
margin-right: calc(var(--base) * 0.5) !important;
}
.logo {
margin-right: calc(var(--base) * 0.5) !important;
}
.innerLogo {
width: 100%;
}
.container {
position: relative;
}
.hr {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background-color: rbg(var(--background-rgb));
height: 2px;
}

View File

@@ -0,0 +1,72 @@
'use client'
import type { PayloadAdminBarProps } from 'payload-admin-bar'
import { useRouter } from 'next/navigation'
import { PayloadAdminBar } from 'payload-admin-bar'
import React, { useState } from 'react'
import { Gutter } from '../Gutter'
import classes from './index.module.scss'
const collectionLabels = {
pages: {
plural: 'Pages',
singular: 'Page',
},
}
const Title: React.FC = () => <span>Dashboard</span>
export const AdminBar: React.FC<{
adminBarProps?: PayloadAdminBarProps
}> = (props) => {
const { adminBarProps } = props || {}
const [show, setShow] = useState(false)
const collection = 'pages'
const router = useRouter()
const onAuthChange = React.useCallback((user) => {
setShow(user?.id)
}, [])
return (
<div className={[classes.adminBar, show && classes.show].filter(Boolean).join(' ')}>
<Gutter className={classes.container}>
<PayloadAdminBar
{...adminBarProps}
className={classes.payloadAdminBar}
classNames={{
controls: classes.controls,
logo: classes.logo,
user: classes.user,
}}
cmsURL={process.env.NEXT_PUBLIC_SERVER_URL}
collection={collection}
collectionLabels={{
plural: collectionLabels[collection]?.plural || 'Pages',
singular: collectionLabels[collection]?.singular || 'Page',
}}
logo={<Title />}
onAuthChange={onAuthChange}
onPreviewExit={() => {
fetch('/next/exit-preview')
.then(() => {
router.push('/')
router.refresh()
})
.catch((error) => {
console.error('Error exiting preview:', error)
})
}}
style={{
backgroundColor: 'transparent',
padding: 0,
position: 'relative',
zIndex: 'unset',
}}
/>
</Gutter>
</div>
)
}

View File

@@ -1,17 +0,0 @@
import React from 'react'
const BeforeLogin: React.FC = () => {
if (process.env.PAYLOAD_PUBLIC_SEED === 'true') {
return (
<p>
{'Log in with the email '}
<strong>demo@payloadcms.com</strong>
{' and the password '}
<strong>demo</strong>.
</p>
)
}
return null
}
export default BeforeLogin

View File

@@ -0,0 +1,55 @@
.button {
border: none;
cursor: pointer;
display: inline-flex;
justify-content: center;
background-color: transparent;
}
.content {
display: flex;
align-items: center;
justify-content: space-around;
svg {
margin-right: calc(var(--base) / 2);
width: var(--base);
height: var(--base);
}
}
.label {
text-align: center;
display: flex;
align-items: center;
}
.button {
text-decoration: none;
display: inline-flex;
padding: 12px 24px;
}
.primary--white {
background-color: black;
color: white;
}
.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;
}

View File

@@ -0,0 +1,75 @@
import type { ElementType } from 'react'
import Link from 'next/link'
import React from 'react'
import classes from './index.module.scss'
export type Props = {
appearance?: 'default' | 'primary' | 'secondary'
className?: string
disabled?: boolean
el?: 'a' | 'button' | 'link'
href?: string
label?: string
newTab?: boolean | null
onClick?: () => void
type?: 'button' | 'submit'
}
export const Button: React.FC<Props> = ({
type = 'button',
appearance,
className: classNameFromProps,
disabled,
el: elFromProps = 'link',
href,
label,
newTab,
onClick,
}) => {
let el = elFromProps
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
const className = [
classes.button,
classNameFromProps,
classes[`appearance--${appearance}`],
classes.button,
]
.filter(Boolean)
.join(' ')
const content = (
<div className={classes.content}>
{/* <Chevron /> */}
<span className={classes.label}>{label}</span>
</div>
)
if (onClick || type === 'submit') {
el = 'button'
}
if (el === 'link') {
return (
<Link className={className} href={href || ''} {...newTabProps} onClick={onClick}>
{content}
</Link>
)
}
const Element: ElementType = el
return (
<Element
className={className}
href={href}
type={type}
{...newTabProps}
disabled={disabled}
onClick={onClick}
>
{content}
</Element>
)
}

View File

@@ -0,0 +1,73 @@
import Link from 'next/link'
import React from 'react'
import type { Page } from '../../payload-types'
import { Button } from '../Button'
export type CMSLinkType = {
appearance?: 'default' | 'primary' | 'secondary'
children?: React.ReactNode
className?: string
label?: string
newTab?: boolean | null
reference?: {
relationTo: 'pages'
value: number | Page | string
} | null
type?: 'custom' | 'reference' | null
url?: null | string
}
export const CMSLink: React.FC<CMSLinkType> = ({
type,
appearance,
children,
className,
label,
newTab,
reference,
url,
}) => {
const href =
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
? `${reference?.relationTo !== 'pages' ? `/${reference?.relationTo}` : ''}/${
reference.value.slug
}`
: url
if (!href) {
return null
}
if (!appearance) {
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
if (type === 'custom') {
return (
<a href={url || ''} {...newTabProps} className={className}>
{label && label}
{children ? <>{children}</> : null}
</a>
)
}
if (href) {
return (
<Link href={href} {...newTabProps} className={className} prefetch={false}>
{label && label}
{children ? <>{children}</> : null}
</Link>
)
}
}
const buttonProps = {
appearance,
href,
label,
newTab,
}
return <Button className={className} {...buttonProps} el="link" />
}

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More