chore(examples): migrates form-builder example to 3.0 (#9681)

### What?

Migrates the `form-builder` example to payload `3.0`.

`Updates`:

- Now has a next app directly along side payload.

- Removes `form-builder/next-app` & `form-builder/next-pages` example
front-ends and only uses new recommended approach (i.e admin panel &
front-end on the same port - `3000`)
This commit is contained in:
Patrik
2024-12-03 09:47:08 -05:00
committed by GitHub
parent cf34d3aec4
commit 40f5c72e8d
257 changed files with 8454 additions and 32610 deletions

View File

@@ -1,4 +1,3 @@
DATABASE_URI=mongodb://127.0.0.1/payload-template-blank-3-0
DATABASE_URI=mongodb://127.0.0.1/payload-form-builder-example
PAYLOAD_SECRET=YOUR_SECRET_HERE
PAYLOAD_SEED=true
NEXT_PUBLIC_PAYLOAD_URL=http://localhost:3000

View File

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

View File

@@ -0,0 +1,53 @@
# Payload Form Builder Example
The [Payload Form Builder Example](https://github.com/payloadcms/payload/tree/main/examples/form-builder/payload) demonstrates how to implement the official [Form Builder Plugin](https://payloadcms.com/docs/plugins/form-builder) in [Payload](https://github.com/payloadcms/payload).
**IMPORTANT—This example includes a fully integrated Next.js App Router front-end that runs on the same server as Payload.**
## Quick Start
1. Clone this repo
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
3. `cp .env.example .env` to copy the example environment variables
4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
- Press `y` when prompted to seed the database
5. `open http://localhost:3000` to access the home page
6. `open http://localhost:3000/admin` to access the admin panel
- Login with email `demo@payloadcms.com` and password `demo`
That's it! Changes made in `./src` will be
## How it works
The [Form Builder Plugin](https://payloadcms.com/docs/plugins/form-builder) automatically adds the `forms` and `formSubmissions` collections to your config which your front-end can use to query forms and submit form data. You can embed forms into layout building blocks by referring a `forms` document in a relationship field.
See the official [Form Builder Plugin](https://payloadcms.com/docs/plugins/form-builder) for full details.
## Development
To spin up this example locally, follow the [Quick Start](#quick-start).
### Seed
On boot, a seed script is included to scaffold a basic database for you to use as an example. You can remove `pnpm seed` from the `dev` script in the `package.json` to prevent this behavior. You can also freshly seed your project at any time by running `pnpm seed`. This seed creates a user with email `demo@payloadcms.com` and password `demo` along with a home page and an example page with two versions, one published and the other draft.
> NOTICE: seeding the database is destructive because it drops your current database to populate a fresh one from the seed template. Only run this command if you are starting a new project or can afford to lose your current data.
## Production
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
1. Invoke the `next build` script by running `pnpm build` or `npm run build` in your project root. This creates a `.next` directory with a production-ready admin bundle.
1. Finally run `pnpm start` or `npm run start` to run Node in production and serve Payload from the `.build` directory.
### Deployment
The easiest way to deploy your project is to use [Payload Cloud](https://payloadcms.com/new/import), a one-click hosting solution to deploy production-ready instances of your Payload apps directly from your GitHub repo. You can also choose to self-host your app, check out the [Deployment](https://payloadcms.com/docs/production/deployment) docs for more details.
## Questions
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).

View File

@@ -1,10 +0,0 @@
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

View File

@@ -1 +0,0 @@
NEXT_PUBLIC_PAYLOAD_URL=http://localhost:3000

View File

@@ -1,21 +0,0 @@
module.exports = {
extends: ['plugin:@next/next/core-web-vitals', '@payloadcms'],
ignorePatterns: ['**/payload-types.ts'],
overrides: [
{
extends: ['plugin:@typescript-eslint/disable-type-checked'],
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
},
{
files: ['./app/**/*.ts', './app/**/*.tsx'],
rules: {
'no-restricted-exports': 'off',
},
},
],
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
root: true,
}

View File

@@ -1,38 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env

View File

@@ -1,38 +0,0 @@
# Form Builder Example Front-End
This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages). It was made explicitly for Payload's [Form Builder Example](https://github.com/payloadcms/payload/tree/main/examples/form-builder/payload).
> This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/app), we will add an example for that soon.
## Getting Started
### Payload
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/form-builder/payload). If you have not done so already, clone it down and follow the setup instructions there.
### Next.js App
1. Clone this repo
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server
5. `open http://localhost:3001` to see the result
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Form Builder Example](https://github.com/payloadcms/payload/tree/main/examples/form-builder/payload) for full details.
## Learn More
To learn more about Payload and Next.js, take a look at the following resources:
- [Payload Documentation](https://payloadcms.com/docs) - learn about Payload features and API.
- [Form Builder Plugin Documentation](https://github.com/payloadcms/plugin-form-builder) - learn about the plugin's features.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Payload GitHub repository](https://github.com/payloadcms/payload/) as well as [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Payload deployment documentation](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -1,40 +0,0 @@
import type { GetStaticPaths } from 'next'
import React from 'react'
import type { Page } from '../../payload-types'
import Blocks from '../../components/Blocks'
export default async function Page({ params: { slug = 'home' } }) {
const page = await getPage(slug)
return (
<React.Fragment>
<Blocks blocks={page.layout} />
</React.Fragment>
)
}
export const getPage = async (slug: string) => {
const pageQuery = await fetch(
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/pages?where[slug][equals]=${slug}`,
).then((res) => res.json())
return pageQuery.docs[0]
}
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
const pagesQuery: { docs: Page[] } = await fetch(
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/pages?limit=100`,
).then((res) => res.json())
return {
fallback: 'blocking',
paths: pagesQuery.docs.map((page) => ({
params: {
slug: page.slug,
},
})),
}
}

View File

@@ -1,53 +0,0 @@
import { ModalContainer, ModalProvider } from '@faceless-ui/modal'
import React from 'react'
import type { MainMenu } from '../payload-types'
import { CloseModalOnRouteChange } from '../components/CloseModalOnRouteChange'
import { Header } from '../components/Header'
import '../css/app.scss'
import { GlobalsProvider } from '../providers/Globals'
export interface IGlobals {
mainMenu: MainMenu
}
export const metadata = {
description: 'An example of how to authenticate with Payload from a Next.js app.',
title: 'Payload Auth + Next.js App Router Example',
}
export const getAllGlobals = async (): Promise<IGlobals> => {
const [mainMenu] = await Promise.all([
fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/globals/main-menu?depth=1`).then((res) =>
res.json(),
),
])
return {
mainMenu,
}
}
export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props
const globals = await getAllGlobals()
return (
<html lang="en">
<body>
<React.Fragment>
<GlobalsProvider {...globals}>
<ModalProvider classPrefix="form" transTime={0} zIndex="var(--modal-z-index)">
<CloseModalOnRouteChange />
<Header />
{/* <Component {...pageProps} /> */}
{children}
<ModalContainer />
</ModalProvider>
</GlobalsProvider>
</React.Fragment>
</body>
</html>
)
}

View File

@@ -1,62 +0,0 @@
import type { CheckboxField } from 'payload-plugin-form-builder/dist/types'
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
import React, { useState } from 'react'
import { Check } from '../../../icons/Check'
import { Error } from '../Error'
import { Width } from '../Width'
import classes from './index.module.scss'
export const Checkbox: React.FC<
CheckboxField & {
errors: Partial<
FieldErrorsImpl<{
[x: string]: any
}>
>
getValues: any
register: UseFormRegister<FieldValues & any>
setValue: any
}
> = ({
name,
errors,
getValues,
label,
register,
required: requiredFromProps,
setValue,
width,
}) => {
const [checked, setChecked] = useState(false)
const isCheckboxChecked = getValues(name)
return (
<Width width={width}>
<div className={[classes.checkbox, checked && classes.checked].filter(Boolean).join(' ')}>
<div className={classes.container}>
<input
type="checkbox"
{...register(name, { required: requiredFromProps })}
checked={isCheckboxChecked}
/>
<button
onClick={() => {
setValue(name, !checked)
setChecked(!checked)
}}
type="button"
>
<span className={classes.input}>
<Check />
</span>
</button>
<span className={classes.label}>{label}</span>
</div>
{requiredFromProps && errors[name] && checked === false && <Error />}
</div>
</Width>
)
}

View File

@@ -1,49 +0,0 @@
import type { SelectField } from 'payload-plugin-form-builder/dist/types'
import type { Control, FieldErrorsImpl, FieldValues } from 'react-hook-form'
import React from 'react'
import { Controller } from 'react-hook-form'
import ReactSelect from 'react-select'
import { Error } from '../Error'
import { Width } from '../Width'
import classes from './index.module.scss'
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}>
<div className={classes.select}>
<label className={classes.label} htmlFor={name}>
{label}
</label>
<Controller
control={control}
defaultValue=""
name={name}
render={({ field: { onChange, value } }) => (
<ReactSelect
className={classes.reactSelect}
classNamePrefix="rs"
inputId={name}
instanceId={name}
onChange={(val) => onChange(val.value)}
options={options}
value={options.find((s) => s.value === value)}
/>
)}
rules={{ required }}
/>
{required && errors[name] && <Error />}
</div>
</Width>
)
}

View File

@@ -1,50 +0,0 @@
import type { StateField } from 'payload-plugin-form-builder/dist/types'
import type { Control, FieldErrorsImpl, FieldValues } from 'react-hook-form'
import React from 'react'
import { Controller } from 'react-hook-form'
import ReactSelect from 'react-select'
import { Error } from '../Error'
import { Width } from '../Width'
import classes from './index.module.scss'
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}>
<div className={classes.select}>
<label className={classes.label} htmlFor={name}>
{label}
</label>
<Controller
control={control}
defaultValue=""
name={name}
render={({ field: { onChange, value } }) => (
<ReactSelect
className={classes.reactSelect}
classNamePrefix="rs"
id={name}
instanceId={name}
onChange={(val) => onChange(val.value)}
options={stateOptions}
value={stateOptions.find((t) => t.value === value)}
/>
)}
rules={{ required }}
/>
{required && errors[name] && <Error />}
</div>
</Width>
)
}

View File

@@ -1,37 +0,0 @@
import type { TextField } from 'payload-plugin-form-builder/dist/types'
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
import React from 'react'
import { Error } from '../Error'
import { Width } from '../Width'
import classes from './index.module.scss'
export const Textarea: React.FC<
TextField & {
errors: Partial<
FieldErrorsImpl<{
[x: string]: any
}>
>
register: UseFormRegister<FieldValues & any>
rows?: number
}
> = ({ name, errors, label, register, required: requiredFromProps, rows = 3, width }) => {
return (
<Width width={width}>
<div className={classes.wrap}>
<label className={classes.label} htmlFor={name}>
{label}
</label>
<textarea
className={classes.textarea}
id={name}
rows={rows}
{...register(name, { required: requiredFromProps })}
/>
{requiredFromProps && errors[name] && <Error />}
</div>
</Width>
)
}

View File

@@ -1,42 +0,0 @@
import type { FormFieldBlock } from 'payload-plugin-form-builder/dist/types'
export const buildInitialFormState = (fields: FormFieldBlock[]) => {
return fields.reduce((initialSchema, field) => {
if (field.blockType === 'checkbox') {
return {
...initialSchema,
[field.name]: false,
}
}
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]: '',
}
}
}, {})
}

View File

@@ -1,51 +0,0 @@
import React, { Fragment } from 'react'
import type { Page } from '../../payload-types'
import { toKebabCase } from '../../utilities/toKebabCase'
import { VerticalPadding } from '../VerticalPadding'
import { FormBlock } from './Form'
const blockComponents = {
formBlock: FormBlock,
}
const Blocks: React.FC<{
blocks: Page['layout']
}> = (props) => {
const { blocks } = props
const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0
if (hasBlocks) {
return (
<Fragment>
{blocks.map((block, index) => {
const { blockName, blockType, form } = block
const isFormBlock = blockType === 'formBlock'
{
/*@ts-expect-error*/
}
const formID: string = isFormBlock && form && (typeof form === 'string' ? form : form.id)
if (blockType && blockType in blockComponents) {
const Block = blockComponents[blockType]
return (
<VerticalPadding bottom="small" key={isFormBlock ? formID : index} top="small">
{/*@ts-expect-error*/}
<Block id={toKebabCase(blockName)} {...block} />
</VerticalPadding>
)
}
return null
})}
</Fragment>
)
}
return null
}
export default Blocks

View File

@@ -1,18 +0,0 @@
'use client'
import type React from 'react'
import { useModal } from '@faceless-ui/modal'
import { usePathname } from 'next/navigation'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
export const CloseModalOnRouteChange: React.FC = () => {
const { closeAllModals } = useModal()
const pathname = usePathname()
useEffect(() => {
closeAllModals()
}, [pathname, closeAllModals])
return null
}

View File

@@ -1,54 +0,0 @@
'use client'
import { ModalToggler } from '@faceless-ui/modal'
import Link from 'next/link'
import React from 'react'
import { useGlobals } from '../../providers/Globals'
import { Gutter } from '../Gutter'
import { CMSLink } from '../Link'
import { Logo } from '../Logo'
import { MenuIcon } from '../icons/Menu'
import { MobileMenuModal, slug as menuModalSlug } from './MobileMenuModal'
import classes from './index.module.scss'
type HeaderBarProps = {
children?: React.ReactNode
}
export const HeaderBar: React.FC<HeaderBarProps> = ({ children }) => {
return (
<header className={classes.header}>
<Gutter className={classes.wrap}>
<Link href="/">
<Logo />
</Link>
{children}
<ModalToggler className={classes.mobileMenuToggler} slug={menuModalSlug}>
<MenuIcon />
</ModalToggler>
</Gutter>
</header>
)
}
export const Header: React.FC = () => {
const {
mainMenu: { navItems },
} = useGlobals()
return (
<React.Fragment>
<HeaderBar>
<nav className={classes.nav}>
{navItems.map(({ link }, i) => {
return <CMSLink key={i} {...link} />
})}
</nav>
</HeaderBar>
<MobileMenuModal navItems={navItems} />
</React.Fragment>
)
}

View File

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

View File

@@ -1,3 +0,0 @@
.container {
max-width: 480px;
}

View File

@@ -1,26 +0,0 @@
import React from 'react'
import styles from './index.module.scss'
import { serializeLexical } from './serialize'
const RichText: React.FC<{ className?: string; content: any; enableGutter?: boolean }> = ({
className,
content,
enableGutter = true,
}) => {
if (!content) {
return null
}
return (
<div className={[className].filter(Boolean).join(' ')}>
{content &&
!Array.isArray(content) &&
typeof content === 'object' &&
'root' in content &&
serializeLexical({ nodes: content?.root?.children })}
</div>
)
}
export default RichText

View File

@@ -1,128 +0,0 @@
/* eslint-disable regexp/no-obscure-range */
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
// @ts-nocheck
//This copy-and-pasted from lexical here here: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts
import type { ElementFormatType, TextFormatType } from '@payloadcms/richtext-lexical/lexical'
import type { TextDetailType, TextModeType } from 'lexical/nodes/LexicalTextNode'
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
// DOM
export const DOM_ELEMENT_TYPE = 1
export const DOM_TEXT_TYPE = 3
// Reconciling
export const NO_DIRTY_NODES = 0
export const HAS_DIRTY_NODES = 1
export const FULL_RECONCILE = 2
// Text node modes
export const IS_NORMAL = 0
export const IS_TOKEN = 1
export const IS_SEGMENTED = 2
// IS_INERT = 3
// Text node formatting
export const IS_BOLD = 1
export const IS_ITALIC = 1 << 1
export const IS_STRIKETHROUGH = 1 << 2
export const IS_UNDERLINE = 1 << 3
export const IS_CODE = 1 << 4
export const IS_SUBSCRIPT = 1 << 5
export const IS_SUPERSCRIPT = 1 << 6
export const IS_HIGHLIGHT = 1 << 7
export const IS_ALL_FORMATTING =
IS_BOLD |
IS_ITALIC |
IS_STRIKETHROUGH |
IS_UNDERLINE |
IS_CODE |
IS_SUBSCRIPT |
IS_SUPERSCRIPT |
IS_HIGHLIGHT
// Text node details
export const IS_DIRECTIONLESS = 1
export const IS_UNMERGEABLE = 1 << 1
// Element node formatting
export const IS_ALIGN_LEFT = 1
export const IS_ALIGN_CENTER = 2
export const IS_ALIGN_RIGHT = 3
export const IS_ALIGN_JUSTIFY = 4
export const IS_ALIGN_START = 5
export const IS_ALIGN_END = 6
// Reconciliation
export const NON_BREAKING_SPACE = '\u00A0'
const ZERO_WIDTH_SPACE = '\u200b'
export const DOUBLE_LINE_BREAK = '\n\n'
// For FF, we need to use a non-breaking space, or it gets composition
// in a stuck state.
const RTL = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC'
const LTR =
'A-Za-z\u00C0-\u00D6\u00D8-\u00F6' +
'\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u200E\u2C00-\uFB1C' +
'\uFE00-\uFE6F\uFEFD-\uFFFF'
// eslint-disable-next-line
export const RTL_REGEX = new RegExp('^[^' + LTR + ']*[' + RTL + ']')
// eslint-disable-next-line
export const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']')
export const TEXT_TYPE_TO_FORMAT: Record<string | TextFormatType, number> = {
bold: IS_BOLD,
code: IS_CODE,
highlight: IS_HIGHLIGHT,
italic: IS_ITALIC,
strikethrough: IS_STRIKETHROUGH,
subscript: IS_SUBSCRIPT,
superscript: IS_SUPERSCRIPT,
underline: IS_UNDERLINE,
}
export const DETAIL_TYPE_TO_DETAIL: Record<string | TextDetailType, number> = {
directionless: IS_DIRECTIONLESS,
unmergeable: IS_UNMERGEABLE,
}
export const ELEMENT_TYPE_TO_FORMAT: Record<Exclude<ElementFormatType, ''>, number> = {
center: IS_ALIGN_CENTER,
end: IS_ALIGN_END,
justify: IS_ALIGN_JUSTIFY,
left: IS_ALIGN_LEFT,
right: IS_ALIGN_RIGHT,
start: IS_ALIGN_START,
}
export const ELEMENT_FORMAT_TO_TYPE: Record<number, ElementFormatType> = {
[IS_ALIGN_CENTER]: 'center',
[IS_ALIGN_END]: 'end',
[IS_ALIGN_JUSTIFY]: 'justify',
[IS_ALIGN_LEFT]: 'left',
[IS_ALIGN_RIGHT]: 'right',
[IS_ALIGN_START]: 'start',
}
export const TEXT_MODE_TO_TYPE: Record<TextModeType, 0 | 1 | 2> = {
normal: IS_NORMAL,
segmented: IS_SEGMENTED,
token: IS_TOKEN,
}
export const TEXT_TYPE_TO_MODE: Record<number, TextModeType> = {
[IS_NORMAL]: 'normal',
[IS_SEGMENTED]: 'segmented',
[IS_TOKEN]: 'token',
}

View File

@@ -1,208 +0,0 @@
import type { LinkFields, SerializedLinkNode } from '@payloadcms/richtext-lexical'
import type {
SerializedElementNode,
SerializedLexicalNode,
SerializedTextNode,
} from '@payloadcms/richtext-lexical/lexical'
import type {
SerializedListItemNode,
SerializedListNode,
} from '@payloadcms/richtext-lexical/lexical/list'
import type { SerializedHeadingNode } from '@payloadcms/richtext-lexical/lexical/rich-text'
import type { JSX } from 'react'
import React, { Fragment } from 'react'
import { CMSLink } from '../Link'
import {
IS_BOLD,
IS_CODE,
IS_ITALIC,
IS_STRIKETHROUGH,
IS_SUBSCRIPT,
IS_SUPERSCRIPT,
IS_UNDERLINE,
} from './nodeFormat'
interface Props {
nodes: SerializedLexicalNode[]
}
export function serializeLexical({ nodes }: Props): JSX.Element {
return (
<Fragment>
{nodes?.map((_node, index): JSX.Element | null => {
if (_node.type === 'text') {
const node = _node as SerializedTextNode
let text = <React.Fragment key={index}>{node.text}</React.Fragment>
if (node.format & IS_BOLD) {
text = <strong key={index}>{text}</strong>
}
if (node.format & IS_ITALIC) {
text = <em key={index}>{text}</em>
}
if (node.format & IS_STRIKETHROUGH) {
text = (
<span key={index} style={{ textDecoration: 'line-through' }}>
{text}
</span>
)
}
if (node.format & IS_UNDERLINE) {
text = (
<span key={index} style={{ textDecoration: 'underline' }}>
{text}
</span>
)
}
if (node.format & IS_CODE) {
text = <code key={index}>{node.text}</code>
}
if (node.format & IS_SUBSCRIPT) {
text = <sub key={index}>{text}</sub>
}
if (node.format & IS_SUPERSCRIPT) {
text = <sup key={index}>{text}</sup>
}
return text
}
if (_node == null) {
return null
}
// NOTE: Hacky fix for
// https://github.com/facebook/lexical/blob/d10c4e6e55261b2fdd7d1845aed46151d0f06a8c/packages/lexical-list/src/LexicalListItemNode.ts#L133
// which does not return checked: false (only true - i.e. there is no prop for false)
const serializedChildrenFn = (node: SerializedElementNode): JSX.Element | null => {
if (node.children == null) {
return null
} else {
if (node?.type === 'list' && (node as SerializedListNode)?.listType === 'check') {
for (const item of node.children) {
if ('checked' in item) {
if (!item?.checked) {
item.checked = false
}
}
}
return serializeLexical({ nodes: node.children })
} else {
return serializeLexical({ nodes: node.children })
}
}
}
const serializedChildren =
'children' in _node ? serializedChildrenFn(_node as SerializedElementNode) : ''
switch (_node.type) {
case 'heading': {
const node = _node as SerializedHeadingNode
type Heading = Extract<keyof JSX.IntrinsicElements, 'h1' | 'h2' | 'h3' | 'h4' | 'h5'>
const Tag = node?.tag as Heading
return <Tag key={index}>{serializedChildren}</Tag>
}
case 'linebreak': {
return <br key={index} />
}
case 'link': {
const node = _node as SerializedLinkNode
const fields: LinkFields = node.fields
return (
<CMSLink
key={index}
newTab={Boolean(fields?.newTab)}
reference={fields.doc as any}
type={fields.linkType === 'internal' ? 'reference' : 'custom'}
url={fields.url}
>
{serializedChildren}
</CMSLink>
)
}
case 'list': {
const node = _node as SerializedListNode
type List = Extract<keyof JSX.IntrinsicElements, 'ol' | 'ul'>
const Tag = node?.tag as List
return (
<Tag className="list" key={index}>
{serializedChildren}
</Tag>
)
}
case 'listitem': {
const node = _node as SerializedListItemNode
if (node?.checked != null) {
return (
<li
aria-checked={node.checked ? 'true' : 'false'}
className={` ${node.checked ? '' : ''}`}
key={index}
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role="checkbox"
tabIndex={-1}
value={node?.value}
>
{serializedChildren}
</li>
)
} else {
return (
<li key={index} value={node?.value}>
{serializedChildren}
</li>
)
}
}
case 'paragraph': {
return <p key={index}>{serializedChildren}</p>
}
case 'quote': {
return <blockquote key={index}>{serializedChildren}</blockquote>
}
/* case 'block': {
// todo: fix types
//@ts-expect-error
const block = _node.fields
//@ts-expect-error
const blockType = _node.fields?.blockType
if (!block || !blockType) {
return null
}
switch (blockType) {
case 'content':
return <ContentBlock {...block} />
case 'cta':
return <CallToActionBlock {...block} />
case 'archive':
return <ArchiveBlock {...block} />
case 'mediaBlock':
return <MediaBlock {...block} />
case 'banner':
return <BannerBlock {...block} />
case 'code':
return <CodeBlock {...block} />
default:
return null
}
} */
default:
return null
}
})}
</Fragment>
)
}

View File

@@ -1,10 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['localhost', process.env.NEXT_PUBLIC_PAYLOAD_URL || ''].filter(Boolean),
},
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig

View File

@@ -1,41 +0,0 @@
{
"name": "payload-example-form-builder-website",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev -p 3001",
"lint": "next lint",
"start": "next start"
},
"dependencies": {
"@apollo/client": "^3.7.0",
"@faceless-ui/css-grid": "^1.2.0",
"@faceless-ui/modal": "^2.0.2",
"@payloadcms/plugin-form-builder": "beta",
"@payloadcms/richtext-lexical": "beta",
"escape-html": "^1.0.3",
"graphql": "^16.8.1",
"next": "^14.3.0-canary.68",
"react": "18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.41.0",
"react-select": "5.8.3",
"sass": "^1.55.0",
"slate": "^0.84.0"
},
"devDependencies": {
"@next/eslint-plugin-next": "^13.5.6",
"@types/node": "^20",
"@types/react": "^18.0.21",
"eslint": "8.57.0",
"eslint-config-next": "12.3.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-filenames": "^1.3.2",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"typescript": "^5.4"
}
}

View File

@@ -1,241 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {
collections: {
pages: Page
users: User
forms: Form
'form-submissions': FormSubmission
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {
'main-menu': MainMenu
}
}
export interface Page {
id: string
title: string
layout: {
form: string | Form
enableIntro?: boolean
introContent: {
[k: string]: unknown
}[]
id?: string
blockName?: string
blockType: 'formBlock'
}[]
slug?: string
updatedAt: string
createdAt: string
_status?: 'draft' | 'published'
}
export interface Form {
id: string
title: string
fields?: (
| {
name: string
label?: string
width?: number
defaultValue?: string
required?: boolean
id?: string
blockName?: string
blockType: 'text'
}
| {
name: string
label?: string
width?: number
defaultValue?: string
required?: boolean
id?: string
blockName?: string
blockType: 'textarea'
}
| {
name: string
label?: string
width?: number
defaultValue?: string
options?: {
label: string
value: string
id?: string
}[]
required?: boolean
id?: string
blockName?: string
blockType: 'select'
}
| {
name: string
label?: string
width?: number
required?: boolean
id?: string
blockName?: string
blockType: 'email'
}
| {
name: string
label?: string
width?: number
required?: boolean
id?: string
blockName?: string
blockType: 'state'
}
| {
name: string
label?: string
width?: number
required?: boolean
id?: string
blockName?: string
blockType: 'country'
}
| {
name: string
label?: string
width?: number
defaultValue?: number
required?: boolean
id?: string
blockName?: string
blockType: 'number'
}
| {
name: string
label?: string
width?: number
required?: boolean
defaultValue?: boolean
id?: string
blockName?: string
blockType: 'checkbox'
}
| {
message?: {
[k: string]: unknown
}[]
id?: string
blockName?: string
blockType: 'message'
}
)[]
submitButtonLabel?: string
confirmationType?: 'message' | 'redirect'
confirmationMessage: {
[k: string]: unknown
}[]
redirect?: {
url: string
}
emails?: {
emailTo?: string
cc?: string
bcc?: string
replyTo?: string
emailFrom?: string
subject: string
message?: {
[k: string]: unknown
}[]
id?: string
}[]
updatedAt: string
createdAt: string
}
export interface User {
id: string
updatedAt: string
createdAt: string
email: string
resetPasswordToken?: string
resetPasswordExpiration?: string
salt?: string
hash?: string
loginAttempts?: number
lockUntil?: string
password?: string
}
export interface FormSubmission {
id: string
form: string | Form
submissionData?: {
field: string
value: string
id?: string
}[]
updatedAt: string
createdAt: string
}
export interface PayloadPreference {
id: string
user: {
relationTo: 'users'
value: string | User
}
key?: string
value?:
| {
[k: string]: unknown
}
| unknown[]
| string
| number
| boolean
| null
updatedAt: string
createdAt: string
}
export interface PayloadMigration {
id: string
name?: string
batch?: number
updatedAt: string
createdAt: string
}
export interface MainMenu {
id: string
navItems?: {
link: {
type?: 'reference' | 'custom'
newTab?: boolean
reference: {
relationTo: 'pages'
value: string | Page
}
url: string
label: string
}
id?: string
}[]
updatedAt?: string
createdAt?: string
}
declare module 'payload' {
export interface GeneratedTypes {
collections: {
pages: Page
users: User
forms: Form
'form-submissions': FormSubmission
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {
'main-menu': MainMenu
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
'use client'
import React, { createContext, useContext } from 'react'
import type { MainMenu } from '../../payload-types'
export type MainMenuType = MainMenu
export interface IGlobals {
mainMenu: MainMenuType
}
export const GlobalsContext = createContext<IGlobals>({} as IGlobals)
export const useGlobals = (): IGlobals => useContext(GlobalsContext)
export const GlobalsProvider: React.FC<
IGlobals & {
children: React.ReactNode
}
> = (props) => {
const { children, mainMenu } = props
return (
<GlobalsContext.Provider
value={{
mainMenu,
}}
>
{children}
</GlobalsContext.Provider>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,36 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [
"DOM",
"DOM.Iterable",
"ES2022"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
]
},
"include": [
"**/*.ts",
"**/*.tsx",
"next-env.d.ts",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View File

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

View File

@@ -1,29 +0,0 @@
module.exports.formatSlug = (reference) => {
let slug = ''
const { relationTo, value } = reference
if (typeof value === 'object' && value !== null) {
const { slug: referenceSlug, breadcrumbs } = value
// pages could be nested, so use breadcrumbs
if (relationTo === 'pages') {
if (breadcrumbs) {
const { url: lastCrumbURL = '' } = breadcrumbs?.[breadcrumbs.length - 1] || {} // last crumb
slug = lastCrumbURL
} else {
slug = referenceSlug
}
}
if (relationTo !== 'pages') {
slug = `/${relationTo}/${referenceSlug}`
if (relationTo === 'media') {
slug = value.url
}
}
}
return slug
}

View File

@@ -1,10 +0,0 @@
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

View File

@@ -1 +0,0 @@
NEXT_PUBLIC_PAYLOAD_URL=http://localhost:3000

View File

@@ -1,15 +0,0 @@
module.exports = {
extends: ['plugin:@next/next/core-web-vitals', '@payloadcms'],
ignorePatterns: ['**/payload-types.ts'],
overrides: [
{
extends: ['plugin:@typescript-eslint/disable-type-checked'],
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
},
],
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
root: true,
}

View File

@@ -1,38 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env

View File

@@ -1,38 +0,0 @@
# Form Builder Example Front-End
This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages). It was made explicitly for Payload's [Form Builder Example](https://github.com/payloadcms/payload/tree/main/examples/form-builder/payload).
> This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/app), we will add an example for that soon.
## Getting Started
### Payload
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/form-builder/payload). If you have not done so already, clone it down and follow the setup instructions there.
### Next.js App
1. Clone this repo
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server
5. `open http://localhost:3001` to see the result
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Form Builder Example](https://github.com/payloadcms/payload/tree/main/examples/form-builder/payload) for full details.
## Learn More
To learn more about Payload and Next.js, take a look at the following resources:
- [Payload Documentation](https://payloadcms.com/docs) - learn about Payload features and API.
- [Form Builder Plugin Documentation](https://github.com/payloadcms/plugin-form-builder) - learn about the plugin's features.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Payload GitHub repository](https://github.com/payloadcms/payload/) as well as [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Payload deployment documentation](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -1,69 +0,0 @@
@use '../shared.scss';
.checkbox {
position: relative;
margin-bottom: var(--base);
:global {
button {
border: 0;
background: none;
box-shadow: none;
border-radius: 0;
padding: 0;
display: flex;
align-items: center;
cursor: pointer;
font-size: 1rem;
&:focus,
&:active {
outline: none;
}
&:hover {
svg {
opacity: 0.2;
}
}
}
input {
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
}
}
.container {
display: flex;
}
.input {
@include shared.formInput;
padding: 0;
line-height: 0;
position: relative;
width: var(--base);
height: var(--base);
margin-right: calc(var(--base) * 0.5);
margin-bottom: 0;
svg {
opacity: 0;
}
}
.checked {
:global {
svg {
opacity: 1 !important;
}
}
}
.label {
display: block;
}

View File

@@ -1,61 +0,0 @@
import React, { useState } from 'react'
import { CheckboxField } from 'payload-plugin-form-builder/dist/types'
import { UseFormRegister, FieldErrorsImpl, FieldValues } from 'react-hook-form'
import { Check } from '../../../icons/Check'
import { Error } from '../Error'
import { Width } from '../Width'
import classes from './index.module.scss'
export const Checkbox: React.FC<
CheckboxField & {
register: UseFormRegister<FieldValues & any>
setValue: any
getValues: any
errors: Partial<
FieldErrorsImpl<{
[x: string]: any
}>
>
}
> = ({
name,
label,
width,
register,
setValue,
getValues,
required: requiredFromProps,
errors,
}) => {
const [checked, setChecked] = useState(false)
const isCheckboxChecked = getValues(name)
return (
<Width width={width}>
<div className={[classes.checkbox, checked && classes.checked].filter(Boolean).join(' ')}>
<div className={classes.container}>
<input
type="checkbox"
{...register(name, { required: requiredFromProps })}
checked={isCheckboxChecked}
/>
<button
type="button"
onClick={() => {
setValue(name, !checked)
setChecked(!checked)
}}
>
<span className={classes.input}>
<Check />
</span>
</button>
<span className={classes.label}>{label}</span>
</div>
{requiredFromProps && errors[name] && checked === false && <Error />}
</div>
</Width>
)
}

View File

@@ -1,123 +0,0 @@
@use '../shared.scss';
.select {
position: relative;
margin-bottom: var(--base);
}
.label {
margin-bottom: 10px;
display: block;
}
.reactSelect {
display: flex;
:global {
div.rs__control {
@include shared.formInput;
height: auto;
}
.rs__input-container {
color: var(--color-black);
}
.rs__value-container {
padding: 0;
> * {
margin-top: 0;
margin-bottom: 0;
padding-top: 0;
padding-bottom: 0;
}
}
.rs__single-value {
color: var(--color-black);
}
.rs__indicators {
position: absolute;
top: calc(var(--base) * 0.9);
right: calc(var(--base) * 0.9);
.arrow {
transform: rotate(90deg);
}
}
.rs__indicator {
padding: 0px 4px;
cursor: pointer;
svg path {
fill: var(--color-dark-gray);
}
&:hover {
svg path {
fill: var(--color-dark-gray);
}
}
}
.rs__indicator-separator {
display: none;
}
.rs__menu {
color: var(--color-black);
background-color: var(--color-white);
z-index: 2;
border-radius: 0;
box-shadow: 0 4px 11px hsl(0deg 0% 0% / 10%);
}
.rs__menu-list {
padding: calc(var(--base) / 4) 0;
}
.rs__group-heading {
margin-bottom: calc(var(--base) / 2);
}
.rs__option {
font-size: 1rem;
padding: calc(var(--base) / 2) var(--base);
&--is-focused {
background-color: var(--color-light-gray);
color: var(--color-black);
}
&--is-selected {
background-color: var(--color-light-gray);
color: var(--color-black);
}
}
.rs__multi-value {
padding: 0;
background: var(--color-light-gray);
}
.rs__multi-value__label {
max-width: 150px;
color: var(--color-black);
padding: calc(var(--base) / 8) calc(var(--base) / 4);
}
.rs__multi-value__remove {
cursor: pointer;
&:hover {
color: var(--color-black);
background: var(--color-light-gray);
}
}
.rs__clear-indicator {
cursor: pointer;
}
}
}

View File

@@ -1,48 +0,0 @@
import React from 'react'
import ReactSelect from 'react-select'
import { CountryField } from 'payload-plugin-form-builder/dist/types'
import { Controller, Control, FieldValues, FieldErrorsImpl } from 'react-hook-form'
import { countryOptions } from './options'
import { Error } from '../Error'
import { Width } from '../Width'
import classes from './index.module.scss'
export const Country: React.FC<
CountryField & {
control: Control<FieldValues, any>
errors: Partial<
FieldErrorsImpl<{
[x: string]: any
}>
>
}
> = ({ name, label, width, control, required, errors }) => {
return (
<Width width={width}>
<div className={classes.select}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<Controller
control={control}
rules={{ required }}
name={name}
defaultValue=""
render={({ field: { onChange, value } }) => (
<ReactSelect
instanceId={name}
options={countryOptions}
value={countryOptions.find((c) => c.value === value)}
onChange={(val) => onChange(val.value)}
className={classes.reactSelect}
classNamePrefix="rs"
inputId={name}
/>
)}
/>
{required && errors[name] && <Error />}
</div>
</Width>
)
}

View File

@@ -1,982 +0,0 @@
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',
},
]

View File

@@ -1,15 +0,0 @@
@use '../shared.scss';
.wrap {
position: relative;
margin-bottom: var(--base);
}
.input {
@include shared.formInput;
}
.label {
margin-bottom: 10px;
display: block;
}

View File

@@ -1,36 +0,0 @@
import React from 'react'
import { EmailField } from 'payload-plugin-form-builder/dist/types'
import { UseFormRegister, FieldValues, FieldErrorsImpl } from 'react-hook-form'
import { Error } from '../Error'
import { Width } from '../Width'
import classes from './index.module.scss'
export const Email: React.FC<
EmailField & {
register: UseFormRegister<FieldValues & any>
errors: Partial<
FieldErrorsImpl<{
[x: string]: any
}>
>
}
> = ({ name, width, label, register, required: requiredFromProps, errors }) => {
return (
<Width width={width}>
<div className={classes.wrap}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<input
type="text"
placeholder="Email"
className={classes.input}
id={name}
{...register(name, { required: requiredFromProps, pattern: /^\S+@\S+$/i })}
/>
{requiredFromProps && errors[name] && <Error />}
</div>
</Width>
)
}

View File

@@ -1,4 +0,0 @@
.error {
margin-top: 5px;
color: var(--color-red);
}

View File

@@ -1,6 +0,0 @@
import * as React from 'react'
import classes from './index.module.scss'
export const Error: React.FC = () => {
return <div className={classes.error}>This field is required</div>
}

View File

@@ -1,9 +0,0 @@
@use '../.../../../../../css/queries.scss' as *;
.message {
margin: var(--base) 0 var(--base) 0;
@include mid-break {
margin: calc(var(--base) * 0.5) 0 calc(var(--base) * 0.5) 0;
}
}

View File

@@ -1,14 +0,0 @@
import React from 'react'
import { MessageField } from 'payload-plugin-form-builder/dist/types'
import RichText from '../../../RichText'
import { Width } from '../Width'
import classes from './index.module.scss'
export const Message: React.FC<MessageField> = ({ message }) => {
return (
<Width width="100">
<RichText content={message} className={classes.message} />
</Width>
)
}

View File

@@ -1,15 +0,0 @@
@use '../shared.scss';
.wrap {
position: relative;
margin-bottom: var(--base);
}
.input {
@include shared.formInput;
}
.label {
margin-bottom: 10px;
display: block;
}

View File

@@ -1,35 +0,0 @@
import React from 'react'
import { TextField } from 'payload-plugin-form-builder/dist/types'
import { UseFormRegister, FieldValues, FieldErrorsImpl } from 'react-hook-form'
import { Error } from '../Error'
import { Width } from '../Width'
import classes from './index.module.scss'
export const Number: React.FC<
TextField & {
register: UseFormRegister<FieldValues & any>
errors: Partial<
FieldErrorsImpl<{
[x: string]: any
}>
>
}
> = ({ name, label, width, register, required: requiredFromProps, errors }) => {
return (
<Width width={width}>
<div className={classes.wrap}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<input
type="number"
className={classes.input}
id={name}
{...register(name, { required: requiredFromProps })}
/>
{requiredFromProps && errors[name] && <Error />}
</div>
</Width>
)
}

View File

@@ -1,123 +0,0 @@
@use '../shared.scss';
.select {
position: relative;
margin-bottom: var(--base);
}
.label {
margin-bottom: 10px;
display: block;
}
.reactSelect {
display: flex;
:global {
div.rs__control {
@include shared.formInput;
height: auto;
}
.rs__input-container {
color: var(--color-black);
}
.rs__value-container {
padding: 0;
> * {
margin-top: 0;
margin-bottom: 0;
padding-top: 0;
padding-bottom: 0;
}
}
.rs__single-value {
color: var(--color-black);
}
.rs__indicators {
position: absolute;
top: calc(var(--base) * 0.9);
right: calc(var(--base) * 0.9);
.arrow {
transform: rotate(90deg);
}
}
.rs__indicator {
padding: 0px 4px;
cursor: pointer;
svg path {
fill: var(--color-dark-gray);
}
&:hover {
svg path {
fill: var(--color-dark-gray);
}
}
}
.rs__indicator-separator {
display: none;
}
.rs__menu {
color: var(--color-black);
background-color: var(--color-white);
z-index: 2;
border-radius: 0;
box-shadow: 0 4px 11px hsl(0deg 0% 0% / 10%);
}
.rs__menu-list {
padding: calc(var(--base) / 4) 0;
}
.rs__group-heading {
margin-bottom: calc(var(--base) / 2);
}
.rs__option {
font-size: 1rem;
padding: calc(var(--base) / 2) var(--base);
&--is-focused {
background-color: var(--color-light-gray);
color: var(--color-black);
}
&--is-selected {
background-color: var(--color-light-gray);
color: var(--color-black);
}
}
.rs__multi-value {
padding: 0;
background: var(--color-light-gray);
}
.rs__multi-value__label {
max-width: 150px;
color: var(--color-black);
padding: calc(var(--base) / 8) calc(var(--base) / 4);
}
.rs__multi-value__remove {
cursor: pointer;
&:hover {
color: var(--color-black);
background: var(--color-light-gray);
}
}
.rs__clear-indicator {
cursor: pointer;
}
}
}

View File

@@ -1,47 +0,0 @@
import React from 'react'
import ReactSelect from 'react-select'
import { SelectField } from 'payload-plugin-form-builder/dist/types'
import { Controller, Control, FieldValues, FieldErrorsImpl } from 'react-hook-form'
import { Error } from '../Error'
import { Width } from '../Width'
import classes from './index.module.scss'
export const Select: React.FC<
SelectField & {
control: Control<FieldValues, any>
errors: Partial<
FieldErrorsImpl<{
[x: string]: any
}>
>
}
> = ({ name, label, width, options, control, required, errors }) => {
return (
<Width width={width}>
<div className={classes.select}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<Controller
control={control}
rules={{ required }}
name={name}
defaultValue=""
render={({ field: { onChange, value } }) => (
<ReactSelect
instanceId={name}
options={options}
value={options.find((s) => s.value === value)}
onChange={(val) => onChange(val.value)}
className={classes.reactSelect}
classNamePrefix="rs"
inputId={name}
/>
)}
/>
{required && errors[name] && <Error />}
</div>
</Width>
)
}

View File

@@ -1,123 +0,0 @@
@use '../shared.scss';
.select {
position: relative;
margin-bottom: var(--base);
}
.label {
margin-bottom: 10px;
display: block;
}
.reactSelect {
display: flex;
:global {
div.rs__control {
@include shared.formInput;
height: auto;
}
.rs__input-container {
color: var(--color-black);
}
.rs__value-container {
padding: 0;
> * {
margin-top: 0;
margin-bottom: 0;
padding-top: 0;
padding-bottom: 0;
}
}
.rs__single-value {
color: var(--color-black);
}
.rs__indicators {
position: absolute;
top: calc(var(--base) * 0.9);
right: calc(var(--base) * 0.9);
.arrow {
transform: rotate(90deg);
}
}
.rs__indicator {
padding: 0px 4px;
cursor: pointer;
svg path {
fill: var(--color-dark-gray);
}
&:hover {
svg path {
fill: var(--color-dark-gray);
}
}
}
.rs__indicator-separator {
display: none;
}
.rs__menu {
color: var(--color-black);
background-color: var(--color-white);
z-index: 2;
border-radius: 0;
box-shadow: 0 4px 11px hsl(0deg 0% 0% / 10%);
}
.rs__menu-list {
padding: calc(var(--base) / 4) 0;
}
.rs__group-heading {
margin-bottom: calc(var(--base) / 2);
}
.rs__option {
font-size: 1rem;
padding: calc(var(--base) / 2) var(--base);
&--is-focused {
background-color: var(--color-light-gray);
color: var(--color-black);
}
&--is-selected {
background-color: var(--color-light-gray);
color: var(--color-black);
}
}
.rs__multi-value {
padding: 0;
background: var(--color-light-gray);
}
.rs__multi-value__label {
max-width: 150px;
color: var(--color-black);
padding: calc(var(--base) / 8) calc(var(--base) / 4);
}
.rs__multi-value__remove {
cursor: pointer;
&:hover {
color: var(--color-black);
background: var(--color-light-gray);
}
}
.rs__clear-indicator {
cursor: pointer;
}
}
}

View File

@@ -1,48 +0,0 @@
import React from 'react'
import ReactSelect from 'react-select'
import { StateField } from 'payload-plugin-form-builder/dist/types'
import { Controller, Control, FieldValues, FieldErrorsImpl } from 'react-hook-form'
import { stateOptions } from './options'
import { Error } from '../Error'
import { Width } from '../Width'
import classes from './index.module.scss'
export const State: React.FC<
StateField & {
control: Control<FieldValues, any>
errors: Partial<
FieldErrorsImpl<{
[x: string]: any
}>
>
}
> = ({ name, label, width, control, required, errors }) => {
return (
<Width width={width}>
<div className={classes.select}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<Controller
control={control}
rules={{ required }}
name={name}
defaultValue=""
render={({ field: { onChange, value } }) => (
<ReactSelect
instanceId={name}
options={stateOptions}
value={stateOptions.find((t) => t.value === value)}
onChange={(val) => onChange(val.value)}
className={classes.reactSelect}
classNamePrefix="rs"
id={name}
/>
)}
/>
{required && errors[name] && <Error />}
</div>
</Width>
)
}

View File

@@ -1,52 +0,0 @@
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' },
]

View File

@@ -1,15 +0,0 @@
@use '../shared.scss';
.wrap {
position: relative;
margin-bottom: var(--base);
}
.input {
@include shared.formInput;
}
.label {
margin-bottom: 10px;
display: block;
}

View File

@@ -1,35 +0,0 @@
import React from 'react'
import { TextField } from 'payload-plugin-form-builder/dist/types'
import { UseFormRegister, FieldValues, FieldErrorsImpl } from 'react-hook-form'
import { Error } from '../Error'
import { Width } from '../Width'
import classes from './index.module.scss'
export const Text: React.FC<
TextField & {
register: UseFormRegister<FieldValues & any>
errors: Partial<
FieldErrorsImpl<{
[x: string]: any
}>
>
}
> = ({ name, label, width, register, required: requiredFromProps, errors }) => {
return (
<Width width={width}>
<div className={classes.wrap}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<input
type="text"
className={classes.input}
id={name}
{...register(name, { required: requiredFromProps })}
/>
{requiredFromProps && errors[name] && <Error />}
</div>
</Width>
)
}

View File

@@ -1,16 +0,0 @@
@use '../shared.scss';
.wrap {
position: relative;
margin-bottom: var(--base);
}
.textarea {
@include shared.formInput;
height: unset;
}
.label {
margin-bottom: 10px;
display: block;
}

View File

@@ -1,36 +0,0 @@
import React from 'react'
import { TextField } from 'payload-plugin-form-builder/dist/types'
import { UseFormRegister, FieldValues, FieldErrorsImpl } from 'react-hook-form'
import { Error } from '../Error'
import { Width } from '../Width'
import classes from './index.module.scss'
export const Textarea: React.FC<
TextField & {
register: UseFormRegister<FieldValues & any>
rows?: number
errors: Partial<
FieldErrorsImpl<{
[x: string]: any
}>
>
}
> = ({ name, label, width, rows = 3, register, required: requiredFromProps, errors }) => {
return (
<Width width={width}>
<div className={classes.wrap}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<textarea
rows={rows}
className={classes.textarea}
id={name}
{...register(name, { required: requiredFromProps })}
/>
{requiredFromProps && errors[name] && <Error />}
</div>
</Width>
)
}

View File

@@ -1,10 +0,0 @@
@use '../../../../css/queries.scss' as *;
.width {
padding-left: calc(var(--base) * 0.5);
padding-right: calc(var(--base) * 0.5);
@include mid-break {
width: 100% !important;
}
}

View File

@@ -1,13 +0,0 @@
import * as React from 'react'
import classes from './index.module.scss'
export const Width: React.FC<{
width?: string
children: React.ReactNode
}> = ({ width, children }) => {
return (
<div className={classes.width} style={{ width: width ? `${width}%` : undefined }}>
{children}
</div>
)
}

View File

@@ -1,21 +0,0 @@
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,
}

View File

@@ -1,69 +0,0 @@
@use '../.../../../../css/queries.scss' as *;
.form {
display: flex;
flex-direction: column;
}
.hasSubmitted {
align-items: center;
justify-content: center;
height: 80vh;
@include small-break {
height: 100%;
}
}
.intro {
margin-bottom: var(--base);
@include mid-break {
margin-bottom: var(--base);
}
}
.confirmationMessage {
max-width: 800px;
text-align: center;
* {
margin: 0;
}
}
.fieldWrap {
position: relative;
z-index: 2;
display: flex;
flex-wrap: wrap;
margin-left: calc(var(--base) * -0.5);
margin-right: calc(var(--base) * -0.5);
width: calc(100% + #{var(--base)});
> * {
width: 100%;
}
}
.Select {
& label {
position: absolute;
top: calc(var(--base) * -0.5);
}
}
.sidebarContent {
@include mid-break {
display: none;
}
}
.mobileSidebar {
display: none;
@include mid-break {
display: block;
margin-bottom: calc(var(--base) * 2);
}
}

View File

@@ -1,172 +0,0 @@
import type { Form as FormType } from '@payloadcms/plugin-form-builder/types'
import { useRouter } from 'next/router'
import React, { useCallback, useState } from 'react'
import { useForm } from 'react-hook-form'
import { Button } from '../../Button'
import { Gutter } from '../../Gutter'
import RichText from '../../RichText'
import { buildInitialFormState } from './buildInitialFormState'
import { fields } from './fields'
import classes from './index.module.scss'
export type Value = unknown
export interface Property {
[key: string]: Value
}
export interface Data {
[key: string]: Property | Property[] | Value
}
export type FormBlockType = {
blockName?: string
blockType?: 'formBlock'
enableIntro: boolean
form: FormType
introContent?: {
[k: string]: unknown
}[]
}
export const FormBlock: React.FC<
FormBlockType & {
id?: string
}
> = (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 },
getValues,
handleSubmit,
register,
setValue,
} = 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_PAYLOAD_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) void router.push(redirectUrl)
}
} catch (err) {
console.warn(err)
setIsLoading(false)
setError({
message: 'Something went wrong.',
})
}
}
void submitForm()
},
[router, formID, redirect, confirmationType],
)
return (
<Gutter>
<div
className={[classes.form, hasSubmitted && classes.hasSubmitted].filter(Boolean).join(' ')}
>
{enableIntro && introContent && !hasSubmitted && (
<RichText className={classes.intro} content={introContent} />
)}
{!isLoading && hasSubmitted && confirmationType === 'message' && (
<RichText className={classes.confirmationMessage} 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={classes.fieldWrap}>
{formFromProps &&
formFromProps.fields &&
formFromProps.fields.map((field, index) => {
const Field: React.FC<any> = fields?.[field.blockType]
if (Field) {
return (
<React.Fragment key={index}>
<Field
form={formFromProps}
{...field}
{...formMethods}
control={control}
errors={errors}
register={register}
/>
</React.Fragment>
)
}
return null
})}
</div>
<Button appearance="primary" el="button" form={formID} label={submitButtonLabel} />
</form>
)}
</div>
</Gutter>
)
}

