Skip to content

Commit ac20ff7

Browse files
committed
refactor: optimize core components with TypeScript and accessibility improvements
- Add comprehensive component documentation - Add TypeScript interfaces and type definitions - Improve component structure and performance - Enhance accessibility with ARIA labels - Add code comments for better maintainability - Optimize performance with useMemo - Split ArticleList into smaller components
1 parent d688538 commit ac20ff7

File tree

3 files changed

+240
-87
lines changed

3 files changed

+240
-87
lines changed

client/src/components/ArticleList/index.tsx

Lines changed: 132 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,157 @@
1+
/**
2+
* ArticleList Component
3+
*
4+
* This component displays a list of articles in a card format.
5+
* Each article card includes:
6+
* - Cover image (with lazy loading)
7+
* - Title
8+
* - Category tag
9+
* - Summary
10+
* - Meta information (likes, views, publish date)
11+
*
12+
* Features:
13+
* - Lazy loading for images
14+
* - Responsive design
15+
* - Category navigation
16+
* - Article statistics display
17+
*/
18+
119
import { EyeOutlined, FolderOutlined, HeartOutlined, HistoryOutlined } from '@ant-design/icons';
220
import { Spin, Tag } from 'antd';
321
import { useTranslations } from 'next-intl';
422
import Link from 'next/link';
5-
import React, { useContext } from 'react';
23+
import React, { useContext, useMemo } from 'react';
624
import LazyLoad from 'react-lazyload';
725
import LogoSvg from '../../assets/LogoSvg';
826

927
import { LocaleTime } from '@/components/LocaleTime';
10-
1128
import { GlobalContext } from '@/context/global';
1229
import { getColorFromNumber } from '@/utils';
1330
import style from './index.module.scss';
1431

