diff --git a/src/app/(home)/gruppe/[slug]/page.tsx b/src/app/(home)/gruppe/[slug]/page.tsx index 4751e40..6341b60 100644 --- a/src/app/(home)/gruppe/[slug]/page.tsx +++ b/src/app/(home)/gruppe/[slug]/page.tsx @@ -14,7 +14,7 @@ import { getPhoto } from '@/utils/dto/gallery' import { isAuthenticated } from '@/utils/auth' import { AdminMenu } from '@/components/AdminMenu/AdminMenu' import { GroupEvents } from '@/compositions/GroupEvents/GroupEvents' -import { RichText } from '@payloadcms/richtext-lexical/react' +import { RichText } from '@/components/Text/RichText' import { RefreshRouteOnSave } from '@/components/RefreshRouteOnSave/RefreshRouteOnSave' import { draftMode } from 'next/headers' diff --git a/src/app/(home)/spenden/[id]/page.tsx b/src/app/(home)/spenden/[id]/page.tsx index 2dbd6e7..7c89189 100644 --- a/src/app/(home)/spenden/[id]/page.tsx +++ b/src/app/(home)/spenden/[id]/page.tsx @@ -1,7 +1,7 @@ import { PageHeader } from '@/compositions/PageHeader/PageHeader' import { fetchDonationForm } from '@/fetch/donationform' import { notFound } from 'next/navigation' -import { RichText } from '@payloadcms/richtext-lexical/react' +import { RichText } from '@/components/Text/RichText' import { Container } from '@/components/Container/Container' import { Section } from '@/components/Section/Section' import styles from '@/components/DonationForm/styles.module.scss' diff --git a/src/components/Classifieds/Ad.tsx b/src/components/Classifieds/Ad.tsx index c4d1a22..57db730 100644 --- a/src/components/Classifieds/Ad.tsx +++ b/src/components/Classifieds/Ad.tsx @@ -1,10 +1,9 @@ "use client" -import classNames from 'classnames' import styles from '@/components/Classifieds/styles.module.scss' import { useState } from 'react' import { SerializedEditorState } from 'lexical' -import { RichText } from '@payloadcms/richtext-lexical/react' +import { RichText } from '@/components/Text/RichText' type AdProps = { text: SerializedEditorState, diff --git a/src/components/Text/HTMLText.tsx b/src/components/Text/HTMLText.tsx index 1a5e33e..f50e4f8 100644 --- a/src/components/Text/HTMLText.tsx +++ b/src/components/Text/HTMLText.tsx @@ -1,7 +1,7 @@ import styles from "./html.module.scss" import classNames from 'classnames' -import { RichText } from '@payloadcms/richtext-lexical/react' import { SerializedEditorState, SerializedLexicalNode } from 'lexical' +import { RichText } from './RichText' type HTMLTextProps = { width: "1/2" | "3/4", diff --git a/src/components/Text/RichText.tsx b/src/components/Text/RichText.tsx new file mode 100644 index 0000000..cc63591 --- /dev/null +++ b/src/components/Text/RichText.tsx @@ -0,0 +1,16 @@ +import { RichText as PayloadRichText } from '@payloadcms/richtext-lexical/react' +import { SerializedEditorState } from 'lexical' +import { jsxConverters } from './converters' + + +type RichTextProps = { + data: SerializedEditorState +} + +// Thin wrapper around Payload's RichText that always wires up our custom JSX +// converters (e.g. rendering links marked as "Button" via the Button component). +// Use this everywhere instead of importing RichText directly from Payload, so +// the converters can never be forgotten. +export const RichText = ({ data }: RichTextProps) => ( + +) diff --git a/src/components/Text/converters.tsx b/src/components/Text/converters.tsx new file mode 100644 index 0000000..054caac --- /dev/null +++ b/src/components/Text/converters.tsx @@ -0,0 +1,50 @@ +import type { JSXConvertersFunction } from '@payloadcms/richtext-lexical/react' +import type { SerializedLinkNode } from '@payloadcms/richtext-lexical' +import { Button } from '@/components/Button/Button' + +// Lexical link nodes carry their editable metadata (url, newTab, linkType, ...) +// in `fields`. We extend that shape with the custom `appearance` select that is +// added to LinkFeature in `payload.config.ts`, so editors can mark a link as a +// call-to-action button. +type LinkFields = SerializedLinkNode['fields'] & { + appearance?: 'link' | 'button' +} + +// Custom JSX converters passed to wherever rich text is rendered. +// Only the `link` converter is overridden — every other node keeps Payload's +// default rendering via the spread of `defaultConverters`. +export const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({ + ...defaultConverters, + link: (args) => { + const { node, nodesToJSX } = args + const fields = node.fields as LinkFields + + // Normal link → delegate to Payload's built-in link converter. + // The default converter from the package is a function, but the type + // also allows a static ReactNode, so we narrow before calling. + if (fields?.appearance !== 'button') { + const defaultLink = defaultConverters.link + return typeof defaultLink === 'function' ? defaultLink(args) : defaultLink + } + + // Button link → resolve href the same way Payload's default converter does: + // internal links point at the related doc's slug, custom links use `url`. + const href = + fields.linkType === 'internal' && typeof fields.doc?.value === 'object' + ? `/${(fields.doc.value as { slug?: string }).slug ?? ''}` + : (fields.url ?? '#') + + // Render the existing Button component. Schema and size are intentionally + // hardcoded — editors only choose link vs. button, not the styling. + return ( + + ) + }, +}) diff --git a/src/payload.config.ts b/src/payload.config.ts index 2ca8897..bd265e5 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -138,7 +138,21 @@ export default buildConfig({ HeadingFeature({ enabledHeadingSizes: ["h3","h4","h5"]}), AlignFeature(), UnorderedListFeature(), - LinkFeature(), + LinkFeature({ + fields: ({ defaultFields }) => [ + ...defaultFields, + { + name: 'appearance', + type: 'select', + defaultValue: 'link', + label: 'Darstellung', + options: [ + { label: 'Link', value: 'link' }, + { label: 'Button', value: 'button' }, + ], + }, + ], + }), ParagraphFeature(), InlineToolbarFeature(), FixedToolbarFeature()