View File

@@ -1,42 +0,0 @@
@use '../../../css/common.scss' as *;
@mixin formInput() {
all: unset;
-webkit-appearance: none;
box-sizing: border-box;
font-family: var(--font-body);
width: 100%;
border: 1px solid var(--color-black);
background: var(--color-white);
color: var(--color-black);
font-size: 1rem;
height: calc(var(--base) * 2.5);
line-height: var(--base);
padding: calc(var(--base) * 0.75);
&::-moz-placeholder,
&::-webkit-input-placeholder {
color: var(--color-mid-gray);
font-weight: normal;
font-size: 1rem;
}
&:hover {
border-color: var(--color-mid-gray);
}
&:focus,
&:active {
border-color: var(--color-gray);
outline: 0;
}
&:disabled {
background: var(--color-light-gray);
color: var(--color-gray);
&:hover {
border-color: var(--color-light-gray);
}
}
}

View File

@@ -1,51 +0,0 @@
import React, { Fragment } from 'react'
import type { Page } from '../../payload-types'
import { toKebabCase } from '../../utilities/toKebabCase'
import { VerticalPadding } from '../VerticalPadding'
import { FormBlock } from './Form'
const blockComponents = {
formBlock: FormBlock,
}
const Blocks: React.FC<{
blocks: Page['layout']
}> = (props) => {
const { blocks } = props
const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0
if (hasBlocks) {
return (
<Fragment>
{blocks.map((block, index) => {
const { blockName, blockType, form } = block
const isFormBlock = blockType === 'formBlock'
{
/*@ts-expect-error*/
}
const formID: string = isFormBlock && form && (typeof form === 'string' ? form : form.id)
if (blockType && blockType in blockComponents) {
const Block = blockComponents[blockType]
return (
<VerticalPadding bottom="small" key={isFormBlock ? formID : index} top="small">
{/*@ts-expect-error*/}
<Block id={toKebabCase(blockName)} {...block} />
</VerticalPadding>
)
}
return null
})}
</Fragment>
)
}
return null
}
export default Blocks

