From b5ba32bdb5f93a42d0bc1d23749954c6fa9056a6 Mon Sep 17 00:00:00 2001 From: najitwo Date: Sat, 26 Oct 2024 22:00:58 +0900 Subject: [PATCH] feat: implement BoardList component --- components/boards/ArticleImage.tsx | 18 +++- components/boards/BestBoard.module.css | 4 - components/boards/BestBoards.module.css | 9 +- components/boards/Board.module.css | 56 ++++++++++++ components/boards/Board.tsx | 27 ++++++ components/boards/BoardLIst.module.css | 112 ++++++++++++++++++++++++ components/boards/BoardList.tsx | 61 +++++++++++++ components/layout/Container.tsx | 8 ++ components/ui/AuthorInfo.module.css | 19 ++++ components/ui/AuthorInfo.tsx | 27 ++++++ components/ui/Button.module.css | 13 +++ components/ui/Button.tsx | 27 ++++++ components/ui/Dropdown.module.css | 37 ++++++++ components/ui/Dropdown.tsx | 54 ++++++++++++ components/ui/LikeCount.module.css | 7 ++ components/ui/SearchBar.module.css | 32 +++++++ components/ui/SearchBar.tsx | 19 ++++ pages/boards.tsx | 6 +- public/ic_arrow_down.svg | 3 + public/ic_search.svg | 3 + public/ic_sort.svg | 6 ++ public/img_default.svg | 16 ++++ 22 files changed, 555 insertions(+), 9 deletions(-) create mode 100644 components/boards/Board.module.css create mode 100644 components/boards/Board.tsx create mode 100644 components/boards/BoardLIst.module.css create mode 100644 components/boards/BoardList.tsx create mode 100644 components/ui/AuthorInfo.module.css create mode 100644 components/ui/AuthorInfo.tsx create mode 100644 components/ui/Button.module.css create mode 100644 components/ui/Button.tsx create mode 100644 components/ui/Dropdown.module.css create mode 100644 components/ui/Dropdown.tsx create mode 100644 components/ui/SearchBar.module.css create mode 100644 components/ui/SearchBar.tsx create mode 100644 public/ic_arrow_down.svg create mode 100644 public/ic_search.svg create mode 100644 public/ic_sort.svg create mode 100644 public/img_default.svg diff --git a/components/boards/ArticleImage.tsx b/components/boards/ArticleImage.tsx index a55efaf2c..69c02306c 100644 --- a/components/boards/ArticleImage.tsx +++ b/components/boards/ArticleImage.tsx @@ -1,17 +1,31 @@ +import { useState } from "react"; import Image from "next/image"; import Container from "../layout/Container"; import styles from "./ArticleImage.module.css"; +import defaultImg from "@/public/img_default.svg"; interface ImageProps { - src: string; + src: string | null; alt: string; } const ArticleImage = ({ src, alt }: ImageProps) => { + const [imageSrc, setImageSrc] = useState(src ?? defaultImg); + + const handleImageError = () => { + setImageSrc(defaultImg); + }; + return (
- {alt} + {alt}
); diff --git a/components/boards/BestBoard.module.css b/components/boards/BestBoard.module.css index d739cfca8..e95b525bb 100644 --- a/components/boards/BestBoard.module.css +++ b/components/boards/BestBoard.module.css @@ -41,13 +41,9 @@ color: var(--gray400); } -@media screen and (min-width: 768px) { -} - @media screen and (min-width: 1200px) { .content { font-size: 1.25rem; - font-weight: 600; line-height: 2rem; margin-bottom: 18px; } diff --git a/components/boards/BestBoards.module.css b/components/boards/BestBoards.module.css index 838bcf65d..1efec06a4 100644 --- a/components/boards/BestBoards.module.css +++ b/components/boards/BestBoards.module.css @@ -1,3 +1,7 @@ +.wrapper { + padding-bottom: 24px; +} + .wrapper h2 { font-size: 1.125rem; font-weight: 700; @@ -9,7 +13,6 @@ .container { display: flex; gap: 16px; - /* justify-content: space-between; */ } @media screen and (min-width: 768px) { @@ -21,6 +24,10 @@ } @media screen and (min-width: 1200px) { + .wrapper { + padding-bottom: 40px; + } + .container { gap: 24px; } diff --git a/components/boards/Board.module.css b/components/boards/Board.module.css new file mode 100644 index 000000000..53e252743 --- /dev/null +++ b/components/boards/Board.module.css @@ -0,0 +1,56 @@ +.container { + background-color: #fcfcfc; + border-bottom: 1px solid var(--gray200); + padding-bottom: 24px; +} + +.content { + display: flex; + justify-content: space-between; + font-size: 1.125rem; + font-weight: 600; + line-height: 1.625rem; + color: var(--gray800); +} + +.info { + display: flex; + justify-content: space-between; +} + +.authorInfo { + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5rem; +} + +.authorInfo img { + width: 24px; + height: 24px; +} + +.authorInfo span { + color: var(--gray600); +} + +.authorInfo time { + color: var(--gray400); +} + +.like img { + width: 24px; + height: 24px; +} + +.like span { + font-size: 1rem; + line-height: 1.625rem; +} + +@media screen and (min-width: 768px) { + .content { + font-size: 1.25rem; + line-height: 2rem; + margin-bottom: 18px; + } +} diff --git a/components/boards/Board.tsx b/components/boards/Board.tsx new file mode 100644 index 000000000..2c3c357fb --- /dev/null +++ b/components/boards/Board.tsx @@ -0,0 +1,27 @@ +import { ArticleProps } from "@/types/articleTypes"; +import Container from "../layout/Container"; +import styles from "./Board.module.css"; +import ArticleImage from "./ArticleImage"; +import LikeCount from "../ui/LikeCount"; +import AuthorInfo from "../ui/AuthorInfo"; + +const Board = ({ board }: { board: ArticleProps }) => { + return ( + +
+ {board.title} + +
+
+ + +
+
+ ); +}; + +export default Board; diff --git a/components/boards/BoardLIst.module.css b/components/boards/BoardLIst.module.css new file mode 100644 index 000000000..124813c50 --- /dev/null +++ b/components/boards/BoardLIst.module.css @@ -0,0 +1,112 @@ +.wrapper h2 { + font-size: 1.125rem; + font-weight: 700; + line-height: 1.625rem; + color: var(--gray900); +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.button { + padding: 11.5px 23px; +} + +.toolbar { + display: flex; + align-items: center; + gap: 13px; + margin-bottom: 16px; +} + +.dropdown { + order: 1; + user-select: none; +} + +.dropdown > div { + display: flex; + justify-content: center; + align-items: center; + width: 42px; + height: 42px; + border-radius: 12px; + border: 1px solid var(--gray200); +} + +.dropdown ul { + width: 130px; + height: 84px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.dropdown li { + text-align: center; + font-size: 1rem; + font-weight: 400; + line-height: 1.625rem; + color: var(--gray800); + padding: 9px 0 7px; +} + +.dropdown li:first-child { + border-bottom: 1px solid var(--gray200); +} + +.container { + display: flex; + flex-direction: column; + gap: 24px; +} + +@media screen and (min-width: 768px) { + .wrapper h2 { + font-size: 1.25rem; + line-height: 1.5rem; + } + + .header { + margin-bottom: 48px; + } + + .toolbar { + gap: 6px; + margin-bottom: 40px; + } + + .dropdown { + order: 0; + } + + .dropdown > div { + justify-content: space-evenly; + width: 130px; + height: 42px; + } + + .dropdown img { + content: url("/ic_arrow_down.svg"); + } + + .dropdown span { + display: inline-block; + } +} + +@media screen and (min-width: 1200px) { + .header { + margin-bottom: 24px; + } + + .toolbar { + gap: 16px; + margin-bottom: 24px; + } +} diff --git a/components/boards/BoardList.tsx b/components/boards/BoardList.tsx new file mode 100644 index 000000000..4e22638e5 --- /dev/null +++ b/components/boards/BoardList.tsx @@ -0,0 +1,61 @@ +import { useState, useCallback, useEffect } from "react"; +import Image from "next/image"; +import Board from "./Board"; +import Button from "../ui/Button"; +import Dropdown, { DropdownOptions } from "../ui/Dropdown"; +import SearchBar from "../ui/SearchBar"; +import { fetchData } from "@/lib/fetchData"; +import styles from "./BoardLIst.module.css"; +import sortIcon from "@/public/ic_sort.svg"; +import Container from "../layout/Container"; +import { ArticleProps } from "@/types/articleTypes"; + +const BoardList = () => { + const [boards, setBoards] = useState([]); + const [order, setOrder] = useState("recent"); + const BASE_URL = "https://panda-market-api.vercel.app/articles"; + const options: DropdownOptions = { + recent: "최신순", + like: "인기순", + }; + + const handleLoad = useCallback(async () => { + const { list } = await fetchData(BASE_URL, { + query: { + orderBy: order, + }, + }); + setBoards(list); + }, [order]); + + useEffect(() => { + handleLoad(); + }, [handleLoad]); + + return ( +
+
+

게시글

+ +
+
+ + + {options[order]} + 드롭다운 + +
+ + {boards.map((board: ArticleProps) => ( + + ))} + +
+ ); +}; + +export default BoardList; diff --git a/components/layout/Container.tsx b/components/layout/Container.tsx index 137483c9f..62a1835a4 100644 --- a/components/layout/Container.tsx +++ b/components/layout/Container.tsx @@ -16,6 +16,14 @@ export default function Container({ page ? styles.page : "" } ${className}`; + if (page) { + return ( +
+ {children} +
+ ); + } + return (
{children} diff --git a/components/ui/AuthorInfo.module.css b/components/ui/AuthorInfo.module.css new file mode 100644 index 000000000..e6b88210d --- /dev/null +++ b/components/ui/AuthorInfo.module.css @@ -0,0 +1,19 @@ +.authorInfo { + display: flex; + align-items: center; + gap: 8px; +} + +.user { + display: flex; + align-items: center; + gap: 8px; +} + +.nickname { + color: var(--gray600); +} + +.date { + color: var(--gray400); +} diff --git a/components/ui/AuthorInfo.tsx b/components/ui/AuthorInfo.tsx new file mode 100644 index 000000000..0fa748016 --- /dev/null +++ b/components/ui/AuthorInfo.tsx @@ -0,0 +1,27 @@ +import Image from "next/image"; +import styles from "./AuthorInfo.module.css"; +import profileIcon from "@/public/ic_profile.svg"; +import { formatDate } from "@/lib/formatDate"; + +interface Props { + className?: string; + nickname: string; + image?: string; + date: string; +} + +const AuthorInfo = ({ className, nickname, image, date }: Props) => { + return ( +
+ 프로필 아이콘 +
+ {nickname} + +
+
+ ); +}; + +export default AuthorInfo; diff --git a/components/ui/Button.module.css b/components/ui/Button.module.css new file mode 100644 index 000000000..ae5ce549d --- /dev/null +++ b/components/ui/Button.module.css @@ -0,0 +1,13 @@ +.button { + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + color: #ffffff; + background-color: var(--blue); + cursor: pointer; +} + +.button:disabled { + background-color: var(--gray400); + cursor: auto; +} diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx new file mode 100644 index 000000000..ff2f9314a --- /dev/null +++ b/components/ui/Button.tsx @@ -0,0 +1,27 @@ +import { ButtonHTMLAttributes, PropsWithChildren } from "react"; +import styles from "./Button.module.css"; + +interface ButtonProps extends ButtonHTMLAttributes { + className?: string; +} + +const Button = ({ + type = "button", + className = "", + disabled = false, + children, + onClick = () => {}, +}: PropsWithChildren) => { + return ( + + ); +}; + +export default Button; diff --git a/components/ui/Dropdown.module.css b/components/ui/Dropdown.module.css new file mode 100644 index 000000000..e702f7c2c --- /dev/null +++ b/components/ui/Dropdown.module.css @@ -0,0 +1,37 @@ +.dropdown { + position: relative; +} + +.dropdownButton { + cursor: pointer; +} + +.dropdownButton > span { + display: none; +} + +.dropdownButton > img { + display: block; + width: 24px; + height: 24px; +} + +.dropdownMenu { + position: absolute; + border: 1px solid var(--gray200); + border-radius: 12px; + background-color: #ffffff; + right: 0; + top: 120%; + z-index: 1; +} + +.dropdownMenu li { + width: 100%; + cursor: pointer; +} + +.dropdownMenu button { + width: 100%; + cursor: pointer; +} diff --git a/components/ui/Dropdown.tsx b/components/ui/Dropdown.tsx new file mode 100644 index 000000000..89519a356 --- /dev/null +++ b/components/ui/Dropdown.tsx @@ -0,0 +1,54 @@ +import { MouseEvent, ReactNode, useState } from "react"; +import styles from "./Dropdown.module.css"; + +export interface DropdownOptions { + [key: string]: string; +} + +interface Props { + className: string; + options: DropdownOptions; + onSelect: (value: string) => void; + children: ReactNode; +} + +const Dropdown = ({ + className = "", + options = {}, + onSelect, + children, +}: Props) => { + const [isOpen, setIsOpen] = useState(false); + + const toggleDropdown = () => { + setIsOpen(!isOpen); + }; + + const handleSelect = (event: MouseEvent) => { + onSelect(event.currentTarget.value); + setIsOpen(false); + }; + + return ( +
+
+ {children} +
+ {isOpen && ( +
    + {Object.keys(options).map((option) => { + return ( +
  • + +
  • + ); + })} +
+ )} +
+ ); +}; + +export default Dropdown; diff --git a/components/ui/LikeCount.module.css b/components/ui/LikeCount.module.css index 2d61c07f0..680886bef 100644 --- a/components/ui/LikeCount.module.css +++ b/components/ui/LikeCount.module.css @@ -5,6 +5,13 @@ color: var(--gray500); } +.container span { + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5rem; + color: var(--gray500); +} + .image { width: 16px; height: 16px; diff --git a/components/ui/SearchBar.module.css b/components/ui/SearchBar.module.css new file mode 100644 index 000000000..3b578ebad --- /dev/null +++ b/components/ui/SearchBar.module.css @@ -0,0 +1,32 @@ +.container { + position: relative; + flex: 1; +} + +.image { + position: absolute; + width: 24px; + height: 24px; + top: 50%; + left: 16px; + transform: translateY(-50%); +} + +.input { + width: 100%; + background-color: var(--gray100); + border-radius: 12px; + font-size: 16px; + font-weight: 400; + line-height: 1.625rem; + color: var(--gray800); + padding: 9px 0 9px 44px; +} + +.input:focus { + outline-color: var(--blue); +} + +.input::placeholder { + color: var(--gray400); +} diff --git a/components/ui/SearchBar.tsx b/components/ui/SearchBar.tsx new file mode 100644 index 000000000..0fcb99e30 --- /dev/null +++ b/components/ui/SearchBar.tsx @@ -0,0 +1,19 @@ +import Image from "next/image"; +import Container from "../layout/Container"; +import styles from "./SearchBar.module.css"; +import searchIcon from "@/public/ic_search.svg"; + +const SearchBar = () => { + return ( + + 검색 + + + ); +}; + +export default SearchBar; diff --git a/pages/boards.tsx b/pages/boards.tsx index 8b0890867..74663ad10 100644 --- a/pages/boards.tsx +++ b/pages/boards.tsx @@ -1,10 +1,12 @@ import BestBoards from "@/components/boards/BestBoards"; +import BoardList from "@/components/boards/BoardList"; const Boards = () => { return ( -
+ <> -
+ + ); }; diff --git a/public/ic_arrow_down.svg b/public/ic_arrow_down.svg new file mode 100644 index 000000000..8308690fd --- /dev/null +++ b/public/ic_arrow_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/ic_search.svg b/public/ic_search.svg new file mode 100644 index 000000000..52241e6d8 --- /dev/null +++ b/public/ic_search.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/ic_sort.svg b/public/ic_sort.svg new file mode 100644 index 000000000..ab89188fd --- /dev/null +++ b/public/ic_sort.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/img_default.svg b/public/img_default.svg new file mode 100644 index 000000000..f650ec463 --- /dev/null +++ b/public/img_default.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + +