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:
46
apps/backend/src/heros/HighImpact/index.tsx
Normal file
46
apps/backend/src/heros/HighImpact/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
import { useHeaderTheme } from '@/providers/HeaderTheme'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
import type { Page } from '@/payload-types'
|
||||
|
||||
import { CMSLink } from '@/components/Link'
|
||||
import { Media } from '@/components/Media'
|
||||
import RichText from '@/components/RichText'
|
||||
|
||||
export const HighImpactHero: React.FC<Page['hero']> = ({ links, media, richText }) => {
|
||||
const { setHeaderTheme } = useHeaderTheme()
|
||||
|
||||
useEffect(() => {
|
||||
setHeaderTheme('dark')
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative -mt-[10.4rem] flex items-center justify-center text-white"
|
||||
data-theme="dark"
|
||||
>
|
||||
<div className="container mb-8 z-10 relative flex items-center justify-center">
|
||||
<div className="max-w-[36.5rem] md:text-center">
|
||||
{richText && <RichText className="mb-6" data={richText} enableGutter={false} />}
|
||||
{Array.isArray(links) && links.length > 0 && (
|
||||
<ul className="flex md:justify-center gap-4">
|
||||
{links.map(({ link }, i) => {
|
||||
return (
|
||||
<li key={i}>
|
||||
<CMSLink {...link} />
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-[80vh] select-none">
|
||||
{media && typeof media === 'object' && (
|
||||
<Media fill imgClassName="-z-10 object-cover" priority resource={media} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
apps/backend/src/heros/LowImpact/index.tsx
Normal file
25
apps/backend/src/heros/LowImpact/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { Page } from '@/payload-types'
|
||||
|
||||
import RichText from '@/components/RichText'
|
||||
|
||||
type LowImpactHeroType =
|
||||
| {
|
||||
children?: React.ReactNode
|
||||
richText?: never
|
||||
}
|
||||
| (Omit<Page['hero'], 'richText'> & {
|
||||
children?: never
|
||||
richText?: Page['hero']['richText']
|
||||
})
|
||||
|
||||
export const LowImpactHero: React.FC<LowImpactHeroType> = ({ children, richText }) => {
|
||||
return (
|
||||
<div className="container mt-16">
|
||||
<div className="max-w-[48rem]">
|
||||
{children || (richText && <RichText data={richText} enableGutter={false} />)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
46
apps/backend/src/heros/MediumImpact/index.tsx
Normal file
46
apps/backend/src/heros/MediumImpact/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { Page } from '@/payload-types'
|
||||
|
||||
import { CMSLink } from '@/components/Link'
|
||||
import { Media } from '@/components/Media'
|
||||
import RichText from '@/components/RichText'
|
||||
|
||||
export const MediumImpactHero: React.FC<Page['hero']> = ({ links, media, richText }) => {
|
||||
return (
|
||||
<div className="">
|
||||
<div className="container mb-8">
|
||||
{richText && <RichText className="mb-6" data={richText} enableGutter={false} />}
|
||||
|
||||
{Array.isArray(links) && links.length > 0 && (
|
||||
<ul className="flex gap-4">
|
||||
{links.map(({ link }, i) => {
|
||||
return (
|
||||
<li key={i}>
|
||||
<CMSLink {...link} />
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="container ">
|
||||
{media && typeof media === 'object' && (
|
||||
<div>
|
||||
<Media
|
||||
className="-mx-4 md:-mx-8 2xl:-mx-16"
|
||||
imgClassName=""
|
||||
priority
|
||||
resource={media}
|
||||
/>
|
||||
{media?.caption && (
|
||||
<div className="mt-3">
|
||||
<RichText data={media.caption} enableGutter={false} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
73
apps/backend/src/heros/PostHero/index.tsx
Normal file
73
apps/backend/src/heros/PostHero/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { formatDateTime } from 'src/utilities/formatDateTime'
|
||||
import React from 'react'
|
||||
|
||||
import type { Post } from '@/payload-types'
|
||||
|
||||
import { Media } from '@/components/Media'
|
||||
import { formatAuthors } from '@/utilities/formatAuthors'
|
||||
|
||||
export const PostHero: React.FC<{
|
||||
post: Post
|
||||
}> = ({ post }) => {
|
||||
const { categories, heroImage, populatedAuthors, publishedAt, title } = post
|
||||
|
||||
const hasAuthors =
|
||||
populatedAuthors && populatedAuthors.length > 0 && formatAuthors(populatedAuthors) !== ''
|
||||
|
||||
return (
|
||||
<div className="relative -mt-[10.4rem] flex items-end">
|
||||
<div className="container z-10 relative lg:grid lg:grid-cols-[1fr_48rem_1fr] text-white pb-8">
|
||||
<div className="col-start-1 col-span-1 md:col-start-2 md:col-span-2">
|
||||
<div className="uppercase text-sm mb-6">
|
||||
{categories?.map((category, index) => {
|
||||
if (typeof category === 'object' && category !== null) {
|
||||
const { title: categoryTitle } = category
|
||||
|
||||
const titleToUse = categoryTitle || 'Untitled category'
|
||||
|
||||
const isLast = index === categories.length - 1
|
||||
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
{titleToUse}
|
||||
{!isLast && <React.Fragment>, </React.Fragment>}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<h1 className="mb-6 text-3xl md:text-5xl lg:text-6xl">{title}</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4 md:gap-16">
|
||||
{hasAuthors && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm">Author</p>
|
||||
|
||||
<p>{formatAuthors(populatedAuthors)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{publishedAt && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm">Date Published</p>
|
||||
|
||||
<time dateTime={publishedAt}>{formatDateTime(publishedAt)}</time>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-[80vh] select-none">
|
||||
{heroImage && typeof heroImage !== 'string' && (
|
||||
<Media fill priority imgClassName="-z-10 object-cover" resource={heroImage} />
|
||||
)}
|
||||
<div className="absolute pointer-events-none left-0 bottom-0 w-full h-1/2 bg-gradient-to-t from-black to-transparent" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
apps/backend/src/heros/RenderHero.tsx
Normal file
25
apps/backend/src/heros/RenderHero.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { Page } from '@/payload-types'
|
||||
|
||||
import { HighImpactHero } from '@/heros/HighImpact'
|
||||
import { LowImpactHero } from '@/heros/LowImpact'
|
||||
import { MediumImpactHero } from '@/heros/MediumImpact'
|
||||
|
||||
const heroes = {
|
||||
highImpact: HighImpactHero,
|
||||
lowImpact: LowImpactHero,
|
||||
mediumImpact: MediumImpactHero,
|
||||
}
|
||||
|
||||
export const RenderHero: React.FC<Page['hero']> = (props) => {
|
||||
const { type } = props || {}
|
||||
|
||||
if (!type || type === 'none') return null
|
||||
|
||||
const HeroToRender = heroes[type]
|
||||
|
||||
if (!HeroToRender) return null
|
||||
|
||||
return <HeroToRender {...props} />
|
||||
}
|
||||
72
apps/backend/src/heros/config.ts
Normal file
72
apps/backend/src/heros/config.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { linkGroup } from '@/fields/linkGroup'
|
||||
|
||||
export const hero: Field = {
|
||||
name: 'hero',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'type',
|
||||
type: 'select',
|
||||
defaultValue: 'lowImpact',
|
||||
label: 'Type',
|
||||
options: [
|
||||
{
|
||||
label: 'None',
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
label: 'High Impact',
|
||||
value: 'highImpact',
|
||||
},
|
||||
{
|
||||
label: 'Medium Impact',
|
||||
value: 'mediumImpact',
|
||||
},
|
||||
{
|
||||
label: 'Low Impact',
|
||||
value: 'lowImpact',
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: false,
|
||||
},
|
||||
linkGroup({
|
||||
overrides: {
|
||||
maxRows: 2,
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'media',
|
||||
type: 'upload',
|
||||
admin: {
|
||||
condition: (_, { type } = {}) => ['highImpact', 'mediumImpact'].includes(type),
|
||||
},
|
||||
relationTo: 'media',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
label: false,
|
||||
}
|
||||
Reference in New Issue
Block a user