summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorBertrand Yuan <bert.yuan@outlook.com>2025-12-16 00:25:04 +0800
committerGitHub <noreply@github.com>2025-12-16 00:25:04 +0800
commit39c83fbb69ef06d2d56790d75abc254ba7e34394 (patch)
treedd006593448c3500bdcb414af3b4656f7a7683d4 /src/components
parent48b07bc308a35734a6a7a305c8fdccbfa47de7d8 (diff)
parent785371bb3eccca455e5ce5fccbe9b6e3752a03f6 (diff)
Merge pull request #1 from bertyuan/feat-introduce-payloadv1.0
Feat: introduce payload
Diffstat (limited to 'src/components')
-rw-r--r--src/components/docs.tsx2
-rw-r--r--src/components/json-ld.tsx33
-rw-r--r--src/components/mdx-layout.tsx2
-rw-r--r--src/components/newsletter-form.tsx2
-rw-r--r--src/components/rich-text/index.tsx42
-rw-r--r--src/components/rich-text/node-format.ts11
-rw-r--r--src/components/rich-text/serialize.tsx201
-rw-r--r--src/components/sections/footer.tsx32
-rw-r--r--src/components/sections/header/menu.tsx2
-rw-r--r--src/components/sections/header/navbar.tsx2
-rw-r--r--src/components/tags/tag-card.tsx9
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>
);