View File

@@ -1,61 +0,0 @@
@import '../../css/type.scss';
.content {
display: flex;
align-items: center;
justify-content: center;
svg {
margin-right: calc(var(--base) / 2);
width: var(--base);
height: var(--base);
}
}
.label {
@extend %label;
display: flex;
align-items: center;
}
.button {
all: unset;
cursor: pointer;
box-sizing: border-box;
display: inline-flex;
position: relative;
text-decoration: none;
padding: 12px 18px;
margin-bottom: var(--base);
align-items: center;
justify-content: center;
}
.appearance--primary {
background-color: var(--color-black);
color: var(--color-white);
&:hover,
&:focus {
background-color: var(--color-white);
color: var(--color-black);
box-shadow: inset 0 0 0 1px var(--color-black);
}
}
.appearance--secondary {
background-color: var(--color-white);
box-shadow: inset 0 0 0 1px var(--color-black);
&:hover,
&:focus {
background-color: var(--color-black);
color: var(--color-white);
box-shadow: inset 0 0 0 1px var(--color-black);
}
}
.appearance--default {
padding: 0;
margin-left: -8px;
}

View File

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

View File

@@ -1,14 +0,0 @@
import React, { useEffect } from 'react'
import { useModal } from '@faceless-ui/modal'
import { useRouter } from 'next/router'
export const CloseModalOnRouteChange: React.FC = () => {
const { closeAllModals } = useModal()
const { asPath } = useRouter()
useEffect(() => {
closeAllModals()
}, [asPath, closeAllModals])
return null
}

View File

@@ -1,7 +0,0 @@
.gutterLeft {
padding-left: var(--gutter-h);
}
.gutterRight {
padding-right: var(--gutter-h);
}

View File

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

View File

@@ -1,29 +0,0 @@
import { Modal } from '@faceless-ui/modal'
import { HeaderBar } from '.'
import { MainMenu } from '../../payload-types'
import { Gutter } from '../Gutter'
import { CMSLink } from '../Link'
import classes from './mobileMenuModal.module.scss'
type Props = {
navItems: MainMenu['navItems']
}
export const slug = 'menu-modal'
export const MobileMenuModal: React.FC<Props> = ({ navItems }) => {
return (
<Modal slug={slug} className={classes.mobileMenuModal}>
<HeaderBar />
<Gutter>
<div className={classes.mobileMenuItems}>
{navItems.map(({ link }, i) => {
return <CMSLink className={classes.menuItem} key={i} {...link} />
})}
</div>
</Gutter>
</Modal>
)
}

View File

@@ -1,36 +0,0 @@
@use '../../css/queries.scss' as *;
.header {
padding: var(--base) 0;
z-index: var(--header-z-index);
}
.wrap {
display: flex;
justify-content: space-between;
}
.nav {
a {
text-decoration: none;
margin-left: var(--base);
}
@include mid-break {
display: none;
}
}
.mobileMenuToggler {
all: unset;
cursor: pointer;
display: none;
&[aria-expanded='true'] {
transform: rotate(-25deg);
}
@include mid-break {
display: block;
}
}

