Skip to content

Commit b9bc8c5

Browse files
authored
[나지원] sprint9 (#122)
* chore: initialize project * feat: add Header component * feat: implement BestBoards component * feat: implement BoardList component * feat: implement search function * feat: implement prefetching for boards * chore: add next-env.d.ts for environment types
1 parent 8d163dd commit b9bc8c5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1263
-475
lines changed

.gitignore

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,5 @@ yarn-error.log*
3030
# vercel
3131
.vercel
3232

33-
# typescript
34-
*.tsbuildinfo
35-
next-env.d.ts
33+
# VS Code
34+
.vscode
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.container {
2+
width: 72px;
3+
height: 72px;
4+
padding: 12px;
5+
border-radius: 8px;
6+
border: 0.75px solid var(--gray200);
7+
background-color: #ffffff;
8+
flex: 0 0 auto;
9+
}
10+
11+
.wrapper {
12+
width: 100%;
13+
height: 100%;
14+
position: relative;
15+
}
16+
17+
.image {
18+
object-fit: contain;
19+
}

components/boards/ArticleImage.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useState } from "react";
2+
import Image from "next/image";
3+
import Container from "../layout/Container";
4+
import styles from "./ArticleImage.module.css";
5+
import defaultImg from "@/public/img_default.svg";
6+
7+
interface ImageProps {
8+
src: string | null;
9+
alt: string;
10+
}
11+
12+
const ArticleImage = ({ src, alt }: ImageProps) => {
13+
const [imageSrc, setImageSrc] = useState(src ?? defaultImg);
14+
15+
const handleImageError = () => {
16+
setImageSrc(defaultImg);
17+
};
18+
19+
return (
20+
<Container className={styles.container}>
21+
<div className={styles.wrapper}>
22+
<Image
23+
fill
24+
src={imageSrc}
25+
alt={alt}
26+
className={styles.image}
27+
onError={handleImageError}
28+
/>
29+
</div>
30+
</Container>
31+
);
32+
};
33+
34+
export default ArticleImage;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
.container {
2+
display: flex;
3+
flex-direction: column;
4+
flex: 1;
5+
background-color: var(--gray50);
6+
padding: 0 24px 16px;
7+
}
8+
9+
.content {
10+
display: flex;
11+
justify-content: space-between;
12+
gap: 40px;
13+
font-size: 1.125rem;
14+
font-weight: 600;
15+
line-height: 1.625rem;
16+
color: var(--gray800);
17+
margin-top: 16px;
18+
margin-bottom: 8px;
19+
}
20+
21+
.info {
22+
display: flex;
23+
justify-content: space-between;
24+
align-items: center;
25+
font-size: 0.875rem;
26+
font-weight: 400;
27+
line-height: 1.5rem;
28+
}
29+
30+
.user {
31+
display: flex;
32+
align-items: center;
33+
gap: 8px;
34+
}
35+
36+
.nickname {
37+
color: var(--gray600);
38+
}
39+
40+
.date {
41+
color: var(--gray400);
42+
}
43+
44+
@media screen and (min-width: 1200px) {
45+
.content {
46+
font-size: 1.25rem;
47+
line-height: 2rem;
48+
margin-bottom: 18px;
49+
}
50+
}

components/boards/BestBoard.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Badge from "../ui/Badge";
2+
import Container from "../layout/Container";
3+
import ArticleImage from "./ArticleImage";
4+
import { ArticleProps } from "@/types/articleTypes";
5+
import styles from "./BestBoard.module.css";
6+
import LikeCount from "../ui/LikeCount";
7+
import { formatDate } from "@/lib/formatDate";
8+
9+
const BestBoard = ({ article }: { article: ArticleProps }) => {
10+
return (
11+
<Container className={styles.container}>
12+
<Badge />
13+
<div className={styles.content}>
14+
{article.title}
15+
<ArticleImage src={article.image} alt={`${article.id} 이미지`} />
16+
</div>
17+
<div className={styles.info}>
18+
<div className={styles.user}>
19+
<div className={styles.nickname}>{article.writer.nickname}</div>
20+
<LikeCount likeCount={article.likeCount} />
21+
</div>
22+
<div className={styles.date}>{formatDate(article.createdAt)}</div>
23+
</div>
24+
</Container>
25+
);
26+
};
27+
28+
export default BestBoard;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.wrapper {
2+
padding-bottom: 24px;
3+
}
4+
5+
.wrapper h2 {
6+
font-size: 1.125rem;
7+
font-weight: 700;
8+
line-height: 1.625rem;
9+
color: var(--gray900);
10+
margin-bottom: 16px;
11+
}
12+
13+
.container {
14+
display: flex;
15+
gap: 16px;
16+
}
17+
18+
@media screen and (min-width: 768px) {
19+
.wrapper h2 {
20+
font-size: 1.25rem;
21+
line-height: 1.5rem;
22+
margin-bottom: 24px;
23+
}
24+
}
25+
26+
@media screen and (min-width: 1200px) {
27+
.wrapper {
28+
padding-bottom: 40px;
29+
}
30+
31+
.container {
32+
gap: 24px;
33+
}
34+
}

