From 16bb9d48ac3a9dc45488db3caa525d75eddfabc4 Mon Sep 17 00:00:00 2001 From: Bersk3r Date: Fri, 19 Jul 2024 23:09:38 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Feat:=20React=20Query=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=20=EC=A0=9C=ED=92=88=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=A0=9C=EC=9E=91=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 Next.js 프로젝트 내 제품 페이지가 존재하지 않아, 기존 페이지를 맞게 컨버팅한 후 React Query 내용 추가 --- api/getProduct.ts | 25 ++++ api/itemApi.js | 62 --------- components/ItemPage/AllItems.module.scss | 93 ++++++++++++++ components/ItemPage/AllItems.tsx | 139 +++++++++++++++++++++ components/ItemPage/BestItems.module.scss | 10 ++ components/ItemPage/BestItems.tsx | 49 ++++++++ components/ItemPage/Products.module.scss | 131 +++++++++++++++++++ components/ItemPage/Products.tsx | 87 +++++++++++++ components/ItemPage/SelectMenu.module.scss | 21 ++++ components/ItemPage/SelectMenu.tsx | 50 ++++++++ components/Layout/Header.tsx | 2 +- lib/sort.ts | 29 +++++ next.config.js | 30 +++-- package-lock.json | 52 ++++++++ package.json | 3 + pages/_app.tsx | 8 +- pages/items/[productId]/index.tsx | 6 + pages/items/[productId]/styles.module.scss | 9 ++ pages/items/index.tsx | 13 ++ pages/items/styles.module.scss | 9 ++ src/hooks/useToggle.ts | 13 ++ src/hooks/useWindowSize.ts | 21 ++++ styles/normalize.scss | 1 + 23 files changed, 785 insertions(+), 78 deletions(-) create mode 100644 api/getProduct.ts delete mode 100644 api/itemApi.js create mode 100644 components/ItemPage/AllItems.module.scss create mode 100644 components/ItemPage/AllItems.tsx create mode 100644 components/ItemPage/BestItems.module.scss create mode 100644 components/ItemPage/BestItems.tsx create mode 100644 components/ItemPage/Products.module.scss create mode 100644 components/ItemPage/Products.tsx create mode 100644 components/ItemPage/SelectMenu.module.scss create mode 100644 components/ItemPage/SelectMenu.tsx create mode 100644 lib/sort.ts create mode 100644 pages/items/[productId]/index.tsx create mode 100644 pages/items/[productId]/styles.module.scss create mode 100644 pages/items/index.tsx create mode 100644 pages/items/styles.module.scss create mode 100644 src/hooks/useToggle.ts create mode 100644 src/hooks/useWindowSize.ts diff --git a/api/getProduct.ts b/api/getProduct.ts new file mode 100644 index 000000000..8613bbea4 --- /dev/null +++ b/api/getProduct.ts @@ -0,0 +1,25 @@ +import axios from "./axios"; + +interface GetProductsQueries { + page?: number; + pageSize?: number; + orderBy?: "recent" | "favorite"; + keyword?: string; +} + +export const getProducts = async ({ + page = 1, + pageSize = 3, + orderBy = "recent", + keyword, +}: GetProductsQueries) => { + try { + const response = await axios.get("/products", { + params: { page, pageSize, orderBy, keyword }, + }); + return response.data; + } catch (error) { + console.error(`Failed to fetch Data: ${error}`); + throw error; + } +}; diff --git a/api/itemApi.js b/api/itemApi.js deleted file mode 100644 index 03d904a56..000000000 --- a/api/itemApi.js +++ /dev/null @@ -1,62 +0,0 @@ -export async function getProducts(params = {}) { - // URLSearchParams을 이용하면 파라미터 값을 자동으로 쉽게 인코딩할 수 있어요. - const query = new URLSearchParams(params).toString(); - - try { - const response = await fetch( - `https://panda-market-api.vercel.app/products?${query}` - ); - if (!response.ok) { - throw new Error(`HTTP error: ${response.status}`); - } - const body = await response.json(); - return body; - } catch (error) { - console.error("Failed to fetch products:", error); - throw error; - } -} - -export async function getProductDetail(productId) { - // Parameter로 넣어줄 상품 아이디가 존재하는지 또는 정상적인지 확인 후에 호출하면 더 안전해요 - if (!productId) { - throw new Error("Invalid product ID"); - } - - try { - const response = await fetch( - `https://panda-market-api.vercel.app/products/${productId}` - ); - if (!response.ok) { - throw new Error(`HTTP error: ${response.status}`); - } - const body = await response.json(); - return body; - } catch (error) { - console.error("Failed to fetch product detail:", error); - throw error; - } -} - -// 상품 댓글 목록 조회 API에는 path parameter 'productId'와 함께 페이지당 보여줄 댓글 개수를 나타내는 'limit'을 query parameter로 보내주고 있어요. -export async function getProductComments({ productId, params }) { - // Parameter로 넣어줄 상품 아이디가 존재하는지 또는 정상적인지 확인 후에 호출하면 더 안전해요 - if (!productId) { - throw new Error("Invalid product ID"); - } - - try { - const query = new URLSearchParams(params).toString(); - const response = await fetch( - `https://panda-market-api.vercel.app/products/${productId}/comments?${query}` - ); - if (!response.ok) { - throw new Error(`HTTP error: ${response.status}`); - } - const body = await response.json(); - return body; - } catch (error) { - console.error("Failed to fetch product comments:", error); - throw error; - } -} diff --git a/components/ItemPage/AllItems.module.scss b/components/ItemPage/AllItems.module.scss new file mode 100644 index 000000000..51fe352ed --- /dev/null +++ b/components/ItemPage/AllItems.module.scss @@ -0,0 +1,93 @@ +@import "/styles/index.scss"; + +.title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding-block: 24px 12px; + + .content { + @include font-base(get_color(black), 700, 20px, 28px); + max-width: 600px; + flex-grow: 3; + } + + .search { + height: 42px; + display: flex; + align-items: center; + gap: 3px; + background-color: get_color(gray_100); + border-radius: 12px; + padding-block: 9px; + padding-inline: 20px 16px; + + .search_image { + width: 16px; + height: 16px; + background-size: 16px; + background-repeat: no-repeat; + background-image: url("../../src/assets/images/icons/ic_search.svg"); + } + + .search_input { + @include font-base(get_color(black), 400, 16px, 24px); + background-color: get_color(gray_100); + border: none; + } + } + + .add_item { + @include font-base(get_color(white), 600, 16px, 19.09px); + text-align: center; + width: 133px; + height: 42px; + border-radius: 8px; + padding: 12px 10px; + background-color: get_color(blue_primary); + text-decoration: none; + flex-shrink: 0; + } + .select_box_area { + position: relative; + max-width: 64px; + background-color: get_color(white); + padding: 3px; + border-radius: 3px; + right: 0; + } +} + +@media screen and (min-width: 768px) and (max-width: 1199px) { + .search { + width: 242px; + } +} + +@media screen and (min-width: 375px) and (max-width: 767px) { + .title { + display: grid; + grid-template: repeat(2, auto) / 300px auto; + + .content { + width: fit-content; + grid-column: 1 / span 1; + grid-row: 1 / span 1; + } + + .search { + grid-column: 1 / span 1; + grid-row: 2 / span 1; + } + .add_item { + grid-column: 2 / span 1; + grid-row: 1 / span 1; + } + } + + .select_box_area { + grid-column: 2 / span 1; + grid-row: 2 / span 1; + } +} diff --git a/components/ItemPage/AllItems.tsx b/components/ItemPage/AllItems.tsx new file mode 100644 index 000000000..71737c6f1 --- /dev/null +++ b/components/ItemPage/AllItems.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from "react"; +import { sortItemsByOrder } from "../../lib/sort"; +import { AllProducts } from "./Products"; +import styles from "./AllItems.module.scss"; +import SelectMenu from "./SelectMenu"; +import Link from "next/link"; +import { ProductSortOption } from "../../types/productTypes"; +import useWindowSize from "../../src/hooks/useWindowSize"; +import { FieldValues, SubmitHandler, useForm } from "react-hook-form"; +import { getProducts } from "../../api/getProduct"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +function AllItems() { + const [order, setOrder] = useState("recent"); + const queryClient = useQueryClient(); + const windowWidth = useWindowSize(); + const { register, watch, handleSubmit, setValue, getValues } = useForm(); + const [pageSize, setPageSize] = useState(10); + const [keyword, setKeyword] = useState(""); + + const { + data: itemsData, + isPending, + isError, + } = useQuery({ + queryKey: ["allitems"], + queryFn: () => getProducts({ pageSize, orderBy: order }), + retry: 0, + }); + + const items = itemsData?.list ?? []; + + const addKeywordMutation = useMutation({ + mutationFn: (keyword: string) => + getProducts({ pageSize, orderBy: order, keyword }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["allitems"], + }); + }, + }); + + const addOrderMutation = useMutation({ + mutationFn: (order: ProductSortOption) => + getProducts({ pageSize, orderBy: order, keyword }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["allitems"], + }); + }, + }); + + const handleKeyword = (keyword: string) => { + addKeywordMutation.mutate(keyword); + }; + + const handleOrder = (order: ProductSortOption) => { + addKeywordMutation.mutate(order); + setOrder(order); + }; + + const sortedItems = sortItemsByOrder(items, order); + + const onSubmit: SubmitHandler = async (data) => { + if (data.keyword) { + setKeyword(data.keyword); + } + handleKeyword(keyword); + }; + + useEffect(() => { + if (windowWidth < 768) { + setPageSize(4); + } else if (windowWidth < 1199) { + setPageSize(6); + } else { + setPageSize(10); + } + }, [windowWidth]); + + // useEffect(() => { + // async ({ + // pageSize, + // order, + // keyword, + // }: { + // pageSize: number; + // order: ProductSortOption; + // keyword: string; + // }) => { + // let result = await getProducts({ pageSize, orderBy: order, keyword }); + // const { list } = result; + // setAllItems(list); + // }; + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [pageSize, order, keyword]); + + return ( + <> +
+
+

+ {windowWidth < 1199 ? "판매 중인 상품" : "전체 상품"} +

+
+ +
+ +
+
+ + 상품 등록하기 + + + + +
+
+ +
+ {/* {isError && {loadingError.message}} */} +
+ {/* */} + + ); +} + +export default AllItems; diff --git a/components/ItemPage/BestItems.module.scss b/components/ItemPage/BestItems.module.scss new file mode 100644 index 000000000..40c1c7376 --- /dev/null +++ b/components/ItemPage/BestItems.module.scss @@ -0,0 +1,10 @@ +@import "/styles/index.scss"; + +.container { + .title { + padding-block: 24px 12px; + .content { + @include font-base(get_color(gray_800), 700, 20px, 28px); + } + } +} diff --git a/components/ItemPage/BestItems.tsx b/components/ItemPage/BestItems.tsx new file mode 100644 index 000000000..bd165c9eb --- /dev/null +++ b/components/ItemPage/BestItems.tsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from "react"; +import { BestProducts } from "./Products"; +import { sortItemsByOrder } from "../../lib/sort"; +import styles from "./BestItems.module.scss"; +import useWindowSize from "../../src/hooks/useWindowSize"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { getProducts } from "../../api/getProduct"; + +function BestItems() { + const windowWidth = useWindowSize(); + const [pageSize, setPageSize] = useState(4); + + const { + data: itemsData, + isPending, + isError, + } = useQuery({ + queryKey: ["bestitems"], + queryFn: () => getProducts({ pageSize, orderBy: "favorite" }), + retry: 0, + }); + + // const items = itemsData?.results ?? []; + const items = itemsData?.list ?? []; + + const sortedItems = sortItemsByOrder(items, "favorite"); + + useEffect(() => { + if (windowWidth < 768) { + setPageSize(1); + } else if (windowWidth < 1199) { + setPageSize(2); + } else { + setPageSize(4); + } + }, [windowWidth]); + + return ( +
+
+

베스트 상품

+
+ + {/* {loadingError?.message && {loadingError.message}} */} +
+ ); +} + +export default BestItems; diff --git a/components/ItemPage/Products.module.scss b/components/ItemPage/Products.module.scss new file mode 100644 index 000000000..2ab9a3b82 --- /dev/null +++ b/components/ItemPage/Products.module.scss @@ -0,0 +1,131 @@ +@import "/styles/index.scss"; + +.container { + display: flex; + width: fit-content; + flex-direction: column; + gap: 7px; + + .likes { + display: flex; + gap: 5px; + align-items: center; + + .like_button { + display: inline-block; + position: relative; + + .toggle_button { + width: 16px; + height: 16px; + display: block; + background-image: url("/src/assets/images/icons/ic_heart.svg"); + cursor: pointer; + } + + .like_toggle { + &:checked { + + .toggle_button { + background-image: url("/src/assets/images/icons/ic_heart.svg"); + } + } + + .like_toggle { + display: none; + } + } + } + + .like_count { + display: inline-block; + @include font-base(get_color(black), 500, 12px, 14.32px); + } + } + .title { + @include font-base(get_color(black), 500, 14px, 16.71px); + text-decoration: none; + } + + .image { + position: relative; + display: inline-block; + aspect-ratio: 1/1; + border: 1px black solid; + border-radius: 16px; + &.all { + width: 221px; + height: 221px; + } + + &.best { + width: 282px; + height: 282px; + } + img { + border-radius: 16px; + } + } + + .price { + @include font-base(get_color(black), 700, 16px, 19.09px); + } +} + +.list { + display: grid; + column-gap: 24px; + + &.all { + grid-template: repeat(2, 301px) / repeat(5, 221px); + row-gap: 10px; + } + + &.best { + grid-template: 1fr / repeat(4, 282px); + } + + .items { + list-style-type: none; + } + + li { + width: fit-content; + } +} + +@media screen and (min-width: 768px) and (max-width: 1199px) { + .list { + &.all { + grid-template: repeat(2, 301px) / repeat(3, 221px); + } + + &.best { + grid-template: 1fr / repeat(2, 336px); + } + } +} + +@media screen and (min-width: 375px) and (max-width: 767px) { + .list { + &.all { + grid-template: repeat(2, 248px) / repeat(2, 168px); + } + + &.best { + grid-template: 1fr / auto; + } + } + + .container { + .image { + &.all { + width: 168px; + height: 168px; + } + &.best { + width: 343px; + height: 343px; + } + } + } +} diff --git a/components/ItemPage/Products.tsx b/components/ItemPage/Products.tsx new file mode 100644 index 000000000..1d87adf9d --- /dev/null +++ b/components/ItemPage/Products.tsx @@ -0,0 +1,87 @@ +import Link from "next/link"; +import { Product } from "../../types/productTypes"; +import styles from "./Products.module.scss"; +import Image from "next/image"; + +// function formatDate(value) { +// const date = new Date(value); +// return `${date.getFullYear()}. ${date.getMonth() + 1}. ${date.getDate()}`; +// } + +function BestProductItem({ item }: { item: Product }) { + return ( +
+
+ {item.name} +
+ + {item.name} + +