View File

@@ -1,53 +0,0 @@
import { ModalToggler } from '@faceless-ui/modal'
import Link from 'next/link'
import React from 'react'
import { useGlobals } from '../../providers/Globals'
import { Gutter } from '../Gutter'
import { MenuIcon } from '../icons/Menu'
import { CMSLink } from '../Link'
import { Logo } from '../Logo'
import { MobileMenuModal, slug as menuModalSlug } from './MobileMenuModal'
import classes from './index.module.scss'
type HeaderBarProps = {
children?: React.ReactNode
}
export const HeaderBar: React.FC<HeaderBarProps> = ({ children }) => {
return (
<header className={classes.header}>
<Gutter className={classes.wrap}>
<Link href="/">
<Logo />
</Link>
{children}
<ModalToggler slug={menuModalSlug} className={classes.mobileMenuToggler}>
<MenuIcon />
</ModalToggler>
</Gutter>
</header>
)
}
export const Header: React.FC = () => {
const {
mainMenu: { navItems },
} = useGlobals()
return (
<>
<HeaderBar>
<nav className={classes.nav}>
{navItems.map(({ link }, i) => {
return <CMSLink key={i} {...link} />
})}
</nav>
</HeaderBar>
<MobileMenuModal navItems={navItems} />
</>
)
}

