Skip to content

Commit 27d8dc3

Browse files
committed
feat: 阅读月刊分类支持排序
1 parent 63d2028 commit 27d8dc3

File tree

9 files changed

+174
-41
lines changed

9 files changed

+174
-41
lines changed

public/locales/en/home.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"bottom_text_nologin": "End of the page! Sign in to read more",
66
"nav": {
77
"all": "All",
8-
"newest": "Newest",
8+
"newest": "Latest",
99
"monthly": "Monthly",
1010
"yearly": "Yearly",
1111
"featured": "Featured"

public/locales/en/periodical.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
"page_next": "Next",
2222
"category": {
2323
"title": "HelloGitHub Monthly {{name}} Collection",
24-
"nav": "Category",
24+
"nav": {
25+
"active": "Active",
26+
"last": "Default"
27+
},
2528
"p_text": "Here you can read past volumes of HelloGitHub Monthly by <strong>category</strong>. You are currently viewing the",
2629
"p_text2": "collection."
2730
},

public/locales/zh/periodical.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
"page_next": "下一页",
2222
"category": {
2323
"title": "《HelloGitHub 月刊》{{name}} 集合",
24-
"nav": "分类",
24+
"nav": {
25+
"active": "活跃",
26+
"last": "默认"
27+
},
2528
"p_text": "这里是按照「<strong>分类</strong>」阅读往期的 HelloGitHub 月刊内容, 您目前在查看",
2629
"p_text2": "集合。"
2730
},

src/components/navbar/ArticleBar.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ const ArticleNavbar = ({ t }: Props) => {
2222

2323
const linkClassName = (sortName: string) =>
2424
classNames(
25-
'flex items-center whitespace-nowrap rounded-lg px-2 py-1 text-xs hover:text-blue-500 dark:hover:bg-gray-700',
25+
'flex items-center whitespace-nowrap rounded-lg text-xs hover:text-blue-500',
2626
{
27-
'text-gray-500 dark:text-gray-200': sort_by !== sortName,
28-
'dark:bg-gray-700 text-blue-500': sort_by === sortName,
27+
'text-gray-500 dark:text-gray-300': sort_by !== sortName,
28+
'text-blue-500': sort_by === sortName,
2929
}
3030
);
3131

@@ -41,10 +41,11 @@ const ArticleNavbar = ({ t }: Props) => {
4141
<div className='w-3/4 truncate text-center font-bold dark:text-gray-300'>
4242
{t('nav.title')}
4343
</div>
44-
<div className='flex justify-end text-sm text-gray-500 dark:text-gray-400'>
44+
<div className='flex justify-end gap-2.5 text-sm text-gray-500 dark:text-gray-400'>
4545
<NoPrefetchLink href='/article?sort_by=last'>
4646
<a className={linkClassName('last')}>{t('nav.last')}</a>
4747
</NoPrefetchLink>
48+
<span className='border-r border-gray-100 dark:border-gray-700' />
4849
<NoPrefetchLink href='/article?sort_by=hot'>
4950
<a className={linkClassName('hot')}>{t('nav.hot')}</a>
5051
</NoPrefetchLink>

src/components/navbar/CategoryBar.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import classNames from 'classnames';
2+
import { useRouter } from 'next/router';
3+
import { AiOutlineArrowLeft } from 'react-icons/ai';
4+
5+
import { NoPrefetchLink } from '@/components/links/CustomLink';
6+
7+
type Props = {
8+
category: string;
9+
middleText: string;
10+
t: (key: string) => string;
11+
};
12+
13+
const CategoryNavbar = ({ category, middleText, t }: Props) => {
14+
const router = useRouter();
15+
const { sort_by = 'last' } = router.query;
16+
17+
const goBack = () => {
18+
if (window.history.length < 2) {
19+
router.push('/');
20+
} else {
21+
router.back();
22+
}
23+
};
24+
25+
const linkClassName = (sortName: string) =>
26+
classNames(
27+
'flex items-center whitespace-nowrap rounded-lg text-xs hover:text-blue-500',
28+
{
29+
'text-gray-500 dark:text-gray-300': sort_by !== sortName,
30+
'text-blue-500': sort_by === sortName,
31+
}
32+
);
33+
34+
return (
35+
<div className='relative my-2 bg-white dark:bg-gray-800 md:rounded-lg'>
36+
<div className='flex h-12 items-center justify-between py-2 px-4'>
37+
<div className='cursor-pointer' onClick={goBack}>
38+
<AiOutlineArrowLeft
39+
className='text-gray-500 hover:text-blue-400'
40+
size={18}
41+
/>
42+
</div>
43+
<div className='w-3/4 truncate text-center font-bold dark:text-gray-300'>
44+
{middleText}
45+
</div>
46+
<div className='flex justify-end gap-2.5 text-sm text-gray-500 dark:text-gray-400'>
47+
<NoPrefetchLink
48+
href={`/periodical/category/${encodeURIComponent(
49+
category
50+
)}?sort_by=last`}
51+
>
52+
<a className={linkClassName('last')}>{t('category.nav.last')}</a>
53+
</NoPrefetchLink>
54+
<span className='border-r border-gray-100 dark:border-gray-700' />
55+
<NoPrefetchLink
56+
href={`/periodical/category/${encodeURIComponent(
57+
category
58+
)}?sort_by=active`}
59+
>
60+
<a className={linkClassName('active')}>
61+
{t('category.nav.active')}
62+
</a>
63+
</NoPrefetchLink>
64+
</div>
65+
</div>
66+
</div>
67+
);
68+
};
69+
70+
export default CategoryNavbar;

src/components/periodical/item.tsx

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NextPage } from 'next';
22
import { useTranslation } from 'next-i18next';
3-
import { GoClock, GoRepoForked } from 'react-icons/go';
3+
import { GoCalendar, GoClock, GoRepoForked } from 'react-icons/go';
44
import { IoIosStarOutline } from 'react-icons/io';
55

66
import { CustomLink, NoPrefetchLink } from '@/components/links/CustomLink';
@@ -15,6 +15,35 @@ import { MDRender } from '../mdRender/MDRender';
1515

1616
import { PeriodicalItem, PeriodicalItemProps } from '@/types/periodical';
1717

18+
const InfoItem = ({
19+
icon: Icon,
20+
text,
21+
link,
22+
className = '',
23+
}: {
24+
icon: React.ElementType;
25+
text: string | number;
26+
link?: string;
27+
className?: string;
28+
}) => {
29+
const content = (
30+
<div className={`mr-2 flex items-center ${className}`}>
31+
<Icon size={15} className='mr-0.5' />
32+
{text}
33+
</div>
34+
);
35+
36+
if (link) {
37+
return (
38+
<CustomLink href={link} className='hover:text-blue-500'>
39+
{content}
40+
</CustomLink>
41+
);
42+
}
43+
44+
return content;
45+
};
46+
1847
const PeriodItem: NextPage<PeriodicalItemProps> = ({ item, index }) => {
1948
const { t, i18n } = useTranslation('periodical');
2049

@@ -31,26 +60,34 @@ const PeriodItem: NextPage<PeriodicalItemProps> = ({ item, index }) => {
3160
<CustomLink href={item.github_url} className='truncate'>
3261
<div
3362
onClick={() => onClickLink(item)}
34-
className='truncate text-ellipsis text-xl capitalize text-blue-500 hover:underline active:text-blue-500'
63+
className='truncate text-ellipsis text-xl text-blue-500 hover:underline active:text-blue-500'
3564
>
3665
{item.name}
3766
</div>
3867
</CustomLink>
3968
</div>
4069
{/* stars forks watch */}
4170
<div className='flex flex-row text-sm text-gray-500 dark:text-gray-400 md:text-base'>
42-
<div className='mr-2 flex items-center'>
43-
<IoIosStarOutline size={15} className='mr-0.5' />
44-
Star {numFormat(item.stars, 1)}
45-
</div>
46-
<div className='mr-2 flex items-center'>
47-
<GoRepoForked size={15} className='mr-0.5' />
48-
Fork {numFormat(item.forks, 1)}
49-
</div>
50-
<div className='mr-2 flex items-center'>
51-
<GoClock size={15} className='mr-0.5' />
52-
{fromNow(item.publish_at, i18n.language)}
53-
</div>
71+
<InfoItem
72+
icon={IoIosStarOutline}
73+
text={`Star ${numFormat(item.stars, 1)}`}
74+
/>
75+
<InfoItem
76+
icon={GoRepoForked}
77+
text={`Fork ${numFormat(item.forks, 1)}`}
78+
className='hidden md:flex'
79+
/>
80+
{item.updated_at && (
81+
<InfoItem
82+
icon={GoCalendar}
83+
text={`Vol.${item.volume_num}`}
84+
link={`/periodical/volume/${item.volume_num}`}
85+
/>
86+
)}
87+
<InfoItem
88+
icon={GoClock}
89+
text={fromNow(item.updated_at || item.publish_at, i18n.language)}
90+
/>
5491
</div>
5592
</div>
5693
<div className='flex h-14 flex-1 flex-row items-center justify-end pr-1'>

src/pages/periodical/category/[name].tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import { Trans, useTranslation } from 'next-i18next';
44
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
55
import { useMemo } from 'react';
66

7-
import Navbar from '@/components/navbar/Navbar';
7+
import CategoryNavbar from '@/components/navbar/CategoryBar';
88
import Pagination from '@/components/pagination/Pagination';
99
import PeriodItem from '@/components/periodical/item';
1010
import Seo from '@/components/Seo';
1111
import ToTop from '@/components/toTop/ToTop';
1212

1313
import { getCategory } from '@/services/category';
1414
import { nameMap } from '@/utils/constants';
15+
import stringify from '@/utils/qs-stringify';
1516
import { getClientIP } from '@/utils/util';
1617

1718
import {
@@ -20,7 +21,10 @@ import {
2021
PeriodicalItem,
2122
} from '@/types/periodical';
2223

23-
const PeriodicalCategoryPage: NextPage<CategoryPageProps> = ({ category }) => {
24+
const PeriodicalCategoryPage: NextPage<CategoryPageProps> = ({
25+
category,
26+
sortBy,
27+
}) => {
2428
const { t, i18n } = useTranslation('periodical');
2529

2630
const router = useRouter();
@@ -45,18 +49,21 @@ const PeriodicalCategoryPage: NextPage<CategoryPageProps> = ({ category }) => {
4549

4650
const onPageChange = (page: number) => {
4751
const name = category?.category_name;
48-
router.push(
49-
`/periodical/category/${encodeURIComponent(name)}?page=${page}`
50-
);
52+
const nextURL = `/periodical/category/${encodeURIComponent(
53+
name
54+
)}?${stringify({
55+
page: page,
56+
sort_by: sortBy ? sortBy : null,
57+
})}`;
58+
router.push(nextURL);
5159
};
5260

5361
if (router.isFallback) {
5462
return (
5563
<div className='mt-20 flex animate-pulse'>
5664
<Seo title='HelloGitHub 月刊' />
5765
<div className='ml-4 mt-2 w-full'>
58-
<h3 className='h-4 rounded-md bg-gray-200'></h3>
59-
66+
<h3 className='h-4 rounded-md bg-gray-200' />
6067
<ul className='mt-5 space-y-3'>
6168
<li className='h-4 w-full rounded-md bg-gray-200' />
6269
<li className='h-4 w-full rounded-md bg-gray-200' />
@@ -72,8 +79,11 @@ const PeriodicalCategoryPage: NextPage<CategoryPageProps> = ({ category }) => {
7279
<>
7380
<Seo title={t('category.title', { name: categoryItem.name })} />
7481
<div className='relative pb-6'>
75-
<Navbar middleText={categoryItem.name} endText={t('category.nav')} />
76-
82+
<CategoryNavbar
83+
category={category.category_name}
84+
middleText={categoryItem.name}
85+
t={t}
86+
/>
7787
<div className='my-2 bg-white p-4 dark:bg-gray-800 md:rounded-lg'>
7888
<div className='text-normal mb-4 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300'>
7989
<div className='whitespace-pre-wrap rounded-lg border bg-white p-2 font-normal leading-8 text-gray-500 dark:bg-gray-800 dark:text-gray-300'>
@@ -122,10 +132,12 @@ export const getServerSideProps: GetServerSideProps = async ({
122132
}) => {
123133
const ip = getClientIP(req);
124134
const name = query['name'] as string;
135+
const sortBy = query['sort_by']?.toString() ?? null;
125136
const data = await getCategory(
126137
ip,
127-
encodeURIComponent(name),
128-
query['page'] as unknown as number
138+
name,
139+
query['page'] as unknown as number,
140+
sortBy
129141
);
130142
if (!data.success) {
131143
return {
@@ -135,6 +147,7 @@ export const getServerSideProps: GetServerSideProps = async ({
135147
return {
136148
props: {
137149
category: data,
150+
sortBy: sortBy,
138151
...(await serverSideTranslations(locale as string, [
139152
'common',
140153
'periodical',

src/services/category.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,21 @@ import { Category } from '@/types/periodical';
77
export const getCategory = async (
88
ip: string,
99
name: string,
10-
page: number
10+
page: number,
11+
sortBy: string | null
1112
): Promise<Category> => {
12-
const req: RequestInit = {};
13-
req.headers = { 'x-real-ip': ip, 'x-forwarded-for': ip };
13+
const req: RequestInit = {
14+
headers: {
15+
'x-real-ip': ip,
16+
'x-forwarded-for': ip,
17+
},
18+
};
1419

1520
try {
16-
let url;
17-
if (page > 1) {
18-
url = makeUrl(`/periodical/category/${name}?page=${page}`);
19-
} else {
20-
url = makeUrl(`/periodical/category/${name}`);
21-
}
21+
const url = makeUrl(`/periodical/category/${encodeURIComponent(name)}`, {
22+
page: page > 1 ? page : null, // 仅当 page > 1 时添加该参数
23+
sort_by: sortBy ? sortBy : null,
24+
});
2225
const data = await fetcher<Category>(url, req);
2326
return data;
2427
} catch (error) {

src/types/periodical.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ export interface PeriodicalItem {
3030
watch: number;
3131
image_url: string | null;
3232
vote_total: number;
33+
volume_num: number;
3334
publish_at: string;
35+
updated_at: string;
3436
}
3537

3638
export interface PeriodicalItemProps {
@@ -54,6 +56,7 @@ export type VolumeAll = {
5456

5557
export interface CategoryPageProps {
5658
category: Category;
59+
sortBy: string | null;
5760
}
5861

5962
export interface Category {

0 commit comments

Comments
 (0)