components/boards/BestBoards.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useCallback, useEffect, useState } from "react";
2+
import BestBoard from "./BestBoard";
3+
import Container from "../layout/Container";
4+
import useResize from "@/hooks/useResize";
5+
import { fetchData } from "@/lib/fetchData";
6+
import { ArticleProps } from "@/types/articleTypes";
7+
import styles from "./BestBoards.module.css";
8+
9+
const getPageSize = (width: number) => {
10+
if (width < 768) return 1;
11+
if (width < 1200) return 2;
12+
13+
return 3;
14+
};
15+
16+
const BestBoards = () => {
17+
const [articles, setArticles] = useState([]);
18+
const [pageSize, setPageSize] = useState<number>();
19+
const viewportWidth = useResize();
20+
const BASE_URL = "https://panda-market-api.vercel.app/articles";
21+
22+
const handleLoad = useCallback(async (size: number) => {
23+
const { list } = await fetchData(BASE_URL, {
24+
query: { pageSize: size, orderBy: "like" },
25+
});
26+
setArticles(list);
27+
}, []);
28+
29+
useEffect(() => {
30+
if (!viewportWidth) return;
31+
32+
const nextPageSize = getPageSize(viewportWidth);
33+
if (nextPageSize !== pageSize) {
34+
setPageSize(nextPageSize);
35+
handleLoad(nextPageSize);
36+
}
37+
}, [viewportWidth, handleLoad, pageSize]);
38+
39+
return (
40+
<section className={styles.wrapper}>
41+
<h2>베스트 게시글</h2>
42+
<Container className={styles.container}>
43+
{articles.map((article: ArticleProps) => (
44+
<BestBoard key={article.id} article={article} />
45+
))}
46+
</Container>
47+
</section>
48+
);
49+
};
50+
51+
export default BestBoards;

components/boards/Board.module.css

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
.container {
2+
background-color: #fcfcfc;
3+
border-bottom: 1px solid var(--gray200);
4+
padding-bottom: 24px;
5+
}
6+
7+
.content {
8+
display: flex;
9+
justify-content: space-between;
10+
font-size: 1.125rem;
11+
font-weight: 600;
12+
line-height: 1.625rem;
13+
color: var(--gray800);
14+
}
15+
16+
.info {
17+
display: flex;
18+
justify-content: space-between;
19+
}
20+
21+
.authorInfo {
22+
font-size: 0.875rem;
23+
font-weight: 400;
24+
line-height: 1.5rem;
25+
}
26+
27+
.authorInfo img {
28+
width: 24px;
29+
height: 24px;
30+
}
31+
32+
.authorInfo span {
33+
color: var(--gray600);
34+
}
35+
36+
.authorInfo time {
37+
color: var(--gray400);
38+
}
39+
40+
.like img {
41+
width: 24px;
42+
height: 24px;
43+
}
44+
45+
.like span {
46+
font-size: 1rem;
47+
line-height: 1.625rem;
48+
}
49+
50+
@media screen and (min-width: 768px) {
51+
.content {
52+
font-size: 1.25rem;
53+
line-height: 2rem;
54+
margin-bottom: 18px;
55+
}
56+
}

components/boards/Board.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ArticleProps } from "@/types/articleTypes";
2+
import Container from "../layout/Container";
3+
import styles from "./Board.module.css";
4+
import ArticleImage from "./ArticleImage";
5+
import LikeCount from "../ui/LikeCount";
6+
import AuthorInfo from "../ui/AuthorInfo";
7+
8+
const Board = ({ board }: { board: ArticleProps }) => {
9+
return (
10+
<Container className={styles.container}>
11+
<div className={styles.content}>
12+
{board.title}
13+
<ArticleImage src={board.image} alt={`${board.id} 이미지`} />
14+
</div>
15+
<div className={styles.info}>
16+
<AuthorInfo
17+
className={styles.authorInfo}
18+
nickname={board.writer.nickname}
19+
date={board.createdAt}
20+
/>
21+
<LikeCount className={styles.like} likeCount={board.likeCount} />
22+
</div>
23+
</Container>
24+
);
25+
};
26+
27+
export default Board;

0 commit comments

Comments
 (0)