15-
interface IProps {
16-
articles: IArticle[];
32+
interface Article {
33+
id: string;
34+
title: string;
35+
cover?: string;
36+
summary: string;
37+
category?: {
38+
value: string;
39+
label: string;
40+
};
41+
likes: number;
42+
views: number;
43+
publishAt: string;
44+
}
45+
46+
interface ArticleListProps {
47+
articles: Article[];
1748
coverHeight?: number;
1849
asRecommend?: boolean;
1950
}
2051

21-
export const ArticleList: React.FC<IProps> = ({ articles = [] }) => {
52+
/**
53+
* ArticleCard Component
54+
* Renders a single article card with all its details
55+
*/
56+
const ArticleCard: React.FC<{ article: Article; categoryIndex: number }> = ({ article, categoryIndex }) => {
57+
return (
58+
<div className={style.articleItem}>
59+
{/* Cover Image Section */}
60+
<div className={style.coverWrapper}>
61+
{article.cover ? (
62+
<LazyLoad height={120} placeholder={<Spin />}>
63+
<div className={style.coverWrapper}>
64+
<Link href={`/article/[id]`} as={`/article/${article.id}`} scroll={false}>
65+
<img src={article.cover} alt={`${article.title} cover`} loading="lazy" />
66+
</Link>
67+
</div>
68+
</LazyLoad>
69+
) : (
70+
<LogoSvg />
71+
)}
72+
</div>
73+
74+
{/* Article Content Section */}
75+
<div className={style.articleWrapper}>
76+
<Link href={`/article/[id]`} as={`/article/${article.id}`} scroll={false}>
77+
<a aria-label={article.title} className={style.link}>
78+
<header>
79+
<div className={style.title} title={article.title}>
80+
{article.title}
81+
</div>
82+
<div className={style.info}>
83+
{article.category && categoryIndex >= 0 && (
84+
<Link
85+
href={`/category/${article?.category?.value}`}
86+
as={`/category/${article?.category?.value}`}
87+
scroll={false}
88+
>
89+
<Tag className={style.antBadge} icon={<FolderOutlined />}>
90+
<span className={style.category}>{article.category?.label}</span>
91+
</Tag>
92+
</Link>
93+
)}
94+
</div>
95+
</header>
96+
97+
{/* Article Summary and Meta Information */}
98+
<main className={style.desc} title={article.title}>
99+
<div className={style.contentWrapper}>
100+
<div className={style.desc} title={article.summary}>
101+
<span dangerouslySetInnerHTML={{ __html: article.summary }} />
102+
</div>
103+
<div className={style.meta}>
104+
<div>
105+
<span>
106+
<HeartOutlined />
107+
<span className={style.number}>{article.likes}</span>
108+
</span>
109+
<span className={style.separator}>·</span>
110+
<span>
111+
<EyeOutlined />
112+
<span className={style.number}>{article.views}</span>
113+
</span>
114+
</div>
115+
<span className={style.time}>
116+
<HistoryOutlined />
117+
<LocaleTime date={article.publishAt} format="yyyy-MM-dd" />
118+
</span>
119+
</div>
120+
</div>
121+
</main>
122+
</a>
123+
</Link>
124+
</div>
125+
<span className={style.badge} />
126+
</div>
127+
);
128+
};
129+
130+
/**
131+
* Main ArticleList Component
132+
* Renders a list of article cards with proper handling of empty state
133+
*/
134+
export const ArticleList: React.FC<ArticleListProps> = ({ articles = [] }) => {
22135
const t = useTranslations();
23136
const { categories } = useContext(GlobalContext);
24137

138+
// Memoize the category indices to avoid recalculating on every render
139+
const categoryIndices = useMemo(() => {
140+
return articles.map(article =>
141+
categories?.findIndex((category) => category?.value === article?.category?.value)
142+
);
143+
}, [articles, categories]);
144+
25145
return (
26146
<div className={style.wrapper}>
27147
{articles && articles.length ? (
28-
articles.map((article: IArticle) => {
29-
const categoryIndex = categories?.findIndex((category) => category?.value === article?.category?.value);
30-
return (
31-
<div key={article.id} className={style.articleItem}>
32-
<div className={style.coverWrapper}>
33-
{article.cover ? (
34-
<LazyLoad height={120} placeholder={<Spin />}>
35-
<div className={style.coverWrapper}>
36-
<Link href={`/article/[id]`} as={`/article/${article.id}`} scroll={false}>
37-
<img src={article.cover} alt="cover" />
38-
</Link>
39-
</div>
40-
</LazyLoad>
41-
) : (
42-
<LogoSvg />
43-
)}
44-
</div>
45-
<div className={style.articleWrapper}>
46-
<Link href={`/article/[id]`} as={`/article/${article.id}`} scroll={false}>
47-
<a aria-label={article.title} className={style.link}>
48-
<header>
49-
<div className={style.title} title={article.title}>
50-
{article.title}
51-
</div>
52-
<div className={style.info}>
53-
{article.category && categoryIndex >= 0 && (
54-
<Link
55-
href={`/category/${article?.category?.value}`}
56-
as={`/category/${article?.category?.value}`}
57-
scroll={false}
58-
>
59-
<Tag className={style.antBadge} icon={<FolderOutlined />}>
60-
<span className={style.category}>{article.category?.label}</span>
61-
</Tag>
62-
</Link>
63-
)}
64-
</div>
65-
</header>
66-
<main className={style.desc} title={article.title}>
67-
<div className={style.contentWrapper}>
68-
<div className={style.desc} title={article.summary}>
69-
<span dangerouslySetInnerHTML={{ __html: article.summary }} />
70-
</div>
71-
<div className={style.meta}>
72-
<div>
73-
<span>
74-
<HeartOutlined />
75-
<span className={style.number}>{article.likes}</span>
76-
</span>
77-
<span className={style.separator}>·</span>
78-
<span>
79-
<EyeOutlined />
80-
<span className={style.number}>{article.views}</span>
81-
</span>
82-
</div>
83-
<span className={style.time}>
84-
<HistoryOutlined />
85-
<LocaleTime date={article.publishAt} format="yyyy-MM-dd" />
86-
</span>
87-
</div>
88-
</div>
89-
</main>
90-
</a>
91-
</Link>
92-
</div>
93-
<span className={style.badge} />
94-
</div>
95-
);
96-
})
148+
articles.map((article, index) => (
149+
<ArticleCard
150+
key={article.id}
151+
article={article}
152+
categoryIndex={categoryIndices[index]}
153+
/>
154+
))
97155
) : (
98156
<div className={'empty'}>{t('empty')}</div>
99157
)}

client/src/components/Footer/index.tsx

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,70 @@
1+
/**
2+
* Footer Component
3+
*
4+
* This component represents the main footer of the application.
5+
* It includes:
6+
* - About Us section
7+
* - System footer information
8+
* - Contact information
9+
*
10+
* Features:
11+
* - Responsive design
12+
* - Customizable background
13+
* - HTML content support for footer info
14+
*/
15+
116
import { ContactInfo } from '@/components/AboutUs';
217
import cls from 'classnames';
18+
import { useTranslations } from 'next-intl';
19+
import React from 'react';
320

421
import style from './index.module.scss';
5-
import { useTranslations } from 'next-intl';
622

7-
export const Footer = ({ setting, className = '', hasBg = false }) => {
23+
interface FooterProps {
24+
setting: {
25+
systemFooterInfo?: string;
26+
};
27+
className?: string;
28+
hasBg?: boolean;
29+
}
30+
31+
/**
32+
* Footer Component
33+
*
34+
* @param {FooterProps} props - Component props
35+
* @param {Object} props.setting - Footer settings
36+
* @param {string} [props.setting.systemFooterInfo] - HTML content for footer info
37+
* @param {string} [props.className] - Additional CSS class name
38+
* @param {boolean} [props.hasBg] - Whether to show background
39+
*/
40+
export const Footer: React.FC<FooterProps> = ({
41+
setting,
42+
className = '',
43+
hasBg = false
44+
}) => {
845
const t = useTranslations();
46+
947
return (
10-
<footer className={cls(style.footer, className, hasBg && style.hasBg)}>
48+
<footer
49+
className={cls(style.footer, className, hasBg && style.hasBg)}
50+
role="contentinfo"
51+
>
1152
<div className={cls('container', style.container)}>
53+
{/* Left Section - About Us and Copyright */}
1254
<ul className={style.left}>
1355
<span className={style.title}>{t('aboutUs')}</span>
14-
{setting && setting.systemFooterInfo && (
56+
{setting?.systemFooterInfo && (
1557
<div
1658
className={style.copyright}
1759
dangerouslySetInnerHTML={{
1860
__html: setting.systemFooterInfo,
1961
}}
20-
></div>
62+
aria-label="Footer information"
63+
/>
2164
)}
2265
</ul>
66+
67+
{/* Right Section - Contact Information */}
2368
<ContactInfo />
2469
</div>
2570
</footer>

0 commit comments

Comments
 (0)