View File

@@ -1,31 +0,0 @@
@use '../../css/common.scss' as *;
.mobileMenuModal {
position: relative;
width: 100%;
height: 100%;
border: none;
padding: 0;
opacity: 1;
display: none;
@include mid-break {
display: block;
}
}
.contentContainer {
padding: 20px;
}
.mobileMenuItems {
display: flex;
flex-direction: column;
height: 100%;
margin-top: 30px;
}
.menuItem {
@extend %h4;
margin-top: 0;
}

View File

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

View File

@@ -1,48 +0,0 @@
import React from 'react'
export const Logo: React.FC = () => {
return (
<svg
width="123"
height="29"
viewBox="0 0 123 29"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M34.7441 22.9997H37.2741V16.3297H41.5981C44.7031 16.3297 46.9801 14.9037 46.9801 11.4537C46.9801 8.00369 44.7031 6.55469 41.5981 6.55469H34.7441V22.9997ZM37.2741 14.1447V8.73969H41.4831C43.3921 8.73969 44.3581 9.59069 44.3581 11.4537C44.3581 13.2937 43.3921 14.1447 41.4831 14.1447H37.2741Z"
fill="black"
/>
<path
d="M51.3652 23.3217C53.2742 23.3217 54.6082 22.5627 55.3672 21.3437H55.4132C55.5512 22.6777 56.1492 23.1147 57.2762 23.1147C57.6442 23.1147 58.0352 23.0687 58.4262 22.9767V21.5967C58.2882 21.6197 58.2192 21.6197 58.1502 21.6197C57.7132 21.6197 57.5982 21.1827 57.5982 20.3317V14.9497C57.5982 11.9137 55.6662 10.9017 53.2512 10.9017C49.6632 10.9017 48.1912 12.6727 48.0762 14.9267H50.3762C50.4912 13.3627 51.1122 12.7187 53.1592 12.7187C54.8842 12.7187 55.3902 13.4317 55.3902 14.2827C55.3902 15.4327 54.2632 15.6627 52.4232 16.0077C49.5022 16.5597 47.5242 17.3417 47.5242 19.9637C47.5242 21.9647 49.0192 23.3217 51.3652 23.3217ZM49.8702 19.8027C49.8702 18.5837 50.7442 18.0087 52.8142 17.5947C54.0102 17.3417 55.0222 17.0887 55.3902 16.7437V18.4227C55.3902 20.4697 53.8952 21.5047 51.8712 21.5047C50.4682 21.5047 49.8702 20.9067 49.8702 19.8027Z"
fill="black"
/>
<path
d="M61.4996 27.1167C63.3166 27.1167 64.4436 26.1737 65.5706 23.2757L70.2166 11.2697H67.8476L64.6276 20.2397H64.5816L61.1546 11.2697H58.6936L63.4316 22.8847C62.9716 24.7247 61.9136 25.1847 61.0166 25.1847C60.6486 25.1847 60.4416 25.1617 60.0506 25.1157V26.9557C60.6486 27.0707 60.9936 27.1167 61.4996 27.1167Z"
fill="black"
/>
<path d="M71.5939 22.9997H73.8479V6.55469H71.5939V22.9997Z" fill="black" />
<path
d="M81.6221 23.3447C85.2791 23.3447 87.4871 20.7917 87.4871 17.1117C87.4871 13.4547 85.2791 10.9017 81.6451 10.9017C77.9651 10.9017 75.7571 13.4777 75.7571 17.1347C75.7571 20.8147 77.9651 23.3447 81.6221 23.3447ZM78.1031 17.1347C78.1031 14.6737 79.2071 12.7877 81.6451 12.7877C84.0371 12.7877 85.1411 14.6737 85.1411 17.1347C85.1411 19.5727 84.0371 21.4817 81.6451 21.4817C79.2071 21.4817 78.1031 19.5727 78.1031 17.1347Z"
fill="black"
/>
<path
d="M92.6484 23.3217C94.5574 23.3217 95.8914 22.5627 96.6504 21.3437H96.6964C96.8344 22.6777 97.4324 23.1147 98.5594 23.1147C98.9274 23.1147 99.3184 23.0687 99.7094 22.9767V21.5967C99.5714 21.6197 99.5024 21.6197 99.4334 21.6197C98.9964 21.6197 98.8814 21.1827 98.8814 20.3317V14.9497C98.8814 11.9137 96.9494 10.9017 94.5344 10.9017C90.9464 10.9017 89.4744 12.6727 89.3594 14.9267H91.6594C91.7744 13.3627 92.3954 12.7187 94.4424 12.7187C96.1674 12.7187 96.6734 13.4317 96.6734 14.2827C96.6734 15.4327 95.5464 15.6627 93.7064 16.0077C90.7854 16.5597 88.8074 17.3417 88.8074 19.9637C88.8074 21.9647 90.3024 23.3217 92.6484 23.3217ZM91.1534 19.8027C91.1534 18.5837 92.0274 18.0087 94.0974 17.5947C95.2934 17.3417 96.3054 17.0887 96.6734 16.7437V18.4227C96.6734 20.4697 95.1784 21.5047 93.1544 21.5047C91.7514 21.5047 91.1534 20.9067 91.1534 19.8027Z"
fill="black"
/>
<path
d="M106.181 23.3217C108.021 23.3217 109.148 22.4477 109.792 21.6197H109.838V22.9997H112.092V6.55469H109.838V12.6957H109.792C109.148 11.7757 108.021 10.9247 106.181 10.9247C103.191 10.9247 100.914 13.2707 100.914 17.1347C100.914 20.9987 103.191 23.3217 106.181 23.3217ZM103.26 17.1347C103.26 14.8347 104.341 12.8107 106.549 12.8107C108.573 12.8107 109.815 14.4667 109.815 17.1347C109.815 19.7797 108.573 21.4587 106.549 21.4587C104.341 21.4587 103.26 19.4347 103.26 17.1347Z"
fill="black"
/>
<path
d="M12.2464 2.33838L22.2871 8.83812V21.1752L14.7265 25.8854V13.5484L4.67383 7.05725L12.2464 2.33838Z"
fill="black"
/>
<path d="M11.477 25.2017V15.5747L3.90039 20.2936L11.477 25.2017Z" fill="black" />
<path
d="M120.442 6.30273C119.086 6.30273 117.998 7.29978 117.998 8.75952C117.998 10.2062 119.086 11.1968 120.442 11.1968C121.791 11.1968 122.879 10.2062 122.879 8.75952C122.879 7.29978 121.791 6.30273 120.442 6.30273ZM120.442 10.7601C119.34 10.7601 118.48 9.95207 118.48 8.75952C118.48 7.54742 119.34 6.73935 120.442 6.73935C121.563 6.73935 122.397 7.54742 122.397 8.75952C122.397 9.95207 121.563 10.7601 120.442 10.7601ZM120.52 8.97457L121.048 9.9651H121.641L121.041 8.86378C121.367 8.72042 121.511 8.45975 121.511 8.17302C121.511 7.49528 121.054 7.36495 120.285 7.36495H119.49V9.9651H120.025V8.97457H120.52ZM120.37 7.78853C120.729 7.78853 120.976 7.86673 120.976 8.17953C120.976 8.43368 120.807 8.56402 120.403 8.56402H120.025V7.78853H120.37Z"
fill="black"
/>
</svg>
)
}

