Skip to content

Commit

Permalink
Merge pull request #126 from JohnsonMao/feature/posts-list
Browse files Browse the repository at this point in the history
✨ add posts list and clamp utils
  • Loading branch information
JohnsonMao authored Jan 5, 2024
2 parents eabb0ec + 59da157 commit 645508d
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 7 deletions.
34 changes: 34 additions & 0 deletions src/app/[lang]/posts/InfiniteList.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<List Item={MemoArticle} items={items.slice(0, limit)} />
{limit < total && (
<Link href={`?limit=${clampLimit(limit + 10)}`} replace scroll={false}>
更多文章
</Link>
)}
</>
);
}

export default InfiniteList;
37 changes: 37 additions & 0 deletions src/app/[lang]/posts/__tests__/infiniteList.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<InfiniteList items={generateMockPosts(20)} />);

const link = screen.getByRole('link', { name: '更多文章' });
const list = screen.getByRole('list');

expect(list).toBeInTheDocument();
expect(link).toBeInTheDocument();
});
});
11 changes: 11 additions & 0 deletions src/app/[lang]/posts/__tests__/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions src/app/[lang]/posts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -30,7 +29,7 @@ async function RootPage({ params: { lang } }: RootParams) {
<p className="text-xl">{metadata.description}</p>
</Container>
<Container as="main" className="py-8">
<List Item={Article} items={posts} />
<InfiniteList items={posts} />
</Container>
</>
);
Expand Down
3 changes: 2 additions & 1 deletion src/components/Link/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ function Link<T extends string = string>({
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 (
<NextLink href={href as Route} className={className} {...otherProps}>
{children}
Expand Down
19 changes: 18 additions & 1 deletion src/utils/__tests__/math.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toFixedNumber } from '../math';
import { toFixedNumber, clamp } from '../math';

describe('math function', () => {
it.each([
Expand All @@ -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);
}
);
});
13 changes: 13 additions & 0 deletions src/utils/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
9 changes: 7 additions & 2 deletions src/utils/mdx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DataFrontmatter[]> {
const dirPath = path.join(ROOT_PATH, 'data', dirType);
const fileNames = fs.readdirSync(dirPath);
const uniqueIdsSet = new Set();
Expand Down Expand Up @@ -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<Data | null> {
try {
const dirPath = path.join(ROOT_PATH, 'data', dirType);
const mdxPath = path.join(dirPath, `${id}.mdx`);
Expand Down
6 changes: 6 additions & 0 deletions types/data.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ type DataFrontmatter = {
readonly description: string;
readonly image?: string;
};

type Data = {
id: string;
content: React.ReactElement;
frontmatter: DataFrontmatter;
};

0 comments on commit 645508d

Please sign in to comment.