Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i
zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7
zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG
z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S
zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr
z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S
zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er
zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa
zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc-
zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V
zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I
zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc
z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E(
zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef
LrJugUA?W`A8`#=m
diff --git a/public/next.svg b/public/next.svg
deleted file mode 100644
index 5174b28c..00000000
--- a/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/vercel.svg b/public/vercel.svg
deleted file mode 100644
index d2f84222..00000000
--- a/public/vercel.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/styles/Home.module.css b/styles/Home.module.css
deleted file mode 100644
index 6676d2c6..00000000
--- a/styles/Home.module.css
+++ /dev/null
@@ -1,229 +0,0 @@
-.main {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- align-items: center;
- padding: 6rem;
- min-height: 100vh;
-}
-
-.description {
- display: inherit;
- justify-content: inherit;
- align-items: inherit;
- font-size: 0.85rem;
- max-width: var(--max-width);
- width: 100%;
- z-index: 2;
- font-family: var(--font-mono);
-}
-
-.description a {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 0.5rem;
-}
-
-.description p {
- position: relative;
- margin: 0;
- padding: 1rem;
- background-color: rgba(var(--callout-rgb), 0.5);
- border: 1px solid rgba(var(--callout-border-rgb), 0.3);
- border-radius: var(--border-radius);
-}
-
-.code {
- font-weight: 700;
- font-family: var(--font-mono);
-}
-
-.grid {
- display: grid;
- grid-template-columns: repeat(4, minmax(25%, auto));
- max-width: 100%;
- width: var(--max-width);
-}
-
-.card {
- padding: 1rem 1.2rem;
- border-radius: var(--border-radius);
- background: rgba(var(--card-rgb), 0);
- border: 1px solid rgba(var(--card-border-rgb), 0);
- transition: background 200ms, border 200ms;
-}
-
-.card span {
- display: inline-block;
- transition: transform 200ms;
-}
-
-.card h2 {
- font-weight: 600;
- margin-bottom: 0.7rem;
-}
-
-.card p {
- margin: 0;
- opacity: 0.6;
- font-size: 0.9rem;
- line-height: 1.5;
- max-width: 30ch;
-}
-
-.center {
- display: flex;
- justify-content: center;
- align-items: center;
- position: relative;
- padding: 4rem 0;
-}
-
-.center::before {
- background: var(--secondary-glow);
- border-radius: 50%;
- width: 480px;
- height: 360px;
- margin-left: -400px;
-}
-
-.center::after {
- background: var(--primary-glow);
- width: 240px;
- height: 180px;
- z-index: -1;
-}
-
-.center::before,
-.center::after {
- content: '';
- left: 50%;
- position: absolute;
- filter: blur(45px);
- transform: translateZ(0);
-}
-
-.logo {
- position: relative;
-}
-/* Enable hover only on non-touch devices */
-@media (hover: hover) and (pointer: fine) {
- .card:hover {
- background: rgba(var(--card-rgb), 0.1);
- border: 1px solid rgba(var(--card-border-rgb), 0.15);
- }
-
- .card:hover span {
- transform: translateX(4px);
- }
-}
-
-@media (prefers-reduced-motion) {
- .card:hover span {
- transform: none;
- }
-}
-
-/* Mobile */
-@media (max-width: 700px) {
- .content {
- padding: 4rem;
- }
-
- .grid {
- grid-template-columns: 1fr;
- margin-bottom: 120px;
- max-width: 320px;
- text-align: center;
- }
-
- .card {
- padding: 1rem 2.5rem;
- }
-
- .card h2 {
- margin-bottom: 0.5rem;
- }
-
- .center {
- padding: 8rem 0 6rem;
- }
-
- .center::before {
- transform: none;
- height: 300px;
- }
-
- .description {
- font-size: 0.8rem;
- }
-
- .description a {
- padding: 1rem;
- }
-
- .description p,
- .description div {
- display: flex;
- justify-content: center;
- position: fixed;
- width: 100%;
- }
-
- .description p {
- align-items: center;
- inset: 0 0 auto;
- padding: 2rem 1rem 1.4rem;
- border-radius: 0;
- border: none;
- border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
- background: linear-gradient(
- to bottom,
- rgba(var(--background-start-rgb), 1),
- rgba(var(--callout-rgb), 0.5)
- );
- background-clip: padding-box;
- backdrop-filter: blur(24px);
- }
-
- .description div {
- align-items: flex-end;
- pointer-events: none;
- inset: auto 0 0;
- padding: 2rem;
- height: 200px;
- background: linear-gradient(
- to bottom,
- transparent 0%,
- rgb(var(--background-end-rgb)) 40%
- );
- z-index: 1;
- }
-}
-
-/* Tablet and Smaller Desktop */
-@media (min-width: 701px) and (max-width: 1120px) {
- .grid {
- grid-template-columns: repeat(2, 50%);
- }
-}
-
-@media (prefers-color-scheme: dark) {
- .vercelLogo {
- filter: invert(1);
- }
-
- .logo {
- filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
- }
-}
-
-@keyframes rotate {
- from {
- transform: rotate(360deg);
- }
- to {
- transform: rotate(0deg);
- }
-}
diff --git a/styles/globals.css b/styles/globals.css
deleted file mode 100644
index d4f491e1..00000000
--- a/styles/globals.css
+++ /dev/null
@@ -1,107 +0,0 @@
-:root {
- --max-width: 1100px;
- --border-radius: 12px;
- --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
- 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
- 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
-
- --foreground-rgb: 0, 0, 0;
- --background-start-rgb: 214, 219, 220;
- --background-end-rgb: 255, 255, 255;
-
- --primary-glow: conic-gradient(
- from 180deg at 50% 50%,
- #16abff33 0deg,
- #0885ff33 55deg,
- #54d6ff33 120deg,
- #0071ff33 160deg,
- transparent 360deg
- );
- --secondary-glow: radial-gradient(
- rgba(255, 255, 255, 1),
- rgba(255, 255, 255, 0)
- );
-
- --tile-start-rgb: 239, 245, 249;
- --tile-end-rgb: 228, 232, 233;
- --tile-border: conic-gradient(
- #00000080,
- #00000040,
- #00000030,
- #00000020,
- #00000010,
- #00000010,
- #00000080
- );
-
- --callout-rgb: 238, 240, 241;
- --callout-border-rgb: 172, 175, 176;
- --card-rgb: 180, 185, 188;
- --card-border-rgb: 131, 134, 135;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --foreground-rgb: 255, 255, 255;
- --background-start-rgb: 0, 0, 0;
- --background-end-rgb: 0, 0, 0;
-
- --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
- --secondary-glow: linear-gradient(
- to bottom right,
- rgba(1, 65, 255, 0),
- rgba(1, 65, 255, 0),
- rgba(1, 65, 255, 0.3)
- );
-
- --tile-start-rgb: 2, 13, 46;
- --tile-end-rgb: 2, 5, 19;
- --tile-border: conic-gradient(
- #ffffff80,
- #ffffff40,
- #ffffff30,
- #ffffff20,
- #ffffff10,
- #ffffff10,
- #ffffff80
- );
-
- --callout-rgb: 20, 20, 20;
- --callout-border-rgb: 108, 108, 108;
- --card-rgb: 100, 100, 100;
- --card-border-rgb: 200, 200, 200;
- }
-}
-
-* {
- box-sizing: border-box;
- padding: 0;
- margin: 0;
-}
-
-html,
-body {
- max-width: 100vw;
- overflow-x: hidden;
-}
-
-body {
- color: rgb(var(--foreground-rgb));
- background: linear-gradient(
- to bottom,
- transparent,
- rgb(var(--background-end-rgb))
- )
- rgb(var(--background-start-rgb));
-}
-
-a {
- color: inherit;
- text-decoration: none;
-}
-
-@media (prefers-color-scheme: dark) {
- html {
- color-scheme: dark;
- }
-}
diff --git a/styles/reset.css b/styles/reset.css
new file mode 100644
index 00000000..d0e4841d
--- /dev/null
+++ b/styles/reset.css
@@ -0,0 +1,33 @@
+* {
+ padding: 0;
+ margin: 0;
+ border: 0;
+ box-sizing: border-box;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
+
+body {
+ font-family: Pretendard Variable, Pretendard, sans-serif;
+}
+
+ul,
+ol {
+ list-style: none;
+}
+
+button,
+input,
+select,
+textarea {
+ -webkit-appearance: none;
+ appearance: none;
+ color: inherit;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ background-color: inherit;
+}
diff --git a/styles/variables.css b/styles/variables.css
new file mode 100644
index 00000000..21a71010
--- /dev/null
+++ b/styles/variables.css
@@ -0,0 +1,13 @@
+:root {
+ --gray900: #111827;
+ --gray800: #1f2937;
+ --gray700: #374151;
+ --gray600: #4b5563;
+ --gray500: #6b7280;
+ --gray400: #9ca3af;
+ --gray200: #e5e7eb;
+ --gray100: #f3f4f6;
+ --gray50: #f9fafb;
+ --blue: #3692ff;
+ --red: #f74747;
+}
From 3de42d0e46d9562834aed2fb87ed119459208813 Mon Sep 17 00:00:00 2001
From: najitwo
Date: Sat, 26 Oct 2024 12:47:17 +0900
Subject: [PATCH 2/7] feat: add Header component
---
components/Header.module.css | 73 ++++++++++++++++++++++++++++++++++++
components/Header.tsx | 51 +++++++++++++++++++++++++
pages/_app.tsx | 2 +
pages/boards.tsx | 5 +++
public/ic_profile.svg | 24 ++++++++++++
public/panda_logo.svg | 15 ++++++++
public/panda_typo.svg | 3 ++
7 files changed, 173 insertions(+)
create mode 100644 components/Header.module.css
create mode 100644 components/Header.tsx
create mode 100644 pages/boards.tsx
create mode 100644 public/ic_profile.svg
create mode 100644 public/panda_logo.svg
create mode 100644 public/panda_typo.svg
diff --git a/components/Header.module.css b/components/Header.module.css
new file mode 100644
index 00000000..b65f811a
--- /dev/null
+++ b/components/Header.module.css
@@ -0,0 +1,73 @@
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px 16px;
+ border-bottom: 1px solid #dfdfdf;
+}
+
+ul.menu {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+ul.menu > li:not(:first-child) {
+ font-size: 1rem;
+ font-weight: 700;
+ line-height: 1.625rem;
+ color: var(--gray600);
+}
+
+.logo {
+ display: block;
+ width: 81px;
+ height: 40px;
+}
+
+.profile {
+ display: block;
+ width: 40px;
+ height: 40px;
+}
+
+.active {
+ color: var(--blue);
+}
+
+@media screen and (min-width: 768px) {
+ .header {
+ padding: 15px 24px;
+ }
+
+ ul.menu {
+ gap: 30px;
+ }
+
+ ul.menu > li:not(:first-child) {
+ font-size: 1.125rem;
+ }
+
+ .logo {
+ content: url("/panda_logo.svg");
+ width: 153px;
+ height: 51px;
+ margin-right: 5px;
+ }
+}
+
+@media screen and (min-width: 1200px) {
+ .header {
+ padding: 15px 200px;
+ }
+
+ .logo {
+ margin-right: 17px;
+ }
+}
+
+@media screen and (min-width: 1920px) {
+ .header {
+ padding: 15px 400px;
+ }
+}
diff --git a/components/Header.tsx b/components/Header.tsx
new file mode 100644
index 00000000..a851c474
--- /dev/null
+++ b/components/Header.tsx
@@ -0,0 +1,51 @@
+import { useRouter } from "next/router";
+import Link from "next/link";
+import Image from "next/image";
+import pandaTypoImg from "@/public/panda_typo.svg";
+import profileImg from "@/public/ic_profile.svg";
+import styles from "./Header.module.css";
+
+const Header = () => {
+ const router = useRouter();
+
+ return (
+
+
+ -
+
+
+
+
+ -
+
+ 자유게시판
+
+
+ -
+
+ 중고마켓
+
+
+
+
+
+
+
+ );
+};
+
+export default Header;
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 0554f107..5762417d 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -2,6 +2,7 @@ import "@/styles/reset.css";
import "@/styles/variables.css";
import Head from "next/head";
import type { AppProps } from "next/app";
+import Header from "@/components/Header";
export default function App({ Component, pageProps }: AppProps) {
return (
@@ -9,6 +10,7 @@ export default function App({ Component, pageProps }: AppProps) {
판다 마켓
+
>
);
diff --git a/pages/boards.tsx b/pages/boards.tsx
new file mode 100644
index 00000000..6ab7b494
--- /dev/null
+++ b/pages/boards.tsx
@@ -0,0 +1,5 @@
+const Boards = () => {
+ return boards ;
+};
+
+export default Boards;
diff --git a/public/ic_profile.svg b/public/ic_profile.svg
new file mode 100644
index 00000000..0480454d
--- /dev/null
+++ b/public/ic_profile.svg
@@ -0,0 +1,24 @@
+
diff --git a/public/panda_logo.svg b/public/panda_logo.svg
new file mode 100644
index 00000000..3799efba
--- /dev/null
+++ b/public/panda_logo.svg
@@ -0,0 +1,15 @@
+
diff --git a/public/panda_typo.svg b/public/panda_typo.svg
new file mode 100644
index 00000000..55a63efc
--- /dev/null
+++ b/public/panda_typo.svg
@@ -0,0 +1,3 @@
+
From cd154cbc8eb5d56f000039a4f2426dd1f12030b0 Mon Sep 17 00:00:00 2001
From: najitwo
Date: Sat, 26 Oct 2024 19:08:37 +0900
Subject: [PATCH 3/7] feat: implement BestBoards component
---
components/boards/ArticleImage.module.css | 19 ++++++++
components/boards/ArticleImage.tsx | 20 +++++++++
components/boards/BestBoard.module.css | 54 +++++++++++++++++++++++
components/boards/BestBoard.tsx | 28 ++++++++++++
components/boards/BestBoards.module.css | 27 ++++++++++++
components/boards/BestBoards.tsx | 51 +++++++++++++++++++++
components/layout/Container.module.css | 16 +++++++
components/layout/Container.tsx | 24 ++++++++++
components/{ => layout}/Header.module.css | 0
components/{ => layout}/Header.tsx | 0
components/ui/Badge.module.css | 19 ++++++++
components/ui/Badge.tsx | 14 ++++++
components/ui/LikeCount.module.css | 11 +++++
components/ui/LikeCount.tsx | 20 +++++++++
hooks/useResize.ts | 17 +++++++
lib/fetchData.ts | 42 ++++++++++++++++++
lib/formatDate.ts | 8 ++++
lib/urlParams.ts | 13 ++++++
next.config.js | 14 +++++-
pages/_app.tsx | 7 ++-
pages/api/hello.ts | 13 ------
pages/boards.tsx | 8 +++-
public/ic_heart.svg | 3 ++
public/ic_medal.svg | 4 ++
types/articleTypes.ts | 15 +++++++
25 files changed, 429 insertions(+), 18 deletions(-)
create mode 100644 components/boards/ArticleImage.module.css
create mode 100644 components/boards/ArticleImage.tsx
create mode 100644 components/boards/BestBoard.module.css
create mode 100644 components/boards/BestBoard.tsx
create mode 100644 components/boards/BestBoards.module.css
create mode 100644 components/boards/BestBoards.tsx
create mode 100644 components/layout/Container.module.css
create mode 100644 components/layout/Container.tsx
rename components/{ => layout}/Header.module.css (100%)
rename components/{ => layout}/Header.tsx (100%)
create mode 100644 components/ui/Badge.module.css
create mode 100644 components/ui/Badge.tsx
create mode 100644 components/ui/LikeCount.module.css
create mode 100644 components/ui/LikeCount.tsx
create mode 100644 hooks/useResize.ts
create mode 100644 lib/fetchData.ts
create mode 100644 lib/formatDate.ts
create mode 100644 lib/urlParams.ts
delete mode 100644 pages/api/hello.ts
create mode 100644 public/ic_heart.svg
create mode 100644 public/ic_medal.svg
create mode 100644 types/articleTypes.ts
diff --git a/components/boards/ArticleImage.module.css b/components/boards/ArticleImage.module.css
new file mode 100644
index 00000000..ecb2c54e
--- /dev/null
+++ b/components/boards/ArticleImage.module.css
@@ -0,0 +1,19 @@
+.container {
+ width: 72px;
+ height: 72px;
+ padding: 12px;
+ border-radius: 8px;
+ border: 0.75px solid var(--gray200);
+ background-color: #ffffff;
+ flex: 0 0 auto;
+}
+
+.wrapper {
+ width: 100%;
+ height: 100%;
+ position: relative;
+}
+
+.image {
+ object-fit: contain;
+}
diff --git a/components/boards/ArticleImage.tsx b/components/boards/ArticleImage.tsx
new file mode 100644
index 00000000..a55efaf2
--- /dev/null
+++ b/components/boards/ArticleImage.tsx
@@ -0,0 +1,20 @@
+import Image from "next/image";
+import Container from "../layout/Container";
+import styles from "./ArticleImage.module.css";
+
+interface ImageProps {
+ src: string;
+ alt: string;
+}
+
+const ArticleImage = ({ src, alt }: ImageProps) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default ArticleImage;
diff --git a/components/boards/BestBoard.module.css b/components/boards/BestBoard.module.css
new file mode 100644
index 00000000..d739cfca
--- /dev/null
+++ b/components/boards/BestBoard.module.css
@@ -0,0 +1,54 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ background-color: var(--gray50);
+ padding: 0 24px 16px;
+}
+
+.content {
+ display: flex;
+ justify-content: space-between;
+ gap: 40px;
+ font-size: 1.125rem;
+ font-weight: 600;
+ line-height: 1.625rem;
+ color: var(--gray800);
+ margin-top: 16px;
+ margin-bottom: 8px;
+}
+
+.info {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 0.875rem;
+ font-weight: 400;
+ line-height: 1.5rem;
+}
+
+.user {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.nickname {
+ color: var(--gray600);
+}
+
+.date {
+ 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/BestBoard.tsx b/components/boards/BestBoard.tsx
new file mode 100644
index 00000000..f1584e97
--- /dev/null
+++ b/components/boards/BestBoard.tsx
@@ -0,0 +1,28 @@
+import Badge from "../ui/Badge";
+import Container from "../layout/Container";
+import ArticleImage from "./ArticleImage";
+import { ArticleProps } from "@/types/articleTypes";
+import styles from "./BestBoard.module.css";
+import LikeCount from "../ui/LikeCount";
+import { formatDate } from "@/lib/formatDate";
+
+const BestBoard = ({ article }: { article: ArticleProps }) => {
+ return (
+
+
+
+
+
+ {article.writer.nickname}
+
+
+ {formatDate(article.createdAt)}
+
+
+ );
+};
+
+export default BestBoard;
diff --git a/components/boards/BestBoards.module.css b/components/boards/BestBoards.module.css
new file mode 100644
index 00000000..838bcf65
--- /dev/null
+++ b/components/boards/BestBoards.module.css
@@ -0,0 +1,27 @@
+.wrapper h2 {
+ font-size: 1.125rem;
+ font-weight: 700;
+ line-height: 1.625rem;
+ color: var(--gray900);
+ margin-bottom: 16px;
+}
+
+.container {
+ display: flex;
+ gap: 16px;
+ /* justify-content: space-between; */
+}
+
+@media screen and (min-width: 768px) {
+ .wrapper h2 {
+ font-size: 1.25rem;
+ line-height: 1.5rem;
+ margin-bottom: 24px;
+ }
+}
+
+@media screen and (min-width: 1200px) {
+ .container {
+ gap: 24px;
+ }
+}
diff --git a/components/boards/BestBoards.tsx b/components/boards/BestBoards.tsx
new file mode 100644
index 00000000..d92761fd
--- /dev/null
+++ b/components/boards/BestBoards.tsx
@@ -0,0 +1,51 @@
+import { useCallback, useEffect, useState } from "react";
+import BestBoard from "./BestBoard";
+import Container from "../layout/Container";
+import useResize from "@/hooks/useResize";
+import { fetchData } from "@/lib/fetchData";
+import { ArticleProps } from "@/types/articleTypes";
+import styles from "./BestBoards.module.css";
+
+const getPageSize = (width: number) => {
+ if (width < 768) return 1;
+ if (width < 1200) return 2;
+
+ return 3;
+};
+
+const BestBoards = () => {
+ const [articles, setArticles] = useState([]);
+ const [pageSize, setPageSize] = useState();
+ const viewportWidth = useResize();
+ const BASE_URL = "https://panda-market-api.vercel.app/articles";
+
+ const handleLoad = useCallback(async (size: number) => {
+ const { list } = await fetchData(BASE_URL, {
+ query: { pageSize: size, orderBy: "like" },
+ });
+ setArticles(list);
+ }, []);
+
+ useEffect(() => {
+ if (!viewportWidth) return;
+
+ const nextPageSize = getPageSize(viewportWidth);
+ if (nextPageSize !== pageSize) {
+ setPageSize(nextPageSize);
+ handleLoad(nextPageSize);
+ }
+ }, [viewportWidth, handleLoad, pageSize]);
+
+ return (
+
+ 베스트 게시글
+
+ {articles.map((article: ArticleProps) => (
+
+ ))}
+
+
+ );
+};
+
+export default BestBoards;
diff --git a/components/layout/Container.module.css b/components/layout/Container.module.css
new file mode 100644
index 00000000..3580b014
--- /dev/null
+++ b/components/layout/Container.module.css
@@ -0,0 +1,16 @@
+.page.container {
+ padding: 16px;
+}
+
+@media screen and (min-width: 768px) {
+ .page.container {
+ padding: 24px;
+ }
+}
+
+@media screen and (min-width: 1200px) {
+ .page.container {
+ margin: 0 auto;
+ max-width: 1200px;
+ }
+}
diff --git a/components/layout/Container.tsx b/components/layout/Container.tsx
new file mode 100644
index 00000000..137483c9
--- /dev/null
+++ b/components/layout/Container.tsx
@@ -0,0 +1,24 @@
+import { PropsWithChildren } from "react";
+import styles from "./Container.module.css";
+
+interface ContainerProps {
+ className?: string;
+ page?: boolean;
+}
+
+export default function Container({
+ className = "",
+ page,
+ children,
+ ...props
+}: PropsWithChildren) {
+ const classNames = `${styles.container} ${
+ page ? styles.page : ""
+ } ${className}`;
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/components/Header.module.css b/components/layout/Header.module.css
similarity index 100%
rename from components/Header.module.css
rename to components/layout/Header.module.css
diff --git a/components/Header.tsx b/components/layout/Header.tsx
similarity index 100%
rename from components/Header.tsx
rename to components/layout/Header.tsx
diff --git a/components/ui/Badge.module.css b/components/ui/Badge.module.css
new file mode 100644
index 00000000..fe9a649e
--- /dev/null
+++ b/components/ui/Badge.module.css
@@ -0,0 +1,19 @@
+.badge {
+ width: 102px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 4px;
+ background-color: var(--blue);
+ border-radius: 0px 0px 16px 16px;
+ font-size: 1rem;
+ font-weight: 600;
+ line-height: 1.625rem;
+ color: #ffffff;
+ padding: 2px 24px;
+}
+
+.medal {
+ width: 16px;
+ height: 16px;
+}
diff --git a/components/ui/Badge.tsx b/components/ui/Badge.tsx
new file mode 100644
index 00000000..9a108e3f
--- /dev/null
+++ b/components/ui/Badge.tsx
@@ -0,0 +1,14 @@
+import Image from "next/image";
+import medalIcon from "@/public/ic_medal.svg";
+import styles from "./Badge.module.css";
+
+const Badge = () => {
+ return (
+
+
+ Best
+
+ );
+};
+
+export default Badge;
diff --git a/components/ui/LikeCount.module.css b/components/ui/LikeCount.module.css
new file mode 100644
index 00000000..2d61c07f
--- /dev/null
+++ b/components/ui/LikeCount.module.css
@@ -0,0 +1,11 @@
+.container {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ color: var(--gray500);
+}
+
+.image {
+ width: 16px;
+ height: 16px;
+}
diff --git a/components/ui/LikeCount.tsx b/components/ui/LikeCount.tsx
new file mode 100644
index 00000000..c9f40479
--- /dev/null
+++ b/components/ui/LikeCount.tsx
@@ -0,0 +1,20 @@
+import Image from "next/image";
+import heartIcon from "@/public/ic_heart.svg";
+import styles from "./LikeCount.module.css";
+import Container from "../layout/Container";
+
+interface LikeCountProps {
+ className?: string;
+ likeCount: number;
+}
+
+const LikeCount = ({ className = "", likeCount }: LikeCountProps) => {
+ return (
+
+
+ {likeCount > 10000 ? "9999+" : likeCount}
+
+ );
+};
+
+export default LikeCount;
diff --git a/hooks/useResize.ts b/hooks/useResize.ts
new file mode 100644
index 00000000..a1bf3635
--- /dev/null
+++ b/hooks/useResize.ts
@@ -0,0 +1,17 @@
+import { useEffect, useState } from "react";
+
+const useResize = () => {
+ const [width, setWidth] = useState(0);
+
+ const handleResize = () => setWidth(window.innerWidth);
+
+ useEffect(() => {
+ window.addEventListener("resize", handleResize);
+ handleResize();
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ return width;
+};
+
+export default useResize;
diff --git a/lib/fetchData.ts b/lib/fetchData.ts
new file mode 100644
index 00000000..ae84aaca
--- /dev/null
+++ b/lib/fetchData.ts
@@ -0,0 +1,42 @@
+import { createURLSearchParams } from "./urlParams";
+
+type FetchOptions = {
+ query?: Record;
+ method?: "GET" | "POST" | "PUT" | "DELETE";
+ headers?: Record;
+ body?: Record | string | null;
+};
+
+export const fetchData = async (url: string, options: FetchOptions = {}) => {
+ try {
+ let fullUrl = url;
+
+ if (options.query) {
+ const queryString = createURLSearchParams(options.query).toString();
+ fullUrl += `?${queryString}`;
+ }
+
+ const { method = "GET", headers = {}, body } = options;
+
+ const response = await fetch(fullUrl, {
+ method,
+ headers: {
+ "Content-Type": "application/json",
+ ...headers,
+ },
+ body: method !== "GET" && body ? JSON.stringify(options.body) : null,
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const responseBody = await response.json();
+ return responseBody;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ throw new Error(`Fetch failed: ${error.message}`);
+ }
+ throw new Error("Unknown error occurred during fetch");
+ }
+};
diff --git a/lib/formatDate.ts b/lib/formatDate.ts
new file mode 100644
index 00000000..5b476e44
--- /dev/null
+++ b/lib/formatDate.ts
@@ -0,0 +1,8 @@
+export const formatDate = (date: string) =>
+ new Date(date)
+ .toLocaleDateString("ko-kr", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ })
+ .replace(/\.$/, "");
diff --git a/lib/urlParams.ts b/lib/urlParams.ts
new file mode 100644
index 00000000..f752e298
--- /dev/null
+++ b/lib/urlParams.ts
@@ -0,0 +1,13 @@
+export const createURLSearchParams = (
+ params: Record
+): URLSearchParams => {
+ const stringifiedParams: Record = {};
+
+ for (const key in params) {
+ if (params[key] !== undefined && params[key] !== null) {
+ stringifiedParams[key] = String(params[key]);
+ }
+ }
+
+ return new URLSearchParams(stringifiedParams);
+};
diff --git a/next.config.js b/next.config.js
index a843cbee..1d527eac 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,6 +1,16 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
-}
+ images: {
+ remotePatterns: [
+ {
+ protocol: "https",
+ hostname: "sprint-fe-project.s3.ap-northeast-2.amazonaws.com",
+ port: "",
+ pathname: "/Sprint_Mission/**",
+ },
+ ],
+ },
+};
-module.exports = nextConfig
+module.exports = nextConfig;
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 5762417d..d020895d 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -2,7 +2,8 @@ import "@/styles/reset.css";
import "@/styles/variables.css";
import Head from "next/head";
import type { AppProps } from "next/app";
-import Header from "@/components/Header";
+import Header from "@/components/layout/Header";
+import Container from "@/components/layout/Container";
export default function App({ Component, pageProps }: AppProps) {
return (
@@ -11,7 +12,9 @@ export default function App({ Component, pageProps }: AppProps) {
판다 마켓
-
+
+
+
>
);
}
diff --git a/pages/api/hello.ts b/pages/api/hello.ts
deleted file mode 100644
index f8bcc7e5..00000000
--- a/pages/api/hello.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
-import type { NextApiRequest, NextApiResponse } from 'next'
-
-type Data = {
- name: string
-}
-
-export default function handler(
- req: NextApiRequest,
- res: NextApiResponse
-) {
- res.status(200).json({ name: 'John Doe' })
-}
diff --git a/pages/boards.tsx b/pages/boards.tsx
index 6ab7b494..8b089086 100644
--- a/pages/boards.tsx
+++ b/pages/boards.tsx
@@ -1,5 +1,11 @@
+import BestBoards from "@/components/boards/BestBoards";
+
const Boards = () => {
- return boards ;
+ return (
+
+
+
+ );
};
export default Boards;
diff --git a/public/ic_heart.svg b/public/ic_heart.svg
new file mode 100644
index 00000000..fd0f1552
--- /dev/null
+++ b/public/ic_heart.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/ic_medal.svg b/public/ic_medal.svg
new file mode 100644
index 00000000..d650c401
--- /dev/null
+++ b/public/ic_medal.svg
@@ -0,0 +1,4 @@
+
diff --git a/types/articleTypes.ts b/types/articleTypes.ts
new file mode 100644
index 00000000..945ba594
--- /dev/null
+++ b/types/articleTypes.ts
@@ -0,0 +1,15 @@
+export interface WriterProps {
+ id: number;
+ nickname: string;
+}
+
+export interface ArticleProps {
+ id: number;
+ title: string;
+ content: string;
+ image: string;
+ likeCount: number;
+ createdAt: string;
+ updatedAt: string;
+ writer: WriterProps;
+}
From b5ba32bdb5f93a42d0bc1d23749954c6fa9056a6 Mon Sep 17 00:00:00 2001
From: najitwo
Date: Sat, 26 Oct 2024 22:00:58 +0900
Subject: [PATCH 4/7] 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 a55efaf2..69c02306 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 (
-
+
);
diff --git a/components/boards/BestBoard.module.css b/components/boards/BestBoard.module.css
index d739cfca..e95b525b 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 838bcf65..1efec06a 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 00000000..53e25274
--- /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 00000000..2c3c357f
--- /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 (
+
+
+
+
+ );
+};
+
+export default Board;
diff --git a/components/boards/BoardLIst.module.css b/components/boards/BoardLIst.module.css
new file mode 100644
index 00000000..124813c5
--- /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 00000000..4e22638e
--- /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 137483c9..62a1835a 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 00000000..e6b88210
--- /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 00000000..0fa74801
--- /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 00000000..ae5ce549
--- /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 00000000..ff2f9314
--- /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 00000000..e702f7c2
--- /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 00000000..89519a35
--- /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 2d61c07f..680886be 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 00000000..3b578eba
--- /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 00000000..0fcb99e3
--- /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 8b089086..74663ad1 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 00000000..8308690f
--- /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 00000000..52241e6d
--- /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 00000000..ab89188f
--- /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 00000000..f650ec46
--- /dev/null
+++ b/public/img_default.svg
@@ -0,0 +1,16 @@
+
From 23bb578fba43570d80522185b60c80087ef7ba86 Mon Sep 17 00:00:00 2001
From: najitwo
Date: Sat, 26 Oct 2024 22:38:58 +0900
Subject: [PATCH 5/7] feat: implement search function
---
components/boards/BoardList.tsx | 34 ++++++++++++++++++++----------
components/ui/SearchBar.module.css | 2 +-
components/ui/SearchBar.tsx | 26 +++++++++++++++++++----
3 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/components/boards/BoardList.tsx b/components/boards/BoardList.tsx
index 4e22638e..d9f44af3 100644
--- a/components/boards/BoardList.tsx
+++ b/components/boards/BoardList.tsx
@@ -1,4 +1,5 @@
import { useState, useCallback, useEffect } from "react";
+import { useRouter } from "next/router";
import Image from "next/image";
import Board from "./Board";
import Button from "../ui/Button";
@@ -13,24 +14,35 @@ import { ArticleProps } from "@/types/articleTypes";
const BoardList = () => {
const [boards, setBoards] = useState([]);
const [order, setOrder] = useState("recent");
+ const router = useRouter();
+ const { keyword } = router.query;
+
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]);
+ const handleLoad = useCallback(
+ async (keyword: string = "") => {
+ const { list } = await fetchData(BASE_URL, {
+ query: {
+ orderBy: order,
+ keyword,
+ },
+ });
+ setBoards(list);
+ },
+ [order]
+ );
useEffect(() => {
- handleLoad();
- }, [handleLoad]);
+ if (typeof keyword === "string") {
+ handleLoad(keyword);
+ } else {
+ handleLoad();
+ }
+ }, [handleLoad, keyword]);
return (
@@ -39,7 +51,7 @@ const BoardList = () => {
-
+
{
+const SearchBar = ({ initialValue = "" }: { initialValue: string }) => {
+ const [value, setValue] = useState(initialValue);
+ const router = useRouter();
+
+ const handleChange = (e: ChangeEvent) => {
+ setValue(e.target.value);
+ };
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ if (!value) {
+ router.push("/boards");
+ return;
+ }
+ router.push(`/boards?keyword=${value}`);
+ };
+
return (
-
+
+
);
};
From fec9eeac1cde7d1b1bf4f0739f59d9c8d8a95863 Mon Sep 17 00:00:00 2001
From: najitwo
Date: Sat, 26 Oct 2024 23:11:14 +0900
Subject: [PATCH 6/7] feat: implement prefetching for boards
---
components/boards/BoardList.tsx | 4 ++--
pages/boards.tsx | 17 +++++++++++++++--
2 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/components/boards/BoardList.tsx b/components/boards/BoardList.tsx
index d9f44af3..887bc9b4 100644
--- a/components/boards/BoardList.tsx
+++ b/components/boards/BoardList.tsx
@@ -11,8 +11,8 @@ import sortIcon from "@/public/ic_sort.svg";
import Container from "../layout/Container";
import { ArticleProps } from "@/types/articleTypes";
-const BoardList = () => {
- const [boards, setBoards] = useState([]);
+const BoardList = ({ initialBoards }: { initialBoards: ArticleProps[] }) => {
+ const [boards, setBoards] = useState(initialBoards);
const [order, setOrder] = useState("recent");
const router = useRouter();
const { keyword } = router.query;
diff --git a/pages/boards.tsx b/pages/boards.tsx
index 74663ad1..a03057be 100644
--- a/pages/boards.tsx
+++ b/pages/boards.tsx
@@ -1,11 +1,24 @@
import BestBoards from "@/components/boards/BestBoards";
import BoardList from "@/components/boards/BoardList";
+import { fetchData } from "@/lib/fetchData";
+import { ArticleProps } from "@/types/articleTypes";
-const Boards = () => {
+export const getStaticProps = async () => {
+ const BASE_URL = "https://panda-market-api.vercel.app/articles";
+ const { list } = await fetchData(BASE_URL);
+
+ return {
+ props: {
+ initialBoards: list,
+ },
+ };
+};
+
+const Boards = ({ initialBoards }: { initialBoards: ArticleProps[] }) => {
return (
<>
-
+
>
);
};
From c3604f6cbac2b67e3250ed72e47dec30ad68afdd Mon Sep 17 00:00:00 2001
From: najitwo
Date: Tue, 29 Oct 2024 11:18:24 +0900
Subject: [PATCH 7/7] chore: add next-env.d.ts for environment types
---
.gitignore | 4 ----
next-env.d.ts | 5 +++++
2 files changed, 5 insertions(+), 4 deletions(-)
create mode 100644 next-env.d.ts
diff --git a/.gitignore b/.gitignore
index 18f57553..fb6a4d96 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,9 +30,5 @@ yarn-error.log*
# vercel
.vercel
-# typescript
-*.tsbuildinfo
-next-env.d.ts
-
# VS Code
.vscode
diff --git a/next-env.d.ts b/next-env.d.ts
new file mode 100644
index 00000000..4f11a03d
--- /dev/null
+++ b/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
|