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/plugin-seo": "^3.59.1",
|
||||||
"@payloadcms/richtext-lexical": "3.59.1",
|
"@payloadcms/richtext-lexical": "3.59.1",
|
||||||
"@payloadcms/ui": "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",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "16.4.7",
|
"dotenv": "16.4.7",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
|
"lucide-react": "^0.545.0",
|
||||||
"next": "15.4.4",
|
"next": "15.4.4",
|
||||||
"payload": "3.59.1",
|
"payload": "3.59.1",
|
||||||
"react": "19.1.0",
|
"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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"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=="],
|
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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/plugin-seo": "^3.59.1",
|
||||||
"@payloadcms/richtext-lexical": "3.59.1",
|
"@payloadcms/richtext-lexical": "3.59.1",
|
||||||
"@payloadcms/ui": "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",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "16.4.7",
|
"dotenv": "16.4.7",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
|
"lucide-react": "^0.545.0",
|
||||||
"next": "15.4.4",
|
"next": "15.4.4",
|
||||||
"payload": "3.59.1",
|
"payload": "3.59.1",
|
||||||
"react": "19.1.0",
|
"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: {
|
globals: {
|
||||||
nav: Nav;
|
nav: Nav;
|
||||||
settings: Setting;
|
settings: Setting;
|
||||||
|
header: Header;
|
||||||
};
|
};
|
||||||
globalsSelect: {
|
globalsSelect: {
|
||||||
nav: NavSelect<false> | NavSelect<true>;
|
nav: NavSelect<false> | NavSelect<true>;
|
||||||
settings: SettingsSelect<false> | SettingsSelect<true>;
|
settings: SettingsSelect<false> | SettingsSelect<true>;
|
||||||
|
header: HeaderSelect<false> | HeaderSelect<true>;
|
||||||
};
|
};
|
||||||
locale: null;
|
locale: null;
|
||||||
user: User & {
|
user: User & {
|
||||||
@@ -473,6 +475,30 @@ export interface Setting {
|
|||||||
updatedAt?: string | null;
|
updatedAt?: string | null;
|
||||||
createdAt?: 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
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "nav_select".
|
* via the `definition` "nav_select".
|
||||||
@@ -498,6 +524,29 @@ export interface SettingsSelect<T extends boolean = true> {
|
|||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
globalType?: 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
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "auth".
|
* via the `definition` "auth".
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Categories } from './collections/Categories'
|
|||||||
import { Nav } from './collections/Nav'
|
import { Nav } from './collections/Nav'
|
||||||
import { plugins } from './plugins'
|
import { plugins } from './plugins'
|
||||||
import { Settings } from './collections/Settings'
|
import { Settings } from './collections/Settings'
|
||||||
|
import { Header } from './Header/config'
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
@@ -36,6 +37,7 @@ export default buildConfig({
|
|||||||
globals: [
|
globals: [
|
||||||
Nav,
|
Nav,
|
||||||
Settings,
|
Settings,
|
||||||
|
Header
|
||||||
],
|
],
|
||||||
editor: lexicalEditor({
|
editor: lexicalEditor({
|
||||||
features: ({ defaultFeatures, rootFeatures }) => [
|
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