diff --git a/apps/cms/package.json b/apps/cms/package.json index cc7a53cf52a..5a685478a03 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -51,8 +51,12 @@ "payload": "3.52.0", "pg": "^8.16.3", "prism-react-renderer": "^2.3.1", + "prismjs": "^1.30.0", "react": "catalog:", "react-dom": "catalog:", + "react-markdown": "^10.1.0", + "rehype-highlight": "^7.0.2", + "remark-gfm": "^4.0.1", "sharp": "~0.34.0", "tailwind-merge": "^1.13.2", "tsx": "^4.19.3", diff --git a/apps/cms/src/app/(payload)/admin/importMap.js b/apps/cms/src/app/(payload)/admin/importMap.js index ab697bf4c47..401fe81380f 100644 --- a/apps/cms/src/app/(payload)/admin/importMap.js +++ b/apps/cms/src/app/(payload)/admin/importMap.js @@ -17,6 +17,7 @@ import { MetaTitleComponent as MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c08 import { MetaImageComponent as MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client' import { MetaDescriptionComponent as MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client' import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client' +import { MarkdownEditor as MarkdownEditor_ead0010f14dbca6b92002dd21c00f8a3 } from '@/fields/MarkdownEditor/Component' import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client' export const importMap = { @@ -39,5 +40,6 @@ export const importMap = { "@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860, "@payloadcms/plugin-seo/client#MetaDescriptionComponent": MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860, "@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860, + "@/fields/MarkdownEditor/Component#MarkdownEditor": MarkdownEditor_ead0010f14dbca6b92002dd21c00f8a3, "@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } diff --git a/apps/cms/src/app/(payload)/custom.scss b/apps/cms/src/app/(payload)/custom.scss index e69de29bb2d..ce4800a54c0 100644 --- a/apps/cms/src/app/(payload)/custom.scss +++ b/apps/cms/src/app/(payload)/custom.scss @@ -0,0 +1,46 @@ +/* Override Payload CMS success colors with Supabase brand green */ +:root { + /* Light theme - Supabase brand green scale */ + --color-success-50: hsla(153, 80%, 99%, 1); + --color-success-100: hsla(148, 88%, 97%, 1); + --color-success-150: hsla(148, 76%, 95%, 1); + --color-success-200: hsla(148, 68%, 92%, 1); + --color-success-250: hsla(148, 59%, 88%, 1); + --color-success-300: hsla(149, 43%, 82%, 1); + --color-success-350: hsla(149, 43%, 75%, 1); + --color-success-400: hsla(149, 43%, 69%, 1); + --color-success-450: hsla(154, 55%, 57%, 1); + --color-success-500: hsla(154, 55%, 45%, 1); + --color-success-550: hsla(153, 60%, 53%, 1); + --color-success-600: hsla(153, 50%, 50%, 1); + --color-success-650: hsla(153, 50%, 42%, 1); + --color-success-700: hsla(153, 50%, 34%, 1); + --color-success-750: hsla(153, 45%, 23%, 1); + --color-success-800: hsla(153, 40%, 18%, 1); + --color-success-850: hsla(153, 40%, 15%, 1); + --color-success-900: hsla(153, 40%, 12%, 1); + --color-success-950: hsla(153, 40%, 8%, 1); +} + +/* Dark theme - Supabase brand green dark scale */ +[data-theme='dark'] { + --color-success-50: hsla(153, 75%, 6%, 1); + --color-success-100: hsla(153, 73%, 7%, 1); + --color-success-150: hsla(154, 69%, 8%, 1); + --color-success-200: hsla(154, 69%, 9%, 1); + --color-success-250: hsla(154, 67%, 10%, 1); + --color-success-300: hsla(154, 67%, 11%, 1); + --color-success-350: hsla(154, 66%, 12%, 1); + --color-success-400: hsla(154, 66%, 13%, 1); + --color-success-450: hsla(154, 64%, 15%, 1); + --color-success-500: hsla(154, 64%, 17%, 1); + --color-success-550: hsla(154, 62%, 19%, 1); + --color-success-600: hsla(154, 62%, 22%, 1); + --color-success-650: hsla(153, 60%, 25%, 1); + --color-success-700: hsla(153, 60%, 28%, 1); + --color-success-750: hsla(153, 60%, 40%, 1); + --color-success-800: hsla(153, 60%, 53%, 1); + --color-success-850: hsla(153, 60%, 62%, 1); + --color-success-900: hsla(153, 60%, 70%, 1); + --color-success-950: hsla(153, 60%, 95%, 1); +} diff --git a/apps/cms/src/collections/Categories.ts b/apps/cms/src/collections/Categories.ts index d5b20b6d0e3..b38312206e8 100644 --- a/apps/cms/src/collections/Categories.ts +++ b/apps/cms/src/collections/Categories.ts @@ -1,5 +1,5 @@ import type { CollectionConfig } from 'payload' -import { isAdmin } from '../access/isAdmin' +import { isAdmin } from '../access/isAdmin.ts' export const Categories: CollectionConfig = { slug: 'categories', diff --git a/apps/cms/src/collections/Posts/index.ts b/apps/cms/src/collections/Posts/index.ts index 72e8830bb39..7c5c365e148 100644 --- a/apps/cms/src/collections/Posts/index.ts +++ b/apps/cms/src/collections/Posts/index.ts @@ -1,22 +1,8 @@ import type { CollectionConfig } from 'payload' -import { - BlocksFeature, - FixedToolbarFeature, - HeadingFeature, - HorizontalRuleFeature, - InlineToolbarFeature, - lexicalEditor, -} from '@payloadcms/richtext-lexical' - import { isAnyone } from '../../access/isAnyone.ts' import { isAuthenticated } from '../../access/isAuthenticated.ts' -import { Banner } from '../../blocks/Banner/config.ts' -import { Code } from '../../blocks/Code/config.ts' -import { MediaBlock } from '../../blocks/MediaBlock/config.ts' -import { Quote } from '../../blocks/Quote/config.ts' -import { YouTube } from '../../blocks/YouTube/config.ts' import { populateAuthors } from './hooks/populateAuthors.ts' import { revalidateDelete, revalidatePost } from './hooks/revalidatePost.ts' @@ -91,21 +77,16 @@ export const Posts: CollectionConfig = { fields: [ { name: 'content', - type: 'richText', - editor: lexicalEditor({ - features: ({ rootFeatures }) => { - return [ - ...rootFeatures, - HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }), - BlocksFeature({ blocks: [Banner, Code, MediaBlock, Quote, YouTube] }), - FixedToolbarFeature(), - InlineToolbarFeature(), - HorizontalRuleFeature(), - ] - }, - }), - label: false, + type: 'textarea', + label: 'Content (Markdown)', required: true, + admin: { + description: + 'Write your content in Markdown format. Supports headings, lists, links, images, code blocks, and more.', + components: { + Field: '@/fields/MarkdownEditor/Component#MarkdownEditor', + }, + }, }, ], label: 'Content', diff --git a/apps/cms/src/components/RichText/index.tsx b/apps/cms/src/components/RichText/index.tsx index 727236737d4..dfd36e54215 100644 --- a/apps/cms/src/components/RichText/index.tsx +++ b/apps/cms/src/components/RichText/index.tsx @@ -1,76 +1,19 @@ -import { MediaBlock } from '@/blocks/MediaBlock/Component' -import { - DefaultNodeTypes, - SerializedBlockNode, - SerializedLinkNode, - type DefaultTypedEditorState, -} from '@payloadcms/richtext-lexical' -import { - JSXConvertersFunction, - LinkJSXConverter, - RichText as ConvertRichText, -} from '@payloadcms/richtext-lexical/react' - -import { CodeBlock, CodeBlockProps } from '@/blocks/Code/Component' - -import type { - BannerBlock as BannerBlockProps, - MediaBlock as MediaBlockProps, - QuoteBlock as QuoteBlockProps, - YouTubeBlock as YouTubeBlockProps, -} from '@/payload-types' -import { BannerBlock } from '@/blocks/Banner/Component' -import { QuoteBlock } from '@/blocks/Quote/Component' -import { YouTubeBlock } from '@/blocks/YouTube/Component' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import rehypeHighlight from 'rehype-highlight' import { cn } from '@/utilities/ui' -type NodeTypes = - | DefaultNodeTypes - | SerializedBlockNode< - MediaBlockProps | BannerBlockProps | CodeBlockProps | QuoteBlockProps | YouTubeBlockProps - > - -const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => { - const { value, relationTo } = linkNode.fields.doc! - if (typeof value !== 'object') { - throw new Error('Expected value to be an object') - } - const slug = value.slug - return relationTo === 'posts' ? `/posts/${slug}` : `/${slug}` -} - -const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({ - ...defaultConverters, - ...LinkJSXConverter({ internalDocToHref }), - blocks: { - banner: ({ node }) => , - mediaBlock: ({ node }) => ( - - ), - code: ({ node }) => , - quote: ({ node }) => , - youtube: ({ node }) => , - }, -}) - type Props = { - data: DefaultTypedEditorState + data: string enableGutter?: boolean enableProse?: boolean } & React.HTMLAttributes export default function RichText(props: Props) { - const { className, enableProse = true, enableGutter = true, ...rest } = props + const { data, className, enableProse = true, enableGutter = true, ...rest } = props + return ( - + > + , + }} + > + {data} + + ) } diff --git a/apps/cms/src/fields/MarkdownEditor/Component.tsx b/apps/cms/src/fields/MarkdownEditor/Component.tsx new file mode 100644 index 00000000000..94e23f7ac27 --- /dev/null +++ b/apps/cms/src/fields/MarkdownEditor/Component.tsx @@ -0,0 +1,71 @@ +'use client' + +import React, { useCallback, useEffect, useRef } from 'react' +import { useField } from '@payloadcms/ui' +import Prism from 'prismjs' +import 'prismjs/components/prism-markdown' +import './styles.css' + +type MarkdownEditorProps = { + path: string + label?: string + required?: boolean +} + +export const MarkdownEditor: React.FC = ({ path, label, required }) => { + const { value, setValue } = useField({ path }) + const highlightRef = useRef(null) + const textareaRef = useRef(null) + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + setValue(e.target.value) + }, + [setValue] + ) + + // Auto-resize textarea + useEffect(() => { + const textarea = textareaRef.current + if (textarea) { + textarea.style.height = 'auto' + textarea.style.height = `${textarea.scrollHeight}px` + } + }, [value]) + + // Highlight using Prism + useEffect(() => { + if (highlightRef.current) { + Prism.highlightElement(highlightRef.current) + } + }, [value]) + + return ( +
+
+ {label && ( + + )} +
+
+ +