Skip to content

Commit

Permalink
[김희진] sprint10 (#125)
Browse files Browse the repository at this point in the history
* refactor: add import type

* refactor: code review

* feat: add select box on board filter

* feat: add board card css

* feat: add infinite scroll

* chore: add image domain

* refactor: extract variable

* fix: remove useApi

* refactor: extract useBorad hook

* refactor: rename file

* chore: remove unused import
  • Loading branch information
devmanta authored Nov 5, 2024
1 parent c4034c1 commit 08ae16c
Show file tree
Hide file tree
Showing 20 changed files with 373 additions and 39 deletions.
7 changes: 6 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
const nextConfig = {
reactStrictMode: true,
images: {
domains: ['sprint-fe-project.s3.ap-northeast-2.amazonaws.com'],
domains: [
'sprint-fe-project.s3.ap-northeast-2.amazonaws.com',
'example.com',
'via.placeholder.com',
'flexible.img.hani.co.kr',
],
},
};

Expand Down
9 changes: 9 additions & 0 deletions pages/boards/[id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useRouter } from 'next/router';
import styles from './[id].module.css';

export default function BoardDetail() {
const router = useRouter();
const { id } = router.query;

return <div>{id}</div>;
}
13 changes: 9 additions & 4 deletions pages/boards/components/BestBoardCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Image from 'next/image';
import styles from './BestBoardCard.module.css';
import medalSvg from '@/src/assets/ic_medal.svg';
import heardSvg from '@/src/assets/ic_heart.svg';
import { Board } from '@/src/apis/boardTypes';
import heartSvg from '@/src/assets/ic_heart.svg';
import type { Board } from '@/src/apis/boardTypes';

interface BestBoardCardProps
extends Pick<
Expand All @@ -26,13 +26,18 @@ export default function BestBoardCard({
<div className={styles.contentContainer}>
<h4 className={styles.contentTitle}>{title}</h4>
<div className={styles.contentImgWrapper}>
<Image src={image} alt="medal" fill style={{ objectFit: 'cover' }} />
<Image
src={image}
alt="게시판 첨부이미지"
fill
style={{ objectFit: 'cover' }}
/>
</div>
</div>
<div className={styles.additionalInfo}>
<span>{writer.nickname}</span>
<div className={styles.likeCountWrapper}>
<Image src={heardSvg} alt="heardIcon" width={16} height={16} />
<Image src={heartSvg} alt="heartIcon" width={16} height={16} />
<span>{likeCount}</span>
</div>
<span>{new Date(createdAt).toLocaleDateString()}</span>
Expand Down
2 changes: 1 addition & 1 deletion pages/boards/components/BestBoards.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Board } from '@/src/apis/boardTypes';
import type { Board } from '@/src/apis/boardTypes';
import BestBoardCard from './BestBoardCard';
import styles from './BestBoards.module.css';

Expand Down
74 changes: 74 additions & 0 deletions pages/boards/components/BoardCard.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
.boardCard {
background: #fcfcfc;
padding: 20px;
position: relative;
display: flex;
flex-direction: column;
gap: 16px;
}

.boardCard::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 1px;
background-color: var(--Secondary-200);
}

.contentContainer {
display: flex;
gap: 8px;
min-height: 72px;
}

.title {
color: var(--Secondary-800);
font-size: 20px;
font-weight: 600;

flex: 1;
}

.imageWrapper {
position: relative;
width: 72px;
height: 72px;

border-radius: 6px;
border: 1px solid var(--Secondary-200, #e5e7eb);
background: #fff;
}

.additionalInfo {
display: flex;
justify-content: space-between;
}

.infoWrapper {
display: flex;
align-items: center;
gap: 8px;
}

.nickname {
color: var(--Secondary-600, #4b5563);
font-size: 14px;
font-weight: 400;
}

.date {
color: var(--Secondary-400, #9ca3af);
font-size: 14px;
font-weight: 400;
}

.likeCountWrapper {
display: flex;
align-items: center;
gap: 8px;
color: var(--Secondary-500, #6b7280);
font-size: 16px;
font-weight: 400;
}
44 changes: 44 additions & 0 deletions pages/boards/components/BoardCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Image from 'next/image';
import styles from './BoardCard.module.css';
import type { Board } from '@/src/apis/boardTypes';
import heartSvg from '@/src/assets/ic_heart.svg';
import avatarSvg from '@/src/assets/avatar.svg';

export default function BoardCard({
title,
image,
writer,
createdAt,
likeCount,
}: Board) {
return (
<div className={styles.boardCard}>
<div className={styles.contentContainer}>
<p className={styles.title}>{title}</p>
{image && (
<div className={styles.imageWrapper}>
<Image
src={image}
alt="게시판 첨부이미지"
fill
style={{ objectFit: 'cover' }}
/>
</div>
)}
</div>
<div className={styles.additionalInfo}>
<div className={styles.infoWrapper}>
<Image src={avatarSvg} alt="avatar" width={24} height={24} />
<span className={styles.nickname}>{writer.nickname}</span>
<span className={styles.date}>
{new Date(createdAt).toLocaleDateString()}
</span>
</div>
<div className={styles.likeCountWrapper}>
<Image src={heartSvg} alt="heardIcon" width={16} height={16} />
<span>{likeCount}</span>
</div>
</div>
</div>
);
}
27 changes: 27 additions & 0 deletions pages/boards/components/Boards.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@
font-weight: 700;
}

.filter {
display: flex;
gap: 20px;
}

.searchBar {
padding: 9px 20px 9px 16px;
border-radius: 12px;
background: var(--Secondary-100, #f3f4f6);
flex: 1;

display: flex;
align-items: center;
Expand All @@ -31,6 +37,27 @@
color: var(--Secondary-400, #9ca3af);
}

.options {
height: 100%;
padding: 12px 20px;
padding-right: 50px;

border-radius: 12px;
border: 1px solid var(--Secondary-200, #e5e7eb);

appearance: none;
background: url('../../../src/assets/ic_arrow_down.svg') no-repeat right 10px
center;
}

.boardsContainer {
margin-top: 24px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 24px;
}

/* tablet */
@media screen and (max-width: 1199px) {
.boardsHeader {
Expand Down
42 changes: 38 additions & 4 deletions pages/boards/components/Boards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,59 @@ import Image from 'next/image';
import Button from '@/src/components/Button';
import styles from './Boards.module.css';
import searchSvg from '@/src/assets/ic_search.svg';
import { useState } from 'react';
import BoardCard from './BoardCard';
import Link from 'next/link';
import { useBoards } from '@/src/hooks/useBoards';

export default function Boards() {
const [orderBy, setOrderBy] = useState('recent');
const { boards, isLoading, error, observerRef, resetBoards } =
useBoards(orderBy);

const handleOptionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newOrder = e.target.value;
setOrderBy(newOrder);
resetBoards();
};

return (
<div>
<div className={styles.boardsHeader}>
<section className={styles.boardsHeader}>
<h3 className={styles.title}>게시글</h3>
<div>
<Button>글쓰기</Button>
</div>
</div>
<div className={styles.filter}>
</section>
<section className={styles.filter}>
<div className={styles.searchBar}>
<Image src={searchSvg} alt="searchIcon" width={24} height={24} />
<input
className={styles.searchData}
placeholder="검색할 상품을 입력해주세요"
/>
</div>
</div>
<div>
<select
className={styles.options}
id="options"
onChange={handleOptionChange}
>
<option value="recent">최신순</option>
<option value="like">좋아요순</option>
</select>
</div>
</section>
<section className={styles.boardsContainer}>
{boards.map((board) => (
<Link key={board.id} href={`/boards/${board.id}`}>
<BoardCard {...board} />
</Link>
))}
{isLoading && <div>Loading...</div>}
{error && <div>{error}</div>}
</section>
<div ref={observerRef} style={{ height: '1px' }}></div>
</div>
);
}
4 changes: 2 additions & 2 deletions pages/boards/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GetStaticProps } from 'next';
import BestBoards from './components/BestBoards';
import { Board } from '@/src/apis/boardTypes';
import type { Board } from '@/src/apis/boardTypes';
import { getBoards } from '@/src/apis/boardsApi';
import Boards from './components/Boards';

Expand Down Expand Up @@ -28,6 +28,6 @@ export const getStaticProps: GetStaticProps = async () => {
props: {
boards: list || [],
},
revalidate: 600, // Re-generate the page every 600 seconds (ISR)
revalidate: 600,
};
};
2 changes: 1 addition & 1 deletion src/apis/boardTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ export interface GetBoardsResponse {
export interface GetBoardsRequestParams {
page?: number;
pageSize?: number;
orderBy?: 'recent' | 'like';
orderBy?: string;
keyword?: string;
}
2 changes: 1 addition & 1 deletion src/apis/boardsApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Board, GetBoardsResponse, GetBoardsRequestParams } from './boardTypes';
import type { GetBoardsResponse, GetBoardsRequestParams } from './boardTypes';

const BASE_URL = 'https://panda-market-api.vercel.app';

Expand Down
5 changes: 5 additions & 0 deletions src/assets/ic_arrow_down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import styles from './Button.module.css';

interface PrimaryButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
className?: string;
}

export default function Button({
children,
className,
className = '',
...props
}: PrimaryButtonProps) {
return (
Expand Down
19 changes: 19 additions & 0 deletions src/components/GlobalLayout.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,22 @@
margin-top: 75px;
width: 100%;
}

.maxContainer {
max-width: var(--size-max-width);
margin: auto;
}

/* tablet */
@media screen and (max-width: 1199px) {
.maxContainer {
max-width: 760px;
}
}

/* mobile */
@media screen and (max-width: 767px) {
.maxContainer {
max-width: 360px;
}
}
2 changes: 1 addition & 1 deletion src/components/GlobalLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function GlobalLayout({ children }: { children: ReactNode }) {
<>
<Header />
<main className={styles.main}>
<div className={'max-container'}>{children}</div>
<div className={styles.maxContainer}>{children}</div>
</main>
</>
);
Expand Down
4 changes: 4 additions & 0 deletions src/components/Header.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
border-bottom: 1px solid #dfdfdf;
}

.active {
color: var(--Primary-100);
}

.headerContainer {
padding: 10px 0;
display: flex;
Expand Down
Loading

0 comments on commit 08ae16c

Please sign in to comment.