diff --git a/package-lock.json b/package-lock.json index 8b6c3e46f..772e4d846 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.2", "classnames": "^2.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -5308,6 +5309,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -8322,9 +8346,9 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -14403,6 +14427,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/package.json b/package.json index ea7b18a19..7a3786a45 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.2", "classnames": "^2.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/Main.js b/src/Main.js index 056dfd213..5a5c100f1 100644 --- a/src/Main.js +++ b/src/Main.js @@ -1,13 +1,19 @@ import { BrowserRouter, Routes, Route } from "react-router-dom"; -import App from "./components/App"; +import App from "./components/ui/App"; import UsedMarketPage from "./pages/UsedMarketPage"; +import ProductRagistrationPage from "./pages/ProductRagistrationPage"; +import FreeBoardPage from "./pages/FreeBoardPage"; +import ProductDetailedPage from "./pages/ProductDetailedPage"; function Main() { return ( + } /> } /> + } /> + } /> diff --git a/src/api.js b/src/api.js index 0d279986a..647090d8e 100644 --- a/src/api.js +++ b/src/api.js @@ -1,8 +1,23 @@ -export async function getProducts() { - const response = await fetch("https://panda-market-api.vercel.app/products/"); - if (!response.ok) { - throw new Error("데이터를 불러오는데 실패했습니다"); - } - const body = await response.json(); - return body; +import axios from "axios"; + +const instance = axios.create({ + baseURL: "https://panda-market-api.vercel.app", + timeout: 3000, +}); + +export async function getProducts(params = {}) { + const query = new URLSearchParams(params).toString(); + const res = await instance.get(`/products?${query}`); + return res.data; +} + +export async function getProduct(productId) { + const res = await instance.get(`/products/${productId}`); + return res.data; +} + +export async function getComment({ productId, params }) { + const query = new URLSearchParams(params).toString(); + const res = await instance.get(`/products/${productId}/comments?${query}`); + return res.data; } diff --git a/src/assets/arrow-down.png b/src/assets/arrow-down.png index 771f146a2..0e312dd6d 100644 Binary files a/src/assets/arrow-down.png and b/src/assets/arrow-down.png differ diff --git a/src/assets/arrow-left.png b/src/assets/arrow-left.png new file mode 100644 index 000000000..5f3584021 Binary files /dev/null and b/src/assets/arrow-left.png differ diff --git a/src/assets/arrow-right.png b/src/assets/arrow-right.png new file mode 100644 index 000000000..c8fe6f123 Binary files /dev/null and b/src/assets/arrow-right.png differ diff --git a/src/assets/back-icon.png b/src/assets/back-icon.png new file mode 100644 index 000000000..c80f4478e Binary files /dev/null and b/src/assets/back-icon.png differ diff --git a/src/assets/heart.png b/src/assets/heart.png new file mode 100644 index 000000000..89b6bbe36 Binary files /dev/null and b/src/assets/heart.png differ diff --git a/src/assets/userIcon.png b/src/assets/userIcon.png new file mode 100644 index 000000000..634ea26db Binary files /dev/null and b/src/assets/userIcon.png differ diff --git a/src/components/Container.js b/src/components/container/Container.jsx similarity index 100% rename from src/components/Container.js rename to src/components/container/Container.jsx diff --git a/src/components/Container.module.css b/src/components/container/Container.module.css similarity index 100% rename from src/components/Container.module.css rename to src/components/container/Container.module.css diff --git a/src/components/container/ListContainer.jsx b/src/components/container/ListContainer.jsx new file mode 100644 index 000000000..3599a1862 --- /dev/null +++ b/src/components/container/ListContainer.jsx @@ -0,0 +1,57 @@ +import ProductListContainer from "../ui/ProductListContainer"; +import { useState, useEffect } from "react"; +import { getProducts } from "../../api"; + +function ListContainer() { + const [products, setProducts] = useState([]); + const [order, setOrder] = useState("createdAt"); + const [favoriteProducts, setFavoriteProducts] = useState([]); + const [searchText, setSearchText] = useState(""); + + const sortedProducts = products.sort((a, b) => b[order] - a[order]); + + const loadList = async () => { + try { + const { list } = await getProducts(); + setProducts(list); + } catch (e) { + if (e.response) { + console.log(e.response.status); + console.log(e.response.data); + } else { + console.log("리퀘스트가 실패했습니다."); + } + } + }; + + useEffect(() => { + loadList(); + }, [order]); + + useEffect(() => { + const sortedFavoriteProducts = [...products].sort( + (a, b) => b.favoriteCount - a.favoriteCount + ); + setFavoriteProducts(sortedFavoriteProducts); + }, [products]); + + const handleToggleClick = (option) => { + if (option === "newest") { + setOrder("createdAt"); + } else if (option === "likes") { + setOrder("favoriteCount"); + } + }; + + return ( + + ); +} + +export default ListContainer; diff --git a/src/components/container/PaginationContainer.jsx b/src/components/container/PaginationContainer.jsx new file mode 100644 index 000000000..ad43a6d15 --- /dev/null +++ b/src/components/container/PaginationContainer.jsx @@ -0,0 +1,8 @@ +// import { getProducts } from "../../api"; +// import PaginationBar from "../ui/PaginationBar"; + +function PaginationContainer() { + return; +} + +export default PaginationContainer; diff --git a/src/components/container/ProductDeatilsContainer.jsx b/src/components/container/ProductDeatilsContainer.jsx new file mode 100644 index 000000000..6c7ca9f56 --- /dev/null +++ b/src/components/container/ProductDeatilsContainer.jsx @@ -0,0 +1,57 @@ +import ProductDetails from "../ui/ProductDetails"; +import { useParams } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { getProduct, getComment } from "../../api"; +import CommentDetails from "../ui/CommentDetails"; + +function ProductDetailsContainer() { + const { productId } = useParams(); + const [product, setProduct] = useState(null); + const [comments, setComments] = useState([]); + + useEffect(() => { + const detailedProduct = async () => { + try { + const productData = await getProduct(productId); + setProduct(productData); + } catch (e) { + if (e.response) { + console.log(e.response.status); + console.log(e.response.data); + } else { + console.log("리퀘스트가 실패했습니다."); + } + } + }; + + const detailedComment = async () => { + const params = { limit: 3 }; + try { + const commentData = await getComment({ productId, params }); + setComments(commentData.list); + } catch (e) { + if (e.response) { + console.log(e.response.status); + console.log(e.response.data); + } else { + console.log("리퀘스트가 실패했습니다."); + } + } + }; + detailedComment(); + detailedProduct(); + }, [productId]); + + if (!product) { + return
로딩중
; + } + + return ( + <> + + + + ); +} + +export default ProductDetailsContainer; diff --git a/src/components/container/RagistrationContainer.jsx b/src/components/container/RagistrationContainer.jsx new file mode 100644 index 000000000..6454c1beb --- /dev/null +++ b/src/components/container/RagistrationContainer.jsx @@ -0,0 +1,46 @@ +import RagistrationForm from "../ui/RagistrationForm"; +import { useState } from "react"; + +function RagistrationContainer() { + const [values, setValues] = useState({ + imageFile: null, + productName: "", + productDescription: "", + productPrice: "", + productTag: "", + }); + + const handleChange = (name, value) => { + setValues((prevValues) => ({ + ...prevValues, + [name]: value, + })); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + handleChange(name, value); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + setValues({ + imageFile: null, + productName: "", + productDescription: "", + productPrice: "", + productTag: "", + }); + }; + return ( + + ); +} + +export default RagistrationContainer; diff --git a/src/components/container/ToggleMenuContainer.jsx b/src/components/container/ToggleMenuContainer.jsx new file mode 100644 index 000000000..82285ef5a --- /dev/null +++ b/src/components/container/ToggleMenuContainer.jsx @@ -0,0 +1,27 @@ +import { useCallback, useEffect, useState } from "react"; +import ToggleMenu from "../ui/ToggleMenu"; + +function ToggleMenuContainer({ onClick }) { + const [isOpen, setIsOpen] = useState(false); + + const onToggleMenu = useCallback((e) => { + e.stopPropagation(); + setIsOpen((nextIsOpen) => !nextIsOpen); + }, []); + + useEffect(() => { + if (!isOpen) return; + + const handleClickOutside = () => setIsOpen(false); + window.addEventListener("click", handleClickOutside); + + return () => { + window.removeEventListener("click", handleClickOutside); + }; + }, [isOpen]); + return ( + + ); +} + +export default ToggleMenuContainer; diff --git a/src/components/App.js b/src/components/ui/App.jsx similarity index 100% rename from src/components/App.js rename to src/components/ui/App.jsx diff --git a/src/components/App.module.css b/src/components/ui/App.module.css similarity index 54% rename from src/components/App.module.css rename to src/components/ui/App.module.css index 65964649f..401c79b8f 100644 --- a/src/components/App.module.css +++ b/src/components/ui/App.module.css @@ -1,14 +1,17 @@ -@import-normalize; /* bring in normalize.css styles */ +@import-normalize; * { box-sizing: border-box; word-break: keep-all; font-family: "Pretendard"; + font-size: 16px; + font-weight: 400; + color: #1f2937; } html { overflow-y: scroll; - background-color: #f9f9f9; + background-color: #fcfcfc; } body { @@ -16,8 +19,8 @@ body { } a { - color: #494949; text-decoration: none; + color: #4b5563; } :global(#root) { @@ -33,3 +36,20 @@ a { width: 100%; margin: 0 auto; } + +button { + border: none; + cursor: pointer; +} + +@media (max-width: 1199px) { + .body { + padding: 0 24px; + } +} + +@media (max-width: 767px) { + .body { + padding: 0 16px; + } +} diff --git a/src/components/ui/BestProduct.jsx b/src/components/ui/BestProduct.jsx new file mode 100644 index 000000000..970d2c1f9 --- /dev/null +++ b/src/components/ui/BestProduct.jsx @@ -0,0 +1,27 @@ +import styles from "./BestProduct.module.css"; +import favoriteImg from "../../assets/favorite.png"; +import { Link } from "react-router-dom"; + +function BestProduct({ product }) { + return ( +
+ + {product.name} + +
+

{product.name}

+

{product.price}원

+
+ favorite-img +

{product.favoriteCount}

+
+
+
+ ); +} + +export default BestProduct; diff --git a/src/components/ui/BestProduct.module.css b/src/components/ui/BestProduct.module.css new file mode 100644 index 000000000..2d0276e7d --- /dev/null +++ b/src/components/ui/BestProduct.module.css @@ -0,0 +1,34 @@ +.bestItem__img { + max-width: 100%; + width: 282px; + height: 282px; + border-radius: 16px; + margin-bottom: 16px; +} + +.best_name { + font-weight: 500; + font-size: 14px; + line-height: 16.71px; + margin: 0; + margin-top: 16px; +} + +.price { + font-weight: 700; + line-height: 19.09px; + margin-top: 6px; + margin-bottom: 6px; +} + +.best_name, +.price { + color: #1f2937; +} + +.favorite { + font-weight: 500; + font-size: 12px; + line-height: 14.32px; + color: #4b5563; +} diff --git a/src/components/ui/BestProductsList.jsx b/src/components/ui/BestProductsList.jsx new file mode 100644 index 000000000..cb697d771 --- /dev/null +++ b/src/components/ui/BestProductsList.jsx @@ -0,0 +1,20 @@ +import styles from "./BestProductsList.module.css"; +import "./global.css"; +import BestProduct from "./BestProduct"; + +function BestProductsList({ products }) { + const limitedFavoriteProducts = products.slice(0, 4); + return ( +
    + {limitedFavoriteProducts.map((product) => { + return ( +
  • + +
  • + ); + })} +
+ ); +} + +export default BestProductsList; diff --git a/src/components/ui/BestProductsList.module.css b/src/components/ui/BestProductsList.module.css new file mode 100644 index 000000000..70d099416 --- /dev/null +++ b/src/components/ui/BestProductsList.module.css @@ -0,0 +1,5 @@ +.bestProductsList { + list-style: none; + display: flex; + padding: 0; +} diff --git a/src/components/ui/Button.jsx b/src/components/ui/Button.jsx new file mode 100644 index 000000000..eb01c8b95 --- /dev/null +++ b/src/components/ui/Button.jsx @@ -0,0 +1,15 @@ +import styles from "./Button.module.css"; + +function Button({ onClick, children, variant, type }) { + const buttonClassName = `${styles.button} ${styles[variant] || ""}`; + + return ( + <> + + + ); +} + +export default Button; diff --git a/src/components/ui/Button.module.css b/src/components/ui/Button.module.css new file mode 100644 index 000000000..51b31d2ce --- /dev/null +++ b/src/components/ui/Button.module.css @@ -0,0 +1,25 @@ +.button { + height: 42px; + width: 88px; + border-radius: 8px; + padding: 12px 23px 12px 23px; + gap: 10px; + background-color: #3692ff; + color: #ffffff; + border: none; + font-size: 16px; + font-weight: 600; + line-height: 19.09px; + cursor: pointer; +} + +.login { + margin-top: 14px; + margin-bottom: 14px; +} + +.productRagistration { + width: 133px; + margin-left: 12px; + margin-right: 12px; +} diff --git a/src/components/ui/CommentDetails.jsx b/src/components/ui/CommentDetails.jsx new file mode 100644 index 000000000..dd94af0fa --- /dev/null +++ b/src/components/ui/CommentDetails.jsx @@ -0,0 +1,50 @@ +import styles from "../ui/CommentDetails.module.css"; +import backIcon from "../../assets/back-icon.png"; +import { Link } from "react-router-dom"; +import { elapsedTime } from "../../utils"; + +function CommentDetails({ comments }) { + return ( +
+
+

문의하기

+
+ +
+ +
+
+
+
+ {comments.map((comment) => ( +
+

{comment.content}

+
+ 작성자 이미지 +
+

{comment.writer.nickname}

+

+ {elapsedTime(comment.updatedAt)} +

+
+
+
+ ))} +
+ + 목록으로 돌아가기 + 돌아가기 아이콘 + +
+ ); +} + +export default CommentDetails; diff --git a/src/components/ui/CommentDetails.module.css b/src/components/ui/CommentDetails.module.css new file mode 100644 index 000000000..b344375f2 --- /dev/null +++ b/src/components/ui/CommentDetails.module.css @@ -0,0 +1,109 @@ +.commentWrapper { + border-top: 1px solid #e5e7eb; + display: flex; + flex-direction: column; +} +.commentWrapper h2 { + font-weight: 600; + line-height: 19.09px; + margin-top: 24px; + margin-bottom: 16px; +} + +.commentWrapper input { + border: none; + outline: none; + border-radius: 12px; + padding: 16px 24px 16px 24px; + width: 100%; + min-height: 104px; + background-color: #f3f4f6; +} + +.buttonWrapper { + text-align: right; + margin-top: 16px; + margin-bottom: 24px; +} + +.disabledButton { + background-color: #9ca3af; + color: #ffffff; + cursor: default; + padding: 12px 20px 12px 20px; + height: 42px; + width: 88px; + border-radius: 8px; + font-weight: 600; + line-height: 19.09px; +} + +form:valid .disabledButton { + background-color: #3692ff; + cursor: pointer; +} + +.content { + line-height: 22.4px; + margin-top: 24px; + margin-bottom: 24px; +} + +.contentWrapper { + display: flex; + border-bottom: 1px solid #e5e7eb; + margin-bottom: 40px; +} + +.comment img { + width: 40px; + height: 40px; +} + +.commentContent { + margin-left: 8px; + margin-bottom: 5px; +} + +.nickname { + margin: 0; + margin-bottom: 4px; + font-size: 14px; + line-height: 16.71px; + color: #4b5563; +} + +.updatedAt { + font-size: 12px; + line-height: 14.32px; + margin: 0; + margin-bottom: 29px; + color: #9ca3af; +} + +.goBackButton { + width: 240px; + height: 48px; + border-radius: 40px; + padding: 0; + background-color: #3692ff; + border: none; + cursor: pointer; + padding: 12px 71px 12px 71px; + margin: 0 auto 250px; + display: flex; + align-items: center; + justify-content: center; + text-wrap: nowrap; +} + +.goBackButton span { + color: #ffffff; + font-weight: 600; + font-size: 18px; + line-height: 24px; +} + +.backIcon { + margin-left: 10px; +} diff --git a/src/components/ui/FileInput.jsx b/src/components/ui/FileInput.jsx new file mode 100644 index 000000000..e0f8236e6 --- /dev/null +++ b/src/components/ui/FileInput.jsx @@ -0,0 +1,67 @@ +import { useRef } from "react"; +import iconImg from "../../assets/plus-icon.png"; +import styles from "./FileInput.module.css"; + +function FileInput({ name, value, onChange }) { + const inputRef = useRef(); + + let preview = null; + + const handleChange = (e) => { + const nextValue = e.target.files[0]; + onChange(name, nextValue); + }; + + const handleClearClick = () => { + const inputNode = inputRef.current; + if (!inputNode) return; + + inputNode.value = ""; + onChange(name, null); + }; + + return ( +
+
+ + +
+ {value && ( + 이미지 + )} + {preview && ( + 이미지 + )} + {value && ( + + )} +
+ ); +} + +export default FileInput; diff --git a/src/components/ui/FileInput.module.css b/src/components/ui/FileInput.module.css new file mode 100644 index 000000000..3ffe34949 --- /dev/null +++ b/src/components/ui/FileInput.module.css @@ -0,0 +1,59 @@ +.image_upload { + display: flex; +} + +.image_upload_frame { + width: 282px; + height: 315px; + margin-bottom: 24px; + margin-right: 24px; + border-radius: 12px; + background-color: #f3f4f6; + cursor: pointer; +} + +.image_upload_frame_wrapper { + display: flex; + flex-direction: column; + align-items: center; + margin: 99px auto; +} + +.image_upload_frame_wrapper img { + width: 48px; + height: 48px; +} + +.image_upload_frame_wrapper p { + color: #9ca3af; + font-weight: 600; +} + +img.image_upload__image { + width: 282px; + height: 315px; + border-radius: 12px; + border: 1px; +} + +.image_delete_button { + background-color: #3692ff; + color: #ffffff; + border-radius: 50%; + width: 20px; + height: 20px; + margin-left: -30px; + margin-top: 10px; +} + +.file_input { + display: none; +} + +input { + background-color: #f3f4f6; +} + +.image_upload__p { + color: #9ca3af; +} diff --git a/src/components/ui/Logo.jsx b/src/components/ui/Logo.jsx new file mode 100644 index 000000000..6c257cd8e --- /dev/null +++ b/src/components/ui/Logo.jsx @@ -0,0 +1,15 @@ +import { Link } from "react-router-dom"; +import styles from "./Logo.module.css"; +import logoImg from "../../assets/logo.png"; + +function Logo() { + return ( + <> + + 판다마켓 로고 + + + ); +} + +export default Logo; diff --git a/src/components/ui/Logo.module.css b/src/components/ui/Logo.module.css new file mode 100644 index 000000000..4321dd15c --- /dev/null +++ b/src/components/ui/Logo.module.css @@ -0,0 +1,11 @@ +/* Tablet size*/ +@media screen and (max-width: 1199px) { + .logo { + } +} +/*Mobile size*/ +@media screen and (max-width: 767px) { + .logo { + height: 40px; + } +} diff --git a/src/components/ui/Nav.jsx b/src/components/ui/Nav.jsx new file mode 100644 index 000000000..75711e4bf --- /dev/null +++ b/src/components/ui/Nav.jsx @@ -0,0 +1,55 @@ +import { NavLink, useLocation } from "react-router-dom"; +import Container from "../container/Container"; +import styles from "./Nav.module.css"; +import Logo from "./Logo"; +import Button from "./Button"; +import userIcon from "../../assets/userIcon.png"; + +function Nav() { + const location = useLocation(); + + return ( +
+ +
    + +
  • + { + return { + color: + isActive && location.pathname === "/free" ? "#3692ff" : "", + }; + }} + > + 자유게시판 + +
  • +
  • + { + return { + color: + isActive || location.pathname === "/additem" + ? "#3692ff" + : "", + }; + }} + > + 중고마켓 + +
  • +
