This commit is contained in:
parent
f9ee909e01
commit
60f7a4e4f2
@ -23,8 +23,10 @@
|
||||
"dependencies": {
|
||||
"@payloadcms/db-mongodb": "3.0.0-beta.40",
|
||||
"@payloadcms/next": "3.0.0-beta.40",
|
||||
"@payloadcms/plugin-nested-docs": "3.0.0-beta.35",
|
||||
"@payloadcms/richtext-lexical": "3.0.0-beta.40",
|
||||
"@payloadcms/ui": "3.0.0-beta.40",
|
||||
"babel-plugin-react-compiler": "^0.0.0-experimental-592953e-20240517",
|
||||
"cross-env": "^7.0.3",
|
||||
"graphql": "^16.8.1",
|
||||
"next": "15.0.0-rc.0",
|
||||
@ -32,7 +34,7 @@
|
||||
"react": "19.0.0-rc-f994737d14-20240522",
|
||||
"react-dom": "19.0.0-rc-f994737d14-20240522",
|
||||
"sharp": "0.32.6",
|
||||
"babel-plugin-react-compiler": "^0.0.0-experimental-592953e-20240517"
|
||||
"tailwind-merge": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.12",
|
||||
|
@ -27,6 +27,7 @@ import Media from '@payload/collections/Media'
|
||||
import Authors from '@payload/collections/Authors'
|
||||
import Posts from '@payload/collections/Posts'
|
||||
import Users from '@payload/collections/Users'
|
||||
import Pages from '@payload/collections/Pages'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@ -40,28 +41,7 @@ export default buildConfig({
|
||||
},
|
||||
},
|
||||
editor: lexicalEditor(),
|
||||
collections: [
|
||||
Users,
|
||||
Posts,
|
||||
Authors,
|
||||
Media,
|
||||
{
|
||||
slug: 'pages',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
collections: [Users, Posts, Authors, Media, Pages],
|
||||
secret: process.env.PAYLOAD_SECRET || '',
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'types/payload-types.ts'),
|
||||
|
0
src/app/(app)/globals.css
Normal file
0
src/app/(app)/globals.css
Normal file
@ -1,191 +0,0 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding-block: 80px;
|
||||
border-inline-width: 1px;
|
||||
border-inline-style: solid;
|
||||
border-image: linear-gradient(180deg, #ffffff00, #ffffff00, #ffffff1a, #ffffff00) 1;
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
padding-block: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
article {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 60px 80px;
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
padding: 40px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2.6px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #ffffff;
|
||||
font-size: 4rem;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
letter-spacing: -0.02rem;
|
||||
margin: 0;
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
line-height: 28px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ffffff;
|
||||
text-decoration: underline;
|
||||
transition: color 0.2s ease-out;
|
||||
|
||||
&:hover {
|
||||
color: #ffffff80;
|
||||
}
|
||||
}
|
||||
|
||||
.codeBlock {
|
||||
position: relative;
|
||||
background-color: #00000066;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 60px 80px;
|
||||
overflow-x: auto;
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
padding: 40px 40px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: calc(50% - 50vw);
|
||||
width: 100vw;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, #ffffff00, #ffffff1a, #ffffff1a, #ffffff00);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: url('/crosshair.svg');
|
||||
display: block;
|
||||
height: 19px;
|
||||
width: 19px;
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
left: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 14px;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: calc(50% - 50vw);
|
||||
width: 100vw;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, #ffffff00, #ffffff1a, #ffffff1a, #ffffff00);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: url('/crosshair.svg');
|
||||
display: block;
|
||||
height: 19px;
|
||||
width: 19px;
|
||||
position: absolute;
|
||||
bottom: -9px;
|
||||
right: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
|
||||
div.blur {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('/blur.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 400px 400px;
|
||||
background-blend-mode: soft-light, normal;
|
||||
backdrop-filter: blur(60px);
|
||||
}
|
||||
|
||||
div.gradient {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('/gradient.webp');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
z-index: -2;
|
||||
}
|
||||
}
|
@ -1,16 +1,23 @@
|
||||
import React from 'react'
|
||||
import './globals.scss'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
/* import { Inter } from 'next/font/google'
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
*/
|
||||
/* Our app sits here to not cause any conflicts with payload's root layout */
|
||||
const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
{
|
||||
/* <html className={inter.className}>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
*/
|
||||
}
|
||||
|
||||
return (
|
||||
<html className={inter.className}>
|
||||
<html>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
|
@ -2,49 +2,19 @@ import { Badge } from '@/components/Badge'
|
||||
import { Background } from '@/components/Background'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import LexicalContent from '@/components/LexicalContent'
|
||||
|
||||
const Page = () => {
|
||||
/* const url = 'http://localhost:3000/api/pages/66781d2fb752297de17a3368?depth=1&draft=false'
|
||||
|
||||
const content = fetch(url)
|
||||
console.log(content) */
|
||||
|
||||
return (
|
||||
<>
|
||||
<main>
|
||||
<article>
|
||||
<Badge />
|
||||
<h1>Payload 3.0</h1>
|
||||
<p>
|
||||
This BETA is rapidly evolving, you can report any bugs against{' '}
|
||||
<Link href="https://github.com/payloadcms/payload-3.0-demo/issues" target="_blank">
|
||||
the repo
|
||||
</Link>{' '}
|
||||
or in the{' '}
|
||||
<Link
|
||||
href="https://discord.com/channels/967097582721572934/1215659716538273832"
|
||||
target="_blank"
|
||||
>
|
||||
dedicated channel in Discord
|
||||
</Link>
|
||||
. Payload is running at <Link href="/admin">/admin</Link>. An example of a custom route
|
||||
running the Local API can be found at <Link href="/my-route">/my-route</Link>.
|
||||
</p>
|
||||
<p>You can use the Local API in your server components like this:</p>
|
||||
</article>
|
||||
<div className="codeBlock">
|
||||
<pre>
|
||||
<code>
|
||||
{`import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import configPromise from '@payload-config'
|
||||
const payload = await getPayloadHMR({ config: configPromise })
|
||||
<h1>Lexical content hereinafter</h1>
|
||||
|
||||
const data = await payload.find({
|
||||
collection: 'posts',
|
||||
})
|
||||
|
||||
return <Posts data={data} />
|
||||
`}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</main>
|
||||
<Background />
|
||||
{/* <LexicalContent lazyLoadImages={false} childrenNodes={content?.root?.children} locale={''} /> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
35
src/app/(app)/pages/[[...path]]/page.tsx
Normal file
35
src/app/(app)/pages/[[...path]]/page.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import Blocks from '@/components/Blocks'
|
||||
import { COLLECTION_SLUG_PAGE } from '@payload/collections/config'
|
||||
import { getDocument } from '@/utils/getDocument'
|
||||
import { generateMeta } from '@/utils/generateMeta'
|
||||
import { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
type PageArgs = {
|
||||
params: {
|
||||
path: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageArgs): Promise<Metadata> {
|
||||
const page = await getDocument({
|
||||
collection: COLLECTION_SLUG_PAGE,
|
||||
path: params.path,
|
||||
depth: 3,
|
||||
})
|
||||
if (!page) notFound()
|
||||
|
||||
return generateMeta(params?.path)
|
||||
}
|
||||
|
||||
const Page = async ({ params }: PageArgs) => {
|
||||
const page = await getDocument({
|
||||
collection: COLLECTION_SLUG_PAGE,
|
||||
path: params.path,
|
||||
depth: 3,
|
||||
})
|
||||
if (!page) notFound()
|
||||
return <Blocks blocks={page?.blocks} locale="en" />
|
||||
}
|
||||
|
||||
export default Page
|
@ -40,3 +40,15 @@ export const isAdminOrSelf = ({ req: { user } }: any) => {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const isAdminOrPublished = ({ req: { user } }: any) => {
|
||||
if (user && user?.role === 'admin') {
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
4
src/app/(payload)/blocks/index.ts
Normal file
4
src/app/(payload)/blocks/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import RichText from '@payload/blocks/rich-text'
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default [RichText]
|
14
src/app/(payload)/blocks/rich-text.ts
Normal file
14
src/app/(payload)/blocks/rich-text.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { Block } from 'payload/types'
|
||||
|
||||
const RichText: Block = {
|
||||
slug: 'RichText',
|
||||
interfaceName: 'RichTextBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default RichText
|
62
src/app/(payload)/collections/Pages.ts
Normal file
62
src/app/(payload)/collections/Pages.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
import { slugField, pathField } from '@payload/fields/'
|
||||
import { blocksField } from '@payload/fields/blocks'
|
||||
import { COLLECTION_SLUG_PAGE } from './config'
|
||||
import { createBreadcrumbsField } from '@payloadcms/plugin-nested-docs'
|
||||
import { revalidateTag } from 'next/cache'
|
||||
import { generateDocumentCacheKey } from '@/utils/getDocument'
|
||||
import { isAdmin, isAdminOrPublished } from '@payload/access/isAdmin'
|
||||
|
||||
const Pages: CollectionConfig = {
|
||||
slug: COLLECTION_SLUG_PAGE,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'path', 'updatedAt', 'createdAt'],
|
||||
},
|
||||
versions: {
|
||||
drafts: {
|
||||
autosave: false,
|
||||
},
|
||||
maxPerDoc: 10,
|
||||
},
|
||||
access: {
|
||||
read: isAdminOrPublished,
|
||||
create: isAdmin,
|
||||
update: isAdmin,
|
||||
delete: isAdmin,
|
||||
},
|
||||
/* hooks: {
|
||||
afterChange: [
|
||||
async ({ doc, collection }) => {
|
||||
revalidateTag(generateDocumentCacheKey(collection.slug, doc.path))
|
||||
},
|
||||
],
|
||||
}, */
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'Content',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
},
|
||||
blocksField(),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
slugField(),
|
||||
pathField(),
|
||||
createBreadcrumbsField(COLLECTION_SLUG_PAGE, {
|
||||
name: 'breadcrumbs',
|
||||
admin: {
|
||||
disabled: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
export default Pages
|
10
src/app/(payload)/collections/config.ts
Normal file
10
src/app/(payload)/collections/config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/* export const COLLECTION_SLUG_USER = 'users' as const
|
||||
export const COLLECTION_SLUG_SESSIONS = 'sessions' as const
|
||||
export const COLLECTION_SLUG_FORMS = 'forms' as const
|
||||
export const COLLECTION_SLUG_MEDIA = 'media' as const
|
||||
*/
|
||||
export const COLLECTION_SLUG_PAGE = 'pages' as const
|
||||
|
||||
/* export const COLLECTION_SLUG_PRODUCTS = 'products' as const
|
||||
export const COLLECTION_SLUG_PRICES = 'prices' as const
|
||||
export const COLLECTION_SLUG_SUBSCRIPTIONS = 'subscriptions' as const */
|
16
src/app/(payload)/fields/blocks.ts
Executable file
16
src/app/(payload)/fields/blocks.ts
Executable file
@ -0,0 +1,16 @@
|
||||
import type { Field } from 'payload/types'
|
||||
import blocks from '@payload/blocks'
|
||||
import deepMerge from 'deepmerge'
|
||||
|
||||
type BlocksField = (overrides?: Partial<Field>) => Field
|
||||
|
||||
export const blocksField: BlocksField = (overrides) => {
|
||||
return deepMerge<Field, Partial<Field>>(
|
||||
{
|
||||
name: 'blocks',
|
||||
type: 'blocks',
|
||||
blocks,
|
||||
},
|
||||
overrides || {},
|
||||
)
|
||||
}
|
26
src/app/(payload)/fields/icon.ts
Normal file
26
src/app/(payload)/fields/icon.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import deepMerge from 'deepmerge'
|
||||
import type { Field } from 'payload/types'
|
||||
import * as HiIcons from 'react-icons/hi2'
|
||||
|
||||
const iconOptions = Object.entries(HiIcons)
|
||||
.filter(([key, value]) => typeof value === 'function')
|
||||
.map(([key]) => ({
|
||||
value: key,
|
||||
label: key.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
}))
|
||||
|
||||
type IconField = (overrides?: Partial<Field>) => Field
|
||||
|
||||
const iconField: IconField = (overrides = {}) => {
|
||||
return deepMerge<Field, Partial<Field>>(
|
||||
{
|
||||
type: 'select',
|
||||
name: 'icon',
|
||||
label: 'Icon',
|
||||
options: iconOptions
|
||||
},
|
||||
overrides
|
||||
)
|
||||
}
|
||||
|
||||
export default iconField
|
2
src/app/(payload)/fields/index.ts
Normal file
2
src/app/(payload)/fields/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as pathField } from "./path";
|
||||
export { default as slugField } from "./slug";
|
122
src/app/(payload)/fields/path.ts
Normal file
122
src/app/(payload)/fields/path.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { COLLECTION_SLUG_PAGE } from '@payload/collections/config'
|
||||
import generateBreadcrumbsUrl from '@/utils/generateBreadcrumbsUrl'
|
||||
import { getParents } from '@payloadcms/plugin-nested-docs'
|
||||
import deepmerge from 'deepmerge'
|
||||
import { APIError } from 'payload/errors'
|
||||
import type { Field, Payload, Where } from 'payload/types'
|
||||
import type { Config } from 'types/payload-types'
|
||||
import generateRandomString from '@/utils/generateRandomString'
|
||||
|
||||
type Collection = keyof Config['collections']
|
||||
type WillPathConflictParams = {
|
||||
payload: Payload
|
||||
path: string
|
||||
originalDoc?: { id?: string }
|
||||
collection: Collection
|
||||
uniquePathFieldCollections?: Collection[]
|
||||
}
|
||||
|
||||
export const willPathConflict = async ({
|
||||
payload,
|
||||
path,
|
||||
originalDoc,
|
||||
collection,
|
||||
uniquePathFieldCollections = [],
|
||||
}: WillPathConflictParams): Promise<boolean> => {
|
||||
if (!payload || !uniquePathFieldCollections.includes(collection)) return false
|
||||
|
||||
const queries = uniquePathFieldCollections.map((targetCollection) => {
|
||||
const whereCondition: Where = {
|
||||
path: { equals: path },
|
||||
}
|
||||
if (originalDoc?.id && collection === targetCollection) {
|
||||
whereCondition.id = { not_equals: originalDoc.id }
|
||||
}
|
||||
|
||||
return payload.find({
|
||||
collection: targetCollection,
|
||||
where: whereCondition,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
})
|
||||
})
|
||||
|
||||
const results = await Promise.allSettled(queries)
|
||||
return results.some((result) => result.status === 'fulfilled' && result.value.docs.length > 0)
|
||||
}
|
||||
type GetNewPathParams = {
|
||||
req: any
|
||||
collection: Collection
|
||||
currentDoc: any
|
||||
operation?: string
|
||||
}
|
||||
|
||||
export async function getNewPath({
|
||||
req,
|
||||
collection,
|
||||
currentDoc,
|
||||
operation,
|
||||
}: GetNewPathParams): Promise<string> {
|
||||
const isAutoSave = operation === 'create' && currentDoc?._status === 'draft'
|
||||
if (isAutoSave || currentDoc?.slug == null || !collection)
|
||||
return `/${currentDoc?.id || generateRandomString(20)}`
|
||||
const newPath = currentDoc?.breadcrumbs?.at(-1)?.url
|
||||
if (newPath) return newPath
|
||||
const docs = await getParents(
|
||||
req,
|
||||
{ parentFieldSlug: 'parent' } as any,
|
||||
collection as any,
|
||||
currentDoc,
|
||||
[currentDoc],
|
||||
)
|
||||
|
||||
return generateBreadcrumbsUrl(docs, currentDoc)
|
||||
}
|
||||
|
||||
const pathField = (overrides?: Partial<Field>): Field =>
|
||||
deepmerge<Field, Partial<Field>>(
|
||||
{
|
||||
type: 'text',
|
||||
name: 'path',
|
||||
unique: true,
|
||||
index: true,
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
async ({ collection, req, siblingData, originalDoc, operation }) => {
|
||||
const currentDoc = { ...originalDoc, ...siblingData }
|
||||
const newPath = await getNewPath({
|
||||
req,
|
||||
collection: collection?.slug as Collection,
|
||||
currentDoc,
|
||||
operation,
|
||||
})
|
||||
const isNewPathConflicting = await willPathConflict({
|
||||
payload: req.payload,
|
||||
path: newPath,
|
||||
originalDoc,
|
||||
collection: collection ? (collection.slug as Collection) : COLLECTION_SLUG_PAGE,
|
||||
uniquePathFieldCollections: [COLLECTION_SLUG_PAGE], // Add more collections as needed
|
||||
})
|
||||
|
||||
if (isNewPathConflicting) {
|
||||
const error = new APIError(
|
||||
'This will create a conflict with an existing path.',
|
||||
400,
|
||||
[{ field: 'slug', message: 'This will create a conflict with an existing path.' }],
|
||||
false,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
return newPath
|
||||
},
|
||||
],
|
||||
},
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
overrides || {},
|
||||
)
|
||||
|
||||
export default pathField
|
49
src/app/(payload)/fields/slug.ts
Normal file
49
src/app/(payload)/fields/slug.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import type { Field, FieldHook } from 'payload/types'
|
||||
import deepMerge from 'deepmerge'
|
||||
|
||||
const format = (val: string): string =>
|
||||
val
|
||||
.replace(/ /g, '-')
|
||||
.replace(/[^\w-]+/g, '')
|
||||
.toLowerCase()
|
||||
|
||||
const formatSlug =
|
||||
(fallback: string): FieldHook =>
|
||||
({ operation, value, originalDoc, data }) => {
|
||||
if (typeof value === 'string' && value.length > 0) {
|
||||
return format(value)
|
||||
}
|
||||
|
||||
if (operation === 'create') {
|
||||
const fallbackData = (data && data[fallback]) || (originalDoc && originalDoc[fallback])
|
||||
|
||||
if (fallbackData && typeof fallbackData === 'string') {
|
||||
return format(fallbackData)
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
type Slug = (fieldToUse?: string, overrides?: Partial<Field>) => Field
|
||||
|
||||
const slugField: Slug = (fieldToUse = 'title', overrides = {}) => {
|
||||
return deepMerge<Field, Partial<Field>>(
|
||||
{
|
||||
name: 'slug',
|
||||
label: 'Slug',
|
||||
type: 'text',
|
||||
index: true,
|
||||
required: false, // Need to be false so that we can use beforeValidate hook to set slug.
|
||||
admin: {
|
||||
position: 'sidebar'
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate: [formatSlug(fieldToUse)]
|
||||
}
|
||||
},
|
||||
overrides
|
||||
)
|
||||
}
|
||||
|
||||
export default slugField
|
22
src/components/Blocks/RichText.tsx
Normal file
22
src/components/Blocks/RichText.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import type { AdditionalBlockProps } from '@/components/Blocks'
|
||||
import Container from '@/components/Container'
|
||||
import LexicalContent from '@/components/LexicalContent'
|
||||
import type { RichTextBlock } from 'types/payload-types'
|
||||
|
||||
export default function RichText({ content, locale }: RichTextBlock & AdditionalBlockProps) {
|
||||
if (content?.root?.children?.length === 0) return null
|
||||
return (
|
||||
<section className="py-10 first:mt-16">
|
||||
<Container>
|
||||
<div className="prose dark:prose-invert md:prose-lg">
|
||||
<LexicalContent
|
||||
// @ts-ignore
|
||||
childrenNodes={content?.root?.children}
|
||||
locale={locale}
|
||||
lazyLoadImages={false}
|
||||
/>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
31
src/components/Blocks/index.tsx
Normal file
31
src/components/Blocks/index.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import Author from './Author'
|
||||
|
||||
export type AdditionalBlockProps = {
|
||||
blockIndex: number
|
||||
locale: string
|
||||
}
|
||||
|
||||
const blockComponents = {
|
||||
Author: Author,
|
||||
}
|
||||
|
||||
const Blocks = ({ blocks, locale }: any) => {
|
||||
return (
|
||||
<>
|
||||
{blocks
|
||||
?.filter(
|
||||
(block: any) =>
|
||||
block && block.blockType && blockComponents.hasOwnProperty(block.blockType),
|
||||
)
|
||||
.map((block: any, ix: number) => {
|
||||
// @ts-ignore
|
||||
const BlockComponent = blockComponents[block.blockType] ?? null
|
||||
return BlockComponent ? (
|
||||
<BlockComponent key={ix} {...block} blockIndex={ix} locale={locale} />
|
||||
) : null
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Blocks
|
10
src/components/Container.tsx
Executable file
10
src/components/Container.tsx
Executable file
@ -0,0 +1,10 @@
|
||||
import cn from '@/utils/cn'
|
||||
import type { ComponentPropsWithoutRef } from 'react'
|
||||
|
||||
const Container = ({ children, className }: ComponentPropsWithoutRef<'div'>) => {
|
||||
return (
|
||||
<div className={cn('container mx-auto w-full max-w-screen-lg px-3', className)}>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Container
|
51
src/components/LexicalContent/RichTextNodeFormat.ts
Normal file
51
src/components/LexicalContent/RichTextNodeFormat.ts
Normal file
@ -0,0 +1,51 @@
|
||||
//This copy-and-pasted from somewhere in lexical here: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts
|
||||
|
||||
// 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
|
||||
|
||||
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
|
||||
|
||||
export const TEXT_TYPE_TO_FORMAT: Record<TextFormatType | string, number> = {
|
||||
bold: IS_BOLD,
|
||||
code: IS_CODE,
|
||||
italic: IS_ITALIC,
|
||||
strikethrough: IS_STRIKETHROUGH,
|
||||
subscript: IS_SUBSCRIPT,
|
||||
superscript: IS_SUPERSCRIPT,
|
||||
underline: IS_UNDERLINE
|
||||
}
|
||||
|
||||
export type TextFormatType = 'bold' | 'underline' | 'strikethrough' | 'italic' | 'code' | 'subscript' | 'superscript'
|
213
src/components/LexicalContent/index.tsx
Normal file
213
src/components/LexicalContent/index.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
/* eslint-disable react/no-children-prop */
|
||||
import normalizePath from '@/utils/normalizePath'
|
||||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
import React, { CSSProperties, type FC, type ReactElement } from 'react'
|
||||
import Image from 'next/image'
|
||||
import {
|
||||
IS_BOLD,
|
||||
IS_CODE,
|
||||
IS_ITALIC,
|
||||
IS_STRIKETHROUGH,
|
||||
IS_SUBSCRIPT,
|
||||
IS_SUPERSCRIPT,
|
||||
IS_UNDERLINE,
|
||||
} from './RichTextNodeFormat'
|
||||
|
||||
type SerializedLexicalNode = {
|
||||
children?: SerializedLexicalNode[]
|
||||
direction: string
|
||||
format: number
|
||||
indent?: string | number
|
||||
type: string
|
||||
version: number
|
||||
style?: string
|
||||
mode?: string
|
||||
text?: string
|
||||
[other: string]: any
|
||||
}
|
||||
|
||||
type TextComponentProps = {
|
||||
children: ReactElement | string
|
||||
format: number
|
||||
}
|
||||
|
||||
const getLinkForDocument = (doc: any, locale?: string): string => {
|
||||
let path = doc?.path
|
||||
if (!path || path.startsWith('/home') || path === '/' || path === '') path = '/'
|
||||
return normalizePath(`/${locale}${path}`)
|
||||
}
|
||||
|
||||
function gcd(a: number, b: number): number {
|
||||
return b === 0 ? a : gcd(b, a % b)
|
||||
}
|
||||
|
||||
function calculateAspectRatio(width: number, height: number): string {
|
||||
const divisor = gcd(width, height)
|
||||
const simplifiedWidth = width / divisor
|
||||
const simplifiedHeight = height / divisor
|
||||
|
||||
return `${simplifiedWidth}x${simplifiedHeight}`
|
||||
}
|
||||
|
||||
const TextComponent: FC<TextComponentProps> = ({ children, format }) => {
|
||||
const formatFunctions: { [key: number]: (child: ReactElement | string) => ReactElement } = {
|
||||
[IS_BOLD]: (child) => <strong>{child}</strong>,
|
||||
[IS_ITALIC]: (child) => <em>{child}</em>,
|
||||
[IS_STRIKETHROUGH]: (child) => <del>{child}</del>,
|
||||
[IS_UNDERLINE]: (child) => <u>{child}</u>,
|
||||
[IS_CODE]: (child) => <code>{child}</code>,
|
||||
[IS_SUBSCRIPT]: (child) => <sub>{child}</sub>,
|
||||
[IS_SUPERSCRIPT]: (child) => <sup>{child}</sup>,
|
||||
}
|
||||
|
||||
const formattedText = Object.entries(formatFunctions).reduce(
|
||||
(formattedText, [key, formatter]) => {
|
||||
return format & Number(key) ? formatter(formattedText) : formattedText
|
||||
},
|
||||
children,
|
||||
)
|
||||
|
||||
return <>{formattedText}</>
|
||||
}
|
||||
|
||||
const SerializedLink: React.FC<{
|
||||
node: SerializedLexicalNode
|
||||
locale: string
|
||||
children: JSX.Element | null
|
||||
}> = ({ node, locale, children }) => {
|
||||
const { doc, url, newTab, linkType } = node.fields as any
|
||||
const document = doc?.value
|
||||
const href = linkType === 'custom' ? url : getLinkForDocument(document, locale)
|
||||
const target = newTab ? '_blank' : undefined
|
||||
|
||||
return (
|
||||
<Link href={href} target={target}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const getNodeClassNames = (node: SerializedLexicalNode) => {
|
||||
const attributes: Record<string, any> = {}
|
||||
if (!node) return attributes
|
||||
|
||||
let classNames = ''
|
||||
if (String(node?.format).toString()?.includes('left') && node.direction !== 'ltr')
|
||||
classNames += 'text-left '
|
||||
if (String(node?.format).toString()?.includes('center')) classNames += 'text-center '
|
||||
if (String(node?.format).toString()?.includes('right') && node.direction !== 'rtl')
|
||||
classNames += 'text-right '
|
||||
|
||||
if (classNames.length > 0) attributes.className = classNames.trim()
|
||||
|
||||
const indent = parseInt(`${node?.indent || 0}`)
|
||||
if (!isNaN(indent) && indent !== 0) {
|
||||
attributes.style = { '--indent': `${indent * 10}px` } as CSSProperties
|
||||
attributes.className = `${attributes.className ?? ''} ml-[--indent]`.trim()
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
const LexicalContent: React.FC<{
|
||||
childrenNodes: SerializedLexicalNode[]
|
||||
locale: string
|
||||
className?: string
|
||||
lazyLoadImages: boolean
|
||||
}> = ({ childrenNodes, locale, lazyLoadImages = false }) => {
|
||||
if (!Array.isArray(childrenNodes)) return null
|
||||
|
||||
const renderedChildren = childrenNodes.map((node, ix) => {
|
||||
if (!node) return null
|
||||
const attributes = getNodeClassNames(node || '')
|
||||
if (node.type === 'text') {
|
||||
return (
|
||||
<TextComponent key={ix} format={node.format}>
|
||||
<>
|
||||
{Object.keys(attributes).length > 0 && <span {...attributes}>{node?.text || ''}</span>}
|
||||
{(Object.keys(attributes).length === 0 && node?.text) || ''}
|
||||
</>
|
||||
</TextComponent>
|
||||
)
|
||||
}
|
||||
|
||||
const serializedChildren = node.children ? (
|
||||
<LexicalContent
|
||||
key={ix}
|
||||
childrenNodes={node.children}
|
||||
locale={locale}
|
||||
lazyLoadImages={lazyLoadImages}
|
||||
/>
|
||||
) : null
|
||||
switch (node.type) {
|
||||
case 'linebreak':
|
||||
return <br key={ix} />
|
||||
case 'link':
|
||||
return <SerializedLink key={ix} node={node} locale={locale} children={serializedChildren} />
|
||||
case 'list':
|
||||
const ListTag = node.listType === 'bullet' ? 'ul' : 'ol'
|
||||
attributes.className = clsx(
|
||||
attributes.className,
|
||||
'mb-4 pl-8',
|
||||
ListTag === 'ol' ? 'list-decimal' : 'list-disc',
|
||||
)
|
||||
return (
|
||||
<ListTag key={ix} {...attributes}>
|
||||
{serializedChildren}
|
||||
</ListTag>
|
||||
)
|
||||
case 'listitem':
|
||||
return (
|
||||
<li key={ix} {...attributes}>
|
||||
{serializedChildren}
|
||||
</li>
|
||||
)
|
||||
case 'heading':
|
||||
const HeadingTag = node.tag as keyof JSX.IntrinsicElements
|
||||
return (
|
||||
<HeadingTag key={ix} {...attributes}>
|
||||
{serializedChildren}
|
||||
</HeadingTag>
|
||||
)
|
||||
case 'quote':
|
||||
return (
|
||||
<blockquote key={ix} {...attributes}>
|
||||
{serializedChildren}
|
||||
</blockquote>
|
||||
)
|
||||
case 'upload':
|
||||
const upload = node?.value
|
||||
if (!upload) return null
|
||||
const imageAspectRatio = calculateAspectRatio(upload.width, upload.height)
|
||||
return (
|
||||
<Image
|
||||
key={ix}
|
||||
width={upload.width}
|
||||
height={upload.height}
|
||||
src={upload?.url}
|
||||
loading={lazyLoadImages ? 'lazy' : 'eager'}
|
||||
fetchPriority={lazyLoadImages ? 'low' : 'high'}
|
||||
sizes="(max-width: 768px) 65ch, 100vw"
|
||||
className="max-w-[calc(100%+40px)] translate-x-[-20px]"
|
||||
alt={upload?.alt || upload.filename}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
if (
|
||||
Array.isArray(serializedChildren?.props?.childrenNodes) &&
|
||||
serializedChildren?.props?.childrenNodes.length === 0
|
||||
)
|
||||
return <br key={ix} />
|
||||
return (
|
||||
<p key={ix} {...attributes}>
|
||||
{serializedChildren}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return <>{renderedChildren.filter((node) => node !== null)}</>
|
||||
}
|
||||
|
||||
export default LexicalContent
|
18
src/lib/payload/actions.ts
Normal file
18
src/lib/payload/actions.ts
Normal file
@ -0,0 +1,18 @@
|
||||
'use server'
|
||||
|
||||
import { SESSION_STRATEGY } from '@/lib/auth/config'
|
||||
import { COLLECTION_SLUG_SESSIONS } from '@/payload/collections/config'
|
||||
import { revalidateTag } from 'next/cache'
|
||||
import type { Payload } from 'payload'
|
||||
import type { User } from '~/payload-types'
|
||||
|
||||
export const revalidateUser = async (user: User, payload: Payload) => {
|
||||
revalidateTag(`payload-user-${user.id}`)
|
||||
revalidateTag(`payload-user-email-${user.email}`)
|
||||
if (SESSION_STRATEGY === 'database') {
|
||||
const { docs: sessions } = await (await payload).find({ where: { user: { equals: user.id } }, collection: COLLECTION_SLUG_SESSIONS })
|
||||
sessions.forEach((session) => {
|
||||
revalidateTag(`payload-user-session-${session.sessionToken}`)
|
||||
})
|
||||
}
|
||||
}
|
21
src/lib/payload/index.ts
Normal file
21
src/lib/payload/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayloadHMR as getPayloadInstance } from '@payloadcms/next/utilities'
|
||||
import { headers as getHeaders } from 'next/headers'
|
||||
import type { User } from '~/payload-types'
|
||||
|
||||
export async function getPayload(): ReturnType<typeof getPayloadInstance> {
|
||||
return getPayloadInstance({ config: await configPromise })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user with out needing to import the payload instance & headers.
|
||||
*
|
||||
* @description The difference between this function and the one in the auth/edge.ts file is that here we get
|
||||
* payload instance, just to make other parts of you code cleaner. We can't get the payload instance in the
|
||||
* auth/edge.ts file because that could cause a import loop.
|
||||
*/
|
||||
export async function getCurrentUser(): Promise<User | null> {
|
||||
const headers = getHeaders()
|
||||
const payload = await getPayload()
|
||||
return (await payload.auth({ headers })).user
|
||||
}
|
6
src/utils/cn.ts
Executable file
6
src/utils/cn.ts
Executable file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export default function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
7
src/utils/generateBreadcrumbsUrl.ts
Normal file
7
src/utils/generateBreadcrumbsUrl.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default function generateBreadcrumbsUrl(docs: any, lastDoc: any) {
|
||||
let prefix = ''
|
||||
// You might want different prefixes for different collections.
|
||||
switch (lastDoc._collection) {
|
||||
}
|
||||
return docs.reduce((url: any, doc: any) => `${url}/${doc.slug ?? ''}`, prefix)
|
||||
}
|
37
src/utils/generateMeta.ts
Normal file
37
src/utils/generateMeta.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { Metadata } from 'next'
|
||||
import _get from 'lodash/get'
|
||||
import deepmerge from 'deepmerge'
|
||||
import { getDocument } from '@/utils/getDocument'
|
||||
import normalizePath from './normalizePath'
|
||||
|
||||
const defaultTitle = 'Payload SaaS Starter'
|
||||
const defaultDescription = 'An open-source website built with Payload and Next.js.'
|
||||
const siteName = 'Payload SaaS Starter'
|
||||
|
||||
const defaultOpenGraph: Metadata['openGraph'] = {
|
||||
type: 'website',
|
||||
description: defaultDescription,
|
||||
siteName,
|
||||
title: defaultTitle,
|
||||
}
|
||||
|
||||
export const generateMeta = async (path: string | string[]): Promise<Metadata> => {
|
||||
const doc = await getDocument({
|
||||
collection: 'pages',
|
||||
path,
|
||||
depth: 1,
|
||||
})
|
||||
const metaTitle = _get(doc, 'meta.title', null)
|
||||
const title = metaTitle ?? (_get(doc, 'title', defaultTitle) as string)
|
||||
const description = _get(doc, 'meta.description', defaultDescription)
|
||||
const ogImage = _get(doc, 'meta.image.url', `/api/og${normalizePath(path, true)}image.jpg`)
|
||||
|
||||
return {
|
||||
description,
|
||||
openGraph: deepmerge(defaultOpenGraph, {
|
||||
description,
|
||||
images: ogImage ? [{ url: ogImage }] : undefined,
|
||||
}),
|
||||
title: { absolute: title },
|
||||
}
|
||||
}
|
13
src/utils/generateRandomString.ts
Normal file
13
src/utils/generateRandomString.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export default function generateRandomString(
|
||||
length: number,
|
||||
characters: string = 'abcdefghijklmnopqrstuvwxyz0123456789',
|
||||
): string {
|
||||
let result = ''
|
||||
const charactersLength = characters.length
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
80
src/utils/getDocument.ts
Normal file
80
src/utils/getDocument.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import type { Config } from 'types/payload-types'
|
||||
import { unstable_cache } from 'next/cache'
|
||||
import normalizePath from '@/utils/normalizePath'
|
||||
import { getCurrentUser, getPayload } from '@/lib/payload'
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
type Collection = keyof Config['collections'] | string
|
||||
type Path = string | string[]
|
||||
type CacheOption = 'noDraft' | true | false
|
||||
|
||||
export const generateDocumentCacheKey = (collection: Collection, path?: Path): string => {
|
||||
return `${collection}_path_${normalizePath(path, false)}`
|
||||
}
|
||||
|
||||
export const generateDocumentCacheParams = (collection: Collection, path?: Path): string[] => {
|
||||
return [collection, normalizePath(path)]
|
||||
}
|
||||
|
||||
export const conditionalCache = async <T>(
|
||||
fetchFunction: () => Promise<T>,
|
||||
cacheKey: string,
|
||||
cache: boolean | undefined,
|
||||
revalidate: false | number | undefined,
|
||||
): Promise<T> => {
|
||||
if (!cache) {
|
||||
return fetchFunction()
|
||||
}
|
||||
return unstable_cache(fetchFunction, [cacheKey], { revalidate, tags: [cacheKey] })()
|
||||
}
|
||||
|
||||
interface GetDocumentParams<K extends keyof Config['collections']> {
|
||||
collection: K
|
||||
path?: Path
|
||||
depth?: number
|
||||
cache?: CacheOption
|
||||
revalidate?: false | number | undefined
|
||||
}
|
||||
|
||||
export const getDocument = async <K extends keyof Config['collections']>({
|
||||
collection,
|
||||
path,
|
||||
depth = 0,
|
||||
cache = 'noDraft',
|
||||
revalidate = false,
|
||||
}: GetDocumentParams<K>): Promise<Config['collections'][K] | null> => {
|
||||
const { isEnabled: draft } = draftMode()
|
||||
const payload = await getPayload()
|
||||
const user = draft ? await getCurrentUser() : null
|
||||
|
||||
const normalizedPath = normalizePath(path, false)
|
||||
const where = { path: { equals: normalizedPath } }
|
||||
|
||||
const cacheKey = generateDocumentCacheKey(collection, path)
|
||||
const shouldCache = draft ? false : !!cache
|
||||
return conditionalCache(
|
||||
async () => {
|
||||
return await payload
|
||||
.find({
|
||||
collection,
|
||||
draft,
|
||||
depth,
|
||||
limit: 1,
|
||||
overrideAccess: false,
|
||||
user,
|
||||
where,
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching document:', error)
|
||||
return null
|
||||
})
|
||||
.then((result) => {
|
||||
if (!result || !result.docs || result.docs.length === 0) return null
|
||||
return result.docs.at(0) as Config['collections'][K] | null
|
||||
})
|
||||
},
|
||||
cacheKey,
|
||||
shouldCache,
|
||||
revalidate,
|
||||
)
|
||||
}
|
9
src/utils/normalizePath.ts
Normal file
9
src/utils/normalizePath.ts
Normal file
@ -0,0 +1,9 @@
|
||||
const normalizePath = (path?: string | string[], keepTrailingSlash: boolean = false): string => {
|
||||
if (!path) return '/'
|
||||
if (Array.isArray(path)) path = path.join('/')
|
||||
path = `/${path}/`.replace(/\/+/g, '/')
|
||||
path = path !== '/' && !keepTrailingSlash ? path.replace(/\/$/, '') : path
|
||||
return path
|
||||
}
|
||||
|
||||
export default normalizePath
|
@ -5,3 +5,170 @@
|
||||
* 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: {
|
||||
users: User;
|
||||
posts: Post;
|
||||
authors: Author;
|
||||
media: Media;
|
||||
pages: Page;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
globals: {};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
roles: ('admin' | 'editor' | 'user')[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
title: string;
|
||||
summary?: string | null;
|
||||
publishedDate?: string | null;
|
||||
thumbnail: string | Media;
|
||||
author?: (string | null) | Author;
|
||||
status: 'draft' | 'published' | 'archived';
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: string;
|
||||
alt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "authors".
|
||||
*/
|
||||
export interface Author {
|
||||
id: string;
|
||||
avatar: string | Media;
|
||||
name: string;
|
||||
bio?: string | null;
|
||||
user?: string | User | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
blocks?: RichTextBlock[] | null;
|
||||
slug?: string | null;
|
||||
path?: string | null;
|
||||
breadcrumbs?:
|
||||
| {
|
||||
doc?: (string | null) | Page;
|
||||
url?: string | null;
|
||||
label?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "RichTextBlock".
|
||||
*/
|
||||
export interface RichTextBlock {
|
||||
content?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'RichText';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
Loading…
Reference in New Issue
Block a user