├── .npmrc ├── next-env.scss.d.ts ├── .eslintrc.json ├── css ├── common.scss ├── queries.scss ├── app.scss ├── type.scss └── colors.scss ├── app ├── (site) │ ├── page.tsx │ ├── layout.tsx │ └── [slug] │ │ └── page.tsx ├── (payload) │ └── admin │ │ ├── [...slug] │ │ └── page.tsx │ │ └── page.tsx ├── layout.tsx ├── head.tsx └── api │ └── revalidate │ └── route.ts ├── payload ├── components │ └── UITest │ │ ├── test.scss │ │ ├── index.scss │ │ └── index.tsx ├── blocks │ ├── .DS_Store │ ├── CallToAction │ │ └── index.ts │ ├── Media │ │ └── index.ts │ └── Content │ │ └── index.ts ├── fields │ ├── .DS_Store │ ├── richText │ │ ├── largeBody │ │ │ ├── Element │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── plugin.ts │ │ │ ├── index.ts │ │ │ ├── Button │ │ │ │ └── index.tsx │ │ │ └── Icon │ │ │ │ └── index.tsx │ │ ├── leaves.ts │ │ ├── label │ │ │ ├── Element │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── plugin.ts │ │ │ ├── index.ts │ │ │ ├── Button │ │ │ │ └── index.tsx │ │ │ └── Icon │ │ │ │ └── index.tsx │ │ ├── elements.ts │ │ └── index.ts │ ├── backgroundColor.ts │ ├── slug.ts │ ├── linkGroup.ts │ ├── hero.ts │ └── link.ts ├── access │ └── publishedOnly.ts ├── collections │ ├── Users.ts │ ├── Media.ts │ └── Pages.ts ├── globals │ └── MainMenu.ts ├── utilities │ ├── formatSlug.ts │ ├── regenerateStaticPage.ts │ └── deepMerge.ts ├── payloadClient.ts ├── payload.config.ts └── generated-schema.graphql ├── components ├── Blocks │ ├── Content │ │ ├── index.module.scss │ │ └── index.tsx │ ├── MediaBlock │ │ ├── index.module.scss │ │ └── index.tsx │ ├── CallToAction │ │ ├── index.module.scss │ │ └── index.tsx │ └── index.tsx ├── Label │ ├── index.module.scss │ └── index.tsx ├── RichText │ ├── index.module.scss │ ├── index.tsx │ └── serialize.tsx ├── LargeBody │ ├── index.module.scss │ └── index.tsx ├── Hero │ ├── LowImpact │ │ ├── index.module.scss │ │ └── index.tsx │ ├── index.tsx │ ├── HighImpact │ │ ├── index.module.scss │ │ └── index.tsx │ └── MediumImpact │ │ ├── index.module.scss │ │ └── index.tsx ├── Gutter │ ├── index.module.scss │ └── index.tsx ├── Media │ ├── Image │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Video │ │ ├── index.module.scss │ │ └── index.tsx │ └── index.tsx ├── BackgroundColor │ ├── index.module.scss │ └── index.tsx ├── icons │ ├── Chevron │ │ └── index.tsx │ └── Menu │ │ └── index.tsx ├── VerticalPadding │ ├── index.module.scss │ └── index.tsx ├── Header │ ├── mobileMenuModal.module.scss │ ├── index.module.scss │ ├── MobileMenuModal.tsx │ └── index.tsx ├── AdminBar │ ├── index.module.scss │ └── index.tsx ├── Button │ ├── index.module.scss │ └── index.tsx ├── Layout │ └── index.tsx ├── Link │ └── index.tsx └── Logo │ └── index.tsx ├── nodemon.json ├── public └── favicon.ico ├── cssVariables.js ├── .vscode ├── settings.json └── launch.json ├── utilities ├── toKebabCase.ts └── timestamp.ts ├── vercel.json ├── pages └── api │ ├── access.ts │ ├── graphql.ts │ ├── [collection] │ ├── me.ts │ ├── init.ts │ ├── logout.ts │ ├── access │ │ └── [id].ts │ ├── index.ts │ ├── [id].ts │ ├── login.ts │ ├── unlock.ts │ ├── refresh.ts │ ├── versions │ │ ├── index.ts │ │ └── [id].ts │ ├── refresh-token.ts │ ├── first-register.ts │ ├── forgot-password.ts │ ├── reset-password.ts │ └── verify │ │ └── [token].ts │ ├── graphql-playground.ts │ └── globals │ └── [global] │ ├── access.ts │ ├── index.ts │ └── versions │ ├── index.ts │ └── [id].ts ├── next-env.d.ts ├── README.md ├── .env.example ├── env.d.ts ├── scripts └── pack-next-payload.sh ├── tsconfig.json ├── next.config.js ├── package.json ├── .gitignore └── payload-types.ts /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /next-env.scss.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.scss'; -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /css/common.scss: -------------------------------------------------------------------------------- 1 | @forward './queries.scss'; 2 | @forward './type.scss'; -------------------------------------------------------------------------------- /app/(site)/page.tsx: -------------------------------------------------------------------------------- 1 | import Page from "./[slug]/page"; 2 | 3 | export default Page -------------------------------------------------------------------------------- /payload/components/UITest/test.scss: -------------------------------------------------------------------------------- 1 | .test { 2 | text-transform: uppercase; 3 | } -------------------------------------------------------------------------------- /components/Blocks/Content/index.module.scss: -------------------------------------------------------------------------------- 1 | .link { 2 | margin-top: var(--base); 3 | } -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts", 3 | "exec": "ts-node payload/server.ts" 4 | } 5 | -------------------------------------------------------------------------------- /payload/components/UITest/index.scss: -------------------------------------------------------------------------------- 1 | @import './test.scss'; 2 | 3 | .test { 4 | color: red 5 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/payloadcms/next-payload-demo/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /components/Label/index.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../css/type.scss'; 2 | 3 | .label { 4 | @extend %label; 5 | } -------------------------------------------------------------------------------- /components/RichText/index.module.scss: -------------------------------------------------------------------------------- 1 | .richText { 2 | :first-child { 3 | margin-top: 0; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /payload/blocks/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/payloadcms/next-payload-demo/HEAD/payload/blocks/.DS_Store -------------------------------------------------------------------------------- /payload/fields/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/payloadcms/next-payload-demo/HEAD/payload/fields/.DS_Store -------------------------------------------------------------------------------- /components/LargeBody/index.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../css/type.scss'; 2 | 3 | .largeBody { 4 | @extend %large-body; 5 | } -------------------------------------------------------------------------------- /cssVariables.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | breakpoints: { 3 | s: 768, 4 | m: 1024, 5 | l: 1679, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /components/Hero/LowImpact/index.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../css/type.scss' as *; 2 | 3 | .richText { 4 | h1 { 5 | @extend %h2; 6 | } 7 | } -------------------------------------------------------------------------------- /payload/fields/richText/largeBody/Element/index.scss: -------------------------------------------------------------------------------- 1 | @import "~payload/scss"; 2 | 3 | .rich-text-large-body { 4 | font-size: base(.8); 5 | } 6 | -------------------------------------------------------------------------------- /components/Blocks/MediaBlock/index.module.scss: -------------------------------------------------------------------------------- 1 | .mediaBlock { 2 | position: relative; 3 | } 4 | 5 | .caption { 6 | margin-top: var(--base) 7 | } 8 | -------------------------------------------------------------------------------- /components/Gutter/index.module.scss: -------------------------------------------------------------------------------- 1 | .gutterLeft { 2 | padding-left: var(--gutter-h); 3 | } 4 | 5 | .gutterRight { 6 | padding-right: var(--gutter-h); 7 | } 8 | -------------------------------------------------------------------------------- /utilities/toKebabCase.ts: -------------------------------------------------------------------------------- 1 | export const toKebabCase = (string: string): string => string?.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase(); 2 | 3 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { "source": "/admin/(.*)", "destination": "/admin/index.html" } 4 | ], 5 | "github": { 6 | "silent": true 7 | } 8 | } -------------------------------------------------------------------------------- /app/(payload)/admin/[...slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import PayloadAdmin from "../page"; 2 | 3 | // Need to render the same component for anything within /admin 4 | export default PayloadAdmin; -------------------------------------------------------------------------------- /components/Media/Image/index.module.scss: -------------------------------------------------------------------------------- 1 | .placeholder-color-light { 2 | background-color: rgba(0, 0, 0, 0.05); 3 | } 4 | 5 | .placeholder { 6 | background-color: var(--color-base-50); 7 | } 8 | -------------------------------------------------------------------------------- /pages/api/access.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/access' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | externalResolver: true 8 | } 9 | } -------------------------------------------------------------------------------- /pages/api/graphql.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/graphql' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | externalResolver: true 8 | } 9 | } -------------------------------------------------------------------------------- /pages/api/[collection]/me.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/me' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | externalResolver: true 8 | } 9 | } -------------------------------------------------------------------------------- /pages/api/[collection]/init.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/init' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | externalResolver: true 8 | } 9 | } -------------------------------------------------------------------------------- /pages/api/graphql-playground.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/graphql-playground' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | externalResolver: true 8 | } 9 | } -------------------------------------------------------------------------------- /pages/api/[collection]/logout.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/logout' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | externalResolver: true 8 | } 9 | } -------------------------------------------------------------------------------- /pages/api/globals/[global]/access.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/globals/[global]/access' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | externalResolver: true 8 | } 9 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | return ( 7 | 8 | 9 | {children} 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /components/BackgroundColor/index.module.scss: -------------------------------------------------------------------------------- 1 | .white { 2 | color: var(--color-base-1000); 3 | background-color: var(--color-base-0); 4 | } 5 | 6 | .black { 7 | color: var(--color-base-0); 8 | background-color: var(--color-base-1000); 9 | } -------------------------------------------------------------------------------- /components/Media/Video/index.module.scss: -------------------------------------------------------------------------------- 1 | .video { 2 | max-width: 100%; 3 | width: 100%; 4 | background-color: var(--color-base-50); 5 | } 6 | 7 | .cover { 8 | object-fit: cover; 9 | width: 100%; 10 | height: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /pages/api/[collection]/access/[id].ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/access/[id]' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | externalResolver: true 8 | } 9 | } -------------------------------------------------------------------------------- /payload/fields/richText/leaves.ts: -------------------------------------------------------------------------------- 1 | import { RichTextLeaf } from "@payloadcms/richtext-slate"; 2 | 3 | const defaultLeaves: RichTextLeaf[] = [ 4 | 'bold', 5 | 'italic', 6 | 'underline', 7 | ]; 8 | 9 | export default defaultLeaves; 10 | -------------------------------------------------------------------------------- /app/head.tsx: -------------------------------------------------------------------------------- 1 | export default function Head() { 2 | return ( 3 | <> 4 | 5 | 6 | 7 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /pages/api/[collection]/index.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /payload/fields/richText/label/Element/index.scss: -------------------------------------------------------------------------------- 1 | @import "~payload/scss"; 2 | 3 | .rich-text-label { 4 | text-transform: uppercase; 5 | font-family: 'Roboto Mono', monospace; 6 | letter-spacing: 2px; 7 | font-size: base(.5); 8 | margin: 0 0 base(1); 9 | } -------------------------------------------------------------------------------- /pages/api/[collection]/[id].ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/[id]' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /pages/api/[collection]/login.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/login' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /pages/api/[collection]/unlock.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/unlock' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /pages/api/globals/[global]/index.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/globals/[global]' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /pages/api/[collection]/refresh.ts: -------------------------------------------------------------------------------- 1 | import meHandler from '@payloadcms/next-payload/dist/handlers/[collection]/me' 2 | 3 | export default meHandler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /pages/api/[collection]/versions/index.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/versions' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /pages/api/[collection]/refresh-token.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/refresh-token' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /pages/api/[collection]/versions/[id].ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/versions/[id]' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /utilities/timestamp.ts: -------------------------------------------------------------------------------- 1 | export const timestamp = (label: string) => { 2 | if (!process.env.PAYLOAD_TIME) process.env.PAYLOAD_TIME = String(new Date().getTime()); 3 | const now = new Date(); 4 | console.log(`[${now.getTime() - Number(process.env.PAYLOAD_TIME)}ms] ${label}`); 5 | }; -------------------------------------------------------------------------------- /pages/api/[collection]/first-register.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/first-register' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /pages/api/[collection]/forgot-password.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/forgot-password' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /pages/api/[collection]/reset-password.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/reset-password' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /pages/api/[collection]/verify/[token].ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/[collection]/verify/[token]' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /pages/api/globals/[global]/versions/index.ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/globals/[global]/versions' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /payload/access/publishedOnly.ts: -------------------------------------------------------------------------------- 1 | import type { Access } from "payload/config"; 2 | 3 | export const publishedOnly: Access = ({ req: { user } }) => { 4 | if (Boolean(user)) return true; 5 | 6 | return { 7 | _status: { 8 | equals: 'published' 9 | }, 10 | } 11 | } -------------------------------------------------------------------------------- /pages/api/globals/[global]/versions/[id].ts: -------------------------------------------------------------------------------- 1 | import handler from '@payloadcms/next-payload/dist/handlers/globals/[global]/versions/[id]' 2 | 3 | export default handler 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | externalResolver: true, 9 | } 10 | } -------------------------------------------------------------------------------- /components/Label/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from './index.module.scss'; 3 | 4 | export const Label: React.FC<{ children: React.ReactNode }> = ({ children }) => { 5 | return ( 6 |

7 | {children} 8 |

9 | ) 10 | } -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /components/LargeBody/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from './index.module.scss'; 3 | 4 | export const LargeBody: React.FC<{ children: React.ReactNode }> = ({ children }) => { 5 | return ( 6 |

7 | {children} 8 |

9 | ) 10 | } -------------------------------------------------------------------------------- /payload/collections/Users.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload/types'; 2 | 3 | export const Users: CollectionConfig = { 4 | slug: 'users', 5 | auth: { 6 | useAPIKey: true, 7 | }, 8 | admin: { 9 | useAsTitle: 'email', 10 | }, 11 | fields: [ 12 | // Don't need any user fields here 13 | ], 14 | }; -------------------------------------------------------------------------------- /components/icons/Chevron/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Chevron: React.FC = () => { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } -------------------------------------------------------------------------------- /app/api/revalidate/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { revalidatePath } from 'next/cache' 3 | 4 | export async function GET(request: NextRequest) { 5 | const path = request.nextUrl.searchParams.get('path') || '/' 6 | revalidatePath(path) 7 | return NextResponse.json({ revalidated: true, now: Date.now() }) 8 | } -------------------------------------------------------------------------------- /components/VerticalPadding/index.module.scss: -------------------------------------------------------------------------------- 1 | .top-large { 2 | padding-top: var(--block-padding); 3 | } 4 | 5 | .top-medium { 6 | padding-top: calc(var(--block-padding) / 2); 7 | } 8 | 9 | .bottom-large { 10 | padding-bottom: var(--block-padding); 11 | } 12 | 13 | .bottom-medium { 14 | padding-bottom: calc(var(--block-padding) / 2); 15 | } -------------------------------------------------------------------------------- /payload/fields/richText/label/plugin.ts: -------------------------------------------------------------------------------- 1 | const withLabel = (incomingEditor: any) => { 2 | const editor = incomingEditor; 3 | const { shouldBreakOutOnEnter } = editor; 4 | 5 | editor.shouldBreakOutOnEnter = (element: any) => (element.type === 'label' ? true : shouldBreakOutOnEnter(element)); 6 | 7 | return editor; 8 | }; 9 | 10 | export default withLabel; 11 | -------------------------------------------------------------------------------- /payload/fields/richText/label/index.ts: -------------------------------------------------------------------------------- 1 | import { RichTextCustomElement } from '@payloadcms/richtext-slate'; 2 | import Button from './Button'; 3 | import Element from './Element'; 4 | import withLabel from './plugin'; 5 | 6 | export default { 7 | name: 'label', 8 | Button, 9 | Element, 10 | plugins: [ 11 | withLabel, 12 | ], 13 | } as RichTextCustomElement; 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repo has moved! 2 | 3 | This repo has been merged into the [Packages Directory](https://github.com/payloadcms/payload/tree/main/packages) of the [Payload Monorepo](https://github.com/payloadcms/payload). Please refer to the new location of the [Next Package](https://github.com/payloadcms/payload/tree/main/packages/next) for all future updates, issues, and pull requests. -------------------------------------------------------------------------------- /payload/fields/richText/largeBody/plugin.ts: -------------------------------------------------------------------------------- 1 | const withLargeBody = (incomingEditor: any) => { 2 | const editor = incomingEditor; 3 | const { shouldBreakOutOnEnter } = editor; 4 | 5 | editor.shouldBreakOutOnEnter = (element: any) => (element.type === 'large-body' ? true : shouldBreakOutOnEnter(element)); 6 | 7 | return editor; 8 | }; 9 | 10 | export default withLargeBody; 11 | -------------------------------------------------------------------------------- /payload/fields/richText/elements.ts: -------------------------------------------------------------------------------- 1 | import { RichTextElement } from '@payloadcms/richtext-slate'; 2 | import label from './label'; 3 | import largeBody from './largeBody'; 4 | 5 | const elements: RichTextElement[] = [ 6 | 'blockquote', 7 | 'h2', 8 | 'h3', 9 | 'h4', 10 | 'h5', 11 | 'h6', 12 | 'link', 13 | largeBody, 14 | label, 15 | ]; 16 | 17 | export default elements; 18 | -------------------------------------------------------------------------------- /payload/fields/richText/largeBody/index.ts: -------------------------------------------------------------------------------- 1 | import { RichTextCustomElement } from '@payloadcms/richtext-slate'; 2 | import Button from './Button'; 3 | import Element from './Element'; 4 | import withLargeBody from './plugin'; 5 | 6 | export default { 7 | name: 'large-body', 8 | Button, 9 | Element, 10 | plugins: [ 11 | withLargeBody, 12 | ], 13 | } as RichTextCustomElement; 14 | -------------------------------------------------------------------------------- /app/(payload)/admin/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react'; 4 | import Root from 'payload/dist/admin/Root' 5 | 6 | const PayloadAdmin = () => { 7 | const [mounted, setMounted] = React.useState(false) 8 | 9 | React.useEffect(() => { 10 | setMounted(true) 11 | }, []); 12 | 13 | if (!mounted) return null; 14 | 15 | return 16 | } 17 | 18 | export default PayloadAdmin; -------------------------------------------------------------------------------- /payload/components/UITest/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react'; 4 | // import { Label } from 'payload/components/forms' 5 | import './index.scss' 6 | 7 | export const UIField: React.FC = () => { 8 | return ( 9 | 10 | {/* 13 | ); 14 | } -------------------------------------------------------------------------------- /payload/collections/Media.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload/types'; 2 | 3 | export const Media: CollectionConfig = { 4 | slug: 'media', 5 | upload: { 6 | staticDir: '/tmp', 7 | formatOptions: { 8 | format: 'jpeg', 9 | }, 10 | }, 11 | access: { 12 | read: () => true, 13 | }, 14 | fields: [ 15 | { 16 | name: 'alt', 17 | type: 'text', 18 | required: true, 19 | }, 20 | ] 21 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug build", 11 | "program": "${workspaceFolder}/build.js", 12 | }, 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /components/icons/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const MenuIcon: React.FC = () => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ) 11 | } -------------------------------------------------------------------------------- /payload/fields/richText/largeBody/Element/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import "./index.scss"; 6 | 7 | const baseClass = "rich-text-large-body"; 8 | 9 | const LargeBodyElement: React.FC<{ 10 | attributes: any; 11 | element: any; 12 | children: React.ReactNode; 13 | }> = ({ attributes, children }) => ( 14 |
15 | {children} 16 |
17 | ); 18 | export default LargeBodyElement; 19 | -------------------------------------------------------------------------------- /app/(site)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "../../components/Layout" 2 | import { getPayloadClient } from '../../payload/payloadClient'; 3 | 4 | const SiteLayout = async ({ children }: { children: React.ReactNode }) => { 5 | const payload = await getPayloadClient(); 6 | 7 | const mainMenu = await payload.findGlobal({ 8 | slug: 'main-menu' 9 | }); 10 | 11 | return ( 12 | 13 | {children} 14 | 15 | ) 16 | } 17 | 18 | export default SiteLayout -------------------------------------------------------------------------------- /components/RichText/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import serialize from './serialize'; 3 | 4 | import classes from './index.module.scss'; 5 | 6 | const RichText: React.FC<{ className?: string, content: any }> = ({ className, content }) => { 7 | if (!content) { 8 | return null; 9 | } 10 | 11 | return ( 12 |
13 | {serialize(content)} 14 |
15 | ); 16 | }; 17 | 18 | export default RichText; 19 | -------------------------------------------------------------------------------- /payload/fields/richText/label/Element/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react'; 4 | 5 | import './index.scss'; 6 | 7 | const baseClass = 'rich-text-label'; 8 | 9 | const LabelElement: React.FC<{ 10 | attributes: any 11 | element: any 12 | children: React.ReactNode 13 | }> = ({ attributes, children }) => ( 14 |
17 | 18 | {children} 19 | 20 |
21 | ); 22 | export default LabelElement; 23 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MONGODB_URI=mongodb://127.0.0.1/payload-vercel-functions 2 | POSTGRES_URI=postgres://user:password@url:port/dbname 3 | PAYLOAD_SECRET=YOUR_SECRET_HERE 4 | PAYLOAD_CONFIG_PATH=dist/payload.config.js 5 | NEXT_PUBLIC_APP_URL=http://localhost:3000 6 | PAYLOAD_PUBLIC_CMS_URL=http://localhost:3000 7 | S3_ACCESS_KEY_ID= 8 | S3_SECRET_ACCESS_KEY= 9 | S3_REGION= 10 | NEXT_PUBLIC_S3_ENDPOINT= 11 | NEXT_PUBLIC_S3_BUCKET= 12 | PAYLOAD_PRIVATE_REGENERATION_SECRET= 13 | NEXT_PRIVATE_REGENERATION_SECRET= 14 | ANALYZE=false -------------------------------------------------------------------------------- /payload/globals/MainMenu.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalConfig } from "payload/types"; 2 | import link from "../fields/link"; 3 | 4 | export const MainMenu: GlobalConfig = { 5 | slug: 'main-menu', 6 | graphQL: { 7 | name: 'MainMenu', 8 | }, 9 | access: { 10 | read: () => true, 11 | }, 12 | fields: [ 13 | { 14 | name: 'navItems', 15 | type: 'array', 16 | maxRows: 6, 17 | fields: [ 18 | link({ 19 | appearances: false, 20 | }), 21 | ] 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | MONGODB_URI: string 4 | POSTGRES_URI: string 5 | PAYLOAD_SECRET: string 6 | PAYLOAD_CONFIG_PATH: string 7 | NEXT_PUBLIC_APP_URL: string 8 | PAYLOAD_PUBLIC_CMS_URL: string 9 | S3_ACCESS_KEY_ID: string 10 | S3_SECRET_ACCESS_KEY: string 11 | S3_REGION: string 12 | NEXT_PUBLIC_S3_ENDPOINT: string 13 | NEXT_PUBLIC_S3_BUCKET: string 14 | PAYLOAD_PRIVATE_REGENERATION_SECRET: string 15 | NEXT_PRIVATE_REGENERATION_SECRET: string 16 | ANALYZE: string 17 | } 18 | } -------------------------------------------------------------------------------- /payload/fields/backgroundColor.ts: -------------------------------------------------------------------------------- 1 | import type { Field, SelectField } from "payload/types"; 2 | import deepMerge from '../utilities/deepMerge'; 3 | 4 | type Args = { 5 | overrides?: Partial 6 | } 7 | 8 | export const backgroundColor = ({ overrides = {} }: Args): Field => deepMerge({ 9 | name: 'backgroundColor', 10 | type: 'select', 11 | defaultValue: 'white', 12 | options: [ 13 | { 14 | label: 'White', 15 | value: 'white', 16 | }, 17 | { 18 | label: 'Black', 19 | value: 'black', 20 | } 21 | ] 22 | }, overrides) -------------------------------------------------------------------------------- /components/Header/mobileMenuModal.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../css/common.scss' as *; 2 | 3 | .mobileMenuModal { 4 | position: relative; 5 | width: 100%; 6 | height: 100%; 7 | border: none; 8 | padding: 0; 9 | opacity: 1; 10 | display: none; 11 | 12 | @include mid-break { 13 | display: block; 14 | } 15 | } 16 | 17 | .contentContainer { 18 | padding: 20px; 19 | } 20 | 21 | .mobileMenuItems { 22 | display: flex; 23 | flex-direction: column; 24 | height: 100%; 25 | margin-top: 30px; 26 | } 27 | 28 | .menuItem { 29 | @extend %h4; 30 | margin-top: 0; 31 | } -------------------------------------------------------------------------------- /payload/fields/richText/label/Button/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | /* eslint-disable import/no-extraneous-dependencies */ 4 | // eslint-disable-next-line no-use-before-define 5 | import React from 'react'; 6 | import { ElementButton } from '@payloadcms/richtext-slate'; 7 | import Icon from '../Icon'; 8 | 9 | const baseClass = 'rich-text-label-button'; 10 | 11 | const ToolbarButton: React.FC<{ path: string }> = () => ( 12 | 16 | 17 | 18 | ); 19 | 20 | export default ToolbarButton; 21 | -------------------------------------------------------------------------------- /payload/fields/richText/largeBody/Button/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | /* eslint-disable import/no-extraneous-dependencies */ 4 | // eslint-disable-next-line no-use-before-define 5 | import React from 'react'; 6 | import { ElementButton } from "@payloadcms/richtext-slate"; 7 | import Icon from '../Icon'; 8 | 9 | const baseClass = 'rich-text-large-body-button'; 10 | 11 | const ToolbarButton: React.FC<{ path: string }> = () => ( 12 | 16 | 17 | 18 | ); 19 | 20 | export default ToolbarButton; 21 | -------------------------------------------------------------------------------- /components/Hero/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react'; 4 | import { Page } from '../../payload-types'; 5 | import { HighImpactHero } from './HighImpact'; 6 | import { MediumImpactHero } from './MediumImpact'; 7 | import { LowImpactHero } from './LowImpact'; 8 | 9 | const heroes = { 10 | highImpact: HighImpactHero, 11 | mediumImpact: MediumImpactHero, 12 | lowImpact: LowImpactHero, 13 | } 14 | 15 | export const Hero: React.FC = (props) => { 16 | const { type } = props; 17 | const HeroToRender = heroes[type]; 18 | 19 | if (!HeroToRender) return null; 20 | return 21 | } -------------------------------------------------------------------------------- /components/Header/index.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../css/queries.scss' as *; 2 | 3 | .header { 4 | padding: var(--base) 0; 5 | z-index: var(--header-z-index); 6 | } 7 | 8 | .wrap { 9 | display: flex; 10 | justify-content: space-between; 11 | } 12 | 13 | .nav { 14 | a { 15 | text-decoration: none; 16 | margin-left: var(--base); 17 | } 18 | 19 | @include mid-break { 20 | display: none; 21 | } 22 | } 23 | 24 | .mobileMenuToggler { 25 | all: unset; 26 | cursor: pointer; 27 | display: none; 28 | 29 | &[aria-expanded="true"] { 30 | transform: rotate(-25deg); 31 | } 32 | 33 | @include mid-break { 34 | display: block; 35 | } 36 | } -------------------------------------------------------------------------------- /payload/fields/slug.ts: -------------------------------------------------------------------------------- 1 | import type { Field } from 'payload/types'; 2 | import formatSlug from '../utilities/formatSlug'; 3 | import deepMerge from '../utilities/deepMerge'; 4 | 5 | type Slug = (fieldToUse?: string, overrides?: Partial) => Field 6 | 7 | export const slugField: Slug = (fieldToUse = 'title', overrides) => deepMerge>( 8 | { 9 | name: 'slug', 10 | label: 'Slug', 11 | type: 'text', 12 | index: true, 13 | admin: { 14 | position: 'sidebar', 15 | }, 16 | hooks: { 17 | beforeValidate: [ 18 | formatSlug(fieldToUse), 19 | ], 20 | }, 21 | }, 22 | overrides || {}, 23 | ); 24 | -------------------------------------------------------------------------------- /payload/blocks/CallToAction/index.ts: -------------------------------------------------------------------------------- 1 | import type { Block } from "payload/types"; 2 | import { backgroundColor } from "../../fields/backgroundColor"; 3 | import linkGroup from "../../fields/linkGroup"; 4 | // import richText from "../../fields/richText"; 5 | 6 | export const CallToAction: Block = { 7 | slug: 'cta', 8 | labels: { 9 | singular: 'Call to Action', 10 | plural: 'Calls to Action', 11 | }, 12 | fields: [ 13 | backgroundColor({ overrides: { name: 'ctaBackgroundColor' } }), 14 | // richText(), 15 | linkGroup({ 16 | appearances: ['primary', 'secondary'], 17 | overrides: { 18 | maxRows: 2, 19 | } 20 | }), 21 | ] 22 | } -------------------------------------------------------------------------------- /css/queries.scss: -------------------------------------------------------------------------------- 1 | $breakpoint-xs-width: 400px; 2 | $breakpoint-s-width: 768px; 3 | $breakpoint-m-width: 1024px; 4 | $breakpoint-l-width: 1440px; 5 | 6 | //////////////////////////// 7 | // MEDIA QUERIES 8 | ///////////////////////////// 9 | 10 | @mixin extra-small-break { 11 | @media (max-width: #{$breakpoint-xs-width}) { 12 | @content; 13 | } 14 | } 15 | 16 | @mixin small-break { 17 | @media (max-width: #{$breakpoint-s-width}) { 18 | @content; 19 | } 20 | } 21 | 22 | @mixin mid-break { 23 | @media (max-width: #{$breakpoint-m-width}) { 24 | @content; 25 | } 26 | } 27 | 28 | @mixin large-break { 29 | @media (max-width: #{$breakpoint-l-width}) { 30 | @content; 31 | } 32 | } -------------------------------------------------------------------------------- /components/Blocks/CallToAction/index.module.scss: -------------------------------------------------------------------------------- 1 | @use '../.../../../../css/queries.scss' as *; 2 | 3 | $spacer-h: calc(var(--block-padding) / 2); 4 | 5 | .callToAction { 6 | padding-left: $spacer-h; 7 | padding-right: $spacer-h; 8 | } 9 | 10 | .background--white { 11 | background-color: var(--color-base-1000); 12 | color: var(--color-base-0); 13 | } 14 | 15 | .richText { 16 | :last-child { 17 | margin-bottom: 0; 18 | } 19 | } 20 | 21 | .linkGroup { 22 | display: flex; 23 | flex-direction: column; 24 | justify-content: center; 25 | height: 100%; 26 | 27 | :last-child { 28 | margin-bottom: 0; 29 | } 30 | 31 | @include mid-break { 32 | margin-top: 20px 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /payload/utilities/formatSlug.ts: -------------------------------------------------------------------------------- 1 | import type { FieldHook } from 'payload/types'; 2 | 3 | const format = (val: string): string => val.replace(/ /g, '-').replace(/[^\w-]+/g, '').toLowerCase(); 4 | 5 | const formatSlug = (fallback: string): FieldHook => ({ operation, value, originalDoc, data }) => { 6 | if (typeof value === 'string') { 7 | return format(value); 8 | } 9 | 10 | if (operation === 'create') { 11 | const fallbackData = (data && data[fallback]) || (originalDoc && originalDoc[fallback]); 12 | 13 | if (fallbackData && typeof fallbackData === 'string') { 14 | return format(fallbackData); 15 | } 16 | } 17 | 18 | return value; 19 | }; 20 | 21 | export default formatSlug; 22 | -------------------------------------------------------------------------------- /components/Hero/LowImpact/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Cell, Grid } from '@faceless-ui/css-grid'; 3 | import { Page } from '../../../payload-types'; 4 | import { Gutter } from '../../Gutter'; 5 | import RichText from '../../RichText'; 6 | import { VerticalPadding } from '../../VerticalPadding'; 7 | 8 | import classes from './index.module.scss' 9 | 10 | export const LowImpactHero: React.FC = ({ richText }) => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } -------------------------------------------------------------------------------- /scripts/pack-next-payload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | script_directory="$(cd "$(dirname "$0")" && pwd)" 4 | cd "$script_directory/../../next-payload" 5 | 6 | # Pack the package 7 | yarn build 8 | yarn_pack_output=$(yarn pack) 9 | 10 | # Helper variables 11 | archive_file=$(echo "$yarn_pack_output" | grep -o '".*"' | awk -F '"' '{print $2}') 12 | filename=$(basename "$archive_file") 13 | 14 | # Move tgz into next-payload-demo directory 15 | mv "$archive_file" "../next-payload-demo" 16 | # Move into next-payload-demo directory 17 | cd "../next-payload-demo" 18 | 19 | # Remove all files in yarn cache .tmp directory 20 | rm -rf "$(yarn cache dir)/.tmp/"* 21 | # Install the package 22 | yarn add ./$filename 23 | # Cleanup the archive file 24 | rm -rf ./$filename -------------------------------------------------------------------------------- /components/VerticalPadding/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from './index.module.scss'; 3 | 4 | export type VerticalPaddingOptions = 'large' | 'medium' | 'none'; 5 | 6 | type Props = { 7 | top?: VerticalPaddingOptions 8 | bottom?: VerticalPaddingOptions 9 | children: React.ReactNode 10 | className?: string 11 | } 12 | 13 | export const VerticalPadding: React.FC = ({ 14 | top = 'medium', 15 | bottom = 'medium', 16 | className, 17 | children, 18 | }) => { 19 | return ( 20 |
27 | {children} 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /components/AdminBar/index.module.scss: -------------------------------------------------------------------------------- 1 | .adminBar { 2 | z-index: 10; 3 | width: 100%; 4 | background-color: var(--color-base-1000); 5 | color: var(--color-white); 6 | padding: 5px 0; 7 | font-size: calc(#{var(--html-font-size)} * 1px); 8 | display: none; 9 | } 10 | 11 | .show { 12 | display: block; 13 | } 14 | 15 | .controls { 16 | & > *:not(:last-child) { 17 | margin-right: 10px !important; 18 | } 19 | } 20 | 21 | .user { 22 | margin-right: 10px !important; 23 | } 24 | 25 | .logo { 26 | margin-right: 10px !important; 27 | } 28 | 29 | .blockContainer { 30 | position: relative; 31 | } 32 | 33 | .hr { 34 | position: absolute; 35 | bottom: 0; 36 | left: 0; 37 | width: 100%; 38 | background-color: var(--light-gray); 39 | height: 2px; 40 | } 41 | -------------------------------------------------------------------------------- /payload/fields/linkGroup.ts: -------------------------------------------------------------------------------- 1 | import type { Field } from 'payload/types'; 2 | import type { ArrayField } from 'payload/dist/fields/config/types'; 3 | import deepMerge from '../utilities/deepMerge'; 4 | import link, { LinkAppearances } from './link'; 5 | 6 | type LinkGroupType = (options?: { 7 | overrides?: Partial 8 | appearances?: LinkAppearances[] | false 9 | }) => Field; 10 | 11 | const linkGroup: LinkGroupType = ({ 12 | overrides = {}, 13 | appearances, 14 | } = {}) => { 15 | const generatedLinkGroup: Field = { 16 | name: 'links', 17 | type: 'array', 18 | fields: [ 19 | link({ 20 | appearances, 21 | }), 22 | ], 23 | }; 24 | 25 | return deepMerge(generatedLinkGroup, overrides); 26 | }; 27 | 28 | export default linkGroup; 29 | -------------------------------------------------------------------------------- /payload/fields/richText/label/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | import React from 'react'; 4 | 5 | const Icon = () => ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | 13 | export default Icon; 14 | -------------------------------------------------------------------------------- /payload/utilities/regenerateStaticPage.ts: -------------------------------------------------------------------------------- 1 | import type { AfterChangeHook } from 'payload/dist/collections/config/types'; 2 | 3 | export const regenerateStaticPage: AfterChangeHook = async ({ req: { payload }, doc }) => { 4 | let path = `/${doc.slug}`; 5 | 6 | if (path === '/home') { 7 | path = '/' 8 | } 9 | 10 | try { 11 | const res = await fetch(`${process.env.PAYLOAD_PUBLIC_CMS_URL}/api/revalidate?secret=${process.env.PAYLOAD_PRIVATE_REGENERATION_SECRET}&path=${path}`); 12 | if (res.ok) { 13 | payload.logger.info(`Now regenerating path '${path}'`); 14 | } else { 15 | payload.logger.info(`Error regenerating path '${path}'`); 16 | } 17 | } catch (err) { 18 | payload.logger.info(`Error hitting regeneration route for '${path}'`); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /payload/fields/richText/largeBody/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | import React from 'react'; 4 | 5 | const Icon = () => ( 6 | 10 | 14 | 18 | 19 | ); 20 | 21 | export default Icon; 22 | -------------------------------------------------------------------------------- /components/Gutter/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, Ref } from 'react'; 2 | import classes from './index.module.scss'; 3 | 4 | type Props = { 5 | left?: boolean 6 | right?: boolean 7 | className?: string 8 | children: React.ReactNode 9 | ref?: Ref 10 | } 11 | 12 | export const Gutter: React.FC = forwardRef((props, ref) => { 13 | const { 14 | left = true, 15 | right = true, 16 | className, 17 | children 18 | } = props; 19 | 20 | return ( 21 |
29 | {children} 30 |
31 | ) 32 | }); 33 | 34 | 35 | Gutter.displayName = 'Gutter'; 36 | -------------------------------------------------------------------------------- /components/Header/MobileMenuModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Modal } from "@faceless-ui/modal"; 3 | import { HeaderBar } from "."; 4 | import { MainMenu } from "../../payload-types" 5 | import { Gutter } from "../Gutter"; 6 | import { CMSLink } from "../Link"; 7 | 8 | import classes from './mobileMenuModal.module.scss'; 9 | 10 | type Props = { 11 | navItems: MainMenu['navItems']; 12 | } 13 | 14 | export const slug = 'menu-modal'; 15 | 16 | export const MobileMenuModal: React.FC = ({ navItems }) => { 17 | return ( 18 | 19 | 20 | 21 | 22 |
23 | {navItems.map(({ link }, i) => { 24 | return ( 25 | 26 | ) 27 | })} 28 |
29 |
30 |
31 | ) 32 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "strict": false, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "outDir": "./dist", 14 | "jsx": "preserve", 15 | "forceConsistentCasingInFileNames": true, 16 | "incremental": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "paths": { 22 | "payload/generated-types": [ 23 | "./payload-types.ts" 24 | ] 25 | }, 26 | "noEmit": true, 27 | "plugins": [ 28 | { 29 | "name": "next" 30 | } 31 | ], 32 | "strictNullChecks": true 33 | }, 34 | "include": [ 35 | "next-env.d.ts", 36 | "**/*.ts", 37 | "**/*.tsx", 38 | ".next/types/**/*.ts" 39 | ], 40 | "exclude": [ 41 | "node_modules" 42 | ] 43 | } -------------------------------------------------------------------------------- /components/Hero/HighImpact/index.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../../css/queries'; 2 | 3 | .hero { 4 | padding-top: calc(var(--base) * 3); 5 | 6 | @include mid-break { 7 | padding-top: var(--base); 8 | } 9 | } 10 | 11 | .media { 12 | margin-top: calc(var(--base) * 3); 13 | width: calc(100% + var(--gutter-h)); 14 | position: relative; 15 | 16 | @include mid-break { 17 | margin-top: var(--base); 18 | margin-left: calc(var(--gutter-h) * -1); 19 | width: calc(100% + var(--gutter-h) * 2); 20 | } 21 | } 22 | 23 | .links { 24 | list-style: none; 25 | margin: 0; 26 | padding: 0; 27 | display: flex; 28 | position: absolute; 29 | background: white; 30 | padding: 0 48px 24px 0; 31 | 32 | li { 33 | margin-right: 12px; 34 | } 35 | 36 | @include mid-break { 37 | position: static; 38 | padding: 0 var(--gutter-h); 39 | display: block; 40 | 41 | li { 42 | margin-right: 0; 43 | 44 | >* { 45 | width: 100%; 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /components/Button/index.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../css/type.scss'; 2 | 3 | .content { 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-around; 7 | 8 | svg { 9 | margin-right: calc(var(--base) / 2); 10 | width: var(--base); 11 | height: var(--base); 12 | } 13 | } 14 | 15 | .label { 16 | @extend %label; 17 | display: flex; 18 | align-items: center; 19 | } 20 | 21 | .button { 22 | text-decoration: none; 23 | display: inline-flex; 24 | padding: 12px 18px; 25 | margin-bottom: var(--base); 26 | } 27 | 28 | .primary--white { 29 | background-color: black; 30 | color: white; 31 | } 32 | 33 | .primary--black { 34 | background-color: white; 35 | color: black; 36 | } 37 | 38 | .secondary--white { 39 | background-color: white; 40 | box-shadow: inset 0 0 0 1px black; 41 | } 42 | 43 | .secondary--black { 44 | background-color: black; 45 | box-shadow: inset 0 0 0 1px white; 46 | } 47 | 48 | .appearance--default { 49 | padding: 0; 50 | margin-left: -8px; 51 | } 52 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { withPayload } = require("@payloadcms/next-payload"); 2 | const path = require("path"); 3 | 4 | /** @type {import('next').NextConfig} */ 5 | const nextConfig = withPayload( 6 | { 7 | eslint: { 8 | ignoreDuringBuilds: true, 9 | }, 10 | transpilePackages: [ 11 | "@payloadcms/plugin-seo", 12 | "payload/components/forms", 13 | "payload/components", 14 | ], 15 | images: { 16 | domains: [ 17 | "localhost", 18 | "nextjs-vercel.payloadcms.com", 19 | process.env.NEXT_PUBLIC_APP_URL, 20 | `${process.env.NEXT_PUBLIC_S3_ENDPOINT}`.replace("https://", ""), 21 | ], 22 | }, 23 | webpack: { 24 | resolve: { 25 | alias: {}, 26 | }, 27 | }, 28 | }, 29 | { 30 | configPath: path.resolve(__dirname, "./payload/payload.config"), 31 | } 32 | ); 33 | 34 | const withBundleAnalyzer = require("@next/bundle-analyzer")({ 35 | enabled: true, 36 | }); 37 | 38 | module.exports = 39 | process.env.ANALYZE === "true" ? withBundleAnalyzer(nextConfig) : nextConfig; 40 | -------------------------------------------------------------------------------- /payload/utilities/deepMerge.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple object check. 3 | * @param item 4 | * @returns {boolean} 5 | */ 6 | export function isObject(item: unknown): boolean { 7 | return Boolean(item && typeof item === 'object' && !Array.isArray(item)); 8 | } 9 | 10 | type ObjectWithKeys = { [key: string]: any } 11 | 12 | /** 13 | * Deep merge two objects. 14 | * @param target 15 | * @param ...sources 16 | */ 17 | export default function deepMerge(target: T, source: R): T { 18 | const output = { ...target }; 19 | if (isObject(target) && isObject(source)) { 20 | Object.keys(source).forEach((key) => { 21 | if (isObject(source[key])) { 22 | // @ts-ignore 23 | if (!(key in target)) { 24 | Object.assign(output, { [key]: source[key] }); 25 | } else { 26 | // @ts-ignore 27 | output[key] = deepMerge(target[key], source[key]); 28 | } 29 | } else { 30 | Object.assign(output, { [key]: source[key] }); 31 | } 32 | }); 33 | } 34 | 35 | return output; 36 | } 37 | -------------------------------------------------------------------------------- /payload/fields/richText/index.ts: -------------------------------------------------------------------------------- 1 | import type { RichTextField } from 'payload/dist/fields/config/types'; 2 | import type { AdapterArguments, RichTextElement, RichTextLeaf } from '@payloadcms/richtext-slate'; 3 | import deepMerge from '../../utilities/deepMerge'; 4 | import elements from './elements'; 5 | import leaves from './leaves'; 6 | import { slateEditor } from '@payloadcms/richtext-slate'; 7 | 8 | type RichText = ( 9 | overrides?: AdapterArguments, 10 | additions?: { 11 | elements?: RichTextElement[] 12 | leaves?: RichTextLeaf[] 13 | } 14 | ) => RichTextField 15 | 16 | const richText: RichText = ( 17 | overrides, 18 | additions = { 19 | elements: [], 20 | leaves: [], 21 | }, 22 | ) => ({ 23 | name: 'richText', 24 | type: 'richText', 25 | required: true, 26 | editor: slateEditor(deepMerge({ 27 | admin: { 28 | elements: [ 29 | ...elements, 30 | ...additions.elements || [], 31 | ], 32 | leaves: [ 33 | ...leaves, 34 | ...additions.leaves || [], 35 | ], 36 | }, 37 | }, overrides || {})) 38 | }); 39 | 40 | export default richText; 41 | -------------------------------------------------------------------------------- /components/Blocks/MediaBlock/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Gutter } from '../../Gutter'; 3 | import { Media } from '../../Media'; 4 | import { Media as MediaType } from '../../../payload-types'; 5 | import RichText from '../../RichText'; 6 | import classes from './index.module.scss'; 7 | 8 | export const MediaBlock: React.FC<{ 9 | media?: MediaType 10 | caption?: string 11 | position?: 'default' | 'fullscreen' 12 | mediaBackgroundColor?: string 13 | }> = (props) => { 14 | const { 15 | media, 16 | caption, 17 | position = 'default', 18 | } = props; 19 | 20 | return ( 21 |
22 | {position === 'fullscreen' && ( 23 |
24 | 27 |
28 | )} 29 | {position === 'default' && ( 30 | 31 |
32 | 35 |
36 |
37 | )} 38 | {caption && ( 39 | 40 | 41 | 42 | )} 43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /components/Hero/HighImpact/index.tsx: -------------------------------------------------------------------------------- 1 | import { Cell, Grid } from '@faceless-ui/css-grid'; 2 | import React from 'react'; 3 | import { Page } from '../../../payload-types'; 4 | import { Gutter } from '../../Gutter'; 5 | import { CMSLink } from '../../Link'; 6 | import { Media } from '../../Media'; 7 | import RichText from '../../RichText'; 8 | 9 | import classes from './index.module.scss'; 10 | 11 | export const HighImpactHero: React.FC = ({ richText, media, links }) => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | {Array.isArray(links) && links.length > 0 && ( 21 |
    22 | {links.map(({ link }, i) => { 23 | return ( 24 |
  • 25 | 26 |
  • 27 | ) 28 | })} 29 |
30 | )} 31 | {typeof media === 'object' && ( 32 | 33 | )} 34 |
35 |
36 | ) 37 | } -------------------------------------------------------------------------------- /payload/blocks/Media/index.ts: -------------------------------------------------------------------------------- 1 | import type { Block } from "payload/types"; 2 | import { backgroundColor } from "../../fields/backgroundColor"; 3 | // import { slateEditor } from "@payloadcms/richtext-slate"; 4 | 5 | export const MediaBlock: Block = { 6 | slug: 'mediaBlock', 7 | fields: [ 8 | { 9 | type: 'row', 10 | fields: [ 11 | backgroundColor({ overrides: { name: 'mediaBlockBackgroundColor' } }), 12 | { 13 | name: 'position', 14 | type: 'select', 15 | defaultValue: 'default', 16 | options: [ 17 | { 18 | label: 'Default', 19 | value: 'default', 20 | }, 21 | { 22 | label: 'Fullscreen', 23 | value: 'fullscreen', 24 | } 25 | ] 26 | }, 27 | ] 28 | }, 29 | { 30 | name: 'media', 31 | type: 'upload', 32 | relationTo: 'media', 33 | required: true, 34 | }, 35 | // { 36 | // name: 'caption', 37 | // type: 'richText', 38 | // editor: slateEditor({ 39 | // admin: { 40 | // elements: [ 41 | // 'link', 42 | // ] 43 | // } 44 | // }) 45 | // } 46 | ] 47 | } -------------------------------------------------------------------------------- /app/(site)/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { notFound } from 'next/navigation' 3 | import { getPayloadClient } from '../../../payload/payloadClient'; 4 | import Blocks from '../../../components/Blocks'; 5 | import { Hero } from '../../../components/Hero'; 6 | import { AdminBar } from '../../../components/AdminBar'; 7 | 8 | const Page = async ({ params: { slug } }: { params: { slug: string } }) => { 9 | const payload = await getPayloadClient(); 10 | 11 | const pages = await payload.find({ 12 | collection: 'pages', 13 | where: { 14 | slug: { 15 | equals: slug || 'home', 16 | }, 17 | } 18 | }); 19 | 20 | const page = pages.docs[0]; 21 | 22 | if (!page) return notFound() 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | export async function generateStaticParams() { 34 | const payload = await getPayloadClient(); 35 | 36 | const pages = await payload.find({ 37 | collection: 'pages', 38 | limit: 0, 39 | }) 40 | 41 | return pages.docs.map(({ slug }: { slug: string }) => ({ slug })) 42 | } 43 | 44 | export default Page; -------------------------------------------------------------------------------- /payload/payloadClient.ts: -------------------------------------------------------------------------------- 1 | import { getPayload } from "payload/dist/payload"; 2 | import config from './payload.config'; 3 | 4 | if (!process.env.PAYLOAD_SECRET) { 5 | throw new Error('PAYLOAD_SECRET environment variable is missing') 6 | } 7 | 8 | /** 9 | * Global is used here to maintain a cached connection across hot reloads 10 | * in development. This prevents connections growing exponentially 11 | * during API Route usage. 12 | * 13 | * Source: https://github.com/vercel/next.js/blob/canary/examples/with-mongodb-mongoose/lib/dbConnect.js 14 | */ 15 | let cached = (global as any).payload 16 | 17 | if (!cached) { 18 | cached = (global as any).payload = { client: null, promise: null } 19 | } 20 | 21 | export const getPayloadClient = async () => { 22 | if (cached.client) { 23 | return cached.client 24 | } 25 | 26 | if (!cached.promise) { 27 | cached.promise = await getPayload({ 28 | // Make sure that your environment variables are filled out accordingly 29 | secret: process.env.PAYLOAD_SECRET as string, 30 | config: config, 31 | }) 32 | } 33 | 34 | try { 35 | cached.client = await cached.promise 36 | } catch (e) { 37 | cached.promise = null 38 | throw e 39 | } 40 | 41 | return cached.client 42 | }; 43 | 44 | export default getPayloadClient; -------------------------------------------------------------------------------- /components/Hero/MediumImpact/index.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../css/common.scss' as *; 2 | 3 | .hero { 4 | padding-top: calc(var(--base) * 3); 5 | 6 | @include mid-break { 7 | padding-top: var(--base); 8 | } 9 | } 10 | 11 | .richText { 12 | position: relative; 13 | 14 | h1 { 15 | @extend %h2; 16 | } 17 | 18 | &::after { 19 | content: ''; 20 | display: block; 21 | position: absolute; 22 | width: 100vw; 23 | left: calc(var(--gutter-h) * -1); 24 | height: 200px; 25 | background: linear-gradient(to bottom, var(--color-base-100), transparent); 26 | top: calc(100% + (var(--base) * 2)); 27 | right: 0; 28 | 29 | @include mid-break { 30 | display: none; 31 | } 32 | } 33 | } 34 | 35 | .links { 36 | position: relative; 37 | list-style: none; 38 | margin: 0; 39 | padding: 0; 40 | display: flex; 41 | margin-top: calc(var(--base) * 4); 42 | 43 | li { 44 | margin-right: 12px; 45 | } 46 | 47 | @include mid-break { 48 | display: block; 49 | margin-top: var(--base); 50 | 51 | li { 52 | margin-right: 0; 53 | } 54 | } 55 | } 56 | 57 | .link { 58 | @include mid-break { 59 | width: 100%; 60 | } 61 | } 62 | 63 | .media { 64 | position: relative; 65 | width: calc(100% + var(--gutter-h)); 66 | } 67 | -------------------------------------------------------------------------------- /components/Media/Video/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import classes from './index.module.scss' 3 | import { Media, Props } from '..'; 4 | 5 | export const Video: React.FC = (props) => { 6 | const { 7 | videoClassName, 8 | resource, 9 | onClick, 10 | } = props; 11 | 12 | const videoRef = useRef(null); 13 | const [showFallback] = useState(); 14 | 15 | useEffect(() => { 16 | const { current: video } = videoRef; 17 | if (video) { 18 | video.addEventListener('suspend', () => { 19 | // setShowFallback(true); 20 | // console.warn('Video was suspended, rendering fallback image.') 21 | }); 22 | } 23 | }, []); 24 | 25 | if (resource && typeof resource !== 'string') { 26 | const { 27 | filename, 28 | } = resource 29 | 30 | return ( 31 | 46 | ); 47 | }; 48 | 49 | return null 50 | } 51 | -------------------------------------------------------------------------------- /components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { GridProvider } from '@faceless-ui/css-grid'; 4 | import { ModalContainer, ModalProvider } from '@faceless-ui/modal'; 5 | import React from 'react'; 6 | import { Header } from '../Header'; 7 | import { MainMenu } from '../../payload-types'; 8 | import cssVariables from '../../cssVariables'; 9 | import '../../css/app.scss'; 10 | 11 | type Props = { 12 | mainMenu: MainMenu 13 | children: React.ReactNode 14 | } 15 | 16 | const Layout = ({ 17 | mainMenu, 18 | children, 19 | }: Props): React.ReactElement => { 20 | return ( 21 | 22 | 41 | 42 |
43 | {children} 44 | 45 | 46 | 47 | 48 | ) 49 | } 50 | 51 | export default Layout 52 | -------------------------------------------------------------------------------- /components/Media/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ElementType, Fragment, Ref } from 'react'; 2 | import { Video } from './Video'; 3 | import { Image } from './Image'; 4 | import { StaticImageData } from 'next/image'; 5 | import { Media as MediaType } from '../../payload-types'; 6 | 7 | export type Props = { 8 | src?: StaticImageData // for static media 9 | alt?: string 10 | resource?: MediaType // for Payload media 11 | size?: string // for NextImage only 12 | priority?: boolean // for NextImage only 13 | fill?: boolean // for NextImage only 14 | className?: string 15 | imgClassName?: string 16 | videoClassName?: string 17 | htmlElement?: ElementType | null 18 | onClick?: () => void 19 | onLoad?: () => void 20 | ref?: Ref<(null | HTMLImageElement | HTMLVideoElement)> 21 | } 22 | 23 | export const Media: React.FC = (props) => { 24 | const { 25 | className, 26 | resource, 27 | htmlElement = 'div' 28 | } = props; 29 | 30 | const isVideo = typeof resource !== 'string' && resource?.mimeType?.includes('video'); 31 | const Tag = htmlElement as ElementType || Fragment; 32 | 33 | return ( 34 | 39 | {isVideo ? ( 40 | 45 | ) 46 | }; 47 | -------------------------------------------------------------------------------- /components/BackgroundColor/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useContext, createContext } from 'react'; 4 | import { VerticalPadding, VerticalPaddingOptions } from '../VerticalPadding'; 5 | import classes from './index.module.scss'; 6 | 7 | export type BackgroundColor = 'white' | 'black' 8 | 9 | export const BackgroundColorContext = createContext('white'); 10 | 11 | export const useBackgroundColor = (): BackgroundColor => useContext(BackgroundColorContext); 12 | 13 | 14 | type Props = { 15 | color?: BackgroundColor 16 | paddingTop?: VerticalPaddingOptions 17 | paddingBottom?: VerticalPaddingOptions 18 | className?: string 19 | children?: React.ReactNode 20 | id?: string 21 | } 22 | 23 | export const BackgroundColor: React.FC = (props) => { 24 | const { 25 | id, 26 | className, 27 | children, 28 | paddingTop, 29 | paddingBottom, 30 | color = 'white', 31 | } = props; 32 | 33 | return ( 34 |
41 | 42 | 46 | {children} 47 | 48 | 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /payload/fields/hero.ts: -------------------------------------------------------------------------------- 1 | import { Field } from 'payload/types'; 2 | import linkGroup from './linkGroup'; 3 | // import richText from './richText'; 4 | import label from './richText/label'; 5 | import largeBody from './richText/largeBody'; 6 | 7 | export const hero: Field = { 8 | name: 'hero', 9 | label: false, 10 | type: 'group', 11 | fields: [ 12 | { 13 | type: 'select', 14 | name: 'type', 15 | label: 'Type', 16 | required: true, 17 | defaultValue: 'lowImpact', 18 | options: [ 19 | { 20 | label: 'High Impact', 21 | value: 'highImpact', 22 | }, 23 | { 24 | label: 'Medium Impact', 25 | value: 'mediumImpact', 26 | }, 27 | { 28 | label: 'Low Impact', 29 | value: 'lowImpact', 30 | }, 31 | ], 32 | }, 33 | // richText({ 34 | // admin: { 35 | // elements: [ 36 | // 'h1', 37 | // largeBody, 38 | // label, 39 | // 'link', 40 | // ], 41 | // leaves: [], 42 | // } 43 | // }), 44 | linkGroup({ 45 | overrides: { 46 | maxRows: 2, 47 | } 48 | }), 49 | { 50 | name: 'media', 51 | type: 'upload', 52 | relationTo: 'media', 53 | required: true, 54 | admin: { 55 | condition: (_, { type } = {}) => ['highImpact', 'mediumImpact'].includes(type), 56 | }, 57 | }, 58 | ], 59 | }; 60 | -------------------------------------------------------------------------------- /components/AdminBar/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState } from 'react'; 4 | import { PayloadMeUser, PayloadAdminBarProps, PayloadAdminBar } from 'payload-admin-bar'; 5 | import { Gutter } from '../Gutter'; 6 | import classes from './index.module.scss' 7 | 8 | const Title: React.FC = () => ( 9 | 10 | Payload + Vercel 11 | 12 | ) 13 | 14 | export const AdminBar: React.FC<{ 15 | adminBarProps: PayloadAdminBarProps 16 | }> = (props) => { 17 | const { 18 | adminBarProps 19 | } = props; 20 | 21 | const [user, setUser] = useState(); 22 | 23 | return ( 24 |
30 | 31 | } 42 | style={{ 43 | position: 'relative', 44 | zIndex: 'unset', 45 | padding: 0, 46 | backgroundColor: 'transparent' 47 | }} 48 | /> 49 | 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { ModalToggler } from '@faceless-ui/modal'; 2 | import Link from 'next/link'; 3 | import React from 'react'; 4 | import { MainMenu } from '../../payload-types'; 5 | import { Gutter } from '../Gutter'; 6 | import { MenuIcon } from '../icons/Menu'; 7 | import { CMSLink } from '../Link'; 8 | import { Logo } from '../Logo'; 9 | import { MobileMenuModal, slug as menuModalSlug } from './MobileMenuModal'; 10 | 11 | import classes from './index.module.scss'; 12 | 13 | type HeaderBarProps = { 14 | children?: React.ReactNode; 15 | } 16 | export const HeaderBar: React.FC = ({ children }) => { 17 | return ( 18 |
19 | 20 | 21 | 22 | 23 | 24 | {children} 25 | 26 | 27 | 28 | 29 | 30 |
31 | ) 32 | } 33 | 34 | export const Header: React.FC<{ mainMenu: MainMenu }> = ({ mainMenu }) => { 35 | const navItems = mainMenu?.navItems || []; 36 | 37 | return ( 38 | <> 39 | 40 | 47 | 48 | 49 | 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /components/Hero/MediumImpact/index.tsx: -------------------------------------------------------------------------------- 1 | import { Cell, Grid } from '@faceless-ui/css-grid'; 2 | import React from 'react'; 3 | import { Page } from '../../../payload-types'; 4 | import { Gutter } from '../../Gutter'; 5 | import { CMSLink } from '../../Link'; 6 | import { Media } from '../../Media'; 7 | import RichText from '../../RichText'; 8 | 9 | import classes from './index.module.scss'; 10 | 11 | export const MediumImpactHero: React.FC = (props) => { 12 | const { 13 | richText, 14 | media, 15 | links 16 | } = props; 17 | 18 | return ( 19 | 20 | 21 | 22 | 26 | {Array.isArray(links) && ( 27 |
    28 | {links.map(({ link }, i) => { 29 | return ( 30 |
  • 31 | 35 |
  • 36 | ) 37 | })} 38 |
39 | )} 40 |
41 | 42 | {typeof media === 'object' && ( 43 | 47 | )} 48 | 49 |
50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React, { ElementType } from 'react'; 3 | import { useBackgroundColor } from '../BackgroundColor'; 4 | import { Chevron } from '../icons/Chevron'; 5 | import classes from './index.module.scss'; 6 | 7 | export type Props = { 8 | label?: string 9 | appearance?: 'default' | 'primary' | 'secondary' 10 | el?: 'button' | 'link' | 'a' 11 | onClick?: () => void 12 | href?: string 13 | newTab?: boolean 14 | className?: string 15 | } 16 | 17 | export const Button: React.FC = ({ 18 | el = 'button', 19 | label, 20 | newTab, 21 | href, 22 | appearance, 23 | className: classNameFromProps 24 | }) => { 25 | const backgroundColor = useBackgroundColor(); 26 | const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}; 27 | const className = [classNameFromProps, classes[`appearance--${appearance}`], classes[`${appearance}--${backgroundColor}`], classes.button].filter(Boolean).join(' '); 28 | 29 | const content = ( 30 |
31 | 32 | 33 | {label} 34 | 35 |
36 | ) 37 | 38 | if (el === 'link') { 39 | return ( 40 | 45 | {content} 46 | 47 | ) 48 | } 49 | 50 | const Element: ElementType = el; 51 | 52 | return ( 53 | 58 | {content} 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /components/Link/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React from 'react'; 3 | import { Page } from '../../payload-types'; 4 | import { Button } from '../Button'; 5 | 6 | type CMSLinkType = { 7 | type?: 'custom' | 'reference' 8 | url?: string 9 | newTab?: boolean 10 | reference?: { 11 | value: string | Page 12 | relationTo: 'pages' 13 | } 14 | label?: string 15 | appearance?: 'default' | 'primary' | 'secondary' 16 | children?: React.ReactNode 17 | className?: string 18 | } 19 | 20 | export const CMSLink: React.FC = ({ 21 | type, 22 | url, 23 | newTab, 24 | reference, 25 | label, 26 | appearance, 27 | children, 28 | className, 29 | }) => { 30 | const href = (type === 'reference' && typeof reference?.value === 'object' && reference.value.slug) ? `/${reference.value.slug}` : url; 31 | 32 | if (!appearance) { 33 | const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}; 34 | 35 | if (type === 'custom') { 36 | return ( 37 | 38 | {label && label} 39 | {children && children} 40 | 41 | ) 42 | } 43 | 44 | if (href) { 45 | return ( 46 | 51 | {label && label} 52 | {children && children} 53 | 54 | ) 55 | } 56 | } 57 | 58 | const buttonProps = { 59 | newTab, 60 | href, 61 | appearance, 62 | label, 63 | } 64 | 65 | return ( 66 |