added header global
This commit is contained in:
11
bun.lock
11
bun.lock
@@ -10,9 +10,12 @@
|
||||
"@payloadcms/plugin-seo": "^3.59.1",
|
||||
"@payloadcms/richtext-lexical": "3.59.1",
|
||||
"@payloadcms/ui": "3.59.1",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "16.4.7",
|
||||
"graphql": "^16.8.1",
|
||||
"lucide-react": "^0.545.0",
|
||||
"next": "15.4.4",
|
||||
"payload": "3.59.1",
|
||||
"react": "19.1.0",
|
||||
@@ -482,6 +485,10 @@
|
||||
|
||||
"@playwright/test": ["@playwright/test@1.54.1", "", { "dependencies": { "playwright": "1.54.1" }, "bin": { "playwright": "cli.js" } }, "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw=="],
|
||||
|
||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.11", "", {}, "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="],
|
||||
@@ -882,6 +889,8 @@
|
||||
|
||||
"ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
|
||||
|
||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||
|
||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
@@ -1320,6 +1329,8 @@
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.545.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw=="],
|
||||
|
||||
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
||||
|
||||
@@ -25,9 +25,12 @@
|
||||
"@payloadcms/plugin-seo": "^3.59.1",
|
||||
"@payloadcms/richtext-lexical": "3.59.1",
|
||||
"@payloadcms/ui": "3.59.1",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "16.4.7",
|
||||
"graphql": "^16.8.1",
|
||||
"lucide-react": "^0.545.0",
|
||||
"next": "15.4.4",
|
||||
"payload": "3.59.1",
|
||||
"react": "19.1.0",
|
||||
|
||||
30
src/Header/Component.client.tsx
Normal file
30
src/Header/Component.client.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import type { Header } from '@/src/payload-types'
|
||||
|
||||
import { HeaderNav } from './Nav'
|
||||
import { Logo } from '../components/Logo/Logo';
|
||||
|
||||
interface HeaderClientProps {
|
||||
data: Header
|
||||
}
|
||||
|
||||
export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
|
||||
/* Storing the value in a useState to avoid hydration errors */
|
||||
const [theme, setTheme] = useState<string | null>(null)
|
||||
const pathname = usePathname()
|
||||
|
||||
return (
|
||||
<header className="container relative z-20 " {...(theme ? { 'data-theme': theme } : {})}>
|
||||
<div className="py-8 flex justify-between">
|
||||
<Link href="/">
|
||||
<Logo loading="eager" priority="high" className="invert dark:invert-0" />
|
||||
</Link>
|
||||
<HeaderNav data={data} />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
12
src/Header/Component.tsx
Normal file
12
src/Header/Component.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
import { getCachedGlobal } from '@/utilities/getGlobals';
|
||||
import React from 'react';
|
||||
|
||||
import type { Header } from '@/src/payload-types';
|
||||
import { HeaderClient } from './Component.client';
|
||||
|
||||
export async function Header() {
|
||||
const headerData: Header = await getCachedGlobal('header', 1)()
|
||||
|
||||
return <HeaderClient data={headerData} />
|
||||
}
|
||||
25
src/Header/Nav/index.tsx
Normal file
25
src/Header/Nav/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import type { Header as HeaderType } from '@/src/payload-types'
|
||||
|
||||
import { CMSLink } from '@/src/components/Link'
|
||||
import Link from 'next/link'
|
||||
import { SearchIcon } from 'lucide-react'
|
||||
|
||||
export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
||||
const navItems = data?.navItems || []
|
||||
|
||||
return (
|
||||
<nav className="flex gap-3 items-center">
|
||||
{navItems.map(({ link }, i) => {
|
||||
return <CMSLink key={i} {...link} appearance="link" />
|
||||
})}
|
||||
<Link href="/search">
|
||||
<span className="sr-only">Search</span>
|
||||
<SearchIcon className="w-5 text-primary" />
|
||||
</Link>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
14
src/Header/RowLabel.tsx
Normal file
14
src/Header/RowLabel.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { Header } from '@/src/payload-types'
|
||||
import { RowLabelProps, useRowLabel } from '@payloadcms/ui';
|
||||
|
||||
export const RowLabel: React.FC<RowLabelProps> = () => {
|
||||
const data = useRowLabel<NonNullable<Header['navItems']>[number]>()
|
||||
|
||||
const label = data?.data?.link?.label
|
||||
? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data?.data?.link?.label}`
|
||||
: 'Row'
|
||||
|
||||
return <div>{label}</div>
|
||||
}
|
||||
31
src/Header/config.ts
Normal file
31
src/Header/config.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { GlobalConfig } from 'payload';
|
||||
import { revalidateHeader } from './hooks/revalidateHeader';
|
||||
import { link } from '../fields/link';
|
||||
|
||||
export const Header: GlobalConfig = {
|
||||
slug: 'header',
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'navItems',
|
||||
type: 'array',
|
||||
fields: [
|
||||
link({
|
||||
appearances: false,
|
||||
}),
|
||||
],
|
||||
maxRows: 6,
|
||||
admin: {
|
||||
initCollapsed: true,
|
||||
components: {
|
||||
RowLabel: '@/src/Header/RowLabel#RowLabel'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
hooks: {
|
||||
afterChange: [revalidateHeader]
|
||||
}
|
||||
}
|
||||
13
src/Header/hooks/revalidateHeader.ts
Normal file
13
src/Header/hooks/revalidateHeader.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { GlobalAfterChangeHook } from 'payload'
|
||||
|
||||
import { revalidateTag } from 'next/cache'
|
||||
|
||||
export const revalidateHeader: GlobalAfterChangeHook = ({ doc, req: { payload, context } }) => {
|
||||
if (!context.disableRevalidate) {
|
||||
payload.logger.info(`Revalidating header`)
|
||||
|
||||
revalidateTag('global_header')
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
66
src/components/Link/index.tsx
Normal file
66
src/components/Link/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Button, type ButtonProps } from '@/src/components/ui/button'
|
||||
import { cn } from '@/utilities/ui'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import type { Page } from '@/src/payload-types'
|
||||
|
||||
type CMSLinkType = {
|
||||
appearance?: 'inline' | ButtonProps['variant']
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
label?: string | null
|
||||
newTab?: boolean | null
|
||||
reference?: {
|
||||
relationTo: 'pages' | 'posts'
|
||||
value: Page | string | number
|
||||
} | null
|
||||
size?: ButtonProps['size'] | null
|
||||
type?: 'custom' | 'reference' | null
|
||||
url?: string | null
|
||||
}
|
||||
|
||||
export const CMSLink: React.FC<CMSLinkType> = (props) => {
|
||||
const {
|
||||
type,
|
||||
appearance = 'inline',
|
||||
children,
|
||||
className,
|
||||
label,
|
||||
newTab,
|
||||
reference,
|
||||
size: sizeFromProps,
|
||||
url,
|
||||
} = props
|
||||
|
||||
const href =
|
||||
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
|
||||
? `${reference?.relationTo !== 'pages' ? `/${reference?.relationTo}` : ''}/${
|
||||
reference.value.slug
|
||||
}`
|
||||
: url
|
||||
|
||||
if (!href) return null
|
||||
|
||||
const size = appearance === 'link' ? 'clear' : sizeFromProps
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
|
||||
/* Ensure we don't break any styles set by richText */
|
||||
if (appearance === 'inline') {
|
||||
return (
|
||||
<Link className={cn(className)} href={href || url || ''} {...newTabProps}>
|
||||
{label && label}
|
||||
{children && children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button asChild className={className} size={size} variant={appearance}>
|
||||
<Link className={cn(className)} href={href || url || ''} {...newTabProps}>
|
||||
{label && label}
|
||||
{children && children}
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
29
src/components/Logo/Logo.tsx
Normal file
29
src/components/Logo/Logo.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import clsx from 'clsx'
|
||||
import React from 'react'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
loading?: 'lazy' | 'eager'
|
||||
priority?: 'auto' | 'high' | 'low'
|
||||
}
|
||||
|
||||
export const Logo = (props: Props) => {
|
||||
const { loading: loadingFromProps, priority: priorityFromProps, className } = props
|
||||
|
||||
const loading = loadingFromProps || 'lazy'
|
||||
const priority = priorityFromProps || 'low'
|
||||
|
||||
return (
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
<img
|
||||
alt="Payload Logo"
|
||||
width={193}
|
||||
height={34}
|
||||
loading={loading}
|
||||
fetchPriority={priority}
|
||||
decoding="async"
|
||||
className={clsx('max-w-[9.375rem] w-full h-[34px]', className)}
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-light.svg"
|
||||
/>
|
||||
)
|
||||
}
|
||||
52
src/components/ui/button.tsx
Normal file
52
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { cn } from '@/utilities/ui'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
defaultVariants: {
|
||||
size: 'default',
|
||||
variant: 'default',
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
clear: '',
|
||||
default: 'h-10 px-4 py-2',
|
||||
icon: 'h-10 w-10',
|
||||
lg: 'h-11 rounded px-8',
|
||||
sm: 'h-9 rounded px-3',
|
||||
},
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
ghost: 'hover:bg-card hover:text-accent-foreground',
|
||||
link: 'text-primary items-start justify-start underline-offset-4 hover:underline',
|
||||
outline: 'border border-border bg-background hover:bg-card hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
ref?: React.Ref<HTMLButtonElement>
|
||||
}
|
||||
|
||||
const Button: React.FC<ButtonProps> = ({
|
||||
asChild = false,
|
||||
className,
|
||||
size,
|
||||
variant,
|
||||
ref,
|
||||
...props
|
||||
}) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return <Comp className={cn(buttonVariants({ className, size, variant }))} ref={ref} {...props} />
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
139
src/fields/link.ts
Normal file
139
src/fields/link.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import type { Field, GroupField } from 'payload'
|
||||
|
||||
import deepMerge from '@/utilities/deepMerge'
|
||||
|
||||
export type LinkAppearances = 'default' | 'outline'
|
||||
|
||||
export const appearanceOptions: Record<LinkAppearances, { label: string; value: string }> = {
|
||||
default: {
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
outline: {
|
||||
label: 'Outline',
|
||||
value: 'outline',
|
||||
},
|
||||
}
|
||||
|
||||
type LinkType = (options?: {
|
||||
appearances?: LinkAppearances[] | false
|
||||
disableLabel?: boolean
|
||||
overrides?: Partial<GroupField>
|
||||
}) => Field
|
||||
|
||||
export const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } = {}) => {
|
||||
const linkResult: GroupField = {
|
||||
name: 'link',
|
||||
type: 'group',
|
||||
admin: {
|
||||
hideGutter: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'type',
|
||||
type: 'radio',
|
||||
admin: {
|
||||
layout: 'horizontal',
|
||||
width: '50%',
|
||||
},
|
||||
defaultValue: 'reference',
|
||||
options: [
|
||||
{
|
||||
label: 'Internal link',
|
||||
value: 'reference',
|
||||
},
|
||||
{
|
||||
label: 'Custom URL',
|
||||
value: 'custom',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'newTab',
|
||||
type: 'checkbox',
|
||||
admin: {
|
||||
style: {
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
width: '50%',
|
||||
},
|
||||
label: 'Open in new tab',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const linkTypes: Field[] = [
|
||||
{
|
||||
name: 'reference',
|
||||
type: 'relationship',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === 'reference',
|
||||
},
|
||||
label: 'Document to link to',
|
||||
relationTo: ['pages',],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === 'custom',
|
||||
},
|
||||
label: 'Custom URL',
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
|
||||
if (!disableLabel) {
|
||||
linkTypes.map((linkType) => ({
|
||||
...linkType,
|
||||
admin: {
|
||||
...linkType.admin,
|
||||
width: '50%',
|
||||
},
|
||||
}))
|
||||
|
||||
linkResult.fields.push({
|
||||
type: 'row',
|
||||
fields: [
|
||||
...linkTypes,
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
admin: {
|
||||
width: '50%',
|
||||
},
|
||||
label: 'Label',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
} else {
|
||||
linkResult.fields = [...linkResult.fields, ...linkTypes]
|
||||
}
|
||||
|
||||
if (appearances !== false) {
|
||||
let appearanceOptionsToUse = [appearanceOptions.default, appearanceOptions.outline]
|
||||
|
||||
if (appearances) {
|
||||
appearanceOptionsToUse = appearances.map((appearance) => appearanceOptions[appearance])
|
||||
}
|
||||
|
||||
linkResult.fields.push({
|
||||
name: 'appearance',
|
||||
type: 'select',
|
||||
admin: {
|
||||
description: 'Choose how the link should be rendered.',
|
||||
},
|
||||
defaultValue: 'default',
|
||||
options: appearanceOptionsToUse,
|
||||
})
|
||||
}
|
||||
|
||||
return deepMerge(linkResult, overrides)
|
||||
}
|
||||
@@ -93,10 +93,12 @@ export interface Config {
|
||||
globals: {
|
||||
nav: Nav;
|
||||
settings: Setting;
|
||||
header: Header;
|
||||
};
|
||||
globalsSelect: {
|
||||
nav: NavSelect<false> | NavSelect<true>;
|
||||
settings: SettingsSelect<false> | SettingsSelect<true>;
|
||||
header: HeaderSelect<false> | HeaderSelect<true>;
|
||||
};
|
||||
locale: null;
|
||||
user: User & {
|
||||
@@ -473,6 +475,30 @@ export interface Setting {
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "header".
|
||||
*/
|
||||
export interface Header {
|
||||
id: number;
|
||||
navItems?:
|
||||
| {
|
||||
link: {
|
||||
type?: ('reference' | 'custom') | null;
|
||||
newTab?: boolean | null;
|
||||
reference?: {
|
||||
relationTo: 'pages';
|
||||
value: number | Page;
|
||||
} | null;
|
||||
url?: string | null;
|
||||
label: string;
|
||||
};
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "nav_select".
|
||||
@@ -498,6 +524,29 @@ export interface SettingsSelect<T extends boolean = true> {
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "header_select".
|
||||
*/
|
||||
export interface HeaderSelect<T extends boolean = true> {
|
||||
navItems?:
|
||||
| T
|
||||
| {
|
||||
link?:
|
||||
| T
|
||||
| {
|
||||
type?: T;
|
||||
newTab?: T;
|
||||
reference?: T;
|
||||
url?: T;
|
||||
label?: T;
|
||||
};
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Categories } from './collections/Categories'
|
||||
import { Nav } from './collections/Nav'
|
||||
import { plugins } from './plugins'
|
||||
import { Settings } from './collections/Settings'
|
||||
import { Header } from './Header/config'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@@ -36,6 +37,7 @@ export default buildConfig({
|
||||
globals: [
|
||||
Nav,
|
||||
Settings,
|
||||
Header
|
||||
],
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures, rootFeatures }) => [
|
||||
|
||||
35
utilities/deepMerge.ts
Normal file
35
utilities/deepMerge.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
|
||||
/**
|
||||
* Simple object check.
|
||||
* @param item
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isObject(item: unknown): item is object {
|
||||
return typeof item === 'object' && !Array.isArray(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge two objects.
|
||||
* @param target
|
||||
* @param ...sources
|
||||
*/
|
||||
export default function deepMerge<T, R>(target: T, source: R): T {
|
||||
const output = { ...target }
|
||||
if (isObject(target) && isObject(source)) {
|
||||
Object.keys(source).forEach((key) => {
|
||||
if (isObject(source[key])) {
|
||||
if (!(key in target)) {
|
||||
Object.assign(output, { [key]: source[key] })
|
||||
} else {
|
||||
output[key] = deepMerge(target[key], source[key])
|
||||
}
|
||||
} else {
|
||||
Object.assign(output, { [key]: source[key] })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
23
utilities/getGlobals.ts
Normal file
23
utilities/getGlobals.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Config } from 'src/payload-types';
|
||||
|
||||
import configPromise from '@payload-config';
|
||||
import { getPayload } from 'payload';
|
||||
import { unstable_cache } from 'next/cache';
|
||||
|
||||
type Global = keyof Config['globals']
|
||||
|
||||
async function getGlobal(slug: Global, depth = 0) {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const global = await payload.findGlobal({
|
||||
slug,
|
||||
depth,
|
||||
})
|
||||
|
||||
return global
|
||||
}
|
||||
|
||||
export const getCachedGlobal = (slug: Global, depth = 0) =>
|
||||
unstable_cache(async () => getGlobal(slug, depth), [slug], {
|
||||
tags: [`global_${slug}`],
|
||||
})
|
||||
Reference in New Issue
Block a user