chore: builds preview example for next.js app router (#2718)
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
MONGODB_URI=mongodb://localhost/payload-example-preview
|
||||
PAYLOAD_SECRET=ENTER-STRING-HERE
|
||||
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3000
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:8000
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://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_SEED=true
|
||||
PAYLOAD_DROP_DATABASE=true
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
legacy-peer-deps=true
|
||||
@@ -1,8 +1,8 @@
|
||||
# Payload Preview Example
|
||||
|
||||
This example demonstrates how to implement preview into Payload using [Versions](https://payloadcms.com/docs/versions/overview) and [Drafts](https://payloadcms.com/docs/versions/drafts).
|
||||
This example demonstrates how to implement preview in [Payload](https://github.com/payloadcms/payload) using [Versions](https://payloadcms.com/docs/versions/overview) and [Drafts](https://payloadcms.com/docs/versions/drafts). Preview allows you to see draft content on your front-end before it is published.
|
||||
|
||||
There is a fully working Next.js app made explicitly for this example which can be found [here](../nextjs). Follow the instructions there to get started. If you are setting up preview for another front-end, please consider contributing to this repo with your own example!
|
||||
There is a fully working Next.js app made explicitly for this example which can be found [here](../next-app). Follow the instructions there to get started. If you are setting up preview for another front-end, please consider contributing to this repo with your own example!
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -10,14 +10,28 @@ There is a fully working Next.js app made explicitly for this example which can
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:8000/admin` to access the admin panel
|
||||
6. Login with email `dev@payloadcms.com` and password `test`
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
|
||||
## How it works
|
||||
|
||||
A `pages` collection is created with `versions: { drafts: true }` and access control that restricts access to only logged-in users and `published` pages. On your front-end, a query similar to this can be used to fetch data and bypass access control in preview mode:
|
||||
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.
|
||||
|
||||
### Collections
|
||||
|
||||
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend any of this functionality.
|
||||
|
||||
- #### Users
|
||||
|
||||
The `users` collection is auth-enabled which provides access to the admin panel. When previewing documents on your front-end, the user's JWT is used to authenticate the request. See [Pages](#pages) for more details.
|
||||
|
||||
For additional help with authentication, see the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs or the official [Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth/cms#readme).
|
||||
|
||||
- #### Pages
|
||||
|
||||
The `pages` collection is draft-enabled and has access control that restricts public users from viewing pages with a `draft` status. To fetch draft documents on your front-end, simply include the `draft=true` query param along with the `Authorization` header once you have entered [Preview Mode](#preview-mode).
|
||||
|
||||
```ts
|
||||
const preview = true; // set this based on your own front-end environment (see `Preview Mode` below)
|
||||
@@ -34,23 +48,38 @@ A `pages` collection is created with `versions: { drafts: true }` and access con
|
||||
})
|
||||
```
|
||||
|
||||
The [`cors`](https://payloadcms.com/docs/production/preventing-abuse#cross-origin-resource-sharing-cors), [`csrf`](https://payloadcms.com/docs/production/preventing-abuse#cross-site-request-forgery-csrf), and [`cookies`](https://payloadcms.com/docs/authentication/config#options) settings are also configured to ensure that the admin panel and front-end can communicate with each other securely.
|
||||
For more details on how to extend this functionality, see the [Authentication](https://payloadcms.com/docs/authentication) docs.
|
||||
|
||||
### Preview Mode
|
||||
|
||||
To enter preview mode we format a custom URL using a [preview function](https://payloadcms.com/docs/configuration/collections#preview) in the collection config. When a user clicks the "Preview" button, they are routed to this URL along with their http-only cookies and revalidation key. Your front-end can then use the `payload-token` and revalidation key to verify the request and enter into its own preview mode.
|
||||
To enter preview mode, 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` and 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.
|
||||
|
||||
### ISR
|
||||
> "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.
|
||||
|
||||
If your front-end is statically generated then you may also want to regenerate the HTML for each page as they are published, sometimes referred to as Incremental Static Regeneration. To do this, we add an `afterChange` hook to the collection that fires a request to your front-end in the background each time the document is updated. You can handle this request on your front-end and regenerate the HTML for your page however needed.
|
||||
### On-demand Revalidation
|
||||
|
||||
If your front-end is statically generated then you may also want to regenerate the HTML for each page individually as they are published, referred to as On-demand Revalidation. This will prevent your static site from having to fully rebuild every page in order to deploy content changes. To do this, we add an `afterChange` hook to the collection that fires a request to your front-end in the background each time the document is updated. You can handle this request on your front-end to revalidate the HTML for your page.
|
||||
|
||||
> On-demand revalidation looks differently for every front-end framework. For instance, check out the differences between Next.js on-demand revalidation in the [Pages Router](https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration) and the [App Router](https://nextjs.org/docs/app/building-your-application/data-fetching/revalidating#on-demand-revalidation). In Next.js, methods are provided that regenerate the HTML for each page, but this may not be the case for all frameworks.
|
||||
|
||||
### Admin Bar
|
||||
|
||||
You might also want to render an admin bar on your front-end so that logged-in users can quickly navigate between the front-end and Payload as they're editing. For React apps, check out the official [Payload Admin Bar](https://github.com/payloadcms/payload-admin-bar). For other frameworks, simply hit the `/me` route with `credentials: include` and render your own admin bar if the user is logged in in order to display quick links to your CMS.
|
||||
|
||||
### CORS
|
||||
|
||||
The [`cors`](https://payloadcms.com/docs/production/preventing-abuse#cross-origin-resource-sharing-cors), [`csrf`](https://payloadcms.com/docs/production/preventing-abuse#cross-site-request-forgery-csrf), and [`cookies`](https://payloadcms.com/docs/authentication/config#options) settings are configured to ensure that the admin panel and front-end can communicate with each other securely. If you are combining your front-end and admin panel into a single application that runs of a shared port and domain, you can remove these settings from your config.
|
||||
|
||||
For more details on this, see the [CORS](https://payloadcms.com/docs/production/preventing-abuse#cross-origin-resource-sharing-cors) docs.
|
||||
|
||||
## Development
|
||||
|
||||
To spin up this example locally, follow the [Quick Start](#quick-start).
|
||||
|
||||
|
||||
### Seed
|
||||
|
||||
On boot, a seed script is included to create a user, a home page, and an example page with two versions, one published and one draft.
|
||||
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_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.
|
||||
|
||||
> 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.
|
||||
|
||||
@@ -63,7 +92,7 @@ To run Payload in production, you need to build and serve the Admin panel. To do
|
||||
|
||||
### Deployment
|
||||
|
||||
The easiest way to deploy your project is to use [Payload Cloud](https://payloadcms.com/new/import), a one-click hosting solution to deploy production-ready instances of your Payload apps directly from your GitHub repo. You can also deploy your app manually, check out the [deployment documentation](https://payloadcms.com/docs/production/deployment) for full details.
|
||||
The easiest way to deploy your project is to use [Payload Cloud](https://payloadcms.com/new/import), a one-click hosting solution to deploy production-ready instances of your Payload apps directly from your GitHub repo. You can also choose to self-host your app, check out the [Deployment](https://payloadcms.com/docs/production/deployment) docs for more details.
|
||||
|
||||
## Questions
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"main": "dist/server.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "cross-env PAYLOAD_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
|
||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
|
||||
"seed": "rm -rf media && cross-env PAYLOAD_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",
|
||||
@@ -19,10 +20,10 @@
|
||||
"dependencies": {
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "^1.7.5"
|
||||
"payload": "^1.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "^0.0.1",
|
||||
"@payloadcms/eslint-config": "^0.0.2",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/node": "18.11.3",
|
||||
"@types/react": "18.0.21",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
import richText from '../../fields/richText'
|
||||
import { loggedIn } from './access/loggedIn'
|
||||
import { publishedOrLoggedIn } from './access/publishedOrLoggedIn'
|
||||
@@ -10,8 +11,13 @@ export const Pages: CollectionConfig = {
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'slug', 'updatedAt'],
|
||||
preview: doc =>
|
||||
`${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/preview?url=${formatAppURL({ doc })}`,
|
||||
preview: doc => {
|
||||
return `${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/preview?url=${encodeURIComponent(
|
||||
formatAppURL({
|
||||
doc,
|
||||
}),
|
||||
)}&secret=${process.env.PAYLOAD_PUBLIC_DRAFT_SECRET}`
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
drafts: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Field } from 'payload/types'
|
||||
|
||||
import deepMerge from '../utilities/deepMerge'
|
||||
|
||||
export const appearanceOptions = {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { RichTextElement, RichTextField, RichTextLeaf } from 'payload/dist/fields/config/types'
|
||||
|
||||
import deepMerge from '../../utilities/deepMerge'
|
||||
import link from '../link'
|
||||
import elements from './elements'
|
||||
import leaves from './leaves'
|
||||
import link from '../link'
|
||||
|
||||
type RichText = (
|
||||
overrides?: Partial<RichTextField>,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { GlobalConfig } from 'payload/types'
|
||||
|
||||
import link from '../fields/link'
|
||||
|
||||
export const MainMenu: GlobalConfig = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
@@ -7,49 +8,52 @@
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
pages: Page
|
||||
users: User
|
||||
}
|
||||
pages: Page;
|
||||
users: User;
|
||||
};
|
||||
globals: {
|
||||
'main-menu': MainMenu
|
||||
}
|
||||
'main-menu': MainMenu;
|
||||
};
|
||||
}
|
||||
export interface Page {
|
||||
id: string
|
||||
title: string
|
||||
slug?: string
|
||||
richText: Array<{
|
||||
[k: string]: unknown
|
||||
}>
|
||||
_status?: 'draft' | 'published'
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
password?: string
|
||||
id: string;
|
||||
title: string;
|
||||
slug?: string;
|
||||
richText: {
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: 'draft' | 'published';
|
||||
}
|
||||
export interface User {
|
||||
id: string
|
||||
email?: string
|
||||
resetPasswordToken?: string
|
||||
resetPasswordExpiration?: string
|
||||
loginAttempts?: number
|
||||
lockUntil?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
password?: string
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
salt?: string;
|
||||
hash?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
password?: string;
|
||||
}
|
||||
export interface MainMenu {
|
||||
id: string
|
||||
navItems: Array<{
|
||||
id: string;
|
||||
navItems?: {
|
||||
link: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
type?: 'reference' | 'custom';
|
||||
newTab?: boolean;
|
||||
reference: {
|
||||
value: string | Page
|
||||
relationTo: 'pages'
|
||||
}
|
||||
url: string
|
||||
label: string
|
||||
}
|
||||
id?: string
|
||||
}>
|
||||
value: string | Page;
|
||||
relationTo: 'pages';
|
||||
};
|
||||
url: string;
|
||||
label: string;
|
||||
};
|
||||
id?: string;
|
||||
}[];
|
||||
updatedAt?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { buildConfig } from 'payload/config'
|
||||
import path from 'path'
|
||||
import { Users } from './collections/Users'
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
import { Pages } from './collections/Pages'
|
||||
import { Users } from './collections/Users'
|
||||
import { MainMenu } from './globals/MainMenu'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [Pages, Users],
|
||||
cors: [process.env.PAYLOAD_PUBLIC_SERVER_URL, process.env.PAYLOAD_PUBLIC_SITE_URL],
|
||||
csrf: [process.env.PAYLOAD_PUBLIC_SERVER_URL, process.env.PAYLOAD_PUBLIC_SITE_URL],
|
||||
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
|
||||
cors: [
|
||||
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
|
||||
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
|
||||
].filter(Boolean),
|
||||
csrf: [
|
||||
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
|
||||
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
|
||||
].filter(Boolean),
|
||||
globals: [MainMenu],
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, 'payload-types.ts'),
|
||||
|
||||
@@ -45,18 +45,10 @@ export const home: Partial<Page> = {
|
||||
{ children: [{ text: '' }] },
|
||||
{
|
||||
children: [
|
||||
{ text: 'Visit the ' },
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
url: 'http://localhost:3000/example-page',
|
||||
children: [{ text: 'example page' }],
|
||||
},
|
||||
{ text: ' to see how access to draft content is controlled. ' },
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
url: 'http://localhost:8000/admin',
|
||||
url: 'http://localhost:3000/admin',
|
||||
newTab: true,
|
||||
children: [{ text: 'Log in' }],
|
||||
},
|
||||
@@ -69,8 +61,15 @@ export const home: Partial<Page> = {
|
||||
children: [{ text: 'Payload Admin Bar' }],
|
||||
},
|
||||
{
|
||||
text: ' appear at the top of the viewport so you can seamlessly navigate between the two apps.',
|
||||
text: ' appear at the top of the viewport. This will allow you to seamlessly navigate between the two apps. Then, navigate to the ',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
url: 'http://localhost:3001/example-page',
|
||||
children: [{ text: 'example page' }],
|
||||
},
|
||||
{ text: ' to see how access to draft content is controlled. ' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import { home } from './home'
|
||||
import { examplePage } from './page'
|
||||
import { examplePageDraft } from './pageDraft'
|
||||
import { home } from './home'
|
||||
|
||||
export const seed = async (payload: Payload): Promise<void> => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'test',
|
||||
email: 'demo@payloadcms.com',
|
||||
password: 'demo',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -26,7 +27,7 @@ export const seed = async (payload: Payload): Promise<void> => {
|
||||
|
||||
const homepageJSON = JSON.parse(JSON.stringify(home).replace('{{DRAFT_PAGE_ID}}', examplePageID))
|
||||
|
||||
await payload.create({
|
||||
const { id: homePageID } = await payload.create({
|
||||
collection: 'pages',
|
||||
data: homepageJSON,
|
||||
})
|
||||
@@ -37,10 +38,13 @@ export const seed = async (payload: Payload): Promise<void> => {
|
||||
navItems: [
|
||||
{
|
||||
link: {
|
||||
type: 'custom',
|
||||
reference: null,
|
||||
label: 'Dashboard',
|
||||
url: 'http://localhost:8000/admin',
|
||||
type: 'reference',
|
||||
reference: {
|
||||
relationTo: 'pages',
|
||||
value: homePageID,
|
||||
},
|
||||
label: 'Home',
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -54,6 +58,14 @@ export const seed = async (payload: Payload): Promise<void> => {
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
link: {
|
||||
type: 'custom',
|
||||
reference: null,
|
||||
label: 'Dashboard',
|
||||
url: 'http://localhost:3000/admin',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -20,12 +20,12 @@ export const examplePage: Partial<Page> = {
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
url: 'http://localhost:8000/admin',
|
||||
url: 'http://localhost:3000/admin',
|
||||
newTab: true,
|
||||
children: [{ text: 'Log in' }],
|
||||
},
|
||||
{
|
||||
text: ' to the admin panel and click "preview" to return to this page and view the latest draft content in Next.js preview mode.',
|
||||
text: ' to the admin panel and click "preview" to return to this page and view the latest draft content in Next.js preview mode. To make additional changes to the draft, click "save draft" before returning to the preview.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -17,12 +17,12 @@ export const examplePageDraft: Partial<Page> = {
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
url: 'http://localhost:8000/admin/logout',
|
||||
url: 'http://localhost:3000/admin/logout',
|
||||
newTab: true,
|
||||
children: [{ text: 'Log out' }],
|
||||
},
|
||||
{
|
||||
text: ' or exit Next.js preview mode to see the latest published content.',
|
||||
text: ' or click "exit preview mode" from the Payload Admin Bar to see the latest published content. To make additional changes to the draft, click "save draft" before returning to the preview.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import payload from 'payload'
|
||||
import { seed } from './seed'
|
||||
|
||||
// eslint-disable-next-line
|
||||
require('dotenv').config({
|
||||
dotenv.config({
|
||||
path: path.resolve(__dirname, '../.env'),
|
||||
})
|
||||
|
||||
import express from 'express'
|
||||
import payload from 'payload'
|
||||
|
||||
import { seed } from './seed'
|
||||
|
||||
const app = express()
|
||||
|
||||
app.get('/', (_, res) => {
|
||||
@@ -29,7 +31,7 @@ const start = async (): Promise<void> => {
|
||||
await seed(payload)
|
||||
}
|
||||
|
||||
app.listen(8000)
|
||||
app.listen(3000)
|
||||
}
|
||||
|
||||
start()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
3
examples/preview/next-app/.env.example
Normal file
3
examples/preview/next-app/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
NEXT_PUBLIC_CMS_URL=http://localhost:3000
|
||||
NEXT_PRIVATE_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
|
||||
NEXT_PRIVATE_REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
|
||||
7
examples/preview/next-app/.eslintrc.js
Normal file
7
examples/preview/next-app/.eslintrc.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['plugin:@next/next/recommended', '@payloadcms'],
|
||||
rules: {
|
||||
'import/extensions': 'off',
|
||||
},
|
||||
}
|
||||
37
examples/preview/next-app/README.md
Normal file
37
examples/preview/next-app/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Payload Preview Example Front-End
|
||||
|
||||
This is a [Next.js](https://nextjs.org) app using the [App Router](https://nextjs.org/docs/app). It was made explicitly for Payload's [Preview Example](https://github.com/payloadcms/payload/tree/master/examples/preview/cms).
|
||||
|
||||
> This example uses the App Router, the latest API of Next.js. If your app is using the legacy [Pages Router](https://nextjs.org/docs/pages), check out the official [Pages Router Example](https://github.com/payloadcms/payload/tree/master/examples/preview/next-pages).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Payload
|
||||
|
||||
First you'll need a running [Payload](https://github.com/payloadcms/payload) app. If you have not done so already, open up the `cms` folder and follow the setup instructions. Take note of your `serverURL`, you'll need this in the next step.
|
||||
|
||||
### Next.js
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server
|
||||
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 [Preview Example](https://github.com/payloadcms/payload/tree/master/examples/preview/cms) for full details.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Payload and Next.js, take a look at the following resources:
|
||||
|
||||
- [Payload Documentation](https://payloadcms.com/docs) - learn about Payload features and API.
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Payload GitHub repository](https://github.com/payloadcms/payload) as well as [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deployment
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. You could also combine this app into a [single Express server](https://github.com/payloadcms/payload/tree/master/examples/custom-server) and deploy in to [Payload Cloud](https://payloadcms.com/new/import).
|
||||
|
||||
Check out our [Payload deployment documentation](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
3
examples/preview/next-app/app/[slug]/index.module.scss
Normal file
3
examples/preview/next-app/app/[slug]/index.module.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.page {
|
||||
margin-top: calc(var(--base) * 2);
|
||||
}
|
||||
46
examples/preview/next-app/app/[slug]/page.tsx
Normal file
46
examples/preview/next-app/app/[slug]/page.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
import { Page } from '../../payload-types'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import RichText from '../_components/RichText'
|
||||
import { fetchPage, fetchPages } from '../cms'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
interface PageParams {
|
||||
params: { slug: string }
|
||||
}
|
||||
|
||||
export const PageTemplate: React.FC<{ page: Page | null | undefined }> = ({ page }) => (
|
||||
<main className={classes.page}>
|
||||
<Gutter>
|
||||
<h1>{page?.title}</h1>
|
||||
<RichText content={page?.richText} />
|
||||
</Gutter>
|
||||
</main>
|
||||
)
|
||||
|
||||
export default async function Page({ params: { slug = 'home' } }: PageParams) {
|
||||
const { isEnabled: isDraftMode } = draftMode()
|
||||
|
||||
const page = await fetchPage(slug, isDraftMode)
|
||||
|
||||
if (page === null) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return <PageTemplate page={page} />
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const pages = await fetchPages()
|
||||
|
||||
return pages.map(({ slug }) =>
|
||||
slug !== 'home'
|
||||
? {
|
||||
slug,
|
||||
}
|
||||
: {},
|
||||
) // eslint-disable-line function-paren-newline
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { PayloadAdminBar, PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
|
||||
|
||||
import { Gutter } from '../Gutter'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const Title: React.FC = () => <span>Dashboard</span>
|
||||
|
||||
export const AdminBarClient: React.FC<PayloadAdminBarProps> = props => {
|
||||
const [user, setUser] = useState<PayloadMeUser>()
|
||||
|
||||
return (
|
||||
<div className={[classes.adminBar, user && classes.show].filter(Boolean).join(' ')}>
|
||||
<Gutter className={classes.container}>
|
||||
<PayloadAdminBar
|
||||
{...props}
|
||||
logo={<Title />}
|
||||
cmsURL={process.env.NEXT_PUBLIC_CMS_URL}
|
||||
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={{
|
||||
position: 'relative',
|
||||
zIndex: 'unset',
|
||||
padding: 0,
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
/>
|
||||
</Gutter>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
.adminBar {
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
background-color: var(--color-dark-gray);
|
||||
color: var(--color-white);
|
||||
padding: calc(var(--base) * 0.25) 0;
|
||||
font-size: calc(#{var(--html-font-size)} * 1px);
|
||||
background-color: rgb(var(--foreground-rgb));
|
||||
padding: calc(var(--base) * 0.5) 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.payloadAdminBar {
|
||||
color: rgb(var(--background-rgb)) !important;
|
||||
}
|
||||
|
||||
.show {
|
||||
display: block;
|
||||
}
|
||||
@@ -39,6 +41,6 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: var(--color-light-gray);
|
||||
background-color: rbg(var(--background-rgb));
|
||||
height: 2px;
|
||||
}
|
||||
16
examples/preview/next-app/app/_components/AdminBar/index.tsx
Normal file
16
examples/preview/next-app/app/_components/AdminBar/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
import { AdminBarClient } from './index.client'
|
||||
|
||||
export function AdminBar() {
|
||||
const { isEnabled: isPreviewMode } = draftMode()
|
||||
|
||||
return (
|
||||
<AdminBarClient
|
||||
preview={isPreviewMode}
|
||||
// id={page?.id} // TODO: is there any way to do this?!
|
||||
collection="pages"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
@import '../../css/type.scss';
|
||||
|
||||
.button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
@@ -21,7 +19,6 @@
|
||||
}
|
||||
|
||||
.label {
|
||||
@extend %label;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
71
examples/preview/next-app/app/_components/Button/index.tsx
Normal file
71
examples/preview/next-app/app/_components/Button/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { ElementType } 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
|
||||
}
|
||||
|
||||
export const Button: React.FC<Props> = ({
|
||||
el: elFromProps = 'link',
|
||||
label,
|
||||
newTab,
|
||||
href,
|
||||
appearance,
|
||||
className: classNameFromProps,
|
||||
onClick,
|
||||
type = 'button',
|
||||
disabled,
|
||||
}) => {
|
||||
let el = elFromProps
|
||||
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
|
||||
const className = [
|
||||
classes.button,
|
||||
classNameFromProps,
|
||||
classes[`appearance--${appearance}`],
|
||||
classes.button,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
const content = (
|
||||
<div className={classes.content}>
|
||||
{/* <Chevron /> */}
|
||||
<span className={classes.label}>{label}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (onClick || type === 'submit') el = 'button'
|
||||
|
||||
if (el === 'link') {
|
||||
return (
|
||||
<Link href={href || ''} className={className} {...newTabProps} onClick={onClick}>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const Element: ElementType = el
|
||||
|
||||
return (
|
||||
<Element
|
||||
href={href}
|
||||
className={className}
|
||||
type={type}
|
||||
{...newTabProps}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{content}
|
||||
</Element>
|
||||
)
|
||||
}
|
||||
66
examples/preview/next-app/app/_components/CMSLink/index.tsx
Normal file
66
examples/preview/next-app/app/_components/CMSLink/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { 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
|
||||
}
|
||||
|
||||
export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
type,
|
||||
url,
|
||||
newTab,
|
||||
reference,
|
||||
label,
|
||||
appearance,
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
const href =
|
||||
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
|
||||
? `/${reference.value.slug === 'home' ? '' : reference.value.slug}`
|
||||
: url
|
||||
|
||||
if (!appearance) {
|
||||
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
|
||||
|
||||
if (type === 'custom') {
|
||||
return (
|
||||
<a href={url} {...newTabProps} className={className}>
|
||||
{label && label}
|
||||
{children && children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={href} {...newTabProps} className={className} prefetch={false}>
|
||||
{label && label}
|
||||
{children && children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const buttonProps = {
|
||||
newTab,
|
||||
href,
|
||||
appearance,
|
||||
label,
|
||||
}
|
||||
|
||||
return <Button className={className} {...buttonProps} el="link" />
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
.gutter {
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.gutterLeft {
|
||||
padding-left: var(--gutter-h);
|
||||
}
|
||||
@@ -16,7 +16,12 @@ export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props,
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={[left && classes.gutterLeft, right && classes.gutterRight, className]
|
||||
className={[
|
||||
classes.gutter,
|
||||
left && classes.gutterLeft,
|
||||
right && classes.gutterRight,
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
@@ -0,0 +1,32 @@
|
||||
.header {
|
||||
padding: var(--base) 0;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: calc(var(--base) / 2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--base);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
gap: 0 calc(var(--base) / 2);
|
||||
}
|
||||
}
|
||||
49
examples/preview/next-app/app/_components/Header/index.tsx
Normal file
49
examples/preview/next-app/app/_components/Header/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { MainMenu } from '../../../payload-types'
|
||||
import { CMSLink } from '../CMSLink'
|
||||
import { Gutter } from '../Gutter'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export async function Header() {
|
||||
const mainMenu: MainMenu = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_CMS_URL}/api/globals/main-menu`,
|
||||
).then(res => res.json())
|
||||
|
||||
const { navItems } = mainMenu
|
||||
|
||||
const hasNavItems = navItems && Array.isArray(navItems) && navItems.length > 0
|
||||
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<Gutter className={classes.wrap}>
|
||||
<Link href="/" className={classes.logo}>
|
||||
<picture>
|
||||
<source
|
||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-light.svg"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<Image
|
||||
width={150}
|
||||
height={30}
|
||||
alt="Payload Logo"
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-dark.svg"
|
||||
/>
|
||||
</picture>
|
||||
</Link>
|
||||
{hasNavItems && (
|
||||
<nav className={classes.nav}>
|
||||
{navItems.map(({ link }, i) => {
|
||||
return <CMSLink key={i} {...link} />
|
||||
})}
|
||||
</nav>
|
||||
)}
|
||||
</Gutter>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -2,4 +2,8 @@
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import escapeHTML from 'escape-html'
|
||||
import { Text } from 'slate'
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
type Children = Leaf[]
|
||||
|
||||
type Leaf = {
|
||||
type: string
|
||||
value?: {
|
||||
url: string
|
||||
alt: string
|
||||
}
|
||||
children: Children
|
||||
url?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const serialize = (children: Children): React.ReactNode[] =>
|
||||
children.map((node, i) => {
|
||||
if (Text.isText(node)) {
|
||||
let text = <span dangerouslySetInnerHTML={{ __html: escapeHTML(node.text) }} />
|
||||
|
||||
if (node.bold) {
|
||||
text = <strong key={i}>{text}</strong>
|
||||
}
|
||||
|
||||
if (node.code) {
|
||||
text = <code key={i}>{text}</code>
|
||||
}
|
||||
|
||||
if (node.italic) {
|
||||
text = <em key={i}>{text}</em>
|
||||
}
|
||||
|
||||
if (node.underline) {
|
||||
text = (
|
||||
<span style={{ textDecoration: 'underline' }} key={i}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (node.strikethrough) {
|
||||
text = (
|
||||
<span style={{ textDecoration: 'line-through' }} key={i}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return <Fragment key={i}>{text}</Fragment>
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case 'h1':
|
||||
return <h1 key={i}>{serialize(node.children)}</h1>
|
||||
case 'h2':
|
||||
return <h2 key={i}>{serialize(node.children)}</h2>
|
||||
case 'h3':
|
||||
return <h3 key={i}>{serialize(node.children)}</h3>
|
||||
case 'h4':
|
||||
return <h4 key={i}>{serialize(node.children)}</h4>
|
||||
case 'h5':
|
||||
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':
|
||||
return (
|
||||
<a href={escapeHTML(node.url)} key={i}>
|
||||
{serialize(node.children)}
|
||||
</a>
|
||||
)
|
||||
|
||||
default:
|
||||
return <p key={i}>{serialize(node.children)}</p>
|
||||
}
|
||||
})
|
||||
|
||||
export default serialize
|
||||
6
examples/preview/next-app/app/api/exit-preview/route.ts
Normal file
6
examples/preview/next-app/app/api/exit-preview/route.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
draftMode().disable()
|
||||
return new Response('Draft mode is disabled')
|
||||
}
|
||||
47
examples/preview/next-app/app/api/preview/route.ts
Normal file
47
examples/preview/next-app/app/api/preview/route.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export async function GET(
|
||||
req: Request & {
|
||||
cookies: {
|
||||
get: (name: string) => {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
},
|
||||
): Promise<Response> {
|
||||
const payloadToken = req.cookies.get('payload-token')?.value
|
||||
const { searchParams } = new URL(req.url)
|
||||
const url = searchParams.get('url')
|
||||
const secret = searchParams.get('secret')
|
||||
|
||||
if (!url) {
|
||||
return new Response('No URL provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!payloadToken) {
|
||||
new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
// validate the Payload token
|
||||
const userReq = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/me`, {
|
||||
headers: {
|
||||
Authorization: `JWT ${payloadToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
const userRes = await userReq.json()
|
||||
|
||||
if (!userReq.ok || !userRes?.user) {
|
||||
draftMode().disable()
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
if (secret !== process.env.NEXT_PRIVATE_DRAFT_SECRET) {
|
||||
return new Response('Invalid token', { status: 401 })
|
||||
}
|
||||
|
||||
draftMode().enable()
|
||||
|
||||
redirect(url)
|
||||
}
|
||||
25
examples/preview/next-app/app/api/revalidate/route.ts
Normal file
25
examples/preview/next-app/app/api/revalidate/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(request: NextRequest): Promise<unknown> {
|
||||
const path = request.nextUrl.searchParams.get('revalidatePath')
|
||||
const secret = request.nextUrl.searchParams.get('secret')
|
||||
|
||||
if (secret !== process.env.NEXT_PRIVATE_REVALIDATION_KEY) {
|
||||
return NextResponse.json({ revalidated: false, now: Date.now() })
|
||||
}
|
||||
|
||||
if (typeof path === 'string') {
|
||||
// there is a known bug with `revalidatePath` where it will not revalidate exact paths of dynamic routes
|
||||
// instead, Next.js expects us to revalidate entire directories, i.e. `/[slug]` instead of `/example-page`
|
||||
// for now we'll make this change but with expectation that it will be fixed so we can use `revalidatePath('/example-page')`
|
||||
// - https://github.com/vercel/next.js/issues/49387
|
||||
// - https://github.com/vercel/next.js/issues/49778#issuecomment-1547028830
|
||||
// revalidatePath(path)
|
||||
revalidatePath('/[slug]')
|
||||
return NextResponse.json({ revalidated: true, now: Date.now() })
|
||||
}
|
||||
|
||||
return NextResponse.json({ revalidated: false, now: Date.now() })
|
||||
}
|
||||
107
examples/preview/next-app/app/app.scss
Normal file
107
examples/preview/next-app/app/app.scss
Normal file
@@ -0,0 +1,107 @@
|
||||
$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;
|
||||
}
|
||||
}
|
||||
39
examples/preview/next-app/app/cms.ts
Normal file
39
examples/preview/next-app/app/cms.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
import type { Page } from '../payload-types'
|
||||
|
||||
export const fetchPage = async (
|
||||
slug: string,
|
||||
draft?: boolean,
|
||||
): Promise<Page | undefined | null> => {
|
||||
const payloadToken = cookies().get('payload-token')
|
||||
|
||||
const pageRes: {
|
||||
docs: Page[]
|
||||
} = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_CMS_URL}/api/pages?where[slug][equals]=${slug}${
|
||||
draft && payloadToken ? '&draft=true' : ''
|
||||
}`,
|
||||
{
|
||||
...(draft && payloadToken
|
||||
? {
|
||||
headers: {
|
||||
Authorization: `JWT ${payloadToken?.value}`,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
).then(res => res.json())
|
||||
|
||||
return pageRes?.docs?.[0] ?? null
|
||||
}
|
||||
|
||||
export const fetchPages = async (): Promise<Page[]> => {
|
||||
const pageRes: {
|
||||
docs: Page[]
|
||||
} = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/pages?depth=0&limit=100`).then(res =>
|
||||
res.json(),
|
||||
) // eslint-disable-line function-paren-newline
|
||||
|
||||
return pageRes?.docs ?? []
|
||||
}
|
||||
32
examples/preview/next-app/app/layout.tsx
Normal file
32
examples/preview/next-app/app/layout.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { AdminBar } from './_components/AdminBar'
|
||||
import { Header } from './_components/Header'
|
||||
|
||||
import './app.scss'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||
const { children } = props
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<AdminBar />
|
||||
{/* The error ignored here is related to `@types/react` and `typescript` not
|
||||
aligning with their implementations of Promise-based server components.
|
||||
This can be removed once these dependencies are resolved in their respective modules.
|
||||
- https://github.com/vercel/next.js/issues/42292
|
||||
- https://github.com/vercel/next.js/issues/43537
|
||||
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>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
3
examples/preview/next-app/app/page.tsx
Normal file
3
examples/preview/next-app/app/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import Page from './[slug]/page'
|
||||
|
||||
export default Page
|
||||
4
examples/preview/next-app/next.config.js
Normal file
4
examples/preview/next-app/next.config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
|
||||
module.exports = nextConfig
|
||||
40
examples/preview/next-app/package.json
Normal file
40
examples/preview/next-app/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "payload-nextjs-preview-example",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3001",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3001",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-html": "^1.0.3",
|
||||
"next": "^13.4.8",
|
||||
"payload-admin-bar": "^1.0.6",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^13.4.8",
|
||||
"@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",
|
||||
"@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-prettier": "^8.5.0",
|
||||
"eslint-plugin-filenames": "^1.3.2",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.62.1",
|
||||
"slate": "^0.82.0",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
59
examples/preview/next-app/payload-types.ts
Normal file
59
examples/preview/next-app/payload-types.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
pages: Page;
|
||||
users: User;
|
||||
};
|
||||
globals: {
|
||||
'main-menu': MainMenu;
|
||||
};
|
||||
}
|
||||
export interface Page {
|
||||
id: string;
|
||||
title: string;
|
||||
slug?: string;
|
||||
richText: {
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: 'draft' | 'published';
|
||||
}
|
||||
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;
|
||||
}
|
||||
export interface MainMenu {
|
||||
id: string;
|
||||
navItems?: {
|
||||
link: {
|
||||
type?: 'reference' | 'custom';
|
||||
newTab?: boolean;
|
||||
reference: {
|
||||
value: string | Page;
|
||||
relationTo: 'pages';
|
||||
};
|
||||
url: string;
|
||||
label: string;
|
||||
};
|
||||
id?: string;
|
||||
}[];
|
||||
updatedAt?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
BIN
examples/preview/next-app/public/favicon.ico
Normal file
BIN
examples/preview/next-app/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
15
examples/preview/next-app/public/favicon.svg
Normal file
15
examples/preview/next-app/public/favicon.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="260" height="260" viewBox="0 0 260 260" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
path {
|
||||
fill: #0F0F0F;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path d="M120.59 8.5824L231.788 75.6142V202.829L148.039 251.418V124.203L36.7866 57.2249L120.59 8.5824Z" />
|
||||
<path d="M112.123 244.353V145.073L28.2114 193.769L112.123 244.353Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 437 B |
28
examples/preview/next-app/tsconfig.json
Normal file
28
examples/preview/next-app/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
2429
examples/preview/next-app/yarn.lock
Normal file
2429
examples/preview/next-app/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
||||
NEXT_PUBLIC_CMS_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_CMS_URL=http://localhost:3000
|
||||
NEXT_PRIVATE_REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
|
||||
6
examples/preview/next-pages/.gitignore
vendored
Normal file
6
examples/preview/next-pages/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.next
|
||||
dist
|
||||
build
|
||||
node_modules
|
||||
.env
|
||||
package-lock.json
|
||||
8
examples/preview/next-pages/.prettierrc.js
Normal file
8
examples/preview/next-pages/.prettierrc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: "typescript",
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
arrowParens: "avoid",
|
||||
};
|
||||
37
examples/preview/next-pages/README.md
Normal file
37
examples/preview/next-pages/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Payload Preview Example Front-End
|
||||
|
||||
This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages). It was made explicitly for Payload's [Preview Example](https://github.com/payloadcms/payload/tree/master/examples/preview/cms).
|
||||
|
||||
> This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/app), check out the official [App Router Example](https://github.com/payloadcms/payload/tree/master/examples/preview/next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Payload
|
||||
|
||||
First you'll need a running [Payload](https://github.com/payloadcms/payload) app. If you have not done so already, open up the `cms` folder and follow the setup instructions. Take note of your `serverURL`, you'll need this in the next step.
|
||||
|
||||
### Next.js
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server
|
||||
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 [Preview Example](https://github.com/payloadcms/payload/tree/master/examples/preview/cms) for full details.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Payload and Next.js, take a look at the following resources:
|
||||
|
||||
- [Payload Documentation](https://payloadcms.com/docs) - learn about Payload features and API.
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Payload GitHub repository](https://github.com/payloadcms/payload) as well as [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deployment
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. You could also combine this app into a [single Express server](https://github.com/payloadcms/payload/tree/master/examples/custom-server) and deploy in to [Payload Cloud](https://payloadcms.com/new/import).
|
||||
|
||||
Check out our [Payload deployment documentation](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
5
examples/preview/next-pages/next-env.d.ts
vendored
Normal file
5
examples/preview/next-pages/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -3,7 +3,7 @@ const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
images: {
|
||||
domains: ['localhost', process.env.NEXT_PUBLIC_CMS_URL],
|
||||
domains: ['localhost', process.env.NEXT_PUBLIC_CMS_URL || ''].filter(Boolean),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
{
|
||||
"name": "nextjs-preview-example",
|
||||
"name": "payload-nextjs-preview-example",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev -p 3001",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "next start -p 3001",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-html": "^1.0.3",
|
||||
"next": "^13.1.6",
|
||||
"payload-admin-bar": "^1.0.5",
|
||||
"next": "^13.4.8",
|
||||
"payload-admin-bar": "^1.0.6",
|
||||
"qs": "^6.11.0",
|
||||
"react": "18.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "18.2.0",
|
||||
"sass": "^1.55.0",
|
||||
"slate": "^0.84.0"
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^13.1.6",
|
||||
"@next/eslint-plugin-next": "^13.4.8",
|
||||
"@payloadcms/eslint-config": "^0.0.2",
|
||||
"@types/node": "18.11.3",
|
||||
"@types/react": "18.0.21",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"@payloadcms/eslint-config": "^0.0.1",
|
||||
"eslint": "8.25.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-filenames": "^1.3.2",
|
||||
@@ -34,6 +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",
|
||||
"slate": "^0.82.0",
|
||||
"typescript": "4.8.4"
|
||||
}
|
||||
}
|
||||
BIN
examples/preview/next-pages/public/favicon.ico
Normal file
BIN
examples/preview/next-pages/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
15
examples/preview/next-pages/public/favicon.svg
Normal file
15
examples/preview/next-pages/public/favicon.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="260" height="260" viewBox="0 0 260 260" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
path {
|
||||
fill: #0F0F0F;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path d="M120.59 8.5824L231.788 75.6142V202.829L148.039 251.418V124.203L36.7866 57.2249L120.59 8.5824Z" />
|
||||
<path d="M112.123 244.353V145.073L28.2114 193.769L112.123 244.353Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 437 B |
@@ -0,0 +1,46 @@
|
||||
.adminBar {
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
background-color: rgb(var(--foreground-rgb));
|
||||
padding: calc(var(--base) * 0.5) 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.payloadAdminBar {
|
||||
color: rgb(var(--background-rgb)) !important;
|
||||
}
|
||||
|
||||
.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react'
|
||||
import { PayloadAdminBar, PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
|
||||
|
||||
import { Gutter } from '../../Gutter'
|
||||
import { Gutter } from '../Gutter'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const Title: React.FC = () => <span>Dashboard</span>
|
||||
|
||||
export const AdminBar: React.FC<{
|
||||
adminBarProps?: PayloadAdminBarProps
|
||||
user?: PayloadMeUser
|
||||
@@ -17,6 +19,7 @@ export const AdminBar: React.FC<{
|
||||
<Gutter className={classes.container}>
|
||||
<PayloadAdminBar
|
||||
{...adminBarProps}
|
||||
logo={<Title />}
|
||||
cmsURL={process.env.NEXT_PUBLIC_CMS_URL}
|
||||
onAuthChange={setUser}
|
||||
className={classes.payloadAdminBar}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
}) => {
|
||||
const href =
|
||||
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
|
||||
? `/${reference.value.slug}`
|
||||
? `/${reference.value.slug === 'home' ? '' : reference.value.slug}`
|
||||
: url
|
||||
|
||||
if (!appearance) {
|
||||
@@ -47,7 +47,7 @@ export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={href} {...newTabProps} className={className}>
|
||||
<Link href={href} {...newTabProps} className={className} prefetch={false}>
|
||||
{label && label}
|
||||
{children && children}
|
||||
</Link>
|
||||
@@ -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);
|
||||
}
|
||||
33
examples/preview/next-pages/src/components/Gutter/index.tsx
Normal file
33
examples/preview/next-pages/src/components/Gutter/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React, { forwardRef, Ref } from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
left?: boolean
|
||||
right?: boolean
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
ref?: Ref<HTMLDivElement>
|
||||
}
|
||||
|
||||
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const { left = true, right = true, className, children } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={[
|
||||
classes.gutter,
|
||||
left && classes.gutterLeft,
|
||||
right && classes.gutterRight,
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
Gutter.displayName = 'Gutter'
|
||||
@@ -0,0 +1,32 @@
|
||||
.header {
|
||||
padding: var(--base) 0;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: calc(var(--base) / 2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--base);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
gap: 0 calc(var(--base) / 2);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
|
||||
|
||||
import { MainMenu } from '../../payload-types'
|
||||
import { AdminBar } from '../AdminBar'
|
||||
import { CMSLink } from '../CMSLink'
|
||||
import { Gutter } from '../Gutter'
|
||||
import { Logo } from '../Logo'
|
||||
import { AdminBar } from './AdminBar'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
@@ -18,8 +18,19 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({ children }) => {
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<Gutter className={classes.wrap}>
|
||||
<Link href="/">
|
||||
<Logo />
|
||||
<Link href="/" className={classes.logo}>
|
||||
<picture>
|
||||
<source
|
||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-light.svg"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<Image
|
||||
width={150}
|
||||
height={30}
|
||||
alt="Payload Logo"
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-dark.svg"
|
||||
/>
|
||||
</picture>
|
||||
</Link>
|
||||
{children}
|
||||
</Gutter>
|
||||
@@ -0,0 +1,9 @@
|
||||
.richText {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
|
||||
import serialize from './serialize'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const RichText: React.FC<{ className?: string; content: any }> = ({ className, content }) => {
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
|
||||
{serialize(content)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RichText
|
||||
3
examples/preview/next-pages/src/pages/[slug].module.scss
Normal file
3
examples/preview/next-pages/src/pages/[slug].module.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.page {
|
||||
margin-top: calc(var(--base) * 2);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { Gutter } from '../components/Gutter'
|
||||
import RichText from '../components/RichText'
|
||||
import type { MainMenu, Page as PageType } from '../payload-types'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
import classes from './[slug].module.scss';
|
||||
|
||||
const Page: React.FC<
|
||||
PageType & {
|
||||
@@ -18,9 +18,9 @@ const Page: React.FC<
|
||||
const { title, richText } = props
|
||||
|
||||
return (
|
||||
<main>
|
||||
<main className={classes.page}>
|
||||
<Gutter>
|
||||
<h1 className={classes.hero}>{title}</h1>
|
||||
<h1>{title}</h1>
|
||||
<RichText content={richText} />
|
||||
</Gutter>
|
||||
</main>
|
||||
@@ -6,7 +6,7 @@ import { useRouter } from 'next/router'
|
||||
import { Header } from '../components/Header'
|
||||
import { MainMenu } from '../payload-types'
|
||||
|
||||
import '../css/app.scss'
|
||||
import './app.scss'
|
||||
|
||||
export interface IGlobals {
|
||||
mainMenu: MainMenu
|
||||
@@ -57,6 +57,11 @@ const PayloadApp = (
|
||||
onPreviewExit,
|
||||
}}
|
||||
/>
|
||||
{/* 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>
|
||||
)
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
const preview = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||
const preview = async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
|
||||
const {
|
||||
cookies: { 'payload-token': payloadToken },
|
||||
query: { url },
|
||||
@@ -19,6 +19,21 @@ const preview = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||
})
|
||||
}
|
||||
|
||||
// validate the Payload token
|
||||
const userReq = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/me`, {
|
||||
headers: {
|
||||
Authorization: `JWT ${payloadToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
const userRes = await userReq.json()
|
||||
|
||||
if (!userReq.ok || !userRes?.user) {
|
||||
return res.status(401).json({
|
||||
message: 'You are not allowed to preview this page',
|
||||
})
|
||||
}
|
||||
|
||||
res.setPreviewData({
|
||||
payloadToken,
|
||||
})
|
||||
214
examples/preview/next-pages/src/pages/app.scss
Normal file
214
examples/preview/next-pages/src/pages/app.scss
Normal file
@@ -0,0 +1,214 @@
|
||||
$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;
|
||||
}
|
||||
}
|
||||
$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;
|
||||
}
|
||||
}
|
||||
59
examples/preview/next-pages/src/payload-types.ts
Normal file
59
examples/preview/next-pages/src/payload-types.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
pages: Page;
|
||||
users: User;
|
||||
};
|
||||
globals: {
|
||||
'main-menu': MainMenu;
|
||||
};
|
||||
}
|
||||
export interface Page {
|
||||
id: string;
|
||||
title: string;
|
||||
slug?: string;
|
||||
richText: {
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: 'draft' | 'published';
|
||||
}
|
||||
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;
|
||||
}
|
||||
export interface MainMenu {
|
||||
id: string;
|
||||
navItems?: {
|
||||
link: {
|
||||
type?: 'reference' | 'custom';
|
||||
newTab?: boolean;
|
||||
reference: {
|
||||
value: string | Page;
|
||||
relationTo: 'pages';
|
||||
};
|
||||
url: string;
|
||||
label: string;
|
||||
};
|
||||
id?: string;
|
||||
}[];
|
||||
updatedAt?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,35 +0,0 @@
|
||||
# Preview Example Front-End
|
||||
|
||||
This is a [Next.js](https://nextjs.org/) app made explicitly for Payload's [Preview Example](https://github.com/payloadcms/payload/tree/master/examples/preview/cms).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Payload
|
||||
|
||||
First you'll need a running Payload app. If you have not done so already, open up the `cms` folder and follow the setup instructions. Take note of your server URL, you'll need this in the next step.
|
||||
|
||||
### Next.js App
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server
|
||||
5. `open http://localhost:3000` 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 your CMS. See the [Preview Example CMS](https://github.com/payloadcms/payload/tree/master/examples/preview/cms) for full details.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about PayloadCMS and Next.js, take a look at the following resources:
|
||||
|
||||
- [Payload Documentation](https://payloadcms.com/docs) - learn about Payload features and API.
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Payload GitHub repository](https://github.com/payloadcms/payload/) as well as [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Payload deployment documentation](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
@@ -1,18 +0,0 @@
|
||||
@use '../../css/queries.scss' as *;
|
||||
|
||||
.header {
|
||||
padding: var(--base) 0;
|
||||
z-index: var(--header-z-index);
|
||||
}
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nav {
|
||||
a {
|
||||
text-decoration: none;
|
||||
margin-left: var(--base);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
export const Logo: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
width="123"
|
||||
height="29"
|
||||
viewBox="0 0 123 29"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M34.7441 22.9997H37.2741V16.3297H41.5981C44.7031 16.3297 46.9801 14.9037 46.9801 11.4537C46.9801 8.00369 44.7031 6.55469 41.5981 6.55469H34.7441V22.9997ZM37.2741 14.1447V8.73969H41.4831C43.3921 8.73969 44.3581 9.59069 44.3581 11.4537C44.3581 13.2937 43.3921 14.1447 41.4831 14.1447H37.2741Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M51.3652 23.3217C53.2742 23.3217 54.6082 22.5627 55.3672 21.3437H55.4132C55.5512 22.6777 56.1492 23.1147 57.2762 23.1147C57.6442 23.1147 58.0352 23.0687 58.4262 22.9767V21.5967C58.2882 21.6197 58.2192 21.6197 58.1502 21.6197C57.7132 21.6197 57.5982 21.1827 57.5982 20.3317V14.9497C57.5982 11.9137 55.6662 10.9017 53.2512 10.9017C49.6632 10.9017 48.1912 12.6727 48.0762 14.9267H50.3762C50.4912 13.3627 51.1122 12.7187 53.1592 12.7187C54.8842 12.7187 55.3902 13.4317 55.3902 14.2827C55.3902 15.4327 54.2632 15.6627 52.4232 16.0077C49.5022 16.5597 47.5242 17.3417 47.5242 19.9637C47.5242 21.9647 49.0192 23.3217 51.3652 23.3217ZM49.8702 19.8027C49.8702 18.5837 50.7442 18.0087 52.8142 17.5947C54.0102 17.3417 55.0222 17.0887 55.3902 16.7437V18.4227C55.3902 20.4697 53.8952 21.5047 51.8712 21.5047C50.4682 21.5047 49.8702 20.9067 49.8702 19.8027Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M61.4996 27.1167C63.3166 27.1167 64.4436 26.1737 65.5706 23.2757L70.2166 11.2697H67.8476L64.6276 20.2397H64.5816L61.1546 11.2697H58.6936L63.4316 22.8847C62.9716 24.7247 61.9136 25.1847 61.0166 25.1847C60.6486 25.1847 60.4416 25.1617 60.0506 25.1157V26.9557C60.6486 27.0707 60.9936 27.1167 61.4996 27.1167Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path d="M71.5939 22.9997H73.8479V6.55469H71.5939V22.9997Z" fill="black" />
|
||||
<path
|
||||
d="M81.6221 23.3447C85.2791 23.3447 87.4871 20.7917 87.4871 17.1117C87.4871 13.4547 85.2791 10.9017 81.6451 10.9017C77.9651 10.9017 75.7571 13.4777 75.7571 17.1347C75.7571 20.8147 77.9651 23.3447 81.6221 23.3447ZM78.1031 17.1347C78.1031 14.6737 79.2071 12.7877 81.6451 12.7877C84.0371 12.7877 85.1411 14.6737 85.1411 17.1347C85.1411 19.5727 84.0371 21.4817 81.6451 21.4817C79.2071 21.4817 78.1031 19.5727 78.1031 17.1347Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M92.6484 23.3217C94.5574 23.3217 95.8914 22.5627 96.6504 21.3437H96.6964C96.8344 22.6777 97.4324 23.1147 98.5594 23.1147C98.9274 23.1147 99.3184 23.0687 99.7094 22.9767V21.5967C99.5714 21.6197 99.5024 21.6197 99.4334 21.6197C98.9964 21.6197 98.8814 21.1827 98.8814 20.3317V14.9497C98.8814 11.9137 96.9494 10.9017 94.5344 10.9017C90.9464 10.9017 89.4744 12.6727 89.3594 14.9267H91.6594C91.7744 13.3627 92.3954 12.7187 94.4424 12.7187C96.1674 12.7187 96.6734 13.4317 96.6734 14.2827C96.6734 15.4327 95.5464 15.6627 93.7064 16.0077C90.7854 16.5597 88.8074 17.3417 88.8074 19.9637C88.8074 21.9647 90.3024 23.3217 92.6484 23.3217ZM91.1534 19.8027C91.1534 18.5837 92.0274 18.0087 94.0974 17.5947C95.2934 17.3417 96.3054 17.0887 96.6734 16.7437V18.4227C96.6734 20.4697 95.1784 21.5047 93.1544 21.5047C91.7514 21.5047 91.1534 20.9067 91.1534 19.8027Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M106.181 23.3217C108.021 23.3217 109.148 22.4477 109.792 21.6197H109.838V22.9997H112.092V6.55469H109.838V12.6957H109.792C109.148 11.7757 108.021 10.9247 106.181 10.9247C103.191 10.9247 100.914 13.2707 100.914 17.1347C100.914 20.9987 103.191 23.3217 106.181 23.3217ZM103.26 17.1347C103.26 14.8347 104.341 12.8107 106.549 12.8107C108.573 12.8107 109.815 14.4667 109.815 17.1347C109.815 19.7797 108.573 21.4587 106.549 21.4587C104.341 21.4587 103.26 19.4347 103.26 17.1347Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M12.2464 2.33838L22.2871 8.83812V21.1752L14.7265 25.8854V13.5484L4.67383 7.05725L12.2464 2.33838Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path d="M11.477 25.2017V15.5747L3.90039 20.2936L11.477 25.2017Z" fill="black" />
|
||||
<path
|
||||
d="M120.442 6.30273C119.086 6.30273 117.998 7.29978 117.998 8.75952C117.998 10.2062 119.086 11.1968 120.442 11.1968C121.791 11.1968 122.879 10.2062 122.879 8.75952C122.879 7.29978 121.791 6.30273 120.442 6.30273ZM120.442 10.7601C119.34 10.7601 118.48 9.95207 118.48 8.75952C118.48 7.54742 119.34 6.73935 120.442 6.73935C121.563 6.73935 122.397 7.54742 122.397 8.75952C122.397 9.95207 121.563 10.7601 120.442 10.7601ZM120.52 8.97457L121.048 9.9651H121.641L121.041 8.86378C121.367 8.72042 121.511 8.45975 121.511 8.17302C121.511 7.49528 121.054 7.36495 120.285 7.36495H119.49V9.9651H120.025V8.97457H120.52ZM120.37 7.78853C120.729 7.78853 120.976 7.86673 120.976 8.17953C120.976 8.43368 120.807 8.56402 120.403 8.56402H120.025V7.78853H120.37Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
@use './queries.scss' as *;
|
||||
@use './colors.scss' as *;
|
||||
@use './type.scss' as *;
|
||||
|
||||
:root {
|
||||
--breakpoint-xs-width : #{$breakpoint-xs-width};
|
||||
--breakpoint-s-width : #{$breakpoint-s-width};
|
||||
--breakpoint-m-width : #{$breakpoint-m-width};
|
||||
--breakpoint-l-width : #{$breakpoint-l-width};
|
||||
--scrollbar-width: 17px;
|
||||
|
||||
--base: 24px;
|
||||
--font-body: system-ui;
|
||||
--font-mono: 'Roboto Mono', monospace;
|
||||
|
||||
--gutter-h: 180px;
|
||||
--block-padding: 120px;
|
||||
|
||||
--header-z-index: 100;
|
||||
--modal-z-index: 90;
|
||||
|
||||
@include large-break {
|
||||
--gutter-h: 144px;
|
||||
--block-padding: 96px;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
--gutter-h: 24px;
|
||||
--block-padding: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// GLOBAL STYLES
|
||||
/////////////////////////////
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
@extend %body;
|
||||
background: var(--color-white);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
color: var(--color-black);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--color-green);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--color-green);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@extend %h1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@extend %h2;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@extend %h3;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@extend %h4;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@extend %h5;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@extend %h6;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: var(--base) 0;
|
||||
|
||||
@include mid-break {
|
||||
margin: calc(var(--base) * .75) 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: var(--base);
|
||||
margin: 0 0 var(--base);
|
||||
}
|
||||
|
||||
a {
|
||||
color: currentColor;
|
||||
|
||||
&:focus {
|
||||
opacity: .8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: .7;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
:root {
|
||||
--color-red: rgb(255,0,0);
|
||||
--color-green: rgb(178, 255, 214);
|
||||
--color-white: rgb(255, 255, 255);
|
||||
--color-dark-gray: rgb(51,52,52);
|
||||
--color-mid-gray: rgb(196,196,196);
|
||||
--color-gray: rgb(212,212,212);
|
||||
--color-light-gray: rgb(244,244,244);
|
||||
--color-black: rgb(0, 0, 0);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
@forward './queries.scss';
|
||||
@forward './type.scss';
|
||||
@@ -1,32 +0,0 @@
|
||||
$breakpoint-xs-width: 400px;
|
||||
$breakpoint-s-width: 768px;
|
||||
$breakpoint-m-width: 1024px;
|
||||
$breakpoint-l-width: 1440px;
|
||||
|
||||
////////////////////////////
|
||||
// MEDIA QUERIES
|
||||
/////////////////////////////
|
||||
|
||||
@mixin extra-small-break {
|
||||
@media (max-width: #{$breakpoint-xs-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin small-break {
|
||||
@media (max-width: #{$breakpoint-s-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin mid-break {
|
||||
@media (max-width: #{$breakpoint-m-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin large-break {
|
||||
@media (max-width: #{$breakpoint-l-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
@use 'queries' as *;
|
||||
|
||||
/////////////////////////////
|
||||
// HEADINGS
|
||||
/////////////////////////////
|
||||
|
||||
%h1,
|
||||
%h2,
|
||||
%h3,
|
||||
%h4,
|
||||
%h5,
|
||||
%h6 {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
%h1 {
|
||||
margin: 50px 0;
|
||||
font-size: 84px;
|
||||
line-height: 1;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 70px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
margin: 24px 0;
|
||||
font-size: 36px;
|
||||
line-height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
%h2 {
|
||||
margin: 32px 0;
|
||||
font-size: 56px;
|
||||
line-height: 1;
|
||||
|
||||
@include mid-break {
|
||||
margin: 36px 0;
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
margin: 24px 0;
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
%h3 {
|
||||
margin: 28px 0;
|
||||
font-size: 48px;
|
||||
line-height: 56px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 40px;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
margin: 24px 0;
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
%h4 {
|
||||
margin: 24px 0;
|
||||
font-size: 40px;
|
||||
line-height: 48px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 33px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
margin: 20px 0;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
%h5 {
|
||||
margin: 20px 0;
|
||||
font-size: 32px;
|
||||
line-height: 42px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 26px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
margin: 16px 0;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
%h6 {
|
||||
margin: 20px 0;
|
||||
font-size: 24px;
|
||||
line-height: 28px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
margin: 16px 0;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// TYPE STYLES
|
||||
/////////////////////////////
|
||||
|
||||
%body {
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
%large-body {
|
||||
font-size: 25px;
|
||||
line-height: 32px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
%label {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 13px;
|
||||
letter-spacing: 2.75px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: 2.625px;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
@import '../css/queries.scss';
|
||||
|
||||
.hero {
|
||||
padding-top: calc(var(--base) * 1.5);
|
||||
|
||||
@include mid-break {
|
||||
padding-top: var(--base);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
pages: Page
|
||||
users: User
|
||||
}
|
||||
globals: {
|
||||
'main-menu': MainMenu
|
||||
}
|
||||
}
|
||||
export interface Page {
|
||||
id: string
|
||||
title: string
|
||||
slug?: string
|
||||
richText: Array<{
|
||||
[k: string]: unknown
|
||||
}>
|
||||
_status?: 'draft' | 'published'
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
password?: string
|
||||
}
|
||||
export interface User {
|
||||
id: string
|
||||
email?: string
|
||||
resetPasswordToken?: string
|
||||
resetPasswordExpiration?: string
|
||||
loginAttempts?: number
|
||||
lockUntil?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
password?: string
|
||||
}
|
||||
export interface MainMenu {
|
||||
id: string
|
||||
navItems: Array<{
|
||||
link: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
value: string | Page
|
||||
relationTo: 'pages'
|
||||
}
|
||||
url: string
|
||||
label: string
|
||||
}
|
||||
id?: string
|
||||
}>
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Reference in New Issue
Block a user