View File

@@ -1,3 +0,0 @@
.container {
max-width: 480px;
}

View File

@@ -1,26 +0,0 @@
import React from 'react'
import styles from './index.module.scss'
import { serializeLexical } from './serialize'
const RichText: React.FC<{ className?: string; content: any; enableGutter?: boolean }> = ({
className,
content,
enableGutter = true,
}) => {
if (!content) {
return null
}
return (
<div className={[className].filter(Boolean).join(' ')}>
{content &&
!Array.isArray(content) &&
typeof content === 'object' &&
'root' in content &&
serializeLexical({ nodes: content?.root?.children })}
</div>
)
}
export default RichText

View File

@@ -1,207 +0,0 @@
import type { LinkFields, SerializedLinkNode } from '@payloadcms/richtext-lexical'
import type {
SerializedElementNode,
SerializedLexicalNode,
SerializedTextNode,
} from '@payloadcms/richtext-lexical/lexical'
import type {
SerializedListItemNode,
SerializedListNode,
} from '@payloadcms/richtext-lexical/lexical/list'
import type { SerializedHeadingNode } from '@payloadcms/richtext-lexical/lexical/rich-text'
import React, { Fragment } from 'react'
import { CMSLink } from '../Link'
import {
IS_BOLD,
IS_CODE,
IS_ITALIC,
IS_STRIKETHROUGH,
IS_SUBSCRIPT,
IS_SUPERSCRIPT,
IS_UNDERLINE,
} from './nodeFormat'
interface Props {
nodes: SerializedLexicalNode[]
}
export function serializeLexical({ nodes }: Props): JSX.Element {
return (
<Fragment>
{nodes?.map((_node, index): JSX.Element | null => {
if (_node.type === 'text') {
const node = _node as SerializedTextNode
let text = <React.Fragment key={index}>{node.text}</React.Fragment>
if (node.format & IS_BOLD) {
text = <strong key={index}>{text}</strong>
}
if (node.format & IS_ITALIC) {
text = <em key={index}>{text}</em>
}
if (node.format & IS_STRIKETHROUGH) {
text = (
<span key={index} style={{ textDecoration: 'line-through' }}>
{text}
</span>
)
}
if (node.format & IS_UNDERLINE) {
text = (
<span key={index} style={{ textDecoration: 'underline' }}>
{text}
</span>
)
}
if (node.format & IS_CODE) {
text = <code key={index}>{node.text}</code>
}
if (node.format & IS_SUBSCRIPT) {
text = <sub key={index}>{text}</sub>
}
if (node.format & IS_SUPERSCRIPT) {
text = <sup key={index}>{text}</sup>
}
return text
}
if (_node == null) {
return null
}
// NOTE: Hacky fix for
// https://github.com/facebook/lexical/blob/d10c4e6e55261b2fdd7d1845aed46151d0f06a8c/packages/lexical-list/src/LexicalListItemNode.ts#L133
// which does not return checked: false (only true - i.e. there is no prop for false)
const serializedChildrenFn = (node: SerializedElementNode): JSX.Element | null => {
if (node.children == null) {
return null
} else {
if (node?.type === 'list' && (node as SerializedListNode)?.listType === 'check') {
for (const item of node.children) {
if ('checked' in item) {
if (!item?.checked) {
item.checked = false
}
}
}
return serializeLexical({ nodes: node.children })
} else {
return serializeLexical({ nodes: node.children })
}
}
}
const serializedChildren =
'children' in _node ? serializedChildrenFn(_node as SerializedElementNode) : ''
switch (_node.type) {
case 'heading': {
const node = _node as SerializedHeadingNode
type Heading = Extract<keyof JSX.IntrinsicElements, 'h1' | 'h2' | 'h3' | 'h4' | 'h5'>
const Tag = node?.tag as Heading
return <Tag key={index}>{serializedChildren}</Tag>
}
case 'linebreak': {
return <br key={index} />
}
case 'link': {
const node = _node as SerializedLinkNode
const fields: LinkFields = node.fields
return (
<CMSLink
key={index}
newTab={Boolean(fields?.newTab)}
reference={fields.doc as any}
type={fields.linkType === 'internal' ? 'reference' : 'custom'}
url={fields.url}
>
{serializedChildren}
</CMSLink>
)
}
case 'list': {
const node = _node as SerializedListNode
type List = Extract<keyof JSX.IntrinsicElements, 'ol' | 'ul'>
const Tag = node?.tag as List
return (
<Tag className="list" key={index}>
{serializedChildren}
</Tag>
)
}
case 'listitem': {
const node = _node as SerializedListItemNode
if (node?.checked != null) {
return (
<li
aria-checked={node.checked ? 'true' : 'false'}
className={` ${node.checked ? '' : ''}`}
key={index}
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role="checkbox"
tabIndex={-1}
value={node?.value}
>
{serializedChildren}
</li>
)
} else {
return (
<li key={index} value={node?.value}>
{serializedChildren}
</li>
)
}
}
case 'paragraph': {
return <p key={index}>{serializedChildren}</p>
}
case 'quote': {
return <blockquote key={index}>{serializedChildren}</blockquote>
}
/* case 'block': {
// todo: fix types
//@ts-expect-error
const block = _node.fields
//@ts-expect-error
const blockType = _node.fields?.blockType
if (!block || !blockType) {
return null
}
switch (blockType) {
case 'content':
return <ContentBlock {...block} />
case 'cta':
return <CallToActionBlock {...block} />
case 'archive':
return <ArchiveBlock {...block} />
case 'mediaBlock':
return <MediaBlock {...block} />
case 'banner':
return <BannerBlock {...block} />
case 'code':
return <CodeBlock {...block} />
default:
return null
}
} */
default:
return null
}
})}
</Fragment>
)
}

View File

@@ -1,23 +0,0 @@
.top-large {
padding-top: var(--block-padding);
}
.top-medium {
padding-top: calc(var(--block-padding) / 2);
}
.top-small {
padding-top: calc(var(--block-padding) / 3);
}
.bottom-large {
padding-bottom: var(--block-padding);
}
.bottom-medium {
padding-bottom: calc(var(--block-padding) / 2);
}
.bottom-small {
padding-bottom: calc(var(--block-padding) / 3);
}

View File

@@ -1,28 +0,0 @@
import React from 'react'
import classes from './index.module.scss'
export type VerticalPaddingOptions = 'large' | 'medium' | 'small' | 'none'
type Props = {
top?: VerticalPaddingOptions
bottom?: VerticalPaddingOptions
children: React.ReactNode
className?: string
}
export const VerticalPadding: React.FC<Props> = ({
top = 'medium',
bottom = 'medium',
className,
children,
}) => {
return (
<div
className={[className, classes[`top-${top}`], classes[`bottom-${bottom}`]]
.filter(Boolean)
.join(' ')}
>
{children}
</div>
)
}

View File

@@ -1,16 +0,0 @@
.checkBox {
height: var(--base);
width: var(--base);
.stroke {
stroke-width: 1px;
fill: none;
stroke: currentColor;
}
&:local() {
.stroke {
stroke-width: 2px;
}
}
}

View File

@@ -1,21 +0,0 @@
import React from 'react'
import classes from './index.module.scss'
export const Check: React.FC = () => {
return (
<svg width="100%" height="100%" viewBox="0 0 25 25" className={classes.checkBox}>
<path
d="M10.6092 16.0192L17.6477 8.98076"
strokeLinecap="square"
strokeLinejoin="bevel"
className={classes.stroke}
/>
<path
d="M7.35229 12.7623L10.6092 16.0192"
className={classes.stroke}
strokeLinecap="square"
strokeLinejoin="bevel"
/>
</svg>
)
}

View File

@@ -1,11 +0,0 @@
import React from 'react'
export const MenuIcon: React.FC = () => {
return (
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3.5" y="4.5" width="18" height="2" fill="currentColor" />
<rect x="3.5" y="11.5" width="18" height="2" fill="currentColor" />
<rect x="3.5" y="18.5" width="18" height="2" fill="currentColor" />
</svg>
)
}

View File

@@ -1,129 +0,0 @@
@use './queries.scss' as *;
@use './colors.scss' as *;
@use './type.scss' as *;
:root {
--breakpoint-xs-width: #{$breakpoint-xs-width};
--breakpoint-s-width: #{$breakpoint-s-width};
--breakpoint-m-width: #{$breakpoint-m-width};
--breakpoint-l-width: #{$breakpoint-l-width};
--scrollbar-width: 17px;
--base: 24px;
--font-body: system-ui;
--font-mono: 'Roboto Mono', monospace;
--gutter-h: 180px;
--block-padding: 120px;
--header-z-index: 100;
--modal-z-index: 90;
@include large-break {
--gutter-h: 144px;
--block-padding: 96px;
}
@include mid-break {
--gutter-h: 24px;
--block-padding: 60px;
}
}
/////////////////////////////
// GLOBAL STYLES
/////////////////////////////
* {
box-sizing: border-box;
}
html {
@extend %body;
background: var(--color-white);
-webkit-font-smoothing: antialiased;
}
html,
body,
#app {
height: 100%;
}
body {
font-family: var(--font-body);
color: var(--color-black);
margin: 0;
}
::selection {
background: var(--color-green);
color: var(--color-black);
}
::-moz-selection {
background: var(--color-green);
color: var(--color-black);
}
img {
max-width: 100%;
height: auto;
display: block;
}
h1 {
@extend %h1;
}
h2 {
@extend %h2;
}
h3 {
@extend %h3;
}
h4 {
@extend %h4;
}
h5 {
@extend %h5;
}
h6 {
@extend %h6;
}
p {
margin: var(--base) 0;
@include mid-break {
margin: calc(var(--base) * 0.75) 0;
}
}
ul,
ol {
padding-left: var(--base);
margin: 0 0 var(--base);
}
a {
color: currentColor;
&:focus {
opacity: 0.8;
outline: none;
}
&:active {
opacity: 0.7;
outline: none;
}
}
svg {
vertical-align: middle;
}

View File

@@ -1,10 +0,0 @@
:root {
--color-red: rgb(255, 0, 0);
--color-green: rgb(178, 255, 214);
--color-white: rgb(255, 255, 255);
--color-dark-gray: rgb(51, 52, 52);
--color-mid-gray: rgb(196, 196, 196);
--color-gray: rgb(212, 212, 212);
--color-light-gray: rgb(244, 244, 244);
--color-black: rgb(0, 0, 0);
}

