refactor: migrate to pnpm monorepo with Payload CMS backend and Astro frontend to support scalable website development and AI-assisted workflows

This commit is contained in:
2025-09-25 03:36:26 +08:00
parent 4efabd168c
commit 74677acf77
243 changed files with 28435 additions and 102 deletions

View File

@@ -0,0 +1,42 @@
'use client'
import { useHeaderTheme } from '@/providers/HeaderTheme'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import React, { useEffect, useState } from 'react'
import type { Header } from '@/payload-types'
import { Logo } from '@/components/Logo/Logo'
import { HeaderNav } from './Nav'
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 { headerTheme, setHeaderTheme } = useHeaderTheme()
const pathname = usePathname()
useEffect(() => {
setHeaderTheme(null)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname])
useEffect(() => {
if (headerTheme && headerTheme !== theme) setTheme(headerTheme)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [headerTheme])
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>
)
}

View File

@@ -0,0 +1,11 @@
import { HeaderClient } from './Component.client'
import { getCachedGlobal } from '@/utilities/getGlobals'
import React from 'react'
import type { Header } from '@/payload-types'
export async function Header() {
const headerData: Header = await getCachedGlobal('header', 1)()
return <HeaderClient data={headerData} />
}

View File

@@ -0,0 +1,25 @@
'use client'
import React from 'react'
import type { Header as HeaderType } from '@/payload-types'
import { CMSLink } from '@/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>
)
}

View File

@@ -0,0 +1,13 @@
'use client'
import { Header } from '@/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>
}

View File

@@ -0,0 +1,32 @@
import type { GlobalConfig } from 'payload'
import { link } from '@/fields/link'
import { revalidateHeader } from './hooks/revalidateHeader'
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: '@/Header/RowLabel#RowLabel',
},
},
},
],
hooks: {
afterChange: [revalidateHeader],
},
}

View 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
}