+ {location.pathname === "/additem" ? ( + 유저 아이콘 + ) : ( + + )} +
+
+ ); +} + +export default Nav; diff --git a/src/components/Nav.module.css b/src/components/ui/Nav.module.css similarity index 82% rename from src/components/Nav.module.css rename to src/components/ui/Nav.module.css index 92276566e..c806e496b 100644 --- a/src/components/Nav.module.css +++ b/src/components/ui/Nav.module.css @@ -1,8 +1,7 @@ .nav { position: relative; z-index: 1; - padding: 15px 0; - background-color: #fff; + background-color: #ffffff; border-bottom: 1px solid #dfdfdf; width: 100%; } @@ -13,7 +12,7 @@ justify-content: space-between; } -.login__button { +.loginBtn { padding: 12px 23px 12px 23px; border-radius: 8px; background-color: #3692ff; @@ -24,10 +23,6 @@ line-height: 19.09px; } -.visit__link:focus { - color: #3692ff; -} - .item { font-weight: 700; line-height: 21.48px; @@ -46,3 +41,10 @@ ul.menu { ul.menu > li:not(:last-child) { margin-right: 30px; } + +@media (max-width: 767px) { + .item { + font-size: 16px; + line-height: 19.09px; + } +} diff --git a/src/components/ui/PaginationBar.jsx b/src/components/ui/PaginationBar.jsx new file mode 100644 index 000000000..6c49a2cd9 --- /dev/null +++ b/src/components/ui/PaginationBar.jsx @@ -0,0 +1,23 @@ +import styles from "./PaginationBar.module.css"; +import arrowLeft from "../../assets/arrow-left.png"; +import arrowRight from "../../assets/arrow-right.png"; + +function PaginationBar() { + const pages = [1, 2, 3, 4, 5]; + + return ( +
+ + {pages.map((page) => ( + + ))} + +
+ ); +} + +export default PaginationBar; diff --git a/src/components/ui/PaginationBar.module.css b/src/components/ui/PaginationBar.module.css new file mode 100644 index 000000000..0cb73ae47 --- /dev/null +++ b/src/components/ui/PaginationBar.module.css @@ -0,0 +1,24 @@ +.paginationBar { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + margin-top: 40px; + margin-bottom: 113px; +} + +.paginationButton, +.currentPageButton { + width: 40px; + height: 40px; + border: 1px solid #e5e7eb; + color: #6b7280; + padding: 12.5px; + border-radius: 40px; + font-weight: 600; + line-height: 19.09px; + display: flex; + align-items: center; + justify-content: center; + gap: 12.5px; +} diff --git a/src/components/ui/Product.jsx b/src/components/ui/Product.jsx new file mode 100644 index 000000000..5c60e35c0 --- /dev/null +++ b/src/components/ui/Product.jsx @@ -0,0 +1,27 @@ +import styles from "./Product.module.css"; +import favoriteImg from "../../assets/favorite.png"; +import { Link } from "react-router-dom"; + +function Product({ product }) { + return ( +
+ + {product.name} + +
+

{product.name}

+

{product.price}원

+
+ favorite-img +

{product.favoriteCount}

+
+
+
+ ); +} + +export default Product; diff --git a/src/components/ui/Product.module.css b/src/components/ui/Product.module.css new file mode 100644 index 000000000..0c948038a --- /dev/null +++ b/src/components/ui/Product.module.css @@ -0,0 +1,34 @@ +.productImg { + max-width: 100%; + width: 282px; + height: 282px; + border-radius: 16px; + margin-bottom: 16px; +} + +.name { + font-weight: 500; + font-size: 14px; + line-height: 16.71px; + margin: 0; + margin-top: 16px; +} + +.price { + font-weight: 700; + line-height: 19.09px; + margin-top: 6px; + margin-bottom: 6px; +} + +.name, +.price { + color: #1f2937; +} + +.favorite { + font-weight: 500; + font-size: 12px; + line-height: 14.32px; + color: #4b5563; +} diff --git a/src/components/ui/ProductDetails.jsx b/src/components/ui/ProductDetails.jsx new file mode 100644 index 000000000..1c23f6bd5 --- /dev/null +++ b/src/components/ui/ProductDetails.jsx @@ -0,0 +1,28 @@ +import styles from "../ui/ProductDetails.module.css"; +import heartIcon from "../../assets/heart.png"; + +function ProductDetails({ product }) { + return ( +
+ {product.name} +
+

{product.name}

+

{product.price}원

+

상품 소개

+

{product.description}

+

상품 태그

+

#{product.tags}

+ +
+
+ ); +} + +export default ProductDetails; diff --git a/src/components/ui/ProductDetails.module.css b/src/components/ui/ProductDetails.module.css new file mode 100644 index 000000000..f3030e477 --- /dev/null +++ b/src/components/ui/ProductDetails.module.css @@ -0,0 +1,127 @@ +.productImg { + width: 486px; + height: 486px; + margin-right: 24px; + border-radius: 20px; +} + +.productWrapper { + max-width: 996px; + display: flex; + justify-content: space-between; + padding-top: 24px; + padding-bottom: 32px; + margin: 0 auto; +} + +.productContent { + width: 486px; + height: 486px; +} + +.productContent h2 { + font-weight: 600; + font-size: 24px; + line-height: 28.64px; + color: #1f2937; + margin: 0; +} + +.productContent h1 { + font-weight: 600; + font-size: 40px; + line-height: 47.73px; + color: #1f2937; + margin: 0; + margin-top: 16px; + padding-bottom: 16px; +} + +.productContent p { + font-weight: 500; + font-size: 14px; + line-height: 16.71px; + color: #4b5563; + margin-top: 16px; + margin-bottom: 8px; +} + +.productContent h3 { + line-height: 22.4px; + color: #1f2937; + margin: 0; +} + +.productContent h3 { + margin-bottom: 24px; +} + +.favoriteTag { + width: 87px; + height: 40px; + border: 1px solid #e5e7eb; + border-radius: 35px; + padding: 4px 12px 4px 12px; + margin-top: 124px; + gap: 10px; + display: flex; + align-items: center; + color: #6b7280; + font-weight: 500; + line-height: 19.09px; +} + +/* Tablet size*/ +@media screen and (max-width: 1199px) { + .productContent h2 { + font-size: 20px; + line-height: 23.87px; + } + + .productContent h1 { + font-size: 32px; + line-height: 38.19px; + } + + .productContent p { + line-height: 19.6px; + } + + .productImg { + width: 340px; + height: 340px; + } + + .favoriteTag { + margin-top: 25px; + } +} +/*Mobile size*/ +@media screen and (max-width: 767px) { + .productContent h2 { + font-size: 16px; + line-height: 19.09px; + } + + .productContent h1 { + font-size: 24px; + line-height: 28.64px; + } + + .productContent p { + line-height: 16.71px; + } + + .favoriteTag img { + width: 24px; + } + + .productImg { + width: 343px; + height: 343px; + } + + .favoriteTag { + margin-top: 25px; + } +} diff --git a/src/components/ui/ProductListContainer.jsx b/src/components/ui/ProductListContainer.jsx new file mode 100644 index 000000000..587848096 --- /dev/null +++ b/src/components/ui/ProductListContainer.jsx @@ -0,0 +1,50 @@ +import ProductsList from "../ui/ProductsList"; +import BestProductsList from "../ui/BestProductsList"; +import styles from "./ProductListContainer.module.css"; +import Button from "../ui/Button"; +import { Link } from "react-router-dom"; +import ToggleMenuContainer from "../container/ToggleMenuContainer"; +import PaginationBar from "./PaginationBar"; + +function ProductListContainer({ + favoriteProducts, + sortedProducts, + searchText, + handleToggleClick, + setSearchText, +}) { + return ( +
+
+

베스트 상품

+ +
+
+

전체 상품

+
+
+
+ setSearchText(e.target.value)} + autoComplete="off" + > +
+ + + +
+ +
+
+
+ +
+ +
+ ); +} + +export default ProductListContainer; diff --git a/src/components/ui/ProductListContainer.module.css b/src/components/ui/ProductListContainer.module.css new file mode 100644 index 000000000..5496c685c --- /dev/null +++ b/src/components/ui/ProductListContainer.module.css @@ -0,0 +1,57 @@ +.search_container h2 { + font-size: 20px; + line-height: 28px; + margin-top: 6px; + margin-bottom: 6px; + color: #1f2937; +} + +.search_container h2, +.best_product { + font-weight: 700; + font-size: 20px; + line-height: 28px; +} + +.best_product { + color: #111827; + margin-top: 24px; +} + +.search_bar { + display: flex; + align-items: center; +} + +.search_container { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 40px; + margin-bottom: 24px; +} + +.search_bar__form { + display: flex; +} + +.search_bar__form input { + background-image: url("../../assets/search.png"); + background-repeat: no-repeat; + border-radius: 12px; + padding: 9px 20px 9px 44px; + background-position: 16px 50%; + gap: 10px; + border: none; + background-color: #f3f4f6; +} + +.search_bar__form input:focus { + outline: none; +} + +.search_bar_wrapper { + position: relative; + display: flex; + align-items: center; +} diff --git a/src/components/ui/ProductsList.jsx b/src/components/ui/ProductsList.jsx new file mode 100644 index 000000000..cd55c3227 --- /dev/null +++ b/src/components/ui/ProductsList.jsx @@ -0,0 +1,19 @@ +import styles from "./ProductsList.module.css"; +import "./global.css"; +import Product from "./Product"; + +function ProductsList({ products }) { + return ( +
    + {products.map((product) => { + return ( +
  • + +
  • + ); + })} +
+ ); +} + +export default ProductsList; diff --git a/src/components/ui/ProductsList.module.css b/src/components/ui/ProductsList.module.css new file mode 100644 index 000000000..e623dd52f --- /dev/null +++ b/src/components/ui/ProductsList.module.css @@ -0,0 +1,8 @@ +.ProductsList { + list-style: none; + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-auto-rows: auto; + gap: 16px; + padding: 0; +} diff --git a/src/components/ui/RagistrationForm.jsx b/src/components/ui/RagistrationForm.jsx new file mode 100644 index 000000000..597efb63a --- /dev/null +++ b/src/components/ui/RagistrationForm.jsx @@ -0,0 +1,78 @@ +import FileInput from "./FileInput"; +import styles from "./RagistrationForm.module.css"; + +function ProductRagistrationForm({ + values, + handleChange, + handleInputChange, + handleSubmit, +}) { + return ( +
+
+

상품등록하기

+ +
+ + + + + + + + + + + {values.productTag && ( +

+ {values.productTag} + +

+ )} +
+ ); +} + +export default ProductRagistrationForm; diff --git a/src/components/ui/RagistrationForm.module.css b/src/components/ui/RagistrationForm.module.css new file mode 100644 index 000000000..6f08acac1 --- /dev/null +++ b/src/components/ui/RagistrationForm.module.css @@ -0,0 +1,96 @@ +form.product_ragistration_form { + display: flex; + flex-direction: column; + margin: 24px auto 0px; +} + +form.product_ragistration_form h1 { + font-size: 28px; + font-weight: 700; + line-height: 33.41px; +} + +textarea { + width: 1200px; + height: 200px; + border: none; + padding: 16px 0 16px 24px; +} + +input { + background-color: #f3f4f6; +} + +form.product_ragistration_form label { + font-size: 18px; + font-weight: 700; + line-height: 21.48px; + margin-bottom: 12px; + cursor: pointer; +} + +form.product_ragistration_form input { + gap: 10px; + border: none; + height: 56px; +} + +.product_ragistration_form input:focus, +textarea:focus { + outline: none; +} + +form.product_ragistration_form input, +textarea { + font-size: 16px; + font-weight: 400; + line-height: 24px; + border-radius: 12px; + background-color: #f3f4f6; + margin-bottom: 24px; +} + +.product_ragistration_form_wrapper { + display: flex; + justify-content: space-between; + align-items: center; +} + +form.product_ragistration_form:valid .disabledButton { + background-color: #3692ff; + cursor: pointer; +} + +.disabledButton { + background-color: #9ca3af; + color: #ffffff; + cursor: default; + padding: 12px 20px 12px 20px; + height: 42px; + width: 88px; + border-radius: 8px; + font-weight: 600; + line-height: 19.09px; +} + +.product_ragistration_form input { + padding: 16px 0 16px 24px; +} + +.tag { + width: 100px; + height: 48px; + background-color: #f9fafb; + border-radius: 26px; + padding: 12px 12px 12px 16px; + display: flex; + align-items: center; + justify-content: center; +} + +.delete_button { + background-color: #9ca3af; + border-radius: 50%; + color: #ffffff; + margin-left: 9px; +} diff --git a/src/components/ui/ToggleMenu.jsx b/src/components/ui/ToggleMenu.jsx new file mode 100644 index 000000000..a918cbbe4 --- /dev/null +++ b/src/components/ui/ToggleMenu.jsx @@ -0,0 +1,38 @@ +import styles from "./ToggleMenu.module.css"; +import arrowDownImg from "../../assets/arrow-down.png"; + +const OPTIONS = { + NEWEST: { label: "최신순", value: "newest" }, + MOST_LIKED: { label: "좋아요순", value: "likes" }, +}; + +function ToggleMenu({ onClick, onToggleMenu, isOpen }) { + return ( +
+ + {isOpen && ( +
    +
    +
  • onClick(OPTIONS.NEWEST.value)} + > + +
  • +
    +
  • onClick(OPTIONS.MOST_LIKED.value)} + > + +
  • +
+ )} +
+ ); +} + +export default ToggleMenu; diff --git a/src/components/ToggleMenu.module.css b/src/components/ui/ToggleMenu.module.css similarity index 94% rename from src/components/ToggleMenu.module.css rename to src/components/ui/ToggleMenu.module.css index b05ea05df..c66e57d1f 100644 --- a/src/components/ToggleMenu.module.css +++ b/src/components/ui/ToggleMenu.module.css @@ -15,6 +15,10 @@ align-items: center; } +.iconText { + font-size: 16px; +} + .toggleMenu .iconText { position: relative; left: -15px; @@ -44,8 +48,7 @@ ul.popup li.disabled { background-color: #ffffff; color: #c4c4c4; user-select: none; - cursor: default; - border: 1px 1px 0px 1px; + cursor: pointer; height: 42px; } diff --git a/src/components/ui/global.css b/src/components/ui/global.css new file mode 100644 index 000000000..c262ed07e --- /dev/null +++ b/src/components/ui/global.css @@ -0,0 +1,16 @@ +.favorite-contents { + display: flex; + align-items: center; +} + +.favorite-contents img { + height: 16px; +} + +.favorite-contents h3 { + margin-left: 4px; +} + +.favorite-contents h3 { + color: #4b5563; +} diff --git a/src/pages/FreeBoardPage.js b/src/pages/FreeBoardPage.js new file mode 100644 index 000000000..b51c453e5 --- /dev/null +++ b/src/pages/FreeBoardPage.js @@ -0,0 +1,9 @@ +function FreeBoardPage() { + return ( + <> +

자유게시판

+ + ); +} + +export default FreeBoardPage; diff --git a/src/pages/ProductDetailedPage.js b/src/pages/ProductDetailedPage.js new file mode 100644 index 000000000..9da6d98e4 --- /dev/null +++ b/src/pages/ProductDetailedPage.js @@ -0,0 +1,7 @@ +import ProductDetailsContainer from "../components/container/ProductDeatilsContainer"; + +function ProductDetailedPage() { + return ; +} + +export default ProductDetailedPage; diff --git a/src/pages/ProductRagistrationPage.js b/src/pages/ProductRagistrationPage.js index ea00b9c8d..c2cf4acb2 100644 --- a/src/pages/ProductRagistrationPage.js +++ b/src/pages/ProductRagistrationPage.js @@ -1,7 +1,7 @@ -import ProductRagistrationForm from "../components/ProductRagistrationForm"; +import RagistrationContainer from "../components/container/RagistrationContainer"; function ProductRagistrationPage() { - return ; + return ; } export default ProductRagistrationPage; diff --git a/src/pages/UsedMarketPage.js b/src/pages/UsedMarketPage.js index e24328602..69309d656 100644 --- a/src/pages/UsedMarketPage.js +++ b/src/pages/UsedMarketPage.js @@ -1,83 +1,7 @@ -import { Link } from "react-router-dom"; -import { useState, useEffect } from "react"; -import { getProducts } from "../api"; -import ProductsList from "../components/ProductsList"; -import BestProductsList from "../components/BestProductsList"; -import "./UsedMarketPage.css"; -import searchImg from "../assets/search.png"; -import ToggleMenu from "../components/ToggleMenu"; +import ListContainer from "../components/container/ListContainer"; function UsedMarketPage() { - const [products, setProducts] = useState([]); - const [order, setOrder] = useState("createdAt"); - const [favoriteProducts, setFavoriteProducts] = useState([]); - - const [searchText, setSearchText] = useState(""); - - const sortedProducts = products.sort((a, b) => b[order] - a[order]); - - const handleLoad = async () => { - const { list } = await getProducts(); - setProducts(list); - }; - - useEffect(() => { - handleLoad(); - }, [order]); - - useEffect(() => { - const sortedFavoriteProducts = [...products].sort( - (a, b) => b.favoriteCount - a.favoriteCount - ); - setFavoriteProducts(sortedFavoriteProducts); - }, [products]); - - const handleToggleClick = (option) => { - if (option === "newest") { - setOrder("createdAt"); - } else if (option === "likes") { - setOrder("favoriteCount"); - } - }; - - return ( -
-
-

베스트 상품

- -
-
-

전체 상품

-
-
-
- setSearchText(e.target.value)} - > - {searchText ? null : ( - 검색 - )} -
- - 상품 등록하기 - -
- -
-
-
- -
-
- ); + return ; } export default UsedMarketPage; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 000000000..025d0ce8b --- /dev/null +++ b/src/utils.js @@ -0,0 +1,18 @@ +export const elapsedTime = (date) => { + const start = new Date(date); + const end = new Date(); + + const seconds = Math.floor((end.getTime() - start.getTime()) / 1000); + if (seconds < 60) return "방금 전"; + + const minutes = seconds / 60; + if (minutes < 60) return `${Math.floor(minutes)}분 전`; + + const hours = minutes / 60; + if (hours < 24) return `${Math.floor(hours)}시간 전`; + + const days = hours / 24; + if (days < 7) return `${Math.floor(days)}일 전`; + + return start.toLocaleDateString(); +};