View File

@@ -1,2 +0,0 @@
@forward './queries.scss';
@forward './type.scss';

View File

@@ -1,32 +0,0 @@
$breakpoint-xs-width: 400px;
$breakpoint-s-width: 768px;
$breakpoint-m-width: 1024px;
$breakpoint-l-width: 1440px;
////////////////////////////
// MEDIA QUERIES
/////////////////////////////
@mixin extra-small-break {
@media (max-width: #{$breakpoint-xs-width}) {
@content;
}
}
@mixin small-break {
@media (max-width: #{$breakpoint-s-width}) {
@content;
}
}
@mixin mid-break {
@media (max-width: #{$breakpoint-m-width}) {
@content;
}
}
@mixin large-break {
@media (max-width: #{$breakpoint-l-width}) {
@content;
}
}

View File

@@ -1,167 +0,0 @@
@use 'queries' as *;
/////////////////////////////
// HEADINGS
/////////////////////////////
%h1,
%h2,
%h3,
%h4,
%h5,
%h6 {
font-weight: 700;
}
%h1 {
margin: 50px 0;
font-size: 84px;
line-height: 1;
@include mid-break {
font-size: 70px;
}
@include small-break {
margin: 24px 0;
font-size: 36px;
line-height: 42px;
}
}
%h2 {
margin: 32px 0;
font-size: 56px;
line-height: 1;
@include mid-break {
margin: 36px 0;
font-size: 48px;
}
@include small-break {
margin: 24px 0;
font-size: 28px;
line-height: 32px;
}
}
%h3 {
margin: 28px 0;
font-size: 48px;
line-height: 56px;
@include mid-break {
font-size: 40px;
line-height: 48px;
}
@include small-break {
margin: 24px 0;
font-size: 24px;
line-height: 30px;
}
}
%h4 {
margin: 24px 0;
font-size: 40px;
line-height: 48px;
@include mid-break {
font-size: 33px;
line-height: 36px;
}
@include small-break {
margin: 20px 0;
font-size: 20px;
line-height: 24px;
}
}
%h5 {
margin: 20px 0;
font-size: 32px;
line-height: 42px;
@include mid-break {
font-size: 26px;
line-height: 32px;
}
@include small-break {
margin: 16px 0;
font-size: 18px;
line-height: 24px;
}
}
%h6 {
margin: 20px 0;
font-size: 24px;
line-height: 28px;
@include mid-break {
font-size: 20px;
line-height: 30px;
}
@include small-break {
margin: 16px 0;
font-size: 16px;
line-height: 22px;
}
}
/////////////////////////////
// TYPE STYLES
/////////////////////////////
%body {
font-size: 18px;
line-height: 32px;
@include mid-break {
font-size: 15px;
line-height: 24px;
}
@include small-break {
font-size: 13px;
line-height: 24px;
}
}
%large-body {
font-size: 25px;
line-height: 32px;
@include mid-break {
font-size: 22px;
line-height: 30px;
}
@include small-break {
font-size: 17px;
line-height: 24px;
}
}
%label {
font-size: 16px;
line-height: 24px;
letter-spacing: 3px;
text-transform: uppercase;
@include mid-break {
font-size: 13px;
letter-spacing: 2.75px;
}
@include small-break {
font-size: 12px;
line-height: 18px;
letter-spacing: 2.625px;
}
}

View File

@@ -1,10 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['localhost', process.env.NEXT_PUBLIC_PAYLOAD_URL || ''].filter(Boolean),
},
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig

View File

@@ -1,41 +0,0 @@
{
"name": "payload-example-form-builder-website",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev -p 3001",
"lint": "next lint",
"start": "next start"
},
"dependencies": {
"@apollo/client": "^3.7.0",
"@faceless-ui/css-grid": "^1.2.0",
"@faceless-ui/modal": "^2.0.2",
"@payloadcms/plugin-form-builder": "beta",
"@payloadcms/richtext-lexical": "beta",
"escape-html": "^1.0.3",
"graphql": "^16.8.1",
"next": "^14.3.0-canary.68",
"react": "18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.41.0",
"react-select": "5.8.3",
"sass": "^1.55.0",
"slate": "^0.84.0"
},
"devDependencies": {
"@next/eslint-plugin-next": "^13.5.6",
"@types/node": "^20",
"@types/react": "^18.0.21",
"eslint": "8.57.0",
"eslint-config-next": "12.3.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-filenames": "^1.3.2",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"typescript": "^5.4"
}
}

View File

@@ -1,52 +0,0 @@
import React from 'react'
import { GetStaticProps, GetStaticPropsContext, GetStaticPaths } from 'next'
import Blocks from '../components/Blocks'
import type { Page, MainMenu } from '../payload-types'
const Page: React.FC<{
mainMenu: MainMenu
page: Page
}> = (props) => {
const {
page: { layout },
} = props
return (
<React.Fragment>
<Blocks blocks={layout} />
</React.Fragment>
)
}
export default Page
export const getStaticProps: GetStaticProps = async (context: GetStaticPropsContext) => {
const { params } = context
const slug = params?.slug || 'home'
const pageQuery = await fetch(
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/pages?where[slug][equals]=${slug}`,
).then((res) => res.json())
return {
props: {
page: pageQuery.docs[0],
},
}
}
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
const pagesQuery: { docs: Page[] } = await fetch(
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/pages?limit=100`,
).then((res) => res.json())
return {
paths: pagesQuery.docs.map((page) => ({
params: {
slug: page.slug,
},
})),
fallback: 'blocking',
}
}

View File

@@ -1,59 +0,0 @@
import App, { AppContext, AppProps } from 'next/app'
import { ModalContainer, ModalProvider } from '@faceless-ui/modal'
import React from 'react'
import { Header } from '../components/Header'
import { GlobalsProvider } from '../providers/Globals'
import { CloseModalOnRouteChange } from '../components/CloseModalOnRouteChange'
import { MainMenu } from '../payload-types'
import '../css/app.scss'
export interface IGlobals {
mainMenu: MainMenu
}
export const getAllGlobals = async (): Promise<IGlobals> => {
const [mainMenu] = await Promise.all([
fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/globals/main-menu?depth=1`).then((res) =>
res.json(),
),
])
return {
mainMenu,
}
}
const PayloadApp = (
appProps: AppProps & {
globals: IGlobals
},
): React.ReactElement => {
const { Component, pageProps, globals } = appProps
return (
<React.Fragment>
<GlobalsProvider {...globals}>
<ModalProvider classPrefix="form" transTime={0} zIndex="var(--modal-z-index)">
<CloseModalOnRouteChange />
<Header />
<Component {...pageProps} />
<ModalContainer />
</ModalProvider>
</GlobalsProvider>
</React.Fragment>
)
}
PayloadApp.getInitialProps = async (appContext: AppContext) => {
const appProps = await App.getInitialProps(appContext)
const globals = await getAllGlobals()
return {
...appProps,
globals,
}
}
export default PayloadApp

View File

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

View File

@@ -1,241 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {
collections: {
pages: Page
users: User
forms: Form
'form-submissions': FormSubmission
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {
'main-menu': MainMenu
}
}
export interface Page {
id: string
title: string
layout: {
form: string | Form
enableIntro?: boolean
introContent: {
[k: string]: unknown
}[]
id?: string
blockName?: string
blockType: 'formBlock'
}[]
slug?: string
updatedAt: string
createdAt: string
_status?: 'draft' | 'published'
}
export interface Form {
id: string
title: string
fields?: (
| {
name: string
label?: string
width?: number
defaultValue?: string
required?: boolean
id?: string
blockName?: string
blockType: 'text'
}
| {
name: string
label?: string
width?: number
defaultValue?: string
required?: boolean
id?: string
blockName?: string
blockType: 'textarea'
}
| {
name: string
label?: string
width?: number
defaultValue?: string
options?: {
label: string
value: string
id?: string
}[]
required?: boolean
id?: string
blockName?: string
blockType: 'select'
}
| {
name: string
label?: string
width?: number
required?: boolean
id?: string
blockName?: string
blockType: 'email'
}
| {
name: string
label?: string
width?: number
required?: boolean
id?: string
blockName?: string
blockType: 'state'
}
| {
name: string
label?: string
width?: number
required?: boolean
id?: string
blockName?: string
blockType: 'country'
}
| {
name: string
label?: string
width?: number
defaultValue?: number
required?: boolean
id?: string
blockName?: string
blockType: 'number'
}
| {
name: string
label?: string
width?: number
required?: boolean
defaultValue?: boolean
id?: string
blockName?: string
blockType: 'checkbox'
}
| {
message?: {
[k: string]: unknown
}[]
id?: string
blockName?: string
blockType: 'message'
}
)[]
submitButtonLabel?: string
confirmationType?: 'message' | 'redirect'
confirmationMessage: {
[k: string]: unknown
}[]
redirect?: {
url: string
}
emails?: {
emailTo?: string
cc?: string
bcc?: string
replyTo?: string
emailFrom?: string
subject: string
message?: {
[k: string]: unknown
}[]
id?: string
}[]
updatedAt: string
createdAt: string
}
export interface User {
id: string
updatedAt: string
createdAt: string
email: string
resetPasswordToken?: string
resetPasswordExpiration?: string
salt?: string
hash?: string
loginAttempts?: number
lockUntil?: string
password?: string
}
export interface FormSubmission {
id: string
form: string | Form
submissionData?: {
field: string
value: string
id?: string
}[]
updatedAt: string
createdAt: string
}
export interface PayloadPreference {
id: string
user: {
relationTo: 'users'
value: string | User
}
key?: string
value?:
| {
[k: string]: unknown
}
| unknown[]
| string
| number
| boolean
| null
updatedAt: string
createdAt: string
}
export interface PayloadMigration {
id: string
name?: string
batch?: number
updatedAt: string
createdAt: string
}
export interface MainMenu {
id: string
navItems?: {
link: {
type?: 'reference' | 'custom'
newTab?: boolean
reference: {
relationTo: 'pages'
value: string | Page
}
url: string
label: string
}
id?: string
}[]
updatedAt?: string
createdAt?: string
}
declare module 'payload' {
export interface GeneratedTypes {
collections: {
pages: Page
users: User
forms: Form
'form-submissions': FormSubmission
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {
'main-menu': MainMenu
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +0,0 @@
import React, { createContext, useContext } from 'react'
import { MainMenu } from '../../payload-types'
export type MainMenuType = MainMenu
export interface IGlobals {
mainMenu: MainMenuType
}
export const GlobalsContext = createContext<IGlobals>({} as IGlobals)
export const useGlobals = (): IGlobals => useContext(GlobalsContext)
export const GlobalsProvider: React.FC<
IGlobals & {
children: React.ReactNode
}
> = (props) => {
const { mainMenu, children } = props
return (
<GlobalsContext.Provider
value={{
mainMenu,
}}
>
{children}
</GlobalsContext.Provider>
)
}

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