From 02d4e34a3ba2794fb44954631be86d5a07c829d7 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Thu, 21 Sep 2023 23:36:45 +0800 Subject: [PATCH 01/13] =?UTF-8?q?=E2=9C=A8=20use=20auto=20reset=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useAutoReset.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/hooks/useAutoReset.ts diff --git a/src/hooks/useAutoReset.ts b/src/hooks/useAutoReset.ts new file mode 100644 index 00000000..dffa8138 --- /dev/null +++ b/src/hooks/useAutoReset.ts @@ -0,0 +1,29 @@ +import { useEffect, useRef, useState } from 'react'; + +/** + * Custom hook that resets a value to its initial state after a specified delay. + * + * @param initialValue - The initial value. + * @param resetDelayMs - The reset delay in milliseconds. Default is 1000. + * @returns A tuple containing the current value, and a function to set a new value. + */ +function useAutoReset(initialValue: T, resetDelayMs = 1000) { + const [internalValue, setInternalValue] = useState(initialValue); + const timerRef = useRef(null); + + const clearTimer = () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + + const setValue = (newValue: T) => { + setInternalValue(newValue); + clearTimer(); + timerRef.current = setTimeout(() => setInternalValue(initialValue), resetDelayMs); + }; + + useEffect(() => clearTimer, []); + + return [internalValue, setValue] as const; +} + +export default useAutoReset; From da5c39036279deb2a6fcb40594ad5250f8c21252 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Fri, 22 Sep 2023 17:56:15 +0800 Subject: [PATCH 02/13] =?UTF-8?q?=E2=9C=85=20use=20auto=20reset=20hook=20u?= =?UTF-8?q?nit=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/__tests__/useAutoReset.test.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/hooks/__tests__/useAutoReset.test.ts diff --git a/src/hooks/__tests__/useAutoReset.test.ts b/src/hooks/__tests__/useAutoReset.test.ts new file mode 100644 index 00000000..d1095835 --- /dev/null +++ b/src/hooks/__tests__/useAutoReset.test.ts @@ -0,0 +1,21 @@ +import { act, renderHook, waitFor } from '@testing-library/react'; +import useAutoReset from '../useAutoReset'; + +describe('useAutoReset hook', () => { + it('should reset state to initial value after a specified delay', async () => { + const initialValue = false; + const newValue = true; + const { result } = renderHook(() => useAutoReset(initialValue)); + + expect(result.current[0]).toBe(initialValue); + + act(() => result.current[1](newValue)); + + expect(result.current[0]).toBe(newValue); + + await waitFor( + () => expect(result.current[0]).toBe(initialValue), + { timeout: 2000 } + ); + }); +}); From ad9e174406186845890a46c6e65cbf295fea660c Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Fri, 22 Sep 2023 18:22:51 +0800 Subject: [PATCH 03/13] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20CodeBox:?= =?UTF-8?q?=20Abstract=20useAutoRest=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CodeBox/CodeBox.tsx | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/src/components/CodeBox/CodeBox.tsx b/src/components/CodeBox/CodeBox.tsx index 31b34a70..6c2712a1 100644 --- a/src/components/CodeBox/CodeBox.tsx +++ b/src/components/CodeBox/CodeBox.tsx @@ -1,38 +1,23 @@ 'use client'; -import { HTMLAttributes, useRef, useState } from 'react'; +import { HTMLAttributes, useRef } from 'react'; import { AiOutlineCopy } from 'react-icons/ai'; +import useAutoReset from '@/hooks/useAutoReset'; import cn from '@/utils/cn'; import { copyToClipboard } from '@/utils/clipboard'; type CodeBoxProps = HTMLAttributes; function CodeBox(props: CodeBoxProps) { - const preRef = useRef(null); - const timerRef = useRef(null); - const [copied, setCopied] = useState(false); - - const resetCopiedState = () => { - setCopied(false); - timerRef.current = null; - }; + const preElementRef = useRef(null); + const [copied, setCopied] = useAutoReset(false); const handleClick = () => { - const text = preRef.current?.textContent; - - if (timerRef.current) { - clearTimeout(timerRef.current); - timerRef.current = null; - } + const text = preElementRef.current?.textContent; if (text) { - copyToClipboard(text) - .then(() => { - setCopied(true); - timerRef.current = setTimeout(resetCopiedState, 1000); - }) - .catch(resetCopiedState); + copyToClipboard(text).then(() => setCopied(true)); } }; @@ -53,7 +38,7 @@ function CodeBox(props: CodeBoxProps) { -
+      
     
   );
 }

