From 42006b0f50623d9659712831c383a281445b2899 Mon Sep 17 00:00:00 2001
From: zhuba-Ahhh <3477826311@qq.com>
Date: Sat, 24 Aug 2024 11:51:14 +0800
Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20:recycle:=20=E6=8B=86=E5=88=86?=
=?UTF-8?q?=E7=BB=84=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/blog/[id]/BlogContent.tsx | 32 +++
src/app/blog/[id]/BlogFooter.tsx | 29 +++
src/app/blog/[id]/BlogHeader.tsx | 29 +++
src/app/blog/[id]/MdxComponents.tsx | 79 +++++++
src/app/blog/[id]/RelatedPosts.tsx | 31 +++
src/app/blog/[id]/ShareButtons.tsx | 37 ++++
src/app/blog/[id]/TableOfContents.tsx | 37 ++++
src/app/blog/[id]/page.tsx | 286 +++-----------------------
src/app/blog/[id]/types.ts | 21 ++
src/utils/blogHelpers.ts | 31 +++
10 files changed, 354 insertions(+), 258 deletions(-)
create mode 100644 src/app/blog/[id]/BlogContent.tsx
create mode 100644 src/app/blog/[id]/BlogFooter.tsx
create mode 100644 src/app/blog/[id]/BlogHeader.tsx
create mode 100644 src/app/blog/[id]/MdxComponents.tsx
create mode 100644 src/app/blog/[id]/RelatedPosts.tsx
create mode 100644 src/app/blog/[id]/ShareButtons.tsx
create mode 100644 src/app/blog/[id]/TableOfContents.tsx
create mode 100644 src/app/blog/[id]/types.ts
create mode 100644 src/utils/blogHelpers.ts
diff --git a/src/app/blog/[id]/BlogContent.tsx b/src/app/blog/[id]/BlogContent.tsx
new file mode 100644
index 0000000..32caad4
--- /dev/null
+++ b/src/app/blog/[id]/BlogContent.tsx
@@ -0,0 +1,32 @@
+import { MDXRemote } from "next-mdx-remote/rsc";
+import { MDXComponents } from "./types";
+
+import remarkGfm from "remark-gfm";
+import rehypeInlineCode from "@/lib/rehypeInlineCode";
+import rehypeSlug from "rehype-slug";
+import rehypeAutolinkHeadings from "rehype-autolink-headings";
+
+interface BlogContentProps {
+ content: string;
+ components: MDXComponents;
+}
+
+export default function BlogContent({ content, components }: BlogContentProps) {
+ return (
+
+ );
+}
diff --git a/src/app/blog/[id]/BlogFooter.tsx b/src/app/blog/[id]/BlogFooter.tsx
new file mode 100644
index 0000000..c8907fb
--- /dev/null
+++ b/src/app/blog/[id]/BlogFooter.tsx
@@ -0,0 +1,29 @@
+import Link from "next/link";
+import { Badge, Button } from "@/components/ui";
+import { BlogPost } from "./types";
+
+interface BlogFooterProps {
+ post: BlogPost;
+}
+
+export default function BlogFooter({ post }: BlogFooterProps) {
+ return (
+ <>
+
+ {post.tags.map((tag) => (
+
+
+ {tag}
+
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/src/app/blog/[id]/BlogHeader.tsx b/src/app/blog/[id]/BlogHeader.tsx
new file mode 100644
index 0000000..6714a53
--- /dev/null
+++ b/src/app/blog/[id]/BlogHeader.tsx
@@ -0,0 +1,29 @@
+import Image from "next/image";
+import { BlogPost } from "./types";
+
+interface BlogHeaderProps {
+ post: BlogPost;
+ readingTime: number;
+}
+
+export default function BlogHeader({ post, readingTime }: BlogHeaderProps) {
+ return (
+ <>
+ {post.title}
+
+ {post.date} | {post.author} |
+ 预计阅读时间: {readingTime} 分钟
+
+ {post.coverImage && (
+
+ )}
+ >
+ );
+}
diff --git a/src/app/blog/[id]/MdxComponents.tsx b/src/app/blog/[id]/MdxComponents.tsx
new file mode 100644
index 0000000..4b9978b
--- /dev/null
+++ b/src/app/blog/[id]/MdxComponents.tsx
@@ -0,0 +1,79 @@
+import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
+import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
+import dynamic from "next/dynamic";
+import { MDXComponents } from "./types";
+
+const CopyButton = dynamic(() => import("@/components/CopyButton"), {
+ ssr: false,
+});
+
+export const mdxComponents: MDXComponents = {
+ h1: (props: any) => (
+
+ ),
+ h2: (props: any) => (
+
+ ),
+ h3: (props: any) => (
+
+ ),
+ p: (props: any) => (
+
+ ),
+ a: (props: any) => (
+
+ ),
+ ol: (props: any) => (
+
+ ),
+ li: (props: any) => (
+
+ ),
+ code: ({ className, ...props }: any) => {
+ const match = /language-(\w+)/.exec(className || "");
+ const language = match ? match[1] : "text";
+ const isInline = className === "inline-code";
+ if (isInline) {
+ return (
+
+ );
+ }
+ return (
+
+
+
+
+ );
+ },
+ pre: (props: any) => ,
+};
diff --git a/src/app/blog/[id]/RelatedPosts.tsx b/src/app/blog/[id]/RelatedPosts.tsx
new file mode 100644
index 0000000..8bf2036
--- /dev/null
+++ b/src/app/blog/[id]/RelatedPosts.tsx
@@ -0,0 +1,31 @@
+import Link from "next/link";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui";
+import { BlogPost } from "./types";
+
+function RelatedPosts({ posts }: { posts: BlogPost[] }) {
+ if (posts.length === 0) return null;
+
+ return (
+
+
+ 相关文章
+
+
+
+ {posts.map((relatedPost) => (
+ -
+
+ {relatedPost.title}
+
+
+ ))}
+
+
+
+ );
+}
+
+export default RelatedPosts;
diff --git a/src/app/blog/[id]/ShareButtons.tsx b/src/app/blog/[id]/ShareButtons.tsx
new file mode 100644
index 0000000..a1933c8
--- /dev/null
+++ b/src/app/blog/[id]/ShareButtons.tsx
@@ -0,0 +1,37 @@
+import { FaTwitter, FaFacebook, FaLinkedin } from "react-icons/fa";
+
+interface ShareButtonsProps {
+ url: string;
+ title: string;
+}
+
+export default function ShareButtons({ url, title }: ShareButtonsProps) {
+ return (
+
+ );
+}
diff --git a/src/app/blog/[id]/TableOfContents.tsx b/src/app/blog/[id]/TableOfContents.tsx
new file mode 100644
index 0000000..9f49b9c
--- /dev/null
+++ b/src/app/blog/[id]/TableOfContents.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+import { Heading } from "./types";
+
+interface TableOfContentsProps {
+ headings: Heading[];
+}
+
+const TableOfContents = React.memo(function TableOfContents({
+ headings,
+}: TableOfContentsProps) {
+ return (
+
+ );
+});
+
+export default TableOfContents;
diff --git a/src/app/blog/[id]/page.tsx b/src/app/blog/[id]/page.tsx
index 724e67b..a585f10 100644
--- a/src/app/blog/[id]/page.tsx
+++ b/src/app/blog/[id]/page.tsx
@@ -1,217 +1,41 @@
-import { MDXRemote } from "next-mdx-remote/rsc";
import { serialize } from "next-mdx-remote/serialize";
-import rehypeSlug from "rehype-slug";
-import rehypeAutolinkHeadings from "rehype-autolink-headings";
import { notFound } from "next/navigation";
-import Link from "next/link";
import { blogPosts } from "@/data/blogPosts";
import Comments from "@/components/Comments";
import ReadingProgress from "@/components/ReadingProgress";
-import { FaTwitter, FaFacebook, FaLinkedin } from "react-icons/fa";
-import {
- Card,
- CardContent,
- CardHeader,
- CardTitle,
- Badge,
- Button,
-} from "@/components/ui";
-import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
-import {
- oneDark,
- oneLight,
-} from "react-syntax-highlighter/dist/esm/styles/prism";
-import remarkGfm from "remark-gfm";
-import rehypeInlineCode from "@/lib/rehypeInlineCode";
-import dynamic from "next/dynamic";
-
-const CopyButton = dynamic(() => import("@/components/CopyButton"), {
- ssr: false,
-});
-
-function ShareButtons({ url, title }: { url: string; title: string }) {
- return (
-
- );
-}
-
-function TableOfContents({
- headings,
-}: {
- headings: { text: string; level: number; slug: string }[];
-}) {
- return (
-
- );
-}
-
-function estimateReadingTime(content: string): number {
- const wordsPerMinute = 200;
- const wordCount = content.split(/\s+/).length;
- return Math.ceil(wordCount / wordsPerMinute);
+import { extractHeadings, estimateReadingTime } from "@/utils/blogHelpers";
+import BlogHeader from "./BlogHeader";
+import BlogContent from "./BlogContent";
+import BlogFooter from "./BlogFooter";
+import RelatedPosts from "./RelatedPosts";
+import ShareButtons from "./ShareButtons";
+import TableOfContents from "./TableOfContents";
+import { mdxComponents } from "./MdxComponents";
+import type { BlogPost, Heading } from "./types";
+
+interface BlogPostParams {
+ params: { id: string };
}
export async function generateStaticParams() {
- const posts = blogPosts; // 假设这个函数可以获取所有博客文章
- return posts.map((post) => ({
- id: post.id.toString(), // 将 id 转换为字符串
+ return blogPosts.map((post) => ({
+ id: post.id.toString(),
}));
}
-const components = {
- h1: (props: any) => (
-
- ),
- h2: (props: any) => (
-
- ),
- h3: (props: any) => (
-
- ),
- p: (props: any) => (
-
- ),
- a: (props: any) => (
-
- ),
- ol: (props: any) => (
-
- ),
- li: (props: any) => (
-
- ),
- code: ({ className, ...props }: any) => {
- const match = /language-(\w+)/.exec(className || "");
- const language = match ? match[1] : "text";
- const isInline = className === "inline-code";
- if (isInline) {
- return (
-
- );
- }
- return (
-
-
-
-
- );
- },
- pre: (props: any) => ,
-};
-
-function extractHeadings(content: string) {
- const headingRegex = /^(#{1,3})\s+(.+)$/gm;
- const headings = [];
- let match;
-
- while ((match = headingRegex.exec(content)) !== null) {
- headings.push({
- level: match[1].length,
- text: match[2],
- slug: match[2].toLowerCase().replace(/\s+/g, "-"),
- });
- }
-
- return headings;
-}
-
-export default async function BlogPost({ params }: { params: { id: string } }) {
- const post = blogPosts.find((p) => p.id.toString() === params.id);
+export default async function BlogPost({ params }: BlogPostParams) {
+ const post = blogPosts.find((p) => p.id.toString() === params.id) as
+ | BlogPost
+ | undefined;
if (!post) {
notFound();
}
- const mdxSource = await serialize(post.content, {
- mdxOptions: {
- rehypePlugins: [
- rehypeSlug,
- [rehypeAutolinkHeadings, { behavior: "wrap" }],
- rehypeInlineCode,
- ],
- },
- parseFrontmatter: true,
- });
-
- const headings = extractHeadings(post.content);
+ const headings: Heading[] = extractHeadings(post.content);
+ const readingTime: number = estimateReadingTime(post.content);
- const relatedPosts = blogPosts
+ const relatedPosts: BlogPost[] = blogPosts
.filter(
(p) => p.id !== post.id && p.tags.some((tag) => post.tags.includes(tag))
)
@@ -225,71 +49,17 @@ export default async function BlogPost({ params }: { params: { id: string } }) {
- {post.title}
-
- {post.date} | {post.author} |
- 预计阅读时间: {estimateReadingTime(post.content)} 分钟
-
-
-
+
-
-
- {post.tags.map((tag) => (
-
-
- {tag}
-
-
- ))}
-
-
-
- {relatedPosts.length > 0 && (
-
-
- 相关文章
-
-
-
- {relatedPosts.map((relatedPost) => (
- -
-
- {relatedPost.title}
-
-
- ))}
-
-
-
- )}
-
+
+
-
@@ -297,4 +67,4 @@ export default async function BlogPost({ params }: { params: { id: string } }) {
);
-}
\ No newline at end of file
+}
diff --git a/src/app/blog/[id]/types.ts b/src/app/blog/[id]/types.ts
new file mode 100644
index 0000000..4e78d78
--- /dev/null
+++ b/src/app/blog/[id]/types.ts
@@ -0,0 +1,21 @@
+import { MDXRemoteSerializeResult } from 'next-mdx-remote';
+
+export interface BlogPost {
+ id: number;
+ title: string;
+ date: string;
+ author: string;
+ content: string;
+ coverImage?: string;
+ tags: string[];
+}
+
+export interface Heading {
+ level: number;
+ text: string;
+ slug: string;
+}
+
+export interface MDXComponents {
+ [key: string]: React.ComponentType;
+}
\ No newline at end of file
diff --git a/src/utils/blogHelpers.ts b/src/utils/blogHelpers.ts
new file mode 100644
index 0000000..d566710
--- /dev/null
+++ b/src/utils/blogHelpers.ts
@@ -0,0 +1,31 @@
+import { useMemo } from 'react';
+
+export function extractHeadings(content: string) {
+ const headingRegex = /^(#{1,3})\s+(.+)$/gm;
+ const headings = [];
+ let match;
+
+ while ((match = headingRegex.exec(content)) !== null) {
+ headings.push({
+ level: match[1].length,
+ text: match[2],
+ slug: match[2].toLowerCase().replace(/\s+/g, "-"),
+ });
+ }
+
+ return headings;
+}
+
+export function estimateReadingTime(content: string): number {
+ const wordsPerMinute = 200;
+ const wordCount = content.split(/\s+/).length;
+ return Math.ceil(wordCount / wordsPerMinute);
+}
+
+export function useExtractHeadings(content: string) {
+ return useMemo(() => extractHeadings(content), [content]);
+}
+
+export function useEstimateReadingTime(content: string) {
+ return useMemo(() => estimateReadingTime(content), [content]);
+}
\ No newline at end of file
From f6abd3c8ba8625c1557a8b9fc18577053dc3db71 Mon Sep 17 00:00:00 2001
From: zhuba-Ahhh <3477826311@qq.com>
Date: Sat, 24 Aug 2024 12:51:44 +0800
Subject: [PATCH 2/2] style: :lipstick: copy
---
public/svg/check-dark.svg | 1 +
public/svg/check.svg | 2 +-
public/svg/copy-dark.svg | 1 +
public/svg/copy.svg | 2 +-
src/components/Comments.tsx | 70 +++++++++++++++-------------------
src/components/CopyButton.tsx | 72 ++++++++++++++++++++++-------------
6 files changed, 80 insertions(+), 68 deletions(-)
create mode 100644 public/svg/check-dark.svg
create mode 100644 public/svg/copy-dark.svg
diff --git a/public/svg/check-dark.svg b/public/svg/check-dark.svg
new file mode 100644
index 0000000..d4c5fa8
--- /dev/null
+++ b/public/svg/check-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svg/check.svg b/public/svg/check.svg
index c41d431..f95f74f 100644
--- a/public/svg/check.svg
+++ b/public/svg/check.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/svg/copy-dark.svg b/public/svg/copy-dark.svg
new file mode 100644
index 0000000..f0c4615
--- /dev/null
+++ b/public/svg/copy-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svg/copy.svg b/public/svg/copy.svg
index 97c5e85..55c7a0e 100644
--- a/public/svg/copy.svg
+++ b/public/svg/copy.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/components/Comments.tsx b/src/components/Comments.tsx
index 6ea6f49..9dbb0f7 100644
--- a/src/components/Comments.tsx
+++ b/src/components/Comments.tsx
@@ -17,48 +17,36 @@ interface Comment {
author: string;
content: string;
date: string;
- replies: Comment[];
+ replies?: Comment[];
}
+type NewComment = Pick;
+
export default function Comments() {
const [comments, setComments] = useState([]);
- const [newComment, setNewComment] = useState({ author: "", content: "" });
+ const [newComment, setNewComment] = useState({
+ author: "",
+ content: "",
+ });
const [sortOrder, setSortOrder] = useState<"newest" | "oldest">("newest");
const sortedComments = useMemo(() => {
return [...comments].sort((a, b) => {
- return sortOrder === "newest"
- ? new Date(b.date).getTime() - new Date(a.date).getTime()
- : new Date(a.date).getTime() - new Date(b.date).getTime();
- });
- }, [comments, sortOrder]);
-
- const addReply = useCallback((parentId: number, replyContent: string) => {
- setComments((prevComments) => {
- const newComments = [...prevComments];
- const parentComment = newComments.find((c) => c.id === parentId);
- if (parentComment) {
- parentComment.replies.push({
- id: Date.now(),
- author: "匿名用户",
- content: replyContent,
- date: new Date().toLocaleString(),
- replies: [],
- });
+ if (sortOrder === "newest") {
+ return new Date(b.date).getTime() - new Date(a.date).getTime();
+ } else {
+ return new Date(a.date).getTime() - new Date(b.date).getTime();
}
- return newComments;
});
- }, []);
+ }, [comments, sortOrder]);
const handleSubmit = useCallback(
- (e: React.FormEvent) => {
+ (e: React.FormEvent) => {
e.preventDefault();
const comment: Comment = {
+ ...newComment,
id: Date.now(),
- author: newComment.author.trim(),
- content: newComment.content.trim(),
- date: new Date().toLocaleString(),
- replies: [],
+ date: new Date().toISOString(),
};
setComments((prevComments) => [...prevComments, comment]);
setNewComment({ author: "", content: "" });
@@ -116,19 +104,21 @@ export default function Comments() {
{comment.content}
-
- {comment.replies.map((reply) => (
-
-
- {reply.author}
- {reply.date}
-
-
- {reply.content}
-
-
- ))}
-
+ {comment?.replies && comment?.replies?.length > 0 && (
+
+ {comment?.replies?.map((reply) => (
+
+
+ {reply.author}
+ {reply.date}
+
+
+ {reply.content}
+
+
+ ))}
+
+ )}
))}
diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx
index 5dba9e9..7db1734 100644
--- a/src/components/CopyButton.tsx
+++ b/src/components/CopyButton.tsx
@@ -1,8 +1,9 @@
"use client";
-import React, { useState, useCallback } from "react";
+import React, { useState, useCallback, useMemo } from "react";
import Image from "next/image";
import { throttle } from "lodash-es";
+import { useTheme } from "next-themes";
interface CopyButtonProps {
text: string;
@@ -11,35 +12,54 @@ interface CopyButtonProps {
const CopyButton: React.FC = ({ text, language }) => {
const [state, setState] = useState<"idle" | "copy" | "copied">("idle");
+ const { theme } = useTheme();
- const throttledCopy = throttle(async () => {
- if (state === "copied") return;
- try {
- await navigator.clipboard.writeText(text);
- setState("copied");
- setTimeout(() => setState("idle"), 1000);
- } catch (err) {
- console.error("复制失败:", err);
- setState("idle");
- }
- }, 300);
+ const throttledCopy = useMemo(
+ () =>
+ throttle(async () => {
+ if (state === "copied") return;
+ try {
+ await navigator.clipboard.writeText(text);
+ setState("copied");
+ setTimeout(() => setState("idle"), 1000);
+ } catch (err) {
+ console.error("复制失败:", err);
+ setState("idle");
+ }
+ }, 300),
+ [text, state]
+ );
+
+ const handleCopy = useCallback(() => {
+ throttledCopy();
+ }, [throttledCopy]);
+
+ const buttonClassName = useMemo(
+ () => `
+ absolute top-2 right-2 p-1.5 rounded-md
+ bg-gray-100 dark:bg-gray-800
+ border border-gray-200 dark:border-gray-700
+ hover:bg-gray-200 dark:hover:bg-gray-700
+ transition-all duration-300 ease-in-out
+ flex items-center justify-center
+ shadow-sm hover:shadow-md dark:shadow-gray-900
+ h-[30px]
+ `,
+ []
+ );
- const handleCopy = useCallback(throttledCopy, [throttledCopy, text, state]);
+ const getImageSrc = (baseName: string) => {
+ return theme === "dark"
+ ? `/svg/${baseName}-dark.svg`
+ : `/svg/${baseName}.svg`;
+ };
return (