diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/docs.tsx | 2 | ||||
| -rw-r--r-- | src/components/json-ld.tsx | 33 | ||||
| -rw-r--r-- | src/components/mdx-layout.tsx | 2 | ||||
| -rw-r--r-- | src/components/newsletter-form.tsx | 2 | ||||
| -rw-r--r-- | src/components/rich-text/index.tsx | 42 | ||||
| -rw-r--r-- | src/components/rich-text/node-format.ts | 11 | ||||
| -rw-r--r-- | src/components/rich-text/serialize.tsx | 201 | ||||
| -rw-r--r-- | src/components/sections/footer.tsx | 32 | ||||
| -rw-r--r-- | src/components/sections/header/menu.tsx | 2 | ||||
| -rw-r--r-- | src/components/sections/header/navbar.tsx | 2 | ||||
| -rw-r--r-- | src/components/tags/tag-card.tsx | 9 |
11 files changed, 289 insertions, 49 deletions
diff --git a/src/components/docs.tsx b/src/components/docs.tsx index 038a52b..0dec2dd 100644 --- a/src/components/docs.tsx +++ b/src/components/docs.tsx @@ -1,5 +1,5 @@ import type { PageTree } from 'fumadocs-core/server'; -import { cn } from 'fumadocs-ui/components/api'; +import { cn } from '@/lib/utils'; import { type SidebarOptions, checkPageTree, diff --git a/src/components/json-ld.tsx b/src/components/json-ld.tsx index cd30274..58cb0ba 100644 --- a/src/components/json-ld.tsx +++ b/src/components/json-ld.tsx @@ -1,33 +1,30 @@ -import { title as homeTitle } from '@/app/layout.config'; -import { owner } from '@/app/layout.config'; +import { title as homeTitle } from '@/app/(main)/layout.config'; +import { owner } from '@/app/(main)/layout.config'; import { baseUrl } from '@/lib/constants'; -import type { Post } from '@/lib/source'; +import type { BlogPost } from '@/lib/payload-posts'; import type { BlogPosting, BreadcrumbList, Graph } from 'schema-dts'; -export const PostJsonLd = ({ page }: { page: Post }) => { - if (!page) { +export const PostJsonLd = ({ post }: { post: BlogPost }) => { + if (!post) { return null; } - const url = new URL(page.url, baseUrl.href).href; + const url = new URL(post.url, baseUrl.href).href; - const post: BlogPosting = { + const blogPosting: BlogPosting = { '@type': 'BlogPosting', - headline: page.data.title, - description: page.data.description, - image: new URL(`/og/${page.slugs.join('/')}/image.png`, baseUrl.href).href, - datePublished: new Date(page.data.date).toISOString(), - dateModified: page.data.lastModified - ? new Date(page.data.lastModified).toISOString() - : undefined, + headline: post.title, + description: post.description, + image: post.image ? new URL(post.image, baseUrl.href).href : undefined, + datePublished: post.date.toISOString(), + dateModified: post.updatedAt.toISOString(), mainEntityOfPage: { '@type': 'WebPage', '@id': url, }, author: { '@type': 'Person', - name: page.data.author, - // url: 'https://techwithanirudh.com/', + name: post.author, }, publisher: { '@type': 'Person', @@ -54,7 +51,7 @@ export const PostJsonLd = ({ page }: { page: Post }) => { { '@type': 'ListItem', position: 3, - name: page.data.title, + name: post.title, item: url, }, ], @@ -62,7 +59,7 @@ export const PostJsonLd = ({ page }: { page: Post }) => { const graph: Graph = { '@context': 'https://schema.org', - '@graph': [post, breadcrumbList], + '@graph': [blogPosting, breadcrumbList], }; return ( diff --git a/src/components/mdx-layout.tsx b/src/components/mdx-layout.tsx index bc68a93..f984dfc 100644 --- a/src/components/mdx-layout.tsx +++ b/src/components/mdx-layout.tsx @@ -1,4 +1,4 @@ -import { PostComments } from '@/app/(home)/posts/[slug]/page.client'; +import { PostComments } from '@/app/(main)/(home)/posts/[slug]/page.client'; import type { TOCItemType } from 'fumadocs-core/server'; import { InlineTOC } from 'fumadocs-ui/components/inline-toc'; import type { ReactNode } from 'react'; diff --git a/src/components/newsletter-form.tsx b/src/components/newsletter-form.tsx index 7dd0cc0..83a9bee 100644 --- a/src/components/newsletter-form.tsx +++ b/src/components/newsletter-form.tsx @@ -18,7 +18,7 @@ import { useForm } from 'react-hook-form'; import { Alert, AlertTitle } from '@/components/ui/alert'; -import { subscribeUser } from '@/app/(home)/actions'; +import { subscribeUser } from '@/app/(main)/(home)/actions'; import { Icons } from '@/components/icons/icons'; export const NewsletterForm = () => { diff --git a/src/components/rich-text/index.tsx b/src/components/rich-text/index.tsx new file mode 100644 index 0000000..556942c --- /dev/null +++ b/src/components/rich-text/index.tsx @@ -0,0 +1,42 @@ +import { cn } from '@/lib/utils' +import React from 'react' +import { serializeLexical } from './serialize' + +type Props = { + className?: string + content: Record<string, unknown> + enableProse?: boolean +} + +export function RichText({ + className, + content, + enableProse = true, +}: Props) { + if (!content) { + return null + } + + return ( + <div + className={cn( + { + 'prose min-w-0 dark:prose-invert': enableProse, + }, + className + )} + > + {content && + !Array.isArray(content) && + typeof content === 'object' && + 'root' in content && + serializeLexical({ + nodes: (content.root as { children: unknown[] })?.children as Parameters< + typeof serializeLexical + >[0]['nodes'], + })} + </div> + ) +} + +export default RichText diff --git a/src/components/rich-text/node-format.ts b/src/components/rich-text/node-format.ts new file mode 100644 index 0000000..84e7d7e --- /dev/null +++ b/src/components/rich-text/node-format.ts @@ -0,0 +1,11 @@ +// From Lexical: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts + +// Text node formatting +export const IS_BOLD = 1 +export const IS_ITALIC = 1 << 1 +export const IS_STRIKETHROUGH = 1 << 2 +export const IS_UNDERLINE = 1 << 3 +export const IS_CODE = 1 << 4 +export const IS_SUBSCRIPT = 1 << 5 +export const IS_SUPERSCRIPT = 1 << 6 +export const IS_HIGHLIGHT = 1 << 7 diff --git a/src/components/rich-text/serialize.tsx b/src/components/rich-text/serialize.tsx new file mode 100644 index 0000000..d75c40f --- /dev/null +++ b/src/components/rich-text/serialize.tsx @@ -0,0 +1,201 @@ +import React, { Fragment, type JSX } from 'react' +import Link from 'next/link' +import { + IS_BOLD, + IS_CODE, + IS_ITALIC, + IS_STRIKETHROUGH, + IS_SUBSCRIPT, + IS_SUPERSCRIPT, + IS_UNDERLINE, +} from './node-format' + +// Lexical 节点类型 +interface LexicalNode { + type: string + format?: number + text?: string + tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'ul' | 'ol' + listType?: 'bullet' | 'number' | 'check' + checked?: boolean + value?: number + children?: LexicalNode[] + fields?: { + linkType?: 'internal' | 'custom' + url?: string + newTab?: boolean + doc?: { + value?: { + slug?: string + } + relationTo?: string + } + } + language?: string + version?: number +} + +type Props = { + nodes: LexicalNode[] +} + +export function serializeLexical({ nodes }: Props): JSX.Element { + return ( + <Fragment> + {nodes?.map((node, index): JSX.Element | null => { + if (node == null) { + return null + } + + if (node.type === 'text') { + let text = <React.Fragment key={index}>{node.text}</React.Fragment> + const format = node.format || 0 + + if (format & IS_BOLD) { + text = <strong key={index}>{text}</strong> + } + if (format & IS_ITALIC) { + text = <em key={index}>{text}</em> + } + if (format & IS_STRIKETHROUGH) { + text = ( + <span key={index} style={{ textDecoration: 'line-through' }}> + {text} + </span> + ) + } + if (format & IS_UNDERLINE) { + text = ( + <span key={index} style={{ textDecoration: 'underline' }}> + {text} + </span> + ) + } + if (format & IS_CODE) { + text = <code key={index}>{node.text}</code> + } + if (format & IS_SUBSCRIPT) { + text = <sub key={index}>{text}</sub> + } + if (format & IS_SUPERSCRIPT) { + text = <sup key={index}>{text}</sup> + } + + return text + } + + // 处理子节点 + const serializedChildrenFn = (node: LexicalNode): JSX.Element | null => { + if (node.children == null) { + return null + } else { + // 处理 checkbox list + if (node?.type === 'list' && node?.listType === 'check') { + for (const item of node.children) { + if ('checked' in item) { + if (!item?.checked) { + item.checked = false + } + } + } + } + return serializeLexical({ nodes: node.children }) + } + } + + const serializedChildren = 'children' in node ? serializedChildrenFn(node) : '' + + switch (node.type) { + case 'linebreak': { + return <br key={index} /> + } + case 'paragraph': { + return <p key={index}>{serializedChildren}</p> + } + case 'heading': { + const Tag = node?.tag || 'h2' + return <Tag key={index}>{serializedChildren}</Tag> + } + case 'list': { + const Tag = node?.tag || 'ul' + return <Tag key={index}>{serializedChildren}</Tag> + } + case 'listitem': { + if (node?.checked != null) { + return ( + <li + aria-checked={node.checked ? 'true' : 'false'} + key={index} + role="checkbox" + tabIndex={-1} + value={node?.value} + > + <input + type="checkbox" + checked={node.checked} + readOnly + className="mr-2" + /> + {serializedChildren} + </li> + ) + } else { + return ( + <li key={index} value={node?.value}> + {serializedChildren} + </li> + ) + } + } + case 'quote': { + return <blockquote key={index}>{serializedChildren}</blockquote> + } + case 'link': { + const fields = node.fields + + if (fields?.linkType === 'internal' && fields?.doc?.value?.slug) { + const href = + fields.doc.relationTo === 'posts' + ? `/posts/${fields.doc.value.slug}` + : `/${fields.doc.value.slug}` + + return ( + <Link key={index} href={href}> + {serializedChildren} + </Link> + ) + } + + return ( + <a + key={index} + href={fields?.url || '#'} + target={fields?.newTab ? '_blank' : undefined} + rel={fields?.newTab ? 'noopener noreferrer' : undefined} + > + {serializedChildren} + </a> + ) + } + case 'code': { + // 代码块 + return ( + <pre key={index} className="overflow-x-auto"> + <code>{serializedChildren}</code> + </pre> + ) + } + case 'horizontalrule': { + return <hr key={index} /> + } + default: + // 如果有子节点,递归渲染 + if (node.children) { + return <Fragment key={index}>{serializedChildren}</Fragment> + } + return null + } + })} + </Fragment> + ) +} diff --git a/src/components/sections/footer.tsx b/src/components/sections/footer.tsx index 8d0d876..a982810 100644 --- a/src/components/sections/footer.tsx +++ b/src/components/sections/footer.tsx @@ -1,18 +1,19 @@ -import { baseOptions, linkItems, postsPerPage } from '@/app/layout.config'; +import { baseOptions, linkItems, postsPerPage } from '@/app/(main)/layout.config'; import { InlineLink } from '@/components/inline-link'; -import { getSortedByDatePosts, getTags } from '@/lib/source'; +import { getPublishedPosts, getAllTags } from '@/lib/payload-posts'; import { cn } from '@/lib/utils'; import { getLinks } from 'fumadocs-ui/layouts/shared'; import { ActiveLink } from '../active-link'; -export function Footer() { +export async function Footer() { const links = getLinks(linkItems, baseOptions.githubUrl); const navItems = links.filter((item) => ['nav', 'all'].includes(item.on ?? 'all'), ); - const posts = getSortedByDatePosts(); - const tags = getTags(); + // 从 Payload 获取文章和标签 + const { posts } = await getPublishedPosts({ limit: postsPerPage }); + const tags = await getAllTags(); return ( <footer className={cn('flex flex-col gap-4')}> @@ -54,7 +55,7 @@ export function Footer() { {posts.slice(0, postsPerPage).map((post, i) => ( <li key={post.url}> <ActiveLink key={i.toString()} href={post.url}> - {post.data.title} + {post.title} </ActiveLink> </li> ))} @@ -65,10 +66,10 @@ export function Footer() { <p className='font-medium text-foreground'>Tags</p> <ul className='flex flex-col gap-3'> - {tags.slice(0, postsPerPage).map((name, i) => ( - <li key={`/tags/${name}`}> - <ActiveLink key={i.toString()} href={`/tags/${name}`}> - <span className='capitalize'>{name}</span> + {tags.slice(0, postsPerPage).map((item, i) => ( + <li key={`/tags/${item.tag}`}> + <ActiveLink key={i.toString()} href={`/tags/${item.tag}`}> + <span className='capitalize'>{item.tag}</span> </ActiveLink> </li> ))} @@ -96,17 +97,6 @@ export function Footer() { </ul> </div> </div> - {/* <Design /> */} </footer> ); } - -function Design() { - return ( - <div className='footer'> - <span className='footer-text font-mono'>john•doe</span> - <div className='footer-grid' /> - <div className='footer-gradient' /> - </div> - ); -} diff --git a/src/components/sections/header/menu.tsx b/src/components/sections/header/menu.tsx index 57ddf12..7eb6644 100644 --- a/src/components/sections/header/menu.tsx +++ b/src/components/sections/header/menu.tsx @@ -2,7 +2,7 @@ import { cva } from 'class-variance-authority'; import Link from 'fumadocs-core/link'; -import { cn } from 'fumadocs-ui/components/api'; +import { cn } from '@/lib/utils'; import { buttonVariants } from 'fumadocs-ui/components/ui/button'; import { NavigationMenuContent, diff --git a/src/components/sections/header/navbar.tsx b/src/components/sections/header/navbar.tsx index 25850a0..bac7d3d 100644 --- a/src/components/sections/header/navbar.tsx +++ b/src/components/sections/header/navbar.tsx @@ -1,7 +1,7 @@ 'use client'; import Link, { type LinkProps } from 'fumadocs-core/link'; -import { cn } from 'fumadocs-ui/components/api'; +import { cn } from '@/lib/utils'; import { NavigationMenu, NavigationMenuLink, diff --git a/src/components/tags/tag-card.tsx b/src/components/tags/tag-card.tsx index a71e4c4..447c73e 100644 --- a/src/components/tags/tag-card.tsx +++ b/src/components/tags/tag-card.tsx @@ -1,19 +1,18 @@ import { Icons } from '@/components/icons/icons'; -import { getPostsByTag } from '@/lib/source'; import { cn } from '@/lib/utils'; import Link from 'next/link'; export const TagCard = ({ name, displayCount = false, + count, className = '', }: { name: string; displayCount?: boolean; + count?: number; className?: string; }) => { - const posts = getPostsByTag(name); - return ( <Link href={`/tags/${name}`} @@ -27,8 +26,8 @@ export const TagCard = ({ className='my-auto text-muted-foreground transition-transform group-hover:rotate-12' /> <span className='text-card-foreground'>{name}</span> - {displayCount && ( - <span className='ml-auto text-muted-foreground'>({posts.length})</span> + {displayCount && count !== undefined && ( + <span className='ml-auto text-muted-foreground'>({count})</span> )} </Link> ); |