{item.price.toLocaleString("ko-KR")}원

+
+ +

{item.favoriteCount}

+
+
+ ); +} + +function AllProductItem({ item }: { item: Product }) { + return ( +
+
+ {item.name} +
+ + {item.name} + +

{item.price.toLocaleString("ko-KR")}원

+
+ +

{item.favoriteCount}

+
+
+ ); +} + +function BestProducts({ items, counts }: { items: Product[]; counts: number }) { + console.log(items); + return ( +
    + {items.map((item, index) => { + if (!(index > counts - 1)) { + return ( +
  • + +
  • + ); + } + })} +
+ ); +} + +function AllProducts({ items, counts }: { items: Product[]; counts: number }) { + return ( +
    + {items.map((item, index) => { + if (index > counts - 1) { + return <>; + } + return ( +
  • + +
  • + ); + })} +
+ ); +} + +export { BestProducts, AllProducts }; diff --git a/components/ItemPage/SelectMenu.module.scss b/components/ItemPage/SelectMenu.module.scss new file mode 100644 index 000000000..4c716ddc7 --- /dev/null +++ b/components/ItemPage/SelectMenu.module.scss @@ -0,0 +1,21 @@ +@import "/styles/index.scss"; + +.sort_button { + border-radius: 16px; + position: relative; + display: flex; +} + +.toggle_menu { + position: absolute; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + top: 20px; + border-radius: 8px; + padding: 5px; + background-color: get_color(white); + row-gap: 3px; + z-index: 3; +} diff --git a/components/ItemPage/SelectMenu.tsx b/components/ItemPage/SelectMenu.tsx new file mode 100644 index 000000000..6884bb68c --- /dev/null +++ b/components/ItemPage/SelectMenu.tsx @@ -0,0 +1,50 @@ +import React, { Dispatch, SetStateAction } from "react"; +import useToggle from "../../src/hooks/useToggle"; +import SortIcon from "../../src/assets/images/icons/ic_sort.svg"; +import { ProductSortOption } from "../../types/productTypes"; +import useWindowSize from "../../src/hooks/useWindowSize"; +import styles from "./SelectMenu.module.scss"; + +export default function SelectMenu({ + order, + setOrder, +}: { + order: ProductSortOption; + setOrder: (order: ProductSortOption) => {}; +}) { + const [isOpen, setIsOpen] = useToggle(false); + const windowWidth = useWindowSize(); + + const onOpen = () => { + setIsOpen(); + }; + + const onClose = () => { + setIsOpen(); + }; + + const orderValue = { + recent: "최신순", + favorite: "좋아요순", + }; + + return ( +
+ + + {isOpen && ( +
+
setOrder("recent")}>최신순
+
+
setOrder("favorite")}>좋아요순
+
+ )} +
+ ); +} diff --git a/components/Layout/Header.tsx b/components/Layout/Header.tsx index 371a0253d..a2ef77a50 100644 --- a/components/Layout/Header.tsx +++ b/components/Layout/Header.tsx @@ -9,7 +9,7 @@ import ProfileImage from "../../src/assets/images/ui/ic_profile.svg"; const menuItems = [ { id: "item1", name: "자유게시판", path: "/board" }, - { id: "item2", name: "중고마켓", path: "/additem" }, + { id: "item2", name: "중고마켓", path: "/items" }, ]; export default function Header() { diff --git a/lib/sort.ts b/lib/sort.ts new file mode 100644 index 000000000..82eb2972a --- /dev/null +++ b/lib/sort.ts @@ -0,0 +1,29 @@ +import { Product, ProductSortOption } from "../types/productTypes"; + +export const sortItemsByOrder = ( + items: Product[], + order: ProductSortOption +) => { + const sortedItems: Product[] = items.sort((a, b): number => { + if (order === "favorite") { + if (a.favoriteCount < b.favoriteCount) { + return 1; + } + if (a.favoriteCount > b.favoriteCount) { + return -1; + } + + return 0; + } else if (order === "recent") { + if (a.createdAt < b.createdAt) { + return 1; + } + if (a.createdAt > b.createdAt) { + return -1; + } + + return 0; + } else return 0; + }); + return sortedItems; +}; diff --git a/next.config.js b/next.config.js index 3332c95d6..d42041d79 100644 --- a/next.config.js +++ b/next.config.js @@ -5,20 +5,24 @@ const nextConfig = { reactStrictMode: true, // assetPrefix: ".", images: { - remotePatterns: [ - { - protocol: "https", - hostname: "sprint-fe-project.s3.ap-northeast-2.amazonaws.com", - port: "", - pathname: "/Sprint_Mission/**", - }, - { - protocol: "https", - hostname: "example.com", - port: "", - pathname: "/", - }, + domains: [ + "sprint-fe-project.s3.ap-northeast-2.amazonaws.com", + "example.com", ], + // remotePatterns: [ + // { + // protocol: "https", + // hostname: "sprint-fe-project.s3.ap-northeast-2.amazonaws.com", + // port: "", + // pathname: "/Sprint_Mission/**", + // }, + // { + // protocol: "https", + // hostname: "example.com", + // port: "", + // pathname: "/", + // }, + // ], }, webpack(config) { diff --git a/package-lock.json b/package-lock.json index 38aefbde3..b162aebe6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,10 @@ "dependencies": { "@hookform/resolvers": "^3.6.0", "@svgr/webpack": "^8.1.0", + "@tanstack/react-query": "^5.51.9", + "@tanstack/react-query-devtools": "^5.51.9", "axios": "^1.7.2", + "lodash": "^4.17.21", "next": "13.5.6", "node-sass": "^9.0.0", "react": "^18", @@ -2542,6 +2545,55 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.51.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.9.tgz", + "integrity": "sha512-HsAwaY5J19MD18ykZDS3aVVh+bAt0i7m6uQlFC2b77DLV9djo+xEN7MWQAQQTR8IM+7r/zbozTQ7P0xr0bHuew==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.51.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.51.9.tgz", + "integrity": "sha512-FQqJynaEDuwQxoFLP3/i10HQwNYh4wxgs0NeSoL24BLWvpUdstgHqUm2zgwRov8Tmh5kjndPIWaXenwl0D47EA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.51.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.9.tgz", + "integrity": "sha512-F8j6i42wfKvFrRcxfOyFyYME+bPfNthAGOSkjdv4UwZZXJjnBnBs/yRQGT0bD23LVCTuBzlIfZ0GKSIyclZ9rQ==", + "dependencies": { + "@tanstack/query-core": "5.51.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.51.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.51.9.tgz", + "integrity": "sha512-ztS5l75gV4xjDUFfEOtBfzcqW5vyfAQ2haWPpGMwq/Ha/3a4gaOE5DKntq+0+upWxUpp4SSvXXm6fMjV5miUcQ==", + "dependencies": { + "@tanstack/query-devtools": "5.51.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.51.9", + "react": "^18 || ^19" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", diff --git a/package.json b/package.json index ca25824f2..5abc7071b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,10 @@ "dependencies": { "@hookform/resolvers": "^3.6.0", "@svgr/webpack": "^8.1.0", + "@tanstack/react-query": "^5.51.9", + "@tanstack/react-query-devtools": "^5.51.9", "axios": "^1.7.2", + "lodash": "^4.17.21", "next": "13.5.6", "node-sass": "^9.0.0", "react": "^18", diff --git a/pages/_app.tsx b/pages/_app.tsx index 88c3c5615..2f1b93243 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -5,12 +5,15 @@ import Container from "../components/Layout/Container"; import Head from "next/head"; import { AuthProvider } from "../contexts/AuthProvider"; import { usePathname } from "next/navigation"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; export default function App({ Component, pageProps }: AppProps) { const path = usePathname(); + const queryClient = new QueryClient(); return ( - <> + - + + ); } diff --git a/pages/items/[productId]/index.tsx b/pages/items/[productId]/index.tsx new file mode 100644 index 000000000..7f019e949 --- /dev/null +++ b/pages/items/[productId]/index.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import style from "./styles.module.scss"; + +export default function ProductDetail() { + return
index
; +} diff --git a/pages/items/[productId]/styles.module.scss b/pages/items/[productId]/styles.module.scss new file mode 100644 index 000000000..eaa354ebb --- /dev/null +++ b/pages/items/[productId]/styles.module.scss @@ -0,0 +1,9 @@ +@import "../../styles/index.scss"; + +.container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 40px; +} diff --git a/pages/items/index.tsx b/pages/items/index.tsx new file mode 100644 index 000000000..3179abdba --- /dev/null +++ b/pages/items/index.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import style from "./styles.module.scss"; +import BestItems from "../../components/ItemPage/BestItems"; +import AllItems from "../../components/ItemPage/AllItems"; + +export default function ItemsPage() { + return ( +
+ + +
+ ); +} diff --git a/pages/items/styles.module.scss b/pages/items/styles.module.scss new file mode 100644 index 000000000..eaa354ebb --- /dev/null +++ b/pages/items/styles.module.scss @@ -0,0 +1,9 @@ +@import "../../styles/index.scss"; + +.container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 40px; +} diff --git a/src/hooks/useToggle.ts b/src/hooks/useToggle.ts new file mode 100644 index 000000000..7ed514522 --- /dev/null +++ b/src/hooks/useToggle.ts @@ -0,0 +1,13 @@ +import { useState } from "react"; + +const useToggle = (initialToggle: boolean): [boolean, () => void] => { + const [toggle, setToggle] = useState(initialToggle ?? false); + + const handleToggle = () => { + setToggle((prevToggle) => !prevToggle); + }; + + return [toggle, handleToggle]; +}; + +export default useToggle; diff --git a/src/hooks/useWindowSize.ts b/src/hooks/useWindowSize.ts new file mode 100644 index 000000000..42e6832ad --- /dev/null +++ b/src/hooks/useWindowSize.ts @@ -0,0 +1,21 @@ +import { debounce } from "../../lib/debounce"; +import { useEffect, useState } from "react"; + +const useWindowSize = () => { + const [windowSize, setWindowSize] = useState(0); + + useEffect(() => { + const handleResize = () => { + setWindowSize(window.innerWidth); + }; + + handleResize(); + const debouncedHandleResize = debounce(handleResize, 300); + window.addEventListener("resize", debouncedHandleResize); + return () => window.removeEventListener("resize", debouncedHandleResize); + }, []); + + return windowSize; +}; + +export default useWindowSize; diff --git a/styles/normalize.scss b/styles/normalize.scss index f214ffabc..1dfed60da 100644 --- a/styles/normalize.scss +++ b/styles/normalize.scss @@ -1,6 +1,7 @@ @import "./index.scss"; :root { + --black: #000000; --gray-900: #111827; --gray-800: #1f2937; --gray-700: #374151;