From 59da15743a4f3961e6a557794e3f257d12dab4e8 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Fri, 5 Jan 2024 21:22:39 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20add=20posts=20list=20and=20clamp=20?= =?UTF-8?q?utils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[lang]/posts/InfiniteList.tsx | 34 +++++++++++++++++ .../posts/__tests__/infiniteList.test.tsx | 37 +++++++++++++++++++ src/app/[lang]/posts/__tests__/page.test.tsx | 11 ++++++ src/app/[lang]/posts/page.tsx | 5 +-- src/components/Link/Link.tsx | 3 +- src/utils/__tests__/math.test.ts | 19 +++++++++- src/utils/math.ts | 13 +++++++ src/utils/mdx.ts | 9 ++++- types/data.d.ts | 6 +++ 9 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 src/app/[lang]/posts/InfiniteList.tsx create mode 100644 src/app/[lang]/posts/__tests__/infiniteList.test.tsx diff --git a/src/app/[lang]/posts/InfiniteList.tsx b/src/app/[lang]/posts/InfiniteList.tsx new file mode 100644 index 0000000..745249b --- /dev/null +++ b/src/app/[lang]/posts/InfiniteList.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { memo } from 'react'; +import { useSearchParams } from 'next/navigation'; +import Link from '@/components/Link'; +import List from '@/components/List'; +import { clamp } from '@/utils/math'; +import Article from '../Article'; + +type InfiniteListProps = { + items: DataFrontmatter[]; +}; + +const MemoArticle = memo(Article); + +function InfiniteList({ items }: InfiniteListProps) { + const searchParams = useSearchParams(); + const total = items.length; + const clampLimit = clamp(1, total); + const limit = clampLimit(parseInt(searchParams.get('limit') || '10', 10)); + + return ( + <> + + {limit < total && ( + + 更多文章 + + )} + + ); +} + +export default InfiniteList; diff --git a/src/app/[lang]/posts/__tests__/infiniteList.test.tsx b/src/app/[lang]/posts/__tests__/infiniteList.test.tsx new file mode 100644 index 0000000..d04b35e --- /dev/null +++ b/src/app/[lang]/posts/__tests__/infiniteList.test.tsx @@ -0,0 +1,37 @@ +import { render, screen } from '@testing-library/react'; +import InfiniteList from '../InfiniteList'; + +jest.mock('next/navigation', () => ({ + useRouter() { + return { + prefetch: () => null, + }; + }, + useSearchParams() { + return new URLSearchParams(); + }, + usePathname() { + return ''; + }, +})); + +describe('InfiniteList component', () => { + it('should render correct element', () => { + const generateMockPosts = (count: number): DataFrontmatter[] => + new Array(count).fill(null).map((_, i) => ({ + id: `test-id-${i}`, + title: `test-title-${i}`, + date: '2024/1/4', + description: `test-description-${i}`, + categories: [], + tags: [], + })); + render(); + + const link = screen.getByRole('link', { name: '更多文章' }); + const list = screen.getByRole('list'); + + expect(list).toBeInTheDocument(); + expect(link).toBeInTheDocument(); + }); +}); diff --git a/src/app/[lang]/posts/__tests__/page.test.tsx b/src/app/[lang]/posts/__tests__/page.test.tsx index 0cb9d11..a1b3e58 100644 --- a/src/app/[lang]/posts/__tests__/page.test.tsx +++ b/src/app/[lang]/posts/__tests__/page.test.tsx @@ -6,6 +6,17 @@ jest.mock('@/utils/mdx', () => ({ getAllDataFrontmatter: () => [], })); +jest.mock('next/navigation', () => ({ + useRouter() { + return { + prefetch: () => null, + }; + }, + useSearchParams() { + return new URLSearchParams(); + }, +})); + describe('Posts page', () => { it('should render correct element', async () => { // Arrange diff --git a/src/app/[lang]/posts/page.tsx b/src/app/[lang]/posts/page.tsx index 663e7ff..13d1d08 100644 --- a/src/app/[lang]/posts/page.tsx +++ b/src/app/[lang]/posts/page.tsx @@ -3,11 +3,10 @@ import type { Metadata } from 'next'; import { getDictionary } from '~/i18n'; import Container from '@/components/Container'; import { H1 } from '@/components/Heading'; -import List from '@/components/List'; import { getAllDataFrontmatter } from '@/utils/mdx'; import type { RootParams } from '../layout'; -import Article from '../Article'; +import InfiniteList from './InfiniteList'; export async function generateMetadata({ params: { lang }, @@ -30,7 +29,7 @@ async function RootPage({ params: { lang } }: RootParams) {

{metadata.description}

- + ); diff --git a/src/components/Link/Link.tsx b/src/components/Link/Link.tsx index 3ceeca2..a4ef6f5 100644 --- a/src/components/Link/Link.tsx +++ b/src/components/Link/Link.tsx @@ -27,10 +27,11 @@ function Link({ const pathname = usePathname(); const isObjectHref = typeof href === 'object' || href.startsWith('#'); + const isQueryLink = !isObjectHref && href.startsWith('?'); const isAnchorLink = !isObjectHref && href.startsWith('#'); const isInternalLink = !isObjectHref && href.startsWith('/'); - if (isObjectHref || isAnchorLink) { + if (isObjectHref || isAnchorLink || isQueryLink) { return ( {children} diff --git a/src/utils/__tests__/math.test.ts b/src/utils/__tests__/math.test.ts index b4bfe1a..fd292e3 100644 --- a/src/utils/__tests__/math.test.ts +++ b/src/utils/__tests__/math.test.ts @@ -1,4 +1,4 @@ -import { toFixedNumber } from '../math'; +import { toFixedNumber, clamp } from '../math'; describe('math function', () => { it.each([ @@ -18,3 +18,20 @@ describe('math function', () => { } ); }); + +describe('clamp function', () => { + it.each([ + [1, 3, 2, 2], + [1, 3, 4, 3], + [1, 3, 0, 1], + [3, 1, 2, 2], + [3, 1, 4, 3], + [3, 1, 0, 1], + ])( + 'should clamp a number between the given range', + (number1, number2, number, expected) => { + const result = clamp(number1, number2)(number); + expect(result).toBe(expected); + } + ); +}); diff --git a/src/utils/math.ts b/src/utils/math.ts index 46b6fea..302f090 100644 --- a/src/utils/math.ts +++ b/src/utils/math.ts @@ -4,3 +4,16 @@ export const toFixedNumber = (digits: number) => (value: number) => { return Math.round(value * pow) / pow; }; + +export const clamp = (number1: number, number2: number) => (number: number) => { + const min = Math.min(number1, number2); + const max = Math.max(number1, number2); + + if (Number.isNaN(number) || number < min) { + return min; + } + if (number > max) { + return max; + } + return number; +}; diff --git a/src/utils/mdx.ts b/src/utils/mdx.ts index c7ed8ed..554cfc7 100644 --- a/src/utils/mdx.ts +++ b/src/utils/mdx.ts @@ -16,7 +16,9 @@ import { compareDates } from './date'; const ROOT_PATH = process.cwd(); /** Retrieve all data front matter sorted by date */ -export async function getAllDataFrontmatter(dirType: DataDirType) { +export async function getAllDataFrontmatter( + dirType: DataDirType +): Promise { const dirPath = path.join(ROOT_PATH, 'data', dirType); const fileNames = fs.readdirSync(dirPath); const uniqueIdsSet = new Set(); @@ -44,7 +46,10 @@ 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) { +export async function getDataById( + dirType: DataDirType, + id: string +): Promise { try { const dirPath = path.join(ROOT_PATH, 'data', dirType); const mdxPath = path.join(dirPath, `${id}.mdx`); diff --git a/types/data.d.ts b/types/data.d.ts index 1c901ca..cea429e 100644 --- a/types/data.d.ts +++ b/types/data.d.ts @@ -11,3 +11,9 @@ type DataFrontmatter = { readonly description: string; readonly image?: string; }; + +type Data = { + id: string; + content: React.ReactElement; + frontmatter: DataFrontmatter; +};