feat(templates): add with-vercel-website (#9144)
Add new `with-vercel-website` that uses the website template as a base.
This commit is contained in:
@@ -6,13 +6,16 @@ import path from 'path'
|
|||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function copyRecursiveSync(src: string, dest: string) {
|
export function copyRecursiveSync(src: string, dest: string, ignoreRegex?: string[]): void {
|
||||||
const exists = fs.existsSync(src)
|
const exists = fs.existsSync(src)
|
||||||
const stats = exists && fs.statSync(src)
|
const stats = exists && fs.statSync(src)
|
||||||
const isDirectory = exists && stats !== false && stats.isDirectory()
|
const isDirectory = exists && stats !== false && stats.isDirectory()
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
fs.mkdirSync(dest, { recursive: true })
|
fs.mkdirSync(dest, { recursive: true })
|
||||||
fs.readdirSync(src).forEach((childItemName) => {
|
fs.readdirSync(src).forEach((childItemName) => {
|
||||||
|
if (ignoreRegex && ignoreRegex.some((regex) => new RegExp(regex).test(childItemName))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName))
|
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ const dirname = path.dirname(filename)
|
|||||||
type TemplateVariations = {
|
type TemplateVariations = {
|
||||||
/** package.json name */
|
/** package.json name */
|
||||||
name: string
|
name: string
|
||||||
|
/** Base template to copy from */
|
||||||
|
base?: string
|
||||||
/** Directory in templates dir */
|
/** Directory in templates dir */
|
||||||
dirname: string
|
dirname: string
|
||||||
db: DbType
|
db: DbType
|
||||||
@@ -35,6 +37,7 @@ type TemplateVariations = {
|
|||||||
dbUri: string
|
dbUri: string
|
||||||
}
|
}
|
||||||
configureConfig?: boolean
|
configureConfig?: boolean
|
||||||
|
generateLockfile?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
main().catch((error) => {
|
||||||
@@ -69,6 +72,27 @@ async function main() {
|
|||||||
dbUri: 'POSTGRES_URL',
|
dbUri: 'POSTGRES_URL',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'payload-vercel-website-template',
|
||||||
|
base: 'website', // This is the base template to copy from
|
||||||
|
dirname: 'with-vercel-website',
|
||||||
|
db: 'vercel-postgres',
|
||||||
|
storage: 'vercelBlobStorage',
|
||||||
|
sharp: false,
|
||||||
|
vercelDeployButtonLink:
|
||||||
|
`https://vercel.com/new/clone?repository-url=` +
|
||||||
|
encodeURI(
|
||||||
|
`${templateRepoUrlBase}/with-vercel-website` +
|
||||||
|
'&project-name=payload-project' +
|
||||||
|
'&env=PAYLOAD_SECRET' +
|
||||||
|
'&build-command=pnpm run ci' +
|
||||||
|
'&stores=[{"type":"postgres"},{"type":"blob"}]', // Postgres and Vercel Blob Storage
|
||||||
|
),
|
||||||
|
envNames: {
|
||||||
|
// This will replace the process.env.DATABASE_URI to process.env.POSTGRES_URL
|
||||||
|
dbUri: 'POSTGRES_URL',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'payload-postgres-template',
|
name: 'payload-postgres-template',
|
||||||
dirname: 'with-postgres',
|
dirname: 'with-postgres',
|
||||||
@@ -110,6 +134,7 @@ async function main() {
|
|||||||
name: 'payload-cloud-mongodb-template',
|
name: 'payload-cloud-mongodb-template',
|
||||||
dirname: 'with-payload-cloud',
|
dirname: 'with-payload-cloud',
|
||||||
db: 'mongodb',
|
db: 'mongodb',
|
||||||
|
generateLockfile: true,
|
||||||
storage: 'payloadCloud',
|
storage: 'payloadCloud',
|
||||||
sharp: true,
|
sharp: true,
|
||||||
},
|
},
|
||||||
@@ -117,8 +142,10 @@ async function main() {
|
|||||||
|
|
||||||
for (const {
|
for (const {
|
||||||
name,
|
name,
|
||||||
|
base,
|
||||||
dirname,
|
dirname,
|
||||||
db,
|
db,
|
||||||
|
generateLockfile,
|
||||||
storage,
|
storage,
|
||||||
vercelDeployButtonLink,
|
vercelDeployButtonLink,
|
||||||
envNames,
|
envNames,
|
||||||
@@ -127,7 +154,14 @@ async function main() {
|
|||||||
} of variations) {
|
} of variations) {
|
||||||
header(`Generating ${name}...`)
|
header(`Generating ${name}...`)
|
||||||
const destDir = path.join(templatesDir, dirname)
|
const destDir = path.join(templatesDir, dirname)
|
||||||
copyRecursiveSync(path.join(templatesDir, '_template'), destDir)
|
copyRecursiveSync(path.join(templatesDir, base || '_template'), destDir, [
|
||||||
|
'node_modules',
|
||||||
|
'\\*\\.tgz',
|
||||||
|
'.next',
|
||||||
|
'.env$',
|
||||||
|
'pnpm-lock.yaml',
|
||||||
|
])
|
||||||
|
|
||||||
log(`Copied to ${destDir}`)
|
log(`Copied to ${destDir}`)
|
||||||
|
|
||||||
if (configureConfig !== false) {
|
if (configureConfig !== false) {
|
||||||
@@ -194,6 +228,11 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (generateLockfile) {
|
||||||
|
log('Generating pnpm-lock.yaml')
|
||||||
|
execSync(`pnpm install --ignore-workspace`, { cwd: destDir })
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Email?
|
// TODO: Email?
|
||||||
|
|
||||||
// TODO: Sharp?
|
// TODO: Sharp?
|
||||||
|
|||||||
4028
templates/blank/pnpm-lock.yaml
generated
4028
templates/blank/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
4993
templates/website/pnpm-lock.yaml
generated
4993
templates/website/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
4013
templates/with-payload-cloud/pnpm-lock.yaml
generated
4013
templates/with-payload-cloud/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
10
templates/with-vercel-website/.editorconfig
Normal file
10
templates/with-vercel-website/.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = null
|
||||||
6
templates/with-vercel-website/.env.example
Normal file
6
templates/with-vercel-website/.env.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Database connection string
|
||||||
|
POSTGRES_URL=mongodb://127.0.0.1/payload-template-website
|
||||||
|
# Used to encrypt JWT tokens
|
||||||
|
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||||
|
# Used to configure CORS, format links and more. No trailing slash
|
||||||
|
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||||
12
templates/with-vercel-website/.eslintignore
Normal file
12
templates/with-vercel-website/.eslintignore
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
|
playwright.config.ts
|
||||||
|
jest.config.js
|
||||||
8
templates/with-vercel-website/.eslintrc.cjs
Normal file
8
templates/with-vercel-website/.eslintrc.cjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: 'next',
|
||||||
|
root: true,
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
10
templates/with-vercel-website/.gitignore
vendored
Normal file
10
templates/with-vercel-website/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
build
|
||||||
|
dist / media
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.next
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# Payload default media upload directory
|
||||||
|
public/media/
|
||||||
14
templates/with-vercel-website/.prettierignore
Normal file
14
templates/with-vercel-website/.prettierignore
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
**/payload-types.ts
|
||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
|
**/docs/**
|
||||||
|
tsconfig.json
|
||||||
|
|
||||||
6
templates/with-vercel-website/.prettierrc.json
Normal file
6
templates/with-vercel-website/.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 100,
|
||||||
|
"semi": false
|
||||||
|
}
|
||||||
14
templates/with-vercel-website/.vscode/launch.json
vendored
Normal file
14
templates/with-vercel-website/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "yarn dev",
|
||||||
|
"name": "Debug Website",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
24
templates/with-vercel-website/Dockerfile
Normal file
24
templates/with-vercel-website/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM node:18.8-alpine as base
|
||||||
|
|
||||||
|
FROM base as builder
|
||||||
|
|
||||||
|
WORKDIR /home/node/app
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN yarn install
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
FROM base as runtime
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
WORKDIR /home/node/app
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY yarn.lock ./
|
||||||
|
|
||||||
|
RUN yarn install --production
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "dist/server.js"]
|
||||||
10
templates/with-vercel-website/README.md
Normal file
10
templates/with-vercel-website/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# payload-vercel-website-template
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https://github.com/payloadcms/payload/tree/beta/templates/with-vercel-website&project-name=payload-project&env=PAYLOAD_SECRET&build-command=pnpm%20run%20ci&stores=%5B%7B%22type%22:%22postgres%22%7D,%7B%22type%22:%22blob%22%7D%5D)
|
||||||
|
|
||||||
|
payload-vercel-website-template
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Database**: vercel-postgres
|
||||||
|
- **Storage Adapter**: vercelBlobStorage
|
||||||
17
templates/with-vercel-website/components.json
Normal file
17
templates/with-vercel-website/components.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/app/(frontend)/globals.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/utilities"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
templates/with-vercel-website/docker-compose.yml
Normal file
31
templates/with-vercel-website/docker-compose.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
payload:
|
||||||
|
image: node:18-alpine
|
||||||
|
ports:
|
||||||
|
- '3000:3000'
|
||||||
|
volumes:
|
||||||
|
- .:/home/node/app
|
||||||
|
- node_modules:/home/node/app/node_modules
|
||||||
|
working_dir: /home/node/app/
|
||||||
|
command: sh -c "yarn install && yarn dev"
|
||||||
|
depends_on:
|
||||||
|
- mongo
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
mongo:
|
||||||
|
image: mongo:latest
|
||||||
|
ports:
|
||||||
|
- '27017:27017'
|
||||||
|
command:
|
||||||
|
- --storageEngine=wiredTiger
|
||||||
|
volumes:
|
||||||
|
- data:/data/db
|
||||||
|
logging:
|
||||||
|
driver: none
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
node_modules:
|
||||||
5
templates/with-vercel-website/next-env.d.ts
vendored
Normal file
5
templates/with-vercel-website/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||||
25
templates/with-vercel-website/next.config.js
Normal file
25
templates/with-vercel-website/next.config.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { withPayload } from '@payloadcms/next/withPayload'
|
||||||
|
|
||||||
|
import redirects from './redirects.js'
|
||||||
|
|
||||||
|
const NEXT_PUBLIC_SERVER_URL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000'
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
...[NEXT_PUBLIC_SERVER_URL /* 'https://example.com' */].map((item) => {
|
||||||
|
const url = new URL(item)
|
||||||
|
|
||||||
|
return {
|
||||||
|
hostname: url.hostname,
|
||||||
|
protocol: url.protocol.replace(':', ''),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
reactStrictMode: true,
|
||||||
|
redirects,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withPayload(nextConfig)
|
||||||
87
templates/with-vercel-website/package.json
Normal file
87
templates/with-vercel-website/package.json
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"name": "payload-vercel-website-template",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Website template for Payload",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
|
||||||
|
"ci": "payload migrate && pnpm build",
|
||||||
|
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||||
|
"dev:prod": "cross-env NODE_OPTIONS=--no-deprecation rm -rf .next && pnpm build && pnpm start",
|
||||||
|
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||||
|
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
||||||
|
"ii": "cross-env NODE_OPTIONS=--no-deprecation pnpm --ignore-workspace install",
|
||||||
|
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
|
||||||
|
"lint:fix": "cross-env NODE_OPTIONS=--no-deprecation next lint --fix",
|
||||||
|
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||||
|
"reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",
|
||||||
|
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@payloadcms/db-vercel-postgres": "beta",
|
||||||
|
"@payloadcms/live-preview-react": "beta",
|
||||||
|
"@payloadcms/next": "beta",
|
||||||
|
"@payloadcms/payload-cloud": "beta",
|
||||||
|
"@payloadcms/plugin-form-builder": "beta",
|
||||||
|
"@payloadcms/plugin-nested-docs": "beta",
|
||||||
|
"@payloadcms/plugin-redirects": "beta",
|
||||||
|
"@payloadcms/plugin-search": "beta",
|
||||||
|
"@payloadcms/plugin-seo": "beta",
|
||||||
|
"@payloadcms/richtext-lexical": "beta",
|
||||||
|
"@payloadcms/storage-vercel-blob": "beta",
|
||||||
|
"@payloadcms/ui": "beta",
|
||||||
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"geist": "^1.3.0",
|
||||||
|
"graphql": "^16.8.2",
|
||||||
|
"jsonwebtoken": "9.0.2",
|
||||||
|
"lexical": "0.20.0",
|
||||||
|
"lucide-react": "^0.378.0",
|
||||||
|
"next": "15.0.0",
|
||||||
|
"payload": "beta",
|
||||||
|
"payload-admin-bar": "^1.0.6",
|
||||||
|
"prism-react-renderer": "^2.3.1",
|
||||||
|
"react": "19.0.0-rc-65a56d0e-20241020",
|
||||||
|
"react-dom": "19.0.0-rc-65a56d0e-20241020",
|
||||||
|
"react-hook-form": "7.45.4",
|
||||||
|
"tailwind-merge": "^2.3.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@next/eslint-plugin-next": "^13.1.6",
|
||||||
|
"@payloadcms/eslint-config": "^1.1.1",
|
||||||
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
|
"@types/escape-html": "^1.0.2",
|
||||||
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
|
"@types/node": "22.5.4",
|
||||||
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"copyfiles": "^2.4.1",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "15.0.0",
|
||||||
|
"postcss": "^8.4.38",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"typescript": "5.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.20.2 || >=20.9.0"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
templates/with-vercel-website/postcss.config.js
Normal file
6
templates/with-vercel-website/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
autoprefixer: {},
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
templates/with-vercel-website/public/favicon.ico
Normal file
BIN
templates/with-vercel-website/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
15
templates/with-vercel-website/public/favicon.svg
Normal file
15
templates/with-vercel-website/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 |
BIN
templates/with-vercel-website/public/website-template-OG.webp
Normal file
BIN
templates/with-vercel-website/public/website-template-OG.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
20
templates/with-vercel-website/redirects.js
Normal file
20
templates/with-vercel-website/redirects.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const redirects = async () => {
|
||||||
|
const internetExplorerRedirect = {
|
||||||
|
destination: '/ie-incompatible.html',
|
||||||
|
has: [
|
||||||
|
{
|
||||||
|
type: 'header',
|
||||||
|
key: 'user-agent',
|
||||||
|
value: '(.*Trident.*)', // all ie browsers
|
||||||
|
},
|
||||||
|
],
|
||||||
|
permanent: false,
|
||||||
|
source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirects = [internetExplorerRedirect]
|
||||||
|
|
||||||
|
return redirects
|
||||||
|
}
|
||||||
|
|
||||||
|
export default redirects
|
||||||
39
templates/with-vercel-website/src/Footer/Component.tsx
Normal file
39
templates/with-vercel-website/src/Footer/Component.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { getCachedGlobal } from '@/utilities/getGlobals'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { Footer } from '@/payload-types'
|
||||||
|
|
||||||
|
import { ThemeSelector } from '@/providers/Theme/ThemeSelector'
|
||||||
|
import { CMSLink } from '@/components/Link'
|
||||||
|
|
||||||
|
export async function Footer() {
|
||||||
|
const footer: Footer = await getCachedGlobal('footer')()
|
||||||
|
|
||||||
|
const navItems = footer?.navItems || []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="border-t border-border bg-black dark:bg-card text-white">
|
||||||
|
<div className="container py-8 gap-8 flex flex-col md:flex-row md:justify-between">
|
||||||
|
<Link className="flex items-center" href="/">
|
||||||
|
<picture>
|
||||||
|
<img
|
||||||
|
alt="Payload Logo"
|
||||||
|
className="max-w-[6rem] invert-0"
|
||||||
|
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-light.svg"
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex flex-col-reverse items-start md:flex-row gap-4 md:items-center">
|
||||||
|
<ThemeSelector />
|
||||||
|
<nav className="flex flex-col md:flex-row gap-4">
|
||||||
|
{navItems.map(({ link }, i) => {
|
||||||
|
return <CMSLink className="text-white" key={i} {...link} />
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
26
templates/with-vercel-website/src/Footer/config.ts
Normal file
26
templates/with-vercel-website/src/Footer/config.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { GlobalConfig } from 'payload'
|
||||||
|
|
||||||
|
import { link } from '@/fields/link'
|
||||||
|
import { revalidateFooter } from './hooks/revalidateFooter'
|
||||||
|
|
||||||
|
export const Footer: GlobalConfig = {
|
||||||
|
slug: 'footer',
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'navItems',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
link({
|
||||||
|
appearances: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
maxRows: 6,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hooks: {
|
||||||
|
afterChange: [revalidateFooter],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { GlobalAfterChangeHook } from 'payload'
|
||||||
|
|
||||||
|
import { revalidateTag } from 'next/cache'
|
||||||
|
|
||||||
|
export const revalidateFooter: GlobalAfterChangeHook = ({ doc, req: { payload } }) => {
|
||||||
|
payload.logger.info(`Revalidating footer`)
|
||||||
|
|
||||||
|
revalidateTag('global_footer')
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
'use client'
|
||||||
|
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import type { Header } from '@/payload-types'
|
||||||
|
|
||||||
|
import { Logo } from '@/components/Logo/Logo'
|
||||||
|
import { HeaderNav } from './Nav'
|
||||||
|
|
||||||
|
interface HeaderClientProps {
|
||||||
|
header: Header
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderClient: React.FC<HeaderClientProps> = ({ header }) => {
|
||||||
|
/* Storing the value in a useState to avoid hydration errors */
|
||||||
|
const [theme, setTheme] = useState<string | null>(null)
|
||||||
|
const { headerTheme, setHeaderTheme } = useHeaderTheme()
|
||||||
|
const pathname = usePathname()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeaderTheme(null)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (headerTheme && headerTheme !== theme) setTheme(headerTheme)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [headerTheme])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className="container relative z-20 py-8 flex justify-between"
|
||||||
|
{...(theme ? { 'data-theme': theme } : {})}
|
||||||
|
>
|
||||||
|
<Link href="/">
|
||||||
|
<Logo />
|
||||||
|
</Link>
|
||||||
|
<HeaderNav header={header} />
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
templates/with-vercel-website/src/Header/Component.tsx
Normal file
11
templates/with-vercel-website/src/Header/Component.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { HeaderClient } from './Component.client'
|
||||||
|
import { getCachedGlobal } from '@/utilities/getGlobals'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { Header } from '@/payload-types'
|
||||||
|
|
||||||
|
export async function Header() {
|
||||||
|
const header: Header = await getCachedGlobal('header', 1)()
|
||||||
|
|
||||||
|
return <HeaderClient header={header} />
|
||||||
|
}
|
||||||
25
templates/with-vercel-website/src/Header/Nav/index.tsx
Normal file
25
templates/with-vercel-website/src/Header/Nav/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { Header as HeaderType } from '@/payload-types'
|
||||||
|
|
||||||
|
import { CMSLink } from '@/components/Link'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { SearchIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
export const HeaderNav: React.FC<{ header: HeaderType }> = ({ header }) => {
|
||||||
|
const navItems = header?.navItems || []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="flex gap-3 items-center">
|
||||||
|
{navItems.map(({ link }, i) => {
|
||||||
|
return <CMSLink key={i} {...link} appearance="link" />
|
||||||
|
})}
|
||||||
|
<Link href="/search">
|
||||||
|
<span className="sr-only">Search</span>
|
||||||
|
<SearchIcon className="w-5 text-primary" />
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
26
templates/with-vercel-website/src/Header/config.ts
Normal file
26
templates/with-vercel-website/src/Header/config.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { GlobalConfig } from 'payload'
|
||||||
|
|
||||||
|
import { link } from '@/fields/link'
|
||||||
|
import { revalidateHeader } from './hooks/revalidateHeader'
|
||||||
|
|
||||||
|
export const Header: GlobalConfig = {
|
||||||
|
slug: 'header',
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'navItems',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
link({
|
||||||
|
appearances: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
maxRows: 6,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hooks: {
|
||||||
|
afterChange: [revalidateHeader],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { GlobalAfterChangeHook } from 'payload'
|
||||||
|
|
||||||
|
import { revalidateTag } from 'next/cache'
|
||||||
|
|
||||||
|
export const revalidateHeader: GlobalAfterChangeHook = ({ doc, req: { payload } }) => {
|
||||||
|
payload.logger.info(`Revalidating header`)
|
||||||
|
|
||||||
|
revalidateTag('global_header')
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}
|
||||||
3
templates/with-vercel-website/src/access/anyone.ts
Normal file
3
templates/with-vercel-website/src/access/anyone.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { Access } from 'payload'
|
||||||
|
|
||||||
|
export const anyone: Access = () => true
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { AccessArgs } from 'payload'
|
||||||
|
|
||||||
|
import type { User } from '@/payload-types'
|
||||||
|
|
||||||
|
type isAuthenticated = (args: AccessArgs<User>) => boolean
|
||||||
|
|
||||||
|
export const authenticated: isAuthenticated = ({ req: { user } }) => {
|
||||||
|
return Boolean(user)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Access } from 'payload'
|
||||||
|
|
||||||
|
export const authenticatedOrPublished: Access = ({ req: { user } }) => {
|
||||||
|
if (user) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
_status: {
|
||||||
|
equals: 'published',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use client'
|
||||||
|
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
|
const PageClient: React.FC = () => {
|
||||||
|
/* Force the header to be dark mode while we have an image behind it */
|
||||||
|
const { setHeaderTheme } = useHeaderTheme()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeaderTheme('light')
|
||||||
|
}, [setHeaderTheme])
|
||||||
|
return <React.Fragment />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageClient
|
||||||
103
templates/with-vercel-website/src/app/(frontend)/[slug]/page.tsx
Normal file
103
templates/with-vercel-website/src/app/(frontend)/[slug]/page.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import { PayloadRedirects } from '@/components/PayloadRedirects'
|
||||||
|
import configPromise from '@payload-config'
|
||||||
|
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||||
|
import { draftMode } from 'next/headers'
|
||||||
|
import React, { cache } from 'react'
|
||||||
|
import { homeStatic } from '@/endpoints/seed/home-static'
|
||||||
|
|
||||||
|
import type { Page as PageType } from '@/payload-types'
|
||||||
|
|
||||||
|
import { RenderBlocks } from '@/blocks/RenderBlocks'
|
||||||
|
import { RenderHero } from '@/heros/RenderHero'
|
||||||
|
import { generateMeta } from '@/utilities/generateMeta'
|
||||||
|
import PageClient from './page.client'
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise })
|
||||||
|
const pages = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
|
draft: false,
|
||||||
|
limit: 1000,
|
||||||
|
overrideAccess: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const params = pages.docs
|
||||||
|
?.filter((doc) => {
|
||||||
|
return doc.slug !== 'home'
|
||||||
|
})
|
||||||
|
.map(({ slug }) => {
|
||||||
|
return { slug }
|
||||||
|
})
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
params: Promise<{
|
||||||
|
slug?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page({ params: paramsPromise }: Args) {
|
||||||
|
const { slug = 'home' } = await paramsPromise
|
||||||
|
const url = '/' + slug
|
||||||
|
|
||||||
|
let page: PageType | null
|
||||||
|
|
||||||
|
page = await queryPageBySlug({
|
||||||
|
slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove this code once your website is seeded
|
||||||
|
if (!page && slug === 'home') {
|
||||||
|
page = homeStatic
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
return <PayloadRedirects url={url} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hero, layout } = page
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="pt-16 pb-24">
|
||||||
|
<PageClient />
|
||||||
|
{/* Allows redirects for valid pages too */}
|
||||||
|
<PayloadRedirects disableNotFound url={url} />
|
||||||
|
|
||||||
|
<RenderHero {...hero} />
|
||||||
|
<RenderBlocks blocks={layout} />
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params: paramsPromise }): Promise<Metadata> {
|
||||||
|
const { slug = 'home' } = await paramsPromise
|
||||||
|
const page = await queryPageBySlug({
|
||||||
|
slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
return generateMeta({ doc: page })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryPageBySlug = cache(async ({ slug }: { slug: string }) => {
|
||||||
|
const { isEnabled: draft } = await draftMode()
|
||||||
|
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise })
|
||||||
|
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
|
draft,
|
||||||
|
limit: 1,
|
||||||
|
overrideAccess: draft,
|
||||||
|
where: {
|
||||||
|
slug: {
|
||||||
|
equals: slug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.docs?.[0] || null
|
||||||
|
})
|
||||||
103
templates/with-vercel-website/src/app/(frontend)/globals.css
Normal file
103
templates/with-vercel-website/src/app/(frontend)/globals.css
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: auto;
|
||||||
|
font-weight: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--card: 240 5% 96%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 240 6% 90%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--radius: 0.2rem;
|
||||||
|
|
||||||
|
--success: 196 52% 74%;
|
||||||
|
--warning: 34 89% 85%;
|
||||||
|
--error: 10 100% 86%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] {
|
||||||
|
--background: 0 0% 0%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--card: 240 6% 10%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 240 4% 16%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
|
||||||
|
--success: 196 100% 14%;
|
||||||
|
--warning: 34 51% 25%;
|
||||||
|
--error: 10 39% 43%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'],
|
||||||
|
html[data-theme='light'] {
|
||||||
|
opacity: initial;
|
||||||
|
}
|
||||||
54
templates/with-vercel-website/src/app/(frontend)/layout.tsx
Normal file
54
templates/with-vercel-website/src/app/(frontend)/layout.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import { cn } from 'src/utilities/cn'
|
||||||
|
import { GeistMono } from 'geist/font/mono'
|
||||||
|
import { GeistSans } from 'geist/font/sans'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { AdminBar } from '@/components/AdminBar'
|
||||||
|
import { Footer } from '@/Footer/Component'
|
||||||
|
import { Header } from '@/Header/Component'
|
||||||
|
import { LivePreviewListener } from '@/components/LivePreviewListener'
|
||||||
|
import { Providers } from '@/providers'
|
||||||
|
import { InitTheme } from '@/providers/Theme/InitTheme'
|
||||||
|
import { mergeOpenGraph } from '@/utilities/mergeOpenGraph'
|
||||||
|
import { draftMode } from 'next/headers'
|
||||||
|
|
||||||
|
import './globals.css'
|
||||||
|
|
||||||
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const { isEnabled } = await draftMode()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html className={cn(GeistSans.variable, GeistMono.variable)} lang="en" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<InitTheme />
|
||||||
|
<link href="/favicon.ico" rel="icon" sizes="32x32" />
|
||||||
|
<link href="/favicon.svg" rel="icon" type="image/svg+xml" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Providers>
|
||||||
|
<AdminBar
|
||||||
|
adminBarProps={{
|
||||||
|
preview: isEnabled,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<LivePreviewListener />
|
||||||
|
|
||||||
|
<Header />
|
||||||
|
{children}
|
||||||
|
<Footer />
|
||||||
|
</Providers>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
metadataBase: new URL(process.env.NEXT_PUBLIC_SERVER_URL || 'https://payloadcms.com'),
|
||||||
|
openGraph: mergeOpenGraph(),
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
creator: '@payloadcms',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { draftMode } from 'next/headers'
|
||||||
|
|
||||||
|
export async function GET(): Promise<Response> {
|
||||||
|
const draft = await draftMode()
|
||||||
|
draft.disable()
|
||||||
|
return new Response('Draft mode is disabled')
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { draftMode } from 'next/headers'
|
||||||
|
|
||||||
|
export async function GET(): Promise<Response> {
|
||||||
|
const draft = await draftMode()
|
||||||
|
draft.disable()
|
||||||
|
return new Response('Draft mode is disabled')
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import { draftMode } from 'next/headers'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||||
|
import configPromise from '@payload-config'
|
||||||
|
import { CollectionSlug } from 'payload'
|
||||||
|
|
||||||
|
const payloadToken = 'payload-token'
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: Request & {
|
||||||
|
cookies: {
|
||||||
|
get: (name: string) => {
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
): Promise<Response> {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise })
|
||||||
|
const token = req.cookies.get(payloadToken)?.value
|
||||||
|
const { searchParams } = new URL(req.url)
|
||||||
|
const path = searchParams.get('path')
|
||||||
|
const collection = searchParams.get('collection') as CollectionSlug
|
||||||
|
const slug = searchParams.get('slug')
|
||||||
|
|
||||||
|
const previewSecret = searchParams.get('previewSecret')
|
||||||
|
|
||||||
|
if (previewSecret) {
|
||||||
|
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||||
|
} else {
|
||||||
|
if (!path) {
|
||||||
|
return new Response('No path provided', { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
return new Response('No path provided', { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
return new Response('No path provided', { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
new Response('You are not allowed to preview this page', { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.startsWith('/')) {
|
||||||
|
new Response('This endpoint can only be used for internal previews', { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
let user
|
||||||
|
|
||||||
|
try {
|
||||||
|
user = jwt.verify(token, payload.secret)
|
||||||
|
} catch (error) {
|
||||||
|
payload.logger.error('Error verifying token for live preview:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const draft = await draftMode()
|
||||||
|
|
||||||
|
// You can add additional checks here to see if the user is allowed to preview this page
|
||||||
|
if (!user) {
|
||||||
|
draft.disable()
|
||||||
|
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the given slug exists
|
||||||
|
try {
|
||||||
|
const docs = await payload.find({
|
||||||
|
collection: collection,
|
||||||
|
draft: true,
|
||||||
|
where: {
|
||||||
|
slug: {
|
||||||
|
equals: slug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!docs.docs.length) {
|
||||||
|
return new Response('Document not found', { status: 404 })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
payload.logger.error('Error verifying token for live preview:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
draft.enable()
|
||||||
|
|
||||||
|
redirect(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<div className="container py-28">
|
||||||
|
<div className="prose max-w-none">
|
||||||
|
<h1 style={{ marginBottom: 0 }}>404</h1>
|
||||||
|
<p className="mb-4">This page could not be found.</p>
|
||||||
|
</div>
|
||||||
|
<Button asChild variant="default">
|
||||||
|
<Link href="/">Go home</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import PageTemplate, { generateMetadata } from './[slug]/page'
|
||||||
|
|
||||||
|
export default PageTemplate
|
||||||
|
|
||||||
|
export { generateMetadata }
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use client'
|
||||||
|
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
|
const PageClient: React.FC = () => {
|
||||||
|
/* Force the header to be dark mode while we have an image behind it */
|
||||||
|
const { setHeaderTheme } = useHeaderTheme()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeaderTheme('dark')
|
||||||
|
}, [setHeaderTheme])
|
||||||
|
return <React.Fragment />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageClient
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import { RelatedPosts } from '@/blocks/RelatedPosts/Component'
|
||||||
|
import { PayloadRedirects } from '@/components/PayloadRedirects'
|
||||||
|
import configPromise from '@payload-config'
|
||||||
|
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||||
|
import { draftMode } from 'next/headers'
|
||||||
|
import React, { cache } from 'react'
|
||||||
|
import RichText from '@/components/RichText'
|
||||||
|
|
||||||
|
import type { Post } from '@/payload-types'
|
||||||
|
|
||||||
|
import { PostHero } from '@/heros/PostHero'
|
||||||
|
import { generateMeta } from '@/utilities/generateMeta'
|
||||||
|
import PageClient from './page.client'
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise })
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
draft: false,
|
||||||
|
limit: 1000,
|
||||||
|
overrideAccess: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const params = posts.docs.map(({ slug }) => {
|
||||||
|
return { slug }
|
||||||
|
})
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
params: Promise<{
|
||||||
|
slug?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Post({ params: paramsPromise }: Args) {
|
||||||
|
const { slug = '' } = await paramsPromise
|
||||||
|
const url = '/posts/' + slug
|
||||||
|
const post = await queryPostBySlug({ slug })
|
||||||
|
|
||||||
|
if (!post) return <PayloadRedirects url={url} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="pt-16 pb-16">
|
||||||
|
<PageClient />
|
||||||
|
|
||||||
|
{/* Allows redirects for valid pages too */}
|
||||||
|
<PayloadRedirects disableNotFound url={url} />
|
||||||
|
|
||||||
|
<PostHero post={post} />
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-4 pt-8">
|
||||||
|
<div className="container lg:mx-0 lg:grid lg:grid-cols-[1fr_48rem_1fr] grid-rows-[1fr]">
|
||||||
|
<RichText
|
||||||
|
className="lg:grid lg:grid-cols-subgrid col-start-1 col-span-3 grid-rows-[1fr]"
|
||||||
|
content={post.content}
|
||||||
|
enableGutter={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{post.relatedPosts && post.relatedPosts.length > 0 && (
|
||||||
|
<RelatedPosts
|
||||||
|
className="mt-12"
|
||||||
|
docs={post.relatedPosts.filter((post) => typeof post === 'object')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params: paramsPromise }: Args): Promise<Metadata> {
|
||||||
|
const { slug = '' } = await paramsPromise
|
||||||
|
const post = await queryPostBySlug({ slug })
|
||||||
|
|
||||||
|
return generateMeta({ doc: post })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryPostBySlug = cache(async ({ slug }: { slug: string }) => {
|
||||||
|
const { isEnabled: draft } = await draftMode()
|
||||||
|
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise })
|
||||||
|
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
draft,
|
||||||
|
limit: 1,
|
||||||
|
overrideAccess: draft,
|
||||||
|
where: {
|
||||||
|
slug: {
|
||||||
|
equals: slug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.docs?.[0] || null
|
||||||
|
})
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use client'
|
||||||
|
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
|
const PageClient: React.FC = () => {
|
||||||
|
/* Force the header to be dark mode while we have an image behind it */
|
||||||
|
const { setHeaderTheme } = useHeaderTheme()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeaderTheme('light')
|
||||||
|
}, [setHeaderTheme])
|
||||||
|
return <React.Fragment />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageClient
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import type { Metadata } from 'next/types'
|
||||||
|
|
||||||
|
import { CollectionArchive } from '@/components/CollectionArchive'
|
||||||
|
import { PageRange } from '@/components/PageRange'
|
||||||
|
import { Pagination } from '@/components/Pagination'
|
||||||
|
import configPromise from '@payload-config'
|
||||||
|
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||||
|
import React from 'react'
|
||||||
|
import PageClient from './page.client'
|
||||||
|
|
||||||
|
export const dynamic = 'force-static'
|
||||||
|
export const revalidate = 600
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise })
|
||||||
|
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
depth: 1,
|
||||||
|
limit: 12,
|
||||||
|
overrideAccess: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-24 pb-24">
|
||||||
|
<PageClient />
|
||||||
|
<div className="container mb-16">
|
||||||
|
<div className="prose dark:prose-invert max-w-none">
|
||||||
|
<h1>Posts</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container mb-8">
|
||||||
|
<PageRange
|
||||||
|
collection="posts"
|
||||||
|
currentPage={posts.page}
|
||||||
|
limit={12}
|
||||||
|
totalDocs={posts.totalDocs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CollectionArchive posts={posts.docs} />
|
||||||
|
|
||||||
|
<div className="container">
|
||||||
|
{posts.totalPages > 1 && posts.page && (
|
||||||
|
<Pagination page={posts.page} totalPages={posts.totalPages} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateMetadata(): Metadata {
|
||||||
|
return {
|
||||||
|
title: `Payload Website Template Posts`,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use client'
|
||||||
|
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
|
const PageClient: React.FC = () => {
|
||||||
|
/* Force the header to be dark mode while we have an image behind it */
|
||||||
|
const { setHeaderTheme } = useHeaderTheme()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeaderTheme('light')
|
||||||
|
}, [setHeaderTheme])
|
||||||
|
return <React.Fragment />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageClient
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import type { Metadata } from 'next/types'
|
||||||
|
|
||||||
|
import { CollectionArchive } from '@/components/CollectionArchive'
|
||||||
|
import { PageRange } from '@/components/PageRange'
|
||||||
|
import { Pagination } from '@/components/Pagination'
|
||||||
|
import configPromise from '@payload-config'
|
||||||
|
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||||
|
import React from 'react'
|
||||||
|
import PageClient from './page.client'
|
||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
|
||||||
|
export const revalidate = 600
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
params: Promise<{
|
||||||
|
pageNumber: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page({ params: paramsPromise }: Args) {
|
||||||
|
const { pageNumber } = await paramsPromise
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise })
|
||||||
|
|
||||||
|
const sanitizedPageNumber = Number(pageNumber)
|
||||||
|
|
||||||
|
if (!Number.isInteger(sanitizedPageNumber)) notFound()
|
||||||
|
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
depth: 1,
|
||||||
|
limit: 12,
|
||||||
|
page: sanitizedPageNumber,
|
||||||
|
overrideAccess: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-24 pb-24">
|
||||||
|
<PageClient />
|
||||||
|
<div className="container mb-16">
|
||||||
|
<div className="prose dark:prose-invert max-w-none">
|
||||||
|
<h1>Posts</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container mb-8">
|
||||||
|
<PageRange
|
||||||
|
collection="posts"
|
||||||
|
currentPage={posts.page}
|
||||||
|
limit={12}
|
||||||
|
totalDocs={posts.totalDocs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CollectionArchive posts={posts.docs} />
|
||||||
|
|
||||||
|
<div className="container">
|
||||||
|
{posts.totalPages > 1 && posts.page && (
|
||||||
|
<Pagination page={posts.page} totalPages={posts.totalPages} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params: paramsPromise }: Args): Promise<Metadata> {
|
||||||
|
const { pageNumber } = await paramsPromise
|
||||||
|
return {
|
||||||
|
title: `Payload Website Template Posts Page ${pageNumber || ''}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise })
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
depth: 0,
|
||||||
|
limit: 10,
|
||||||
|
draft: false,
|
||||||
|
overrideAccess: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const pages: { pageNumber: string }[] = []
|
||||||
|
|
||||||
|
for (let i = 1; i <= posts.totalPages; i++) {
|
||||||
|
pages.push({ pageNumber: String(i) })
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use client'
|
||||||
|
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
|
const PageClient: React.FC = () => {
|
||||||
|
/* Force the header to be dark mode while we have an image behind it */
|
||||||
|
const { setHeaderTheme } = useHeaderTheme()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeaderTheme('light')
|
||||||
|
}, [setHeaderTheme])
|
||||||
|
return <React.Fragment />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageClient
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import type { Metadata } from 'next/types'
|
||||||
|
|
||||||
|
import { CollectionArchive } from '@/components/CollectionArchive'
|
||||||
|
import configPromise from '@payload-config'
|
||||||
|
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||||
|
import React from 'react'
|
||||||
|
import { Post } from '@/payload-types'
|
||||||
|
import { Search } from '@/search/Component'
|
||||||
|
import PageClient from './page.client'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
searchParams: Promise<{
|
||||||
|
q: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
export default async function Page({ searchParams: searchParamsPromise }: Args) {
|
||||||
|
const { q: query } = await searchParamsPromise
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise })
|
||||||
|
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'search',
|
||||||
|
depth: 1,
|
||||||
|
limit: 12,
|
||||||
|
...(query
|
||||||
|
? {
|
||||||
|
where: {
|
||||||
|
or: [
|
||||||
|
{
|
||||||
|
title: {
|
||||||
|
like: query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'meta.description': {
|
||||||
|
like: query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'meta.title': {
|
||||||
|
like: query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: {
|
||||||
|
like: query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-24 pb-24">
|
||||||
|
<PageClient />
|
||||||
|
<div className="container mb-16">
|
||||||
|
<div className="prose dark:prose-invert max-w-none">
|
||||||
|
<h1 className="sr-only">Search</h1>
|
||||||
|
<Search />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{posts.totalDocs > 0 ? (
|
||||||
|
<CollectionArchive posts={posts.docs as unknown as Post[]} />
|
||||||
|
) : (
|
||||||
|
<div className="container">No results found.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateMetadata(): Metadata {
|
||||||
|
return {
|
||||||
|
title: `Payload Website Template Search`,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||||
|
import { importMap } from '../importMap'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
params: Promise<{
|
||||||
|
segments: string[]
|
||||||
|
}>
|
||||||
|
searchParams: Promise<{
|
||||||
|
[key: string]: string | string[]
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||||
|
generatePageMetadata({ config, params, searchParams })
|
||||||
|
|
||||||
|
const NotFound = ({ params, searchParams }: Args) =>
|
||||||
|
NotFoundPage({ config, params, searchParams, importMap })
|
||||||
|
|
||||||
|
export default NotFound
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||||
|
import { importMap } from '../importMap'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
params: Promise<{
|
||||||
|
segments: string[]
|
||||||
|
}>
|
||||||
|
searchParams: Promise<{
|
||||||
|
[key: string]: string | string[]
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||||
|
generatePageMetadata({ config, params, searchParams })
|
||||||
|
|
||||||
|
const Page = ({ params, searchParams }: Args) =>
|
||||||
|
RootPage({ config, params, searchParams, importMap })
|
||||||
|
|
||||||
|
export default Page
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { OverviewComponent as OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||||
|
import { MetaTitleComponent as MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||||
|
import { MetaImageComponent as MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||||
|
import { MetaDescriptionComponent as MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||||
|
import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||||
|
import { SlugComponent as SlugComponent_92cc057d0a2abb4f6cf0307edf59f986 } from '@/fields/slug/SlugComponent'
|
||||||
|
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { LinkToDoc as LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client'
|
||||||
|
import { default as default_1a7510af427896d367a49dbf838d2de6 } from '@/components/BeforeDashboard'
|
||||||
|
import { default as default_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
|
||||||
|
|
||||||
|
export const importMap = {
|
||||||
|
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell':
|
||||||
|
RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalField':
|
||||||
|
RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
'@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient':
|
||||||
|
InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient':
|
||||||
|
FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#HeadingFeatureClient':
|
||||||
|
HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#ParagraphFeatureClient':
|
||||||
|
ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#UnderlineFeatureClient':
|
||||||
|
UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#BoldFeatureClient':
|
||||||
|
BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#ItalicFeatureClient':
|
||||||
|
ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#LinkFeatureClient':
|
||||||
|
LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/plugin-seo/client#OverviewComponent':
|
||||||
|
OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||||
|
'@payloadcms/plugin-seo/client#MetaTitleComponent':
|
||||||
|
MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||||
|
'@payloadcms/plugin-seo/client#MetaImageComponent':
|
||||||
|
MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||||
|
'@payloadcms/plugin-seo/client#MetaDescriptionComponent':
|
||||||
|
MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||||
|
'@payloadcms/plugin-seo/client#PreviewComponent':
|
||||||
|
PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||||
|
'@/fields/slug/SlugComponent#SlugComponent': SlugComponent_92cc057d0a2abb4f6cf0307edf59f986,
|
||||||
|
'@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient':
|
||||||
|
HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#BlocksFeatureClient':
|
||||||
|
BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/plugin-search/client#LinkToDoc': LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634,
|
||||||
|
'@/components/BeforeDashboard#default': default_1a7510af427896d367a49dbf838d2de6,
|
||||||
|
'@/components/BeforeLogin#default': default_8a7ab0eb7ab5c511aba12e68480bfe5e,
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import config from '@payload-config'
|
||||||
|
import '@payloadcms/next/css'
|
||||||
|
import {
|
||||||
|
REST_DELETE,
|
||||||
|
REST_GET,
|
||||||
|
REST_OPTIONS,
|
||||||
|
REST_PATCH,
|
||||||
|
REST_POST,
|
||||||
|
REST_PUT,
|
||||||
|
} from '@payloadcms/next/routes'
|
||||||
|
|
||||||
|
export const GET = REST_GET(config)
|
||||||
|
export const POST = REST_POST(config)
|
||||||
|
export const DELETE = REST_DELETE(config)
|
||||||
|
export const PATCH = REST_PATCH(config)
|
||||||
|
|
||||||
|
export const PUT = REST_PUT(config)
|
||||||
|
export const OPTIONS = REST_OPTIONS(config)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import config from '@payload-config'
|
||||||
|
import '@payloadcms/next/css'
|
||||||
|
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||||
|
|
||||||
|
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||||
|
|
||||||
|
export const POST = GRAPHQL_POST(config)
|
||||||
|
|
||||||
|
export const OPTIONS = REST_OPTIONS(config)
|
||||||
31
templates/with-vercel-website/src/app/(payload)/layout.tsx
Normal file
31
templates/with-vercel-website/src/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import config from '@payload-config'
|
||||||
|
import '@payloadcms/next/css'
|
||||||
|
import type { ServerFunctionClient } from 'payload'
|
||||||
|
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { importMap } from './admin/importMap.js'
|
||||||
|
import './custom.scss'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverFunction: ServerFunctionClient = async function (args) {
|
||||||
|
'use server'
|
||||||
|
return handleServerFunctions({
|
||||||
|
...args,
|
||||||
|
config,
|
||||||
|
importMap,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const Layout = ({ children }: Args) => (
|
||||||
|
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||||
|
{children}
|
||||||
|
</RootLayout>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Layout
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { Post, ArchiveBlock as ArchiveBlockProps } from '@/payload-types'
|
||||||
|
|
||||||
|
import configPromise from '@payload-config'
|
||||||
|
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||||
|
import React from 'react'
|
||||||
|
import RichText from '@/components/RichText'
|
||||||
|
|
||||||
|
import { CollectionArchive } from '@/components/CollectionArchive'
|
||||||
|
|
||||||
|
export const ArchiveBlock: React.FC<
|
||||||
|
ArchiveBlockProps & {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
> = async (props) => {
|
||||||
|
const { id, categories, introContent, limit: limitFromProps, populateBy, selectedDocs } = props
|
||||||
|
|
||||||
|
const limit = limitFromProps || 3
|
||||||
|
|
||||||
|
let posts: Post[] = []
|
||||||
|
|
||||||
|
if (populateBy === 'collection') {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise })
|
||||||
|
|
||||||
|
const flattenedCategories = categories?.map((category) => {
|
||||||
|
if (typeof category === 'object') return category.id
|
||||||
|
else return category
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchedPosts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
depth: 1,
|
||||||
|
limit,
|
||||||
|
...(flattenedCategories && flattenedCategories.length > 0
|
||||||
|
? {
|
||||||
|
where: {
|
||||||
|
categories: {
|
||||||
|
in: flattenedCategories,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
posts = fetchedPosts.docs
|
||||||
|
} else {
|
||||||
|
if (selectedDocs?.length) {
|
||||||
|
const filteredSelectedPosts = selectedDocs.map((post) => {
|
||||||
|
if (typeof post.value === 'object') return post.value
|
||||||
|
}) as Post[]
|
||||||
|
|
||||||
|
posts = filteredSelectedPosts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="my-16" id={`block-${id}`}>
|
||||||
|
{introContent && (
|
||||||
|
<div className="container mb-16">
|
||||||
|
<RichText className="ml-0 max-w-[48rem]" content={introContent} enableGutter={false} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<CollectionArchive posts={posts} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
import {
|
||||||
|
FixedToolbarFeature,
|
||||||
|
HeadingFeature,
|
||||||
|
InlineToolbarFeature,
|
||||||
|
lexicalEditor,
|
||||||
|
} from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
|
export const Archive: Block = {
|
||||||
|
slug: 'archive',
|
||||||
|
interfaceName: 'ArchiveBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'introContent',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ rootFeatures }) => {
|
||||||
|
return [
|
||||||
|
...rootFeatures,
|
||||||
|
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||||
|
FixedToolbarFeature(),
|
||||||
|
InlineToolbarFeature(),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
label: 'Intro Content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'populateBy',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'collection',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Collection',
|
||||||
|
value: 'collection',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Individual Selection',
|
||||||
|
value: 'selection',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'relationTo',
|
||||||
|
type: 'select',
|
||||||
|
admin: {
|
||||||
|
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||||
|
},
|
||||||
|
defaultValue: 'posts',
|
||||||
|
label: 'Collections To Show',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Posts',
|
||||||
|
value: 'posts',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'categories',
|
||||||
|
type: 'relationship',
|
||||||
|
admin: {
|
||||||
|
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||||
|
},
|
||||||
|
hasMany: true,
|
||||||
|
label: 'Categories To Show',
|
||||||
|
relationTo: 'categories',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'limit',
|
||||||
|
type: 'number',
|
||||||
|
admin: {
|
||||||
|
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
defaultValue: 10,
|
||||||
|
label: 'Limit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'selectedDocs',
|
||||||
|
type: 'relationship',
|
||||||
|
admin: {
|
||||||
|
condition: (_, siblingData) => siblingData.populateBy === 'selection',
|
||||||
|
},
|
||||||
|
hasMany: true,
|
||||||
|
label: 'Selection',
|
||||||
|
relationTo: ['posts'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
labels: {
|
||||||
|
plural: 'Archives',
|
||||||
|
singular: 'Archive',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { BannerBlock as BannerBlockProps } from 'src/payload-types'
|
||||||
|
|
||||||
|
import { cn } from 'src/utilities/cn'
|
||||||
|
import React from 'react'
|
||||||
|
import RichText from '@/components/RichText'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string
|
||||||
|
} & BannerBlockProps
|
||||||
|
|
||||||
|
export const BannerBlock: React.FC<Props> = ({ className, content, style }) => {
|
||||||
|
return (
|
||||||
|
<div className={cn('mx-auto my-8 w-full', className)}>
|
||||||
|
<div
|
||||||
|
className={cn('border py-3 px-6 flex items-center rounded', {
|
||||||
|
'border-border bg-card': style === 'info',
|
||||||
|
'border-error bg-error/30': style === 'error',
|
||||||
|
'border-success bg-success/30': style === 'success',
|
||||||
|
'border-warning bg-warning/30': style === 'warning',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<RichText content={content} enableGutter={false} enableProse={false} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
37
templates/with-vercel-website/src/blocks/Banner/config.ts
Normal file
37
templates/with-vercel-website/src/blocks/Banner/config.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
import {
|
||||||
|
FixedToolbarFeature,
|
||||||
|
InlineToolbarFeature,
|
||||||
|
lexicalEditor,
|
||||||
|
} from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
|
export const Banner: Block = {
|
||||||
|
slug: 'banner',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'style',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'info',
|
||||||
|
options: [
|
||||||
|
{ label: 'Info', value: 'info' },
|
||||||
|
{ label: 'Warning', value: 'warning' },
|
||||||
|
{ label: 'Error', value: 'error' },
|
||||||
|
{ label: 'Success', value: 'success' },
|
||||||
|
],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ rootFeatures }) => {
|
||||||
|
return [...rootFeatures, FixedToolbarFeature(), InlineToolbarFeature()]
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
label: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
interfaceName: 'BannerBlock',
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { CallToActionBlock as CTABlockProps } from '@/payload-types'
|
||||||
|
|
||||||
|
import RichText from '@/components/RichText'
|
||||||
|
import { CMSLink } from '@/components/Link'
|
||||||
|
|
||||||
|
export const CallToActionBlock: React.FC<CTABlockProps> = ({ links, richText }) => {
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="bg-card rounded border-border border p-4 flex flex-col gap-8 md:flex-row md:justify-between md:items-center">
|
||||||
|
<div className="max-w-[48rem] flex items-center">
|
||||||
|
{richText && <RichText className="mb-0" content={richText} enableGutter={false} />}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
{(links || []).map(({ link }, i) => {
|
||||||
|
return <CMSLink key={i} size="lg" {...link} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
import {
|
||||||
|
FixedToolbarFeature,
|
||||||
|
HeadingFeature,
|
||||||
|
InlineToolbarFeature,
|
||||||
|
lexicalEditor,
|
||||||
|
} from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
|
import { linkGroup } from '../../fields/linkGroup'
|
||||||
|
|
||||||
|
export const CallToAction: Block = {
|
||||||
|
slug: 'cta',
|
||||||
|
interfaceName: 'CallToActionBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'richText',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ rootFeatures }) => {
|
||||||
|
return [
|
||||||
|
...rootFeatures,
|
||||||
|
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||||
|
FixedToolbarFeature(),
|
||||||
|
InlineToolbarFeature(),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
label: false,
|
||||||
|
},
|
||||||
|
linkGroup({
|
||||||
|
appearances: ['default', 'outline'],
|
||||||
|
overrides: {
|
||||||
|
maxRows: 2,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
labels: {
|
||||||
|
plural: 'Calls to Action',
|
||||||
|
singular: 'Call to Action',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
'use client'
|
||||||
|
import { Highlight, themes } from 'prism-react-renderer'
|
||||||
|
import React from 'react'
|
||||||
|
import { CopyButton } from './CopyButton'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
code: string
|
||||||
|
language?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Code: React.FC<Props> = ({ code, language = '' }) => {
|
||||||
|
if (!code) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Highlight code={code} language={language} theme={themes.vsDark}>
|
||||||
|
{({ getLineProps, getTokenProps, tokens }) => (
|
||||||
|
<pre className="bg-black p-4 border text-xs border-border rounded overflow-x-auto">
|
||||||
|
{tokens.map((line, i) => (
|
||||||
|
<div key={i} {...getLineProps({ className: 'table-row', line })}>
|
||||||
|
<span className="table-cell select-none text-right text-white/25">{i + 1}</span>
|
||||||
|
<span className="table-cell pl-4">
|
||||||
|
{line.map((token, key) => (
|
||||||
|
<span key={key} {...getTokenProps({ token })} />
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<CopyButton code={code} />
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Highlight>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
templates/with-vercel-website/src/blocks/Code/Component.tsx
Normal file
21
templates/with-vercel-website/src/blocks/Code/Component.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { Code } from './Component.client'
|
||||||
|
|
||||||
|
export type CodeBlockProps = {
|
||||||
|
code: string
|
||||||
|
language?: string
|
||||||
|
blockType: 'code'
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = CodeBlockProps & {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CodeBlock: React.FC<Props> = ({ className, code, language }) => {
|
||||||
|
return (
|
||||||
|
<div className={[className, 'not-prose'].filter(Boolean).join(' ')}>
|
||||||
|
<Code code={code} language={language} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
36
templates/with-vercel-website/src/blocks/Code/CopyButton.tsx
Normal file
36
templates/with-vercel-website/src/blocks/Code/CopyButton.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
'use client'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { CopyIcon } from '@payloadcms/ui'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export function CopyButton({ code }: { code: string }) {
|
||||||
|
const [text, setText] = useState('Copy')
|
||||||
|
|
||||||
|
function updateCopyStatus() {
|
||||||
|
if (text === 'Copy') {
|
||||||
|
setText(() => 'Copied!')
|
||||||
|
setTimeout(() => {
|
||||||
|
setText(() => 'Copy')
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-end align-middle">
|
||||||
|
<Button
|
||||||
|
className="flex gap-1"
|
||||||
|
variant={'secondary'}
|
||||||
|
onClick={async () => {
|
||||||
|
await navigator.clipboard.writeText(code)
|
||||||
|
updateCopyStatus()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>{text}</p>
|
||||||
|
|
||||||
|
<div className="w-6 h-6 dark:invert">
|
||||||
|
<CopyIcon />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
33
templates/with-vercel-website/src/blocks/Code/config.ts
Normal file
33
templates/with-vercel-website/src/blocks/Code/config.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const Code: Block = {
|
||||||
|
slug: 'code',
|
||||||
|
interfaceName: 'CodeBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'language',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'typescript',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Typescript',
|
||||||
|
value: 'typescript',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Javascript',
|
||||||
|
value: 'javascript',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'CSS',
|
||||||
|
value: 'css',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'code',
|
||||||
|
type: 'code',
|
||||||
|
label: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { cn } from 'src/utilities/cn'
|
||||||
|
import React from 'react'
|
||||||
|
import RichText from '@/components/RichText'
|
||||||
|
|
||||||
|
import type { ContentBlock as ContentBlockProps } from '@/payload-types'
|
||||||
|
|
||||||
|
import { CMSLink } from '../../components/Link'
|
||||||
|
|
||||||
|
export const ContentBlock: React.FC<ContentBlockProps> = (props) => {
|
||||||
|
const { columns } = props
|
||||||
|
|
||||||
|
const colsSpanClasses = {
|
||||||
|
full: '12',
|
||||||
|
half: '6',
|
||||||
|
oneThird: '4',
|
||||||
|
twoThirds: '8',
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container my-16">
|
||||||
|
<div className="grid grid-cols-4 lg:grid-cols-12 gap-y-8 gap-x-16">
|
||||||
|
{columns &&
|
||||||
|
columns.length > 0 &&
|
||||||
|
columns.map((col, index) => {
|
||||||
|
const { enableLink, link, richText, size } = col
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(`col-span-4 lg:col-span-${colsSpanClasses[size!]}`, {
|
||||||
|
'md:col-span-2': size !== 'full',
|
||||||
|
})}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
{richText && <RichText content={richText} enableGutter={false} />}
|
||||||
|
|
||||||
|
{enableLink && <CMSLink {...link} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
74
templates/with-vercel-website/src/blocks/Content/config.ts
Normal file
74
templates/with-vercel-website/src/blocks/Content/config.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import type { Block, Field } from 'payload'
|
||||||
|
|
||||||
|
import {
|
||||||
|
FixedToolbarFeature,
|
||||||
|
HeadingFeature,
|
||||||
|
InlineToolbarFeature,
|
||||||
|
lexicalEditor,
|
||||||
|
} from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
|
import { link } from '@/fields/link'
|
||||||
|
|
||||||
|
const columnFields: Field[] = [
|
||||||
|
{
|
||||||
|
name: 'size',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'oneThird',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'One Third',
|
||||||
|
value: 'oneThird',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Half',
|
||||||
|
value: 'half',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Two Thirds',
|
||||||
|
value: 'twoThirds',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Full',
|
||||||
|
value: 'full',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'richText',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ rootFeatures }) => {
|
||||||
|
return [
|
||||||
|
...rootFeatures,
|
||||||
|
HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
|
||||||
|
FixedToolbarFeature(),
|
||||||
|
InlineToolbarFeature(),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
label: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'enableLink',
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
link({
|
||||||
|
overrides: {
|
||||||
|
admin: {
|
||||||
|
condition: (_, { enableLink }) => Boolean(enableLink),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
export const Content: Block = {
|
||||||
|
slug: 'content',
|
||||||
|
interfaceName: 'ContentBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'columns',
|
||||||
|
type: 'array',
|
||||||
|
fields: columnFields,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { CheckboxField } from '@payloadcms/plugin-form-builder/types'
|
||||||
|
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { useFormContext } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { Checkbox as CheckboxUi } from '@/components/ui/checkbox'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { Error } from '../Error'
|
||||||
|
import { Width } from '../Width'
|
||||||
|
|
||||||
|
export const Checkbox: React.FC<
|
||||||
|
CheckboxField & {
|
||||||
|
errors: Partial<
|
||||||
|
FieldErrorsImpl<{
|
||||||
|
[x: string]: any
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
getValues: any
|
||||||
|
register: UseFormRegister<FieldValues>
|
||||||
|
setValue: any
|
||||||
|
}
|
||||||
|
> = ({ name, defaultValue, errors, label, register, required: requiredFromProps, width }) => {
|
||||||
|
const props = register(name, { required: requiredFromProps })
|
||||||
|
const { setValue } = useFormContext()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Width width={width}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckboxUi
|
||||||
|
defaultChecked={defaultValue}
|
||||||
|
id={name}
|
||||||
|
{...props}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setValue(props.name, checked)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={name}>{label}</Label>
|
||||||
|
</div>
|
||||||
|
{requiredFromProps && errors[name] && <Error />}
|
||||||
|
</Width>
|
||||||
|
)
|
||||||
|
}
|
||||||
171
templates/with-vercel-website/src/blocks/Form/Component.tsx
Normal file
171
templates/with-vercel-website/src/blocks/Form/Component.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
'use client'
|
||||||
|
import type { Form as FormType } from '@payloadcms/plugin-form-builder/types'
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import React, { useCallback, useState } from 'react'
|
||||||
|
import { useForm, FormProvider } from 'react-hook-form'
|
||||||
|
import RichText from '@/components/RichText'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
import { buildInitialFormState } from './buildInitialFormState'
|
||||||
|
import { fields } from './fields'
|
||||||
|
|
||||||
|
export type Value = unknown
|
||||||
|
|
||||||
|
export interface Property {
|
||||||
|
[key: string]: Value
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Data {
|
||||||
|
[key: string]: Property | Property[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormBlockType = {
|
||||||
|
blockName?: string
|
||||||
|
blockType?: 'formBlock'
|
||||||
|
enableIntro: boolean
|
||||||
|
form: FormType
|
||||||
|
introContent?: {
|
||||||
|
[k: string]: unknown
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormBlock: React.FC<
|
||||||
|
{
|
||||||
|
id?: string
|
||||||
|
} & FormBlockType
|
||||||
|
> = (props) => {
|
||||||
|
const {
|
||||||
|
enableIntro,
|
||||||
|
form: formFromProps,
|
||||||
|
form: { id: formID, confirmationMessage, confirmationType, redirect, submitButtonLabel } = {},
|
||||||
|
introContent,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const formMethods = useForm({
|
||||||
|
defaultValues: buildInitialFormState(formFromProps.fields),
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
} = formMethods
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [hasSubmitted, setHasSubmitted] = useState<boolean>()
|
||||||
|
const [error, setError] = useState<{ message: string; status?: string } | undefined>()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(data: Data) => {
|
||||||
|
let loadingTimerID: ReturnType<typeof setTimeout>
|
||||||
|
const submitForm = async () => {
|
||||||
|
setError(undefined)
|
||||||
|
|
||||||
|
const dataToSend = Object.entries(data).map(([name, value]) => ({
|
||||||
|
field: name,
|
||||||
|
value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// delay loading indicator by 1s
|
||||||
|
loadingTimerID = setTimeout(() => {
|
||||||
|
setIsLoading(true)
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const req = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/form-submissions`, {
|
||||||
|
body: JSON.stringify({
|
||||||
|
form: formID,
|
||||||
|
submissionData: dataToSend,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await req.json()
|
||||||
|
|
||||||
|
clearTimeout(loadingTimerID)
|
||||||
|
|
||||||
|
if (req.status >= 400) {
|
||||||
|
setIsLoading(false)
|
||||||
|
|
||||||
|
setError({
|
||||||
|
message: res.errors?.[0]?.message || 'Internal Server Error',
|
||||||
|
status: res.status,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
setHasSubmitted(true)
|
||||||
|
|
||||||
|
if (confirmationType === 'redirect' && redirect) {
|
||||||
|
const { url } = redirect
|
||||||
|
|
||||||
|
const redirectUrl = url
|
||||||
|
|
||||||
|
if (redirectUrl) router.push(redirectUrl)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err)
|
||||||
|
setIsLoading(false)
|
||||||
|
setError({
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void submitForm()
|
||||||
|
},
|
||||||
|
[router, formID, redirect, confirmationType],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container lg:max-w-[48rem] pb-20">
|
||||||
|
<FormProvider {...formMethods}>
|
||||||
|
{enableIntro && introContent && !hasSubmitted && (
|
||||||
|
<RichText className="mb-8" content={introContent} enableGutter={false} />
|
||||||
|
)}
|
||||||
|
{!isLoading && hasSubmitted && confirmationType === 'message' && (
|
||||||
|
<RichText content={confirmationMessage} />
|
||||||
|
)}
|
||||||
|
{isLoading && !hasSubmitted && <p>Loading, please wait...</p>}
|
||||||
|
{error && <div>{`${error.status || '500'}: ${error.message || ''}`}</div>}
|
||||||
|
{!hasSubmitted && (
|
||||||
|
<form id={formID} onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="mb-4 last:mb-0">
|
||||||
|
{formFromProps &&
|
||||||
|
formFromProps.fields &&
|
||||||
|
formFromProps.fields?.map((field, index) => {
|
||||||
|
const Field: React.FC<any> = fields?.[field.blockType]
|
||||||
|
if (Field) {
|
||||||
|
return (
|
||||||
|
<div className="mb-6 last:mb-0" key={index}>
|
||||||
|
<Field
|
||||||
|
form={formFromProps}
|
||||||
|
{...field}
|
||||||
|
{...formMethods}
|
||||||
|
control={control}
|
||||||
|
errors={errors}
|
||||||
|
register={register}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button form={formID} type="submit" variant="default">
|
||||||
|
{submitButtonLabel}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</FormProvider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import type { CountryField } from '@payloadcms/plugin-form-builder/types'
|
||||||
|
import type { Control, FieldErrorsImpl, FieldValues } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import React from 'react'
|
||||||
|
import { Controller } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { Error } from '../Error'
|
||||||
|
import { Width } from '../Width'
|
||||||
|
import { countryOptions } from './options'
|
||||||
|
|
||||||
|
export const Country: React.FC<
|
||||||
|
CountryField & {
|
||||||
|
control: Control<FieldValues, any>
|
||||||
|
errors: Partial<
|
||||||
|
FieldErrorsImpl<{
|
||||||
|
[x: string]: any
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
> = ({ name, control, errors, label, required, width }) => {
|
||||||
|
return (
|
||||||
|
<Width width={width}>
|
||||||
|
<Label className="" htmlFor={name}>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name={name}
|
||||||
|
render={({ field: { onChange, value } }) => {
|
||||||
|
const controlledValue = countryOptions.find((t) => t.value === value)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select onValueChange={(val) => onChange(val)} value={controlledValue?.value}>
|
||||||
|
<SelectTrigger className="w-full" id={name}>
|
||||||
|
<SelectValue placeholder={label} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{countryOptions.map(({ label, value }) => {
|
||||||
|
return (
|
||||||
|
<SelectItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
rules={{ required }}
|
||||||
|
/>
|
||||||
|
{required && errors[name] && <Error />}
|
||||||
|
</Width>
|
||||||
|
)
|
||||||
|
}
|
||||||
982
templates/with-vercel-website/src/blocks/Form/Country/options.ts
Normal file
982
templates/with-vercel-website/src/blocks/Form/Country/options.ts
Normal file
@@ -0,0 +1,982 @@
|
|||||||
|
export const countryOptions = [
|
||||||
|
{
|
||||||
|
label: 'Afghanistan',
|
||||||
|
value: 'AF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Åland Islands',
|
||||||
|
value: 'AX',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Albania',
|
||||||
|
value: 'AL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Algeria',
|
||||||
|
value: 'DZ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'American Samoa',
|
||||||
|
value: 'AS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Andorra',
|
||||||
|
value: 'AD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Angola',
|
||||||
|
value: 'AO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Anguilla',
|
||||||
|
value: 'AI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Antarctica',
|
||||||
|
value: 'AQ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Antigua and Barbuda',
|
||||||
|
value: 'AG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Argentina',
|
||||||
|
value: 'AR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Armenia',
|
||||||
|
value: 'AM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Aruba',
|
||||||
|
value: 'AW',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Australia',
|
||||||
|
value: 'AU',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Austria',
|
||||||
|
value: 'AT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Azerbaijan',
|
||||||
|
value: 'AZ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bahamas',
|
||||||
|
value: 'BS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bahrain',
|
||||||
|
value: 'BH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bangladesh',
|
||||||
|
value: 'BD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Barbados',
|
||||||
|
value: 'BB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Belarus',
|
||||||
|
value: 'BY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Belgium',
|
||||||
|
value: 'BE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Belize',
|
||||||
|
value: 'BZ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Benin',
|
||||||
|
value: 'BJ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bermuda',
|
||||||
|
value: 'BM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bhutan',
|
||||||
|
value: 'BT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bolivia',
|
||||||
|
value: 'BO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bosnia and Herzegovina',
|
||||||
|
value: 'BA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Botswana',
|
||||||
|
value: 'BW',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bouvet Island',
|
||||||
|
value: 'BV',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Brazil',
|
||||||
|
value: 'BR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'British Indian Ocean Territory',
|
||||||
|
value: 'IO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Brunei Darussalam',
|
||||||
|
value: 'BN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bulgaria',
|
||||||
|
value: 'BG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Burkina Faso',
|
||||||
|
value: 'BF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Burundi',
|
||||||
|
value: 'BI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cambodia',
|
||||||
|
value: 'KH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cameroon',
|
||||||
|
value: 'CM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Canada',
|
||||||
|
value: 'CA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cape Verde',
|
||||||
|
value: 'CV',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cayman Islands',
|
||||||
|
value: 'KY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Central African Republic',
|
||||||
|
value: 'CF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Chad',
|
||||||
|
value: 'TD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Chile',
|
||||||
|
value: 'CL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'China',
|
||||||
|
value: 'CN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Christmas Island',
|
||||||
|
value: 'CX',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cocos (Keeling) Islands',
|
||||||
|
value: 'CC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Colombia',
|
||||||
|
value: 'CO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Comoros',
|
||||||
|
value: 'KM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Congo',
|
||||||
|
value: 'CG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Congo, The Democratic Republic of the',
|
||||||
|
value: 'CD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cook Islands',
|
||||||
|
value: 'CK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Costa Rica',
|
||||||
|
value: 'CR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Cote D'Ivoire",
|
||||||
|
value: 'CI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Croatia',
|
||||||
|
value: 'HR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cuba',
|
||||||
|
value: 'CU',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cyprus',
|
||||||
|
value: 'CY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Czech Republic',
|
||||||
|
value: 'CZ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Denmark',
|
||||||
|
value: 'DK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Djibouti',
|
||||||
|
value: 'DJ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Dominica',
|
||||||
|
value: 'DM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Dominican Republic',
|
||||||
|
value: 'DO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ecuador',
|
||||||
|
value: 'EC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Egypt',
|
||||||
|
value: 'EG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'El Salvador',
|
||||||
|
value: 'SV',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Equatorial Guinea',
|
||||||
|
value: 'GQ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Eritrea',
|
||||||
|
value: 'ER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Estonia',
|
||||||
|
value: 'EE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ethiopia',
|
||||||
|
value: 'ET',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Falkland Islands (Malvinas)',
|
||||||
|
value: 'FK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Faroe Islands',
|
||||||
|
value: 'FO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Fiji',
|
||||||
|
value: 'FJ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Finland',
|
||||||
|
value: 'FI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'France',
|
||||||
|
value: 'FR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'French Guiana',
|
||||||
|
value: 'GF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'French Polynesia',
|
||||||
|
value: 'PF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'French Southern Territories',
|
||||||
|
value: 'TF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Gabon',
|
||||||
|
value: 'GA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Gambia',
|
||||||
|
value: 'GM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Georgia',
|
||||||
|
value: 'GE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Germany',
|
||||||
|
value: 'DE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ghana',
|
||||||
|
value: 'GH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Gibraltar',
|
||||||
|
value: 'GI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Greece',
|
||||||
|
value: 'GR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Greenland',
|
||||||
|
value: 'GL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Grenada',
|
||||||
|
value: 'GD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Guadeloupe',
|
||||||
|
value: 'GP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Guam',
|
||||||
|
value: 'GU',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Guatemala',
|
||||||
|
value: 'GT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Guernsey',
|
||||||
|
value: 'GG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Guinea',
|
||||||
|
value: 'GN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Guinea-Bissau',
|
||||||
|
value: 'GW',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Guyana',
|
||||||
|
value: 'GY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Haiti',
|
||||||
|
value: 'HT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Heard Island and Mcdonald Islands',
|
||||||
|
value: 'HM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Holy See (Vatican City State)',
|
||||||
|
value: 'VA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Honduras',
|
||||||
|
value: 'HN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hong Kong',
|
||||||
|
value: 'HK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hungary',
|
||||||
|
value: 'HU',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Iceland',
|
||||||
|
value: 'IS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'India',
|
||||||
|
value: 'IN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Indonesia',
|
||||||
|
value: 'ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Iran, Islamic Republic Of',
|
||||||
|
value: 'IR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Iraq',
|
||||||
|
value: 'IQ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ireland',
|
||||||
|
value: 'IE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Isle of Man',
|
||||||
|
value: 'IM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Israel',
|
||||||
|
value: 'IL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Italy',
|
||||||
|
value: 'IT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Jamaica',
|
||||||
|
value: 'JM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Japan',
|
||||||
|
value: 'JP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Jersey',
|
||||||
|
value: 'JE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Jordan',
|
||||||
|
value: 'JO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Kazakhstan',
|
||||||
|
value: 'KZ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Kenya',
|
||||||
|
value: 'KE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Kiribati',
|
||||||
|
value: 'KI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Democratic People's Republic of Korea",
|
||||||
|
value: 'KP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Korea, Republic of',
|
||||||
|
value: 'KR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Kosovo',
|
||||||
|
value: 'XK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Kuwait',
|
||||||
|
value: 'KW',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Kyrgyzstan',
|
||||||
|
value: 'KG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Lao People's Democratic Republic",
|
||||||
|
value: 'LA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Latvia',
|
||||||
|
value: 'LV',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Lebanon',
|
||||||
|
value: 'LB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Lesotho',
|
||||||
|
value: 'LS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Liberia',
|
||||||
|
value: 'LR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Libyan Arab Jamahiriya',
|
||||||
|
value: 'LY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Liechtenstein',
|
||||||
|
value: 'LI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Lithuania',
|
||||||
|
value: 'LT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Luxembourg',
|
||||||
|
value: 'LU',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Macao',
|
||||||
|
value: 'MO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Macedonia, The Former Yugoslav Republic of',
|
||||||
|
value: 'MK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Madagascar',
|
||||||
|
value: 'MG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Malawi',
|
||||||
|
value: 'MW',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Malaysia',
|
||||||
|
value: 'MY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Maldives',
|
||||||
|
value: 'MV',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mali',
|
||||||
|
value: 'ML',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Malta',
|
||||||
|
value: 'MT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Marshall Islands',
|
||||||
|
value: 'MH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Martinique',
|
||||||
|
value: 'MQ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mauritania',
|
||||||
|
value: 'MR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mauritius',
|
||||||
|
value: 'MU',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mayotte',
|
||||||
|
value: 'YT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mexico',
|
||||||
|
value: 'MX',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Micronesia, Federated States of',
|
||||||
|
value: 'FM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Moldova, Republic of',
|
||||||
|
value: 'MD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Monaco',
|
||||||
|
value: 'MC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mongolia',
|
||||||
|
value: 'MN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Montenegro',
|
||||||
|
value: 'ME',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Montserrat',
|
||||||
|
value: 'MS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Morocco',
|
||||||
|
value: 'MA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mozambique',
|
||||||
|
value: 'MZ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Myanmar',
|
||||||
|
value: 'MM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Namibia',
|
||||||
|
value: 'NA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Nauru',
|
||||||
|
value: 'NR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Nepal',
|
||||||
|
value: 'NP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Netherlands',
|
||||||
|
value: 'NL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Netherlands Antilles',
|
||||||
|
value: 'AN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'New Caledonia',
|
||||||
|
value: 'NC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'New Zealand',
|
||||||
|
value: 'NZ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Nicaragua',
|
||||||
|
value: 'NI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Niger',
|
||||||
|
value: 'NE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Nigeria',
|
||||||
|
value: 'NG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Niue',
|
||||||
|
value: 'NU',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Norfolk Island',
|
||||||
|
value: 'NF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Northern Mariana Islands',
|
||||||
|
value: 'MP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Norway',
|
||||||
|
value: 'NO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Oman',
|
||||||
|
value: 'OM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pakistan',
|
||||||
|
value: 'PK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Palau',
|
||||||
|
value: 'PW',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Palestinian Territory, Occupied',
|
||||||
|
value: 'PS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Panama',
|
||||||
|
value: 'PA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Papua New Guinea',
|
||||||
|
value: 'PG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Paraguay',
|
||||||
|
value: 'PY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Peru',
|
||||||
|
value: 'PE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Philippines',
|
||||||
|
value: 'PH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pitcairn',
|
||||||
|
value: 'PN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Poland',
|
||||||
|
value: 'PL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Portugal',
|
||||||
|
value: 'PT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Puerto Rico',
|
||||||
|
value: 'PR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Qatar',
|
||||||
|
value: 'QA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reunion',
|
||||||
|
value: 'RE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Romania',
|
||||||
|
value: 'RO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Russian Federation',
|
||||||
|
value: 'RU',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Rwanda',
|
||||||
|
value: 'RW',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Saint Helena',
|
||||||
|
value: 'SH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Saint Kitts and Nevis',
|
||||||
|
value: 'KN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Saint Lucia',
|
||||||
|
value: 'LC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Saint Pierre and Miquelon',
|
||||||
|
value: 'PM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Saint Vincent and the Grenadines',
|
||||||
|
value: 'VC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Samoa',
|
||||||
|
value: 'WS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'San Marino',
|
||||||
|
value: 'SM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Sao Tome and Principe',
|
||||||
|
value: 'ST',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Saudi Arabia',
|
||||||
|
value: 'SA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Senegal',
|
||||||
|
value: 'SN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Serbia',
|
||||||
|
value: 'RS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Seychelles',
|
||||||
|
value: 'SC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Sierra Leone',
|
||||||
|
value: 'SL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Singapore',
|
||||||
|
value: 'SG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Slovakia',
|
||||||
|
value: 'SK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Slovenia',
|
||||||
|
value: 'SI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Solomon Islands',
|
||||||
|
value: 'SB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Somalia',
|
||||||
|
value: 'SO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'South Africa',
|
||||||
|
value: 'ZA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'South Georgia and the South Sandwich Islands',
|
||||||
|
value: 'GS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Spain',
|
||||||
|
value: 'ES',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Sri Lanka',
|
||||||
|
value: 'LK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Sudan',
|
||||||
|
value: 'SD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Suriname',
|
||||||
|
value: 'SR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Svalbard and Jan Mayen',
|
||||||
|
value: 'SJ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Swaziland',
|
||||||
|
value: 'SZ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Sweden',
|
||||||
|
value: 'SE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Switzerland',
|
||||||
|
value: 'CH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Syrian Arab Republic',
|
||||||
|
value: 'SY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Taiwan',
|
||||||
|
value: 'TW',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tajikistan',
|
||||||
|
value: 'TJ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tanzania, United Republic of',
|
||||||
|
value: 'TZ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Thailand',
|
||||||
|
value: 'TH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Timor-Leste',
|
||||||
|
value: 'TL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Togo',
|
||||||
|
value: 'TG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tokelau',
|
||||||
|
value: 'TK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tonga',
|
||||||
|
value: 'TO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Trinidad and Tobago',
|
||||||
|
value: 'TT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tunisia',
|
||||||
|
value: 'TN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Turkey',
|
||||||
|
value: 'TR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Turkmenistan',
|
||||||
|
value: 'TM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Turks and Caicos Islands',
|
||||||
|
value: 'TC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tuvalu',
|
||||||
|
value: 'TV',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Uganda',
|
||||||
|
value: 'UG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ukraine',
|
||||||
|
value: 'UA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'United Arab Emirates',
|
||||||
|
value: 'AE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'United Kingdom',
|
||||||
|
value: 'GB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'United States',
|
||||||
|
value: 'US',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'United States Minor Outlying Islands',
|
||||||
|
value: 'UM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Uruguay',
|
||||||
|
value: 'UY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Uzbekistan',
|
||||||
|
value: 'UZ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Vanuatu',
|
||||||
|
value: 'VU',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Venezuela',
|
||||||
|
value: 'VE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Viet Nam',
|
||||||
|
value: 'VN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Virgin Islands, British',
|
||||||
|
value: 'VG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Virgin Islands, U.S.',
|
||||||
|
value: 'VI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Wallis and Futuna',
|
||||||
|
value: 'WF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Western Sahara',
|
||||||
|
value: 'EH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Yemen',
|
||||||
|
value: 'YE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Zambia',
|
||||||
|
value: 'ZM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Zimbabwe',
|
||||||
|
value: 'ZW',
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { EmailField } from '@payloadcms/plugin-form-builder/types'
|
||||||
|
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { Error } from '../Error'
|
||||||
|
import { Width } from '../Width'
|
||||||
|
|
||||||
|
export const Email: React.FC<
|
||||||
|
EmailField & {
|
||||||
|
errors: Partial<
|
||||||
|
FieldErrorsImpl<{
|
||||||
|
[x: string]: any
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
register: UseFormRegister<FieldValues>
|
||||||
|
}
|
||||||
|
> = ({ name, defaultValue, errors, label, register, required: requiredFromProps, width }) => {
|
||||||
|
return (
|
||||||
|
<Width width={width}>
|
||||||
|
<Label htmlFor={name}>{label}</Label>
|
||||||
|
<Input
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
id={name}
|
||||||
|
type="text"
|
||||||
|
{...register(name, { pattern: /^\S[^\s@]*@\S+$/, required: requiredFromProps })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{requiredFromProps && errors[name] && <Error />}
|
||||||
|
</Width>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export const Error: React.FC = () => {
|
||||||
|
return <div className="mt-2 text-red-500 text-sm">This field is required</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import RichText from '@/components/RichText'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { Width } from '../Width'
|
||||||
|
|
||||||
|
export const Message: React.FC = ({ message }: { message: Record<string, any> }) => {
|
||||||
|
return (
|
||||||
|
<Width className="my-12" width="100">
|
||||||
|
{message && <RichText content={message} />}
|
||||||
|
</Width>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { TextField } from '@payloadcms/plugin-form-builder/types'
|
||||||
|
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { Error } from '../Error'
|
||||||
|
import { Width } from '../Width'
|
||||||
|
export const Number: React.FC<
|
||||||
|
TextField & {
|
||||||
|
errors: Partial<
|
||||||
|
FieldErrorsImpl<{
|
||||||
|
[x: string]: any
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
register: UseFormRegister<FieldValues>
|
||||||
|
}
|
||||||
|
> = ({ name, defaultValue, errors, label, register, required: requiredFromProps, width }) => {
|
||||||
|
return (
|
||||||
|
<Width width={width}>
|
||||||
|
<Label htmlFor={name}>{label}</Label>
|
||||||
|
<Input
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
id={name}
|
||||||
|
type="number"
|
||||||
|
{...register(name, { required: requiredFromProps })}
|
||||||
|
/>
|
||||||
|
{requiredFromProps && errors[name] && <Error />}
|
||||||
|
</Width>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type { SelectField } from '@payloadcms/plugin-form-builder/types'
|
||||||
|
import type { Control, FieldErrorsImpl, FieldValues } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select as SelectComponent,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import React from 'react'
|
||||||
|
import { Controller } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { Error } from '../Error'
|
||||||
|
import { Width } from '../Width'
|
||||||
|
|
||||||
|
export const Select: React.FC<
|
||||||
|
SelectField & {
|
||||||
|
control: Control<FieldValues, any>
|
||||||
|
errors: Partial<
|
||||||
|
FieldErrorsImpl<{
|
||||||
|
[x: string]: any
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
> = ({ name, control, errors, label, options, required, width }) => {
|
||||||
|
return (
|
||||||
|
<Width width={width}>
|
||||||
|
<Label htmlFor={name}>{label}</Label>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name={name}
|
||||||
|
render={({ field: { onChange, value } }) => {
|
||||||
|
const controlledValue = options.find((t) => t.value === value)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectComponent onValueChange={(val) => onChange(val)} value={controlledValue?.value}>
|
||||||
|
<SelectTrigger className="w-full" id={name}>
|
||||||
|
<SelectValue placeholder={label} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{options.map(({ label, value }) => {
|
||||||
|
return (
|
||||||
|
<SelectItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</SelectComponent>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
rules={{ required }}
|
||||||
|
/>
|
||||||
|
{required && errors[name] && <Error />}
|
||||||
|
</Width>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import type { StateField } from '@payloadcms/plugin-form-builder/types'
|
||||||
|
import type { Control, FieldErrorsImpl, FieldValues } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import React from 'react'
|
||||||
|
import { Controller } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { Error } from '../Error'
|
||||||
|
import { Width } from '../Width'
|
||||||
|
import { stateOptions } from './options'
|
||||||
|
|
||||||
|
export const State: React.FC<
|
||||||
|
StateField & {
|
||||||
|
control: Control<FieldValues, any>
|
||||||
|
errors: Partial<
|
||||||
|
FieldErrorsImpl<{
|
||||||
|
[x: string]: any
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
> = ({ name, control, errors, label, required, width }) => {
|
||||||
|
return (
|
||||||
|
<Width width={width}>
|
||||||
|
<Label htmlFor={name}>{label}</Label>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name={name}
|
||||||
|
render={({ field: { onChange, value } }) => {
|
||||||
|
const controlledValue = stateOptions.find((t) => t.value === value)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select onValueChange={(val) => onChange(val)} value={controlledValue?.value}>
|
||||||
|
<SelectTrigger className="w-full" id={name}>
|
||||||
|
<SelectValue placeholder={label} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{stateOptions.map(({ label, value }) => {
|
||||||
|
return (
|
||||||
|
<SelectItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
rules={{ required }}
|
||||||
|
/>
|
||||||
|
{required && errors[name] && <Error />}
|
||||||
|
</Width>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
export const stateOptions = [
|
||||||
|
{ label: 'Alabama', value: 'AL' },
|
||||||
|
{ label: 'Alaska', value: 'AK' },
|
||||||
|
{ label: 'Arizona', value: 'AZ' },
|
||||||
|
{ label: 'Arkansas', value: 'AR' },
|
||||||
|
{ label: 'California', value: 'CA' },
|
||||||
|
{ label: 'Colorado', value: 'CO' },
|
||||||
|
{ label: 'Connecticut', value: 'CT' },
|
||||||
|
{ label: 'Delaware', value: 'DE' },
|
||||||
|
{ label: 'Florida', value: 'FL' },
|
||||||
|
{ label: 'Georgia', value: 'GA' },
|
||||||
|
{ label: 'Hawaii', value: 'HI' },
|
||||||
|
{ label: 'Idaho', value: 'ID' },
|
||||||
|
{ label: 'Illinois', value: 'IL' },
|
||||||
|
{ label: 'Indiana', value: 'IN' },
|
||||||
|
{ label: 'Iowa', value: 'IA' },
|
||||||
|
{ label: 'Kansas', value: 'KS' },
|
||||||
|
{ label: 'Kentucky', value: 'KY' },
|
||||||
|
{ label: 'Louisiana', value: 'LA' },
|
||||||
|
{ label: 'Maine', value: 'ME' },
|
||||||
|
{ label: 'Maryland', value: 'MD' },
|
||||||
|
{ label: 'Massachusetts', value: 'MA' },
|
||||||
|
{ label: 'Michigan', value: 'MI' },
|
||||||
|
{ label: 'Minnesota', value: 'MN' },
|
||||||
|
{ label: 'Mississippi', value: 'MS' },
|
||||||
|
{ label: 'Missouri', value: 'MO' },
|
||||||
|
{ label: 'Montana', value: 'MT' },
|
||||||
|
{ label: 'Nebraska', value: 'NE' },
|
||||||
|
{ label: 'Nevada', value: 'NV' },
|
||||||
|
{ label: 'New Hampshire', value: 'NH' },
|
||||||
|
{ label: 'New Jersey', value: 'NJ' },
|
||||||
|
{ label: 'New Mexico', value: 'NM' },
|
||||||
|
{ label: 'New York', value: 'NY' },
|
||||||
|
{ label: 'North Carolina', value: 'NC' },
|
||||||
|
{ label: 'North Dakota', value: 'ND' },
|
||||||
|
{ label: 'Ohio', value: 'OH' },
|
||||||
|
{ label: 'Oklahoma', value: 'OK' },
|
||||||
|
{ label: 'Oregon', value: 'OR' },
|
||||||
|
{ label: 'Pennsylvania', value: 'PA' },
|
||||||
|
{ label: 'Rhode Island', value: 'RI' },
|
||||||
|
{ label: 'South Carolina', value: 'SC' },
|
||||||
|
{ label: 'South Dakota', value: 'SD' },
|
||||||
|
{ label: 'Tennessee', value: 'TN' },
|
||||||
|
{ label: 'Texas', value: 'TX' },
|
||||||
|
{ label: 'Utah', value: 'UT' },
|
||||||
|
{ label: 'Vermont', value: 'VT' },
|
||||||
|
{ label: 'Virginia', value: 'VA' },
|
||||||
|
{ label: 'Washington', value: 'WA' },
|
||||||
|
{ label: 'West Virginia', value: 'WV' },
|
||||||
|
{ label: 'Wisconsin', value: 'WI' },
|
||||||
|
{ label: 'Wyoming', value: 'WY' },
|
||||||
|
]
|
||||||
33
templates/with-vercel-website/src/blocks/Form/Text/index.tsx
Normal file
33
templates/with-vercel-website/src/blocks/Form/Text/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { TextField } from '@payloadcms/plugin-form-builder/types'
|
||||||
|
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { Error } from '../Error'
|
||||||
|
import { Width } from '../Width'
|
||||||
|
|
||||||
|
export const Text: React.FC<
|
||||||
|
TextField & {
|
||||||
|
errors: Partial<
|
||||||
|
FieldErrorsImpl<{
|
||||||
|
[x: string]: any
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
register: UseFormRegister<FieldValues>
|
||||||
|
}
|
||||||
|
> = ({ name, defaultValue, errors, label, register, required: requiredFromProps, width }) => {
|
||||||
|
return (
|
||||||
|
<Width width={width}>
|
||||||
|
<Label htmlFor={name}>{label}</Label>
|
||||||
|
<Input
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
id={name}
|
||||||
|
type="text"
|
||||||
|
{...register(name, { required: requiredFromProps })}
|
||||||
|
/>
|
||||||
|
{requiredFromProps && errors[name] && <Error />}
|
||||||
|
</Width>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { TextField } from '@payloadcms/plugin-form-builder/types'
|
||||||
|
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Textarea as TextAreaComponent } from '@/components/ui/textarea'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { Error } from '../Error'
|
||||||
|
import { Width } from '../Width'
|
||||||
|
|
||||||
|
export const Textarea: React.FC<
|
||||||
|
TextField & {
|
||||||
|
errors: Partial<
|
||||||
|
FieldErrorsImpl<{
|
||||||
|
[x: string]: any
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
register: UseFormRegister<FieldValues>
|
||||||
|
rows?: number
|
||||||
|
}
|
||||||
|
> = ({
|
||||||
|
name,
|
||||||
|
defaultValue,
|
||||||
|
errors,
|
||||||
|
label,
|
||||||
|
register,
|
||||||
|
required: requiredFromProps,
|
||||||
|
rows = 3,
|
||||||
|
width,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Width width={width}>
|
||||||
|
<Label htmlFor={name}>{label}</Label>
|
||||||
|
|
||||||
|
<TextAreaComponent
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
id={name}
|
||||||
|
rows={rows}
|
||||||
|
{...register(name, { required: requiredFromProps })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{requiredFromProps && errors[name] && <Error />}
|
||||||
|
</Width>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export const Width: React.FC<{
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
width?: number | string
|
||||||
|
}> = ({ children, className, width }) => {
|
||||||
|
return (
|
||||||
|
<div className={className} style={{ maxWidth: width ? `${width}%` : undefined }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import type { FormFieldBlock } from '@payloadcms/plugin-form-builder/types'
|
||||||
|
|
||||||
|
export const buildInitialFormState = (fields: FormFieldBlock[]) => {
|
||||||
|
return fields?.reduce((initialSchema, field) => {
|
||||||
|
if (field.blockType === 'checkbox') {
|
||||||
|
return {
|
||||||
|
...initialSchema,
|
||||||
|
[field.name]: field.defaultValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (field.blockType === 'country') {
|
||||||
|
return {
|
||||||
|
...initialSchema,
|
||||||
|
[field.name]: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (field.blockType === 'email') {
|
||||||
|
return {
|
||||||
|
...initialSchema,
|
||||||
|
[field.name]: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (field.blockType === 'text') {
|
||||||
|
return {
|
||||||
|
...initialSchema,
|
||||||
|
[field.name]: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (field.blockType === 'select') {
|
||||||
|
return {
|
||||||
|
...initialSchema,
|
||||||
|
[field.name]: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (field.blockType === 'state') {
|
||||||
|
return {
|
||||||
|
...initialSchema,
|
||||||
|
[field.name]: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
51
templates/with-vercel-website/src/blocks/Form/config.ts
Normal file
51
templates/with-vercel-website/src/blocks/Form/config.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
import {
|
||||||
|
FixedToolbarFeature,
|
||||||
|
HeadingFeature,
|
||||||
|
InlineToolbarFeature,
|
||||||
|
lexicalEditor,
|
||||||
|
} from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
|
export const FormBlock: Block = {
|
||||||
|
slug: 'formBlock',
|
||||||
|
interfaceName: 'FormBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'form',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'forms',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'enableIntro',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Enable Intro Content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'introContent',
|
||||||
|
type: 'richText',
|
||||||
|
admin: {
|
||||||
|
condition: (_, { enableIntro }) => Boolean(enableIntro),
|
||||||
|
},
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ rootFeatures }) => {
|
||||||
|
return [
|
||||||
|
...rootFeatures,
|
||||||
|
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||||
|
FixedToolbarFeature(),
|
||||||
|
InlineToolbarFeature(),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
label: 'Intro Content',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
graphQL: {
|
||||||
|
singularName: 'FormBlock',
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
plural: 'Form Blocks',
|
||||||
|
singular: 'Form Block',
|
||||||
|
},
|
||||||
|
}
|
||||||
21
templates/with-vercel-website/src/blocks/Form/fields.tsx
Normal file
21
templates/with-vercel-website/src/blocks/Form/fields.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Checkbox } from './Checkbox'
|
||||||
|
import { Country } from './Country'
|
||||||
|
import { Email } from './Email'
|
||||||
|
import { Message } from './Message'
|
||||||
|
import { Number } from './Number'
|
||||||
|
import { Select } from './Select'
|
||||||
|
import { State } from './State'
|
||||||
|
import { Text } from './Text'
|
||||||
|
import { Textarea } from './Textarea'
|
||||||
|
|
||||||
|
export const fields = {
|
||||||
|
checkbox: Checkbox,
|
||||||
|
country: Country,
|
||||||
|
email: Email,
|
||||||
|
message: Message,
|
||||||
|
number: Number,
|
||||||
|
select: Select,
|
||||||
|
state: State,
|
||||||
|
text: Text,
|
||||||
|
textarea: Textarea,
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import type { StaticImageData } from 'next/image'
|
||||||
|
|
||||||
|
import { cn } from 'src/utilities/cn'
|
||||||
|
import React from 'react'
|
||||||
|
import RichText from '@/components/RichText'
|
||||||
|
|
||||||
|
import type { MediaBlock as MediaBlockProps } from '@/payload-types'
|
||||||
|
|
||||||
|
import { Media } from '../../components/Media'
|
||||||
|
|
||||||
|
type Props = MediaBlockProps & {
|
||||||
|
breakout?: boolean
|
||||||
|
captionClassName?: string
|
||||||
|
className?: string
|
||||||
|
enableGutter?: boolean
|
||||||
|
imgClassName?: string
|
||||||
|
staticImage?: StaticImageData
|
||||||
|
disableInnerContainer?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MediaBlock: React.FC<Props> = (props) => {
|
||||||
|
const {
|
||||||
|
captionClassName,
|
||||||
|
className,
|
||||||
|
enableGutter = true,
|
||||||
|
imgClassName,
|
||||||
|
media,
|
||||||
|
position = 'default',
|
||||||
|
staticImage,
|
||||||
|
disableInnerContainer,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
let caption
|
||||||
|
if (media && typeof media === 'object') caption = media.caption
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'',
|
||||||
|
{
|
||||||
|
container: position === 'default' && enableGutter,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{position === 'fullscreen' && (
|
||||||
|
<div className="relative">
|
||||||
|
<Media resource={media} src={staticImage} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{position === 'default' && (
|
||||||
|
<Media imgClassName={cn('rounded', imgClassName)} resource={media} src={staticImage} />
|
||||||
|
)}
|
||||||
|
{caption && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'mt-6',
|
||||||
|
{
|
||||||
|
container: position === 'fullscreen' && !disableInnerContainer,
|
||||||
|
},
|
||||||
|
captionClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<RichText content={caption} enableGutter={false} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const MediaBlock: Block = {
|
||||||
|
slug: 'mediaBlock',
|
||||||
|
interfaceName: 'MediaBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'position',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'default',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Default',
|
||||||
|
value: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Fullscreen',
|
||||||
|
value: 'fullscreen',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'media',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import React from 'react'
|
||||||
|
import RichText from '@/components/RichText'
|
||||||
|
|
||||||
|
import type { Post } from '@/payload-types'
|
||||||
|
|
||||||
|
import { Card } from '../../components/Card'
|
||||||
|
|
||||||
|
export type RelatedPostsProps = {
|
||||||
|
className?: string
|
||||||
|
docs?: Post[]
|
||||||
|
introContent?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RelatedPosts: React.FC<RelatedPostsProps> = (props) => {
|
||||||
|
const { className, docs, introContent } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx('container', className)}>
|
||||||
|
{introContent && <RichText content={introContent} enableGutter={false} />}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-8 items-stretch">
|
||||||
|
{docs?.map((doc, index) => {
|
||||||
|
if (typeof doc === 'string') return null
|
||||||
|
|
||||||
|
return <Card key={index} doc={doc} relationTo="posts" showCategories />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
52
templates/with-vercel-website/src/blocks/RenderBlocks.tsx
Normal file
52
templates/with-vercel-website/src/blocks/RenderBlocks.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { cn } from 'src/utilities/cn'
|
||||||
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
|
import type { Page } from '@/payload-types'
|
||||||
|
|
||||||
|
import { ArchiveBlock } from '@/blocks/ArchiveBlock/Component'
|
||||||
|
import { CallToActionBlock } from '@/blocks/CallToAction/Component'
|
||||||
|
import { ContentBlock } from '@/blocks/Content/Component'
|
||||||
|
import { FormBlock } from '@/blocks/Form/Component'
|
||||||
|
import { MediaBlock } from '@/blocks/MediaBlock/Component'
|
||||||
|
|
||||||
|
const blockComponents = {
|
||||||
|
archive: ArchiveBlock,
|
||||||
|
content: ContentBlock,
|
||||||
|
cta: CallToActionBlock,
|
||||||
|
formBlock: FormBlock,
|
||||||
|
mediaBlock: MediaBlock,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RenderBlocks: React.FC<{
|
||||||
|
blocks: Page['layout'][0][]
|
||||||
|
}> = (props) => {
|
||||||
|
const { blocks } = props
|
||||||
|
|
||||||
|
const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0
|
||||||
|
|
||||||
|
if (hasBlocks) {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{blocks.map((block, index) => {
|
||||||
|
const { blockType } = block
|
||||||
|
|
||||||
|
if (blockType && blockType in blockComponents) {
|
||||||
|
const Block = blockComponents[blockType]
|
||||||
|
|
||||||
|
if (Block) {
|
||||||
|
return (
|
||||||
|
<div className="my-16" key={index}>
|
||||||
|
{/* @ts-expect-error */}
|
||||||
|
<Block {...block} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
26
templates/with-vercel-website/src/collections/Categories.ts
Normal file
26
templates/with-vercel-website/src/collections/Categories.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
import { anyone } from '../access/anyone'
|
||||||
|
import { authenticated } from '../access/authenticated'
|
||||||
|
|
||||||
|
const Categories: CollectionConfig = {
|
||||||
|
slug: 'categories',
|
||||||
|
access: {
|
||||||
|
create: authenticated,
|
||||||
|
delete: authenticated,
|
||||||
|
read: anyone,
|
||||||
|
update: authenticated,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'title',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Categories
|
||||||
73
templates/with-vercel-website/src/collections/Media.ts
Normal file
73
templates/with-vercel-website/src/collections/Media.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
import {
|
||||||
|
FixedToolbarFeature,
|
||||||
|
InlineToolbarFeature,
|
||||||
|
lexicalEditor,
|
||||||
|
} from '@payloadcms/richtext-lexical'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import { anyone } from '../access/anyone'
|
||||||
|
import { authenticated } from '../access/authenticated'
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
export const Media: CollectionConfig = {
|
||||||
|
slug: 'media',
|
||||||
|
access: {
|
||||||
|
create: authenticated,
|
||||||
|
delete: authenticated,
|
||||||
|
read: anyone,
|
||||||
|
update: authenticated,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'alt',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'caption',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ rootFeatures }) => {
|
||||||
|
return [...rootFeatures, FixedToolbarFeature(), InlineToolbarFeature()]
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
upload: {
|
||||||
|
// Upload to the public/media directory in Next.js making them publicly accessible even outside of Payload
|
||||||
|
staticDir: path.resolve(dirname, '../../public/media'),
|
||||||
|
adminThumbnail: 'thumbnail',
|
||||||
|
imageSizes: [
|
||||||
|
{
|
||||||
|
name: 'thumbnail',
|
||||||
|
width: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'square',
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'small',
|
||||||
|
width: 600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'medium',
|
||||||
|
width: 900,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'large',
|
||||||
|
width: 1400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'xlarge',
|
||||||
|
width: 1920,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { CollectionAfterChangeHook } from 'payload'
|
||||||
|
|
||||||
|
import { revalidatePath } from 'next/cache'
|
||||||
|
|
||||||
|
import type { Page } from '../../../payload-types'
|
||||||
|
|
||||||
|
export const revalidatePage: CollectionAfterChangeHook<Page> = ({
|
||||||
|
doc,
|
||||||
|
previousDoc,
|
||||||
|
req: { payload },
|
||||||
|
}) => {
|
||||||
|
if (doc._status === 'published') {
|
||||||
|
const path = doc.slug === 'home' ? '/' : `/${doc.slug}`
|
||||||
|
|
||||||
|
payload.logger.info(`Revalidating page at path: ${path}`)
|
||||||
|
|
||||||
|
revalidatePath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the page was previously published, we need to revalidate the old path
|
||||||
|
if (previousDoc?._status === 'published' && doc._status !== 'published') {
|
||||||
|
const oldPath = previousDoc.slug === 'home' ? '/' : `/${previousDoc.slug}`
|
||||||
|
|
||||||
|
payload.logger.info(`Revalidating old page at path: ${oldPath}`)
|
||||||
|
|
||||||
|
revalidatePath(oldPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}
|
||||||
127
templates/with-vercel-website/src/collections/Pages/index.ts
Normal file
127
templates/with-vercel-website/src/collections/Pages/index.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
import { authenticated } from '../../access/authenticated'
|
||||||
|
import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'
|
||||||
|
import { Archive } from '../../blocks/ArchiveBlock/config'
|
||||||
|
import { CallToAction } from '../../blocks/CallToAction/config'
|
||||||
|
import { Content } from '../../blocks/Content/config'
|
||||||
|
import { FormBlock } from '../../blocks/Form/config'
|
||||||
|
import { MediaBlock } from '../../blocks/MediaBlock/config'
|
||||||
|
import { hero } from '@/heros/config'
|
||||||
|
import { slugField } from '@/fields/slug'
|
||||||
|
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
|
||||||
|
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
|
||||||
|
import { revalidatePage } from './hooks/revalidatePage'
|
||||||
|
|
||||||
|
import {
|
||||||
|
MetaDescriptionField,
|
||||||
|
MetaImageField,
|
||||||
|
MetaTitleField,
|
||||||
|
OverviewField,
|
||||||
|
PreviewField,
|
||||||
|
} from '@payloadcms/plugin-seo/fields'
|
||||||
|
export const Pages: CollectionConfig = {
|
||||||
|
slug: 'pages',
|
||||||
|
access: {
|
||||||
|
create: authenticated,
|
||||||
|
delete: authenticated,
|
||||||
|
read: authenticatedOrPublished,
|
||||||
|
update: authenticated,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
defaultColumns: ['title', 'slug', 'updatedAt'],
|
||||||
|
livePreview: {
|
||||||
|
url: ({ data }) => {
|
||||||
|
const path = generatePreviewPath({
|
||||||
|
slug: typeof data?.slug === 'string' ? data.slug : '',
|
||||||
|
collection: 'pages',
|
||||||
|
})
|
||||||
|
|
||||||
|
return `${process.env.NEXT_PUBLIC_SERVER_URL}${path}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
preview: (data) => {
|
||||||
|
const path = generatePreviewPath({
|
||||||
|
slug: typeof data?.slug === 'string' ? data.slug : '',
|
||||||
|
collection: 'pages',
|
||||||
|
})
|
||||||
|
|
||||||
|
return `${process.env.NEXT_PUBLIC_SERVER_URL}${path}`
|
||||||
|
},
|
||||||
|
useAsTitle: 'title',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'tabs',
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
fields: [hero],
|
||||||
|
label: 'Hero',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'layout',
|
||||||
|
type: 'blocks',
|
||||||
|
blocks: [CallToAction, Content, MediaBlock, Archive, FormBlock],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
label: 'Content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'meta',
|
||||||
|
label: 'SEO',
|
||||||
|
fields: [
|
||||||
|
OverviewField({
|
||||||
|
titlePath: 'meta.title',
|
||||||
|
descriptionPath: 'meta.description',
|
||||||
|
imagePath: 'meta.image',
|
||||||
|
}),
|
||||||
|
MetaTitleField({
|
||||||
|
hasGenerateFn: true,
|
||||||
|
}),
|
||||||
|
MetaImageField({
|
||||||
|
relationTo: 'media',
|
||||||
|
}),
|
||||||
|
|
||||||
|
MetaDescriptionField({}),
|
||||||
|
PreviewField({
|
||||||
|
// if the `generateUrl` function is configured
|
||||||
|
hasGenerateFn: true,
|
||||||
|
|
||||||
|
// field paths to match the target field for data
|
||||||
|
titlePath: 'meta.title',
|
||||||
|
descriptionPath: 'meta.description',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'publishedAt',
|
||||||
|
type: 'date',
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...slugField(),
|
||||||
|
],
|
||||||
|
hooks: {
|
||||||
|
afterChange: [revalidatePage],
|
||||||
|
beforeChange: [populatePublishedAt],
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
drafts: {
|
||||||
|
autosave: {
|
||||||
|
interval: 100, // We set this interval for optimal live preview
|
||||||
|
},
|
||||||
|
},
|
||||||
|
maxPerDoc: 50,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { CollectionAfterReadHook } from 'payload'
|
||||||
|
import { User } from 'src/payload-types'
|
||||||
|
|
||||||
|
// The `user` collection has access control locked so that users are not publicly accessible
|
||||||
|
// This means that we need to populate the authors manually here to protect user privacy
|
||||||
|
// GraphQL will not return mutated user data that differs from the underlying schema
|
||||||
|
// So we use an alternative `populatedAuthors` field to populate the user data, hidden from the admin UI
|
||||||
|
export const populateAuthors: CollectionAfterReadHook = async ({ doc, req, req: { payload } }) => {
|
||||||
|
if (doc?.authors) {
|
||||||
|
const authorDocs: User[] = []
|
||||||
|
|
||||||
|
for (const author of doc.authors) {
|
||||||
|
const authorDoc = await payload.findByID({
|
||||||
|
id: typeof author === 'object' ? author?.id : author,
|
||||||
|
collection: 'users',
|
||||||
|
depth: 0,
|
||||||
|
req,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (authorDoc) {
|
||||||
|
authorDocs.push(authorDoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.populatedAuthors = authorDocs.map((authorDoc) => ({
|
||||||
|
id: authorDoc.id,
|
||||||
|
name: authorDoc.name,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { CollectionAfterChangeHook } from 'payload'
|
||||||
|
|
||||||
|
import { revalidatePath } from 'next/cache'
|
||||||
|
|
||||||
|
import type { Post } from '../../../payload-types'
|
||||||
|
|
||||||
|
export const revalidatePost: CollectionAfterChangeHook<Post> = ({
|
||||||
|
doc,
|
||||||
|
previousDoc,
|
||||||
|
req: { payload },
|
||||||
|
}) => {
|
||||||
|
if (doc._status === 'published') {
|
||||||
|
const path = `/posts/${doc.slug}`
|
||||||
|
|
||||||
|
payload.logger.info(`Revalidating post at path: ${path}`)
|
||||||
|
|
||||||
|
revalidatePath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the post was previously published, we need to revalidate the old path
|
||||||
|
if (previousDoc._status === 'published' && doc._status !== 'published') {
|
||||||
|
const oldPath = `/posts/${previousDoc.slug}`
|
||||||
|
|
||||||
|
payload.logger.info(`Revalidating old post at path: ${oldPath}`)
|
||||||
|
|
||||||
|
revalidatePath(oldPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user