From 32e8193eea57791f5e039a4b770c277855ad04a9 Mon Sep 17 00:00:00 2001
From: Johnson Mao 
Date: Mon, 25 Sep 2023 17:55:13 +0800
Subject: [PATCH 04/13] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20component?=
 =?UTF-8?q?=20and=20style?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/app/[lang]/page.tsx                 |  2 +-
 src/components/Card/Card.tsx            |  2 +-
 src/components/Header/Header.tsx        |  2 +-
 src/components/Heading/Heading.tsx      | 36 +++++++------------------
 src/components/Heading/heading.test.tsx |  2 +-
 src/components/Link/Link.tsx            |  2 +-
 src/components/Link/index.tsx           |  2 --
 src/components/List/List.tsx            |  8 +++---
 src/components/List/list.test.tsx       |  2 +-
 tailwind.config.js                      |  3 +++
 10 files changed, 24 insertions(+), 37 deletions(-)

diff --git a/src/app/[lang]/page.tsx b/src/app/[lang]/page.tsx
index e828e525..86e423be 100644
--- a/src/app/[lang]/page.tsx
+++ b/src/app/[lang]/page.tsx
@@ -31,7 +31,7 @@ async function RootPage({ params: { lang } }: RootParams) {
       

{metadata.title}

- + ); } diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx index 9f0f647c..dc2a86a9 100644 --- a/src/components/Card/Card.tsx +++ b/src/components/Card/Card.tsx @@ -35,7 +35,7 @@ function Card({

{title} diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index fca8384b..2252ee18 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -44,7 +44,7 @@ function Header({ logo, menu }: HeaderProps) {

-

{excerpt}

+

{description}

diff --git a/src/components/Card/card.test.tsx b/src/components/Card/card.test.tsx index 35e6056e..cde9218e 100644 --- a/src/components/Card/card.test.tsx +++ b/src/components/Card/card.test.tsx @@ -15,7 +15,7 @@ describe('Card component', () => { ['categories_B', 'categories_B_1'], ], tags: ['tag_A', 'tag_B'], - excerpt: 'excerpt test', + description: 'description test', }; render(); @@ -29,6 +29,6 @@ describe('Card component', () => { expect(image).toHaveAttribute('alt', `${data.title} cover`); expect(heading).toHaveTextContent(data.title); expect(article).toHaveTextContent(formatDate(data.date)); - expect(article).toHaveTextContent(data.excerpt); + expect(article).toHaveTextContent(data.description); }); }); diff --git a/types/data.d.ts b/types/data.d.ts index 047251c0..1c901ca6 100644 --- a/types/data.d.ts +++ b/types/data.d.ts @@ -8,6 +8,6 @@ type DataFrontmatter = { readonly date: DateOrDateString; readonly categories: string[][]; readonly tags: string[]; - readonly excerpt: string; + readonly description: string; readonly image?: string; }; From 885de38cdd4fd2f2dcb33723913e187548783449 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Wed, 27 Sep 2023 13:29:49 +0800 Subject: [PATCH 07/13] =?UTF-8?q?=E2=9C=A8=20add=20try-catch=20statement?= =?UTF-8?q?=20to=20handle=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[lang]/posts/[postId]/page.tsx | 16 ++----- src/utils/__tests__/mdx.test.ts | 11 +++++ src/utils/mdx.ts | 66 ++++++++++++++------------ 3 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/app/[lang]/posts/[postId]/page.tsx b/src/app/[lang]/posts/[postId]/page.tsx index 54a027fb..e639ac83 100644 --- a/src/app/[lang]/posts/[postId]/page.tsx +++ b/src/app/[lang]/posts/[postId]/page.tsx @@ -21,27 +21,19 @@ export async function generateStaticParams() { export async function generateMetadata({ params: { postId }, }: PostParams): Promise { - const posts = await getAllDataFrontmatter('posts'); - const post = posts.find((post) => post.id === postId); + const post = await getDataById('posts', postId); if (!post) return notFound(); - return { - title: post.title, - description: post.excerpt, - }; + return post.frontmatter; } async function PostPage({ params: { postId } }: PostParams) { - const posts = await getAllDataFrontmatter('posts'); - const post = posts.find((post) => post.id === postId); + const post = await getDataById('posts', postId); if (!post) return notFound(); - const { content, frontmatter, source } = await getDataById( - 'posts', - postId - ); + const { content, frontmatter, source } = post; const formattedDate = formatDate(frontmatter.date); return ( diff --git a/src/utils/__tests__/mdx.test.ts b/src/utils/__tests__/mdx.test.ts index ef961df2..1602f0c8 100644 --- a/src/utils/__tests__/mdx.test.ts +++ b/src/utils/__tests__/mdx.test.ts @@ -91,6 +91,17 @@ describe('Get post data function', () => { source: '---\ndate: 2023/07/09\n---\n\n測試文章C', }); }); + + it('should return null when trying to get post data for a non-existing file', async () => { + mockExists.mockReturnValueOnce(false); + mockReadFile.mockImplementation(() => { + throw new Error('File not found'); + }); + + const postData = await getDataById('posts', 'not_found'); + + expect(postData).toBe(null); + }); }); const mockFileA = `--- diff --git a/src/utils/mdx.ts b/src/utils/mdx.ts index 18c51ffe..0da935e0 100644 --- a/src/utils/mdx.ts +++ b/src/utils/mdx.ts @@ -45,37 +45,41 @@ export async function getAllDataFrontmatter(dirType: DataDirType) { /** Retrieve data content and front matter for a specific data file by its id. */ export async function getDataById(dirType: DataDirType, id: string) { - const dirPath = path.join(ROOT_PATH, 'data', dirType); - const mdxPath = path.join(dirPath, `${id}.mdx`); - const mdPath = path.join(dirPath, `${id}.md`); - const fullPath = fs.existsSync(mdxPath) ? mdxPath : mdPath; - const source = fs.readFileSync(fullPath, 'utf8'); - const { content, frontmatter } = await compileMDX({ - source, - components: { - h1: H1, - h2: H2, - h3: H3, - h4: H4, - h5: H5, - h6: H6, - pre: CodeBox, - img: Image as () => JSX.Element, - a: Link as () => JSX.Element, - }, - options: { - parseFrontmatter: true, - mdxOptions: { - remarkPlugins: [remarkGfm], - rehypePlugins: [ - rehypeSlug, - rehypeCodeTitles, - rehypePrismPlus, - rehypeImageMetadata, - ], + try { + const dirPath = path.join(ROOT_PATH, 'data', dirType); + const mdxPath = path.join(dirPath, `${id}.mdx`); + const mdPath = path.join(dirPath, `${id}.md`); + const fullPath = fs.existsSync(mdxPath) ? mdxPath : mdPath; + const source = fs.readFileSync(fullPath, 'utf8'); + const { content, frontmatter } = await compileMDX({ + source, + components: { + h1: H1, + h2: H2, + h3: H3, + h4: H4, + h5: H5, + h6: H6, + pre: CodeBox, + img: Image as () => JSX.Element, + a: Link as () => JSX.Element, + }, + options: { + parseFrontmatter: true, + mdxOptions: { + remarkPlugins: [remarkGfm], + rehypePlugins: [ + rehypeSlug, + rehypeCodeTitles, + rehypePrismPlus, + rehypeImageMetadata, + ], + }, }, - }, - }); + }); - return { id, content, frontmatter, source }; + return { id, content, frontmatter, source }; + } catch { + return null; + } } From 80798a1b18e45cb3999f9b31fb83790968eda7d0 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Thu, 28 Sep 2023 18:20:59 +0800 Subject: [PATCH 08/13] =?UTF-8?q?=F0=9F=90=9B=20fix=20scroll=20position=20?= =?UTF-8?q?not=20reset=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[lang]/posts/[postId]/page.tsx | 17 ++++++++++------- .../TableOfContents/TableOfContents.tsx | 16 +++------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/app/[lang]/posts/[postId]/page.tsx b/src/app/[lang]/posts/[postId]/page.tsx index e639ac83..33dbe0e5 100644 --- a/src/app/[lang]/posts/[postId]/page.tsx +++ b/src/app/[lang]/posts/[postId]/page.tsx @@ -38,19 +38,22 @@ async function PostPage({ params: { postId } }: PostParams) { return ( <> - - +

{frontmatter.title}

{formattedDate}

{content}
回首頁
+ ); } diff --git a/src/components/TableOfContents/TableOfContents.tsx b/src/components/TableOfContents/TableOfContents.tsx index 33c263eb..19e80c3f 100644 --- a/src/components/TableOfContents/TableOfContents.tsx +++ b/src/components/TableOfContents/TableOfContents.tsx @@ -35,25 +35,15 @@ function TableOfContents({ source }: TableOfContentsProps) { }, [setElementRef]); useEffect(() => { - const visibleHeadings = entry.flatMap((headingElement) => { - if (headingElement.isIntersecting) return headingElement; - return []; - }); + const visibleHeadings = entry.filter( + ({ isIntersecting }) => isIntersecting + ); if (visibleHeadings.length > 0) { setActiveId(visibleHeadings[0].target.id); } }, [entry]); - useEffect(() => { - if (!window.location.hash) { - window.scrollTo({ - top: 0, - behavior: 'instant' - }) - } - }, []); - return (