diff --git a/src/App.jsx b/src/App.jsx index 10f1d00d..99a14d20 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,8 +2,10 @@ import "./App.css"; import "./base.css"; import { Routes, Route } from "react-router-dom"; import Home from "./pages/Home/Home"; -import Login from "./pages/Login/Login"; +import LoginPage from "./pages/Login/LoginPage"; +import SignupPage from "./pages/Login/SignupPage"; import Market from "./pages/Market/Market"; +import Product from "./pages/Product/Product"; import Community from "./pages/Community/Community"; import AddItem from "./pages/AddItem/AddItem"; import NotFound from "./pages/NotFound/NotFound"; @@ -12,9 +14,10 @@ function App() { return ( } /> - } /> - } /> + } /> + } /> } /> + } /> } /> } /> } /> diff --git a/src/api/getComments.js b/src/api/getComments.js new file mode 100644 index 00000000..1359bb79 --- /dev/null +++ b/src/api/getComments.js @@ -0,0 +1,7 @@ +export async function getComments({ id, limit = 8 }) { + const response = await fetch( + `https://panda-market-api.vercel.app/products/${id}/comments?limit=${limit}` + ); + const body = await response.json(); + return body; +} diff --git a/src/api/getProduct.js b/src/api/getProduct.js new file mode 100644 index 00000000..4b171e27 --- /dev/null +++ b/src/api/getProduct.js @@ -0,0 +1,7 @@ +export async function getProduct({ id }) { + const response = await fetch( + `https://panda-market-api.vercel.app/products/${id}` + ); + const body = await response.json(); + return body; +} diff --git a/src/api.js b/src/api/getProducts.js similarity index 100% rename from src/api.js rename to src/api/getProducts.js diff --git a/src/assets/images/fallback.png b/src/assets/images/fallback.png index c009dcb3..4c37cd0a 100644 Binary files a/src/assets/images/fallback.png and b/src/assets/images/fallback.png differ diff --git a/src/assets/images/go_back.svg b/src/assets/images/go_back.svg new file mode 100644 index 00000000..3ebbc2da --- /dev/null +++ b/src/assets/images/go_back.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/ic_heart.svg b/src/assets/images/ic_heart.svg new file mode 100644 index 00000000..7e8db018 --- /dev/null +++ b/src/assets/images/ic_heart.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/ic_options.svg b/src/assets/images/ic_options.svg new file mode 100644 index 00000000..9e5dae1a --- /dev/null +++ b/src/assets/images/ic_options.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/images/ic_profile.svg b/src/assets/images/ic_profile.svg new file mode 100644 index 00000000..834caf76 --- /dev/null +++ b/src/assets/images/ic_profile.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/no_comment.svg b/src/assets/images/no_comment.svg new file mode 100644 index 00000000..5444cbbb --- /dev/null +++ b/src/assets/images/no_comment.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/base.css b/src/base.css index 70189327..0618b8f1 100644 --- a/src/base.css +++ b/src/base.css @@ -18,6 +18,10 @@ box-sizing: border-box; } +h1, +h2, +h3, +h4, html, body, div, diff --git a/src/components/Header.css b/src/components/Header.css index a8bdaac5..5743b9e6 100644 --- a/src/components/Header.css +++ b/src/components/Header.css @@ -38,6 +38,15 @@ border: none; } +.Header .login-button:hover { + background-color: var(--blue200); + color: white; + cursor: pointer; + transition: all 0.3s ease; + transform: translateY(2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + .Header .header-left .link-section .active { color: var(--blue100); } diff --git a/src/hooks/useEmailValidator.jsx b/src/hooks/useEmailValidator.jsx new file mode 100644 index 00000000..25a2d667 --- /dev/null +++ b/src/hooks/useEmailValidator.jsx @@ -0,0 +1,18 @@ +import { useState } from "react"; + +export function useEmailValidator(initial = "") { + const [email, setEmail] = useState(initial); + const [touched, setTouched] = useState(false); + + const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + const hasError = touched && !isValid; + + return { + email, + setEmail, + hasError, + isValid, + onChange: (e) => setEmail(e.target.value), + onBlur: () => setTouched(true), + }; +} diff --git a/src/hooks/usePasswordInput.jsx b/src/hooks/usePasswordInput.jsx new file mode 100644 index 00000000..b1e548af --- /dev/null +++ b/src/hooks/usePasswordInput.jsx @@ -0,0 +1,20 @@ +import { useState } from "react"; + +export function usePasswordInput(initial = "") { + const [value, setValue] = useState(initial); + const [touched, setTouched] = useState(false); + const [visible, setVisible] = useState(false); + + const hasError = touched && value.length < 8; + + return { + value, + setValue, + touched, + visible, + hasError, + toggleVisible: () => setVisible((v) => !v), + onChange: (e) => setValue(e.target.value), + onBlur: () => setTouched(true), + }; +} diff --git a/src/pages/AddItem/AddItem.css b/src/pages/AddItem/AddItem.css index 7e822b5c..e7d0c701 100644 --- a/src/pages/AddItem/AddItem.css +++ b/src/pages/AddItem/AddItem.css @@ -6,7 +6,7 @@ gap: 24px; } -.register { +.add-item .register { display: flex; justify-content: space-between; align-items: center; @@ -14,7 +14,7 @@ font-weight: 700; } -.register button { +.add-item .register button { padding: 12px 23px; background-color: var(--blue100); color: var(--gray100); @@ -24,12 +24,21 @@ font-weight: 600; } -.register button:disabled { +.add-item .register button:disabled { background-color: var(--gray400); cursor: not-allowed; } -.info-section { +.add-item .register button:not(:disabled):hover { + background-color: var(--blue200); + color: white; + cursor: pointer; + transition: all 0.3s ease; + transform: translateY(2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.add-item .info-section { display: flex; flex-direction: column; gap: 32px; @@ -38,26 +47,26 @@ line-height: 26px; } -.info-section.size-down { +.add-item .info-section.size-down { gap: 24px; } -.image-section, -.name-section, -.description-section, -.price-section, -.tag-section { +.add-item .image-section, +.add-item .name-section, +.add-item .description-section, +.add-item .price-section, +.add-item .tag-section { display: flex; flex-direction: column; gap: 16px; } -.upload-section { +.add-item .upload-section { display: flex; gap: 24px; } -.upload-button { +.add-item .upload-button { width: 282px; height: 282px; display: flex; @@ -68,21 +77,21 @@ cursor: pointer; } -.image-upload { +.add-item .image-upload { display: flex; flex-direction: column; align-items: center; gap: 12px; } -.image-upload span { +.add-item .image-upload span { font-size: 16px; font-weight: 400; line-height: 26px; color: var(--gray400); } -.image-uploaded { +.add-item .image-uploaded { width: 282px; height: 282px; border-radius: 12px; @@ -90,21 +99,21 @@ position: relative; } -.image-uploaded img { +.add-item .image-uploaded img { width: 100%; height: 100%; } -.image-uploaded .delete-button { +.add-item .image-uploaded .delete-button { position: absolute; top: 12px; right: 12px; } -.name-input, -.description-input, -.price-input, -.tag-input { +.add-item .name-input, +.add-item .description-input, +.add-item .price-input, +.add-item .tag-input { height: 56px; padding: 16px 24px; border-radius: 12px; @@ -115,25 +124,25 @@ line-height: 26px; } -.description-input { +.add-item .description-input { height: 282px; resize: none; letter-spacing: -0.7px; /* textarea와 input의 간격 차이 때문에 추가 */ } -.show-tag { +.add-item .show-tag { display: flex; flex-direction: column; gap: 14px; } -.tag-list ul { +.add-item .tag-list ul { display: flex; flex-wrap: wrap; gap: 12px; } -.tag-list li { +.add-item .tag-list li { display: flex; justify-content: center; align-items: center; @@ -146,7 +155,7 @@ font-weight: 400; } -.delete-button { +.add-item .delete-button { background-image: url(../../assets/images/button_x.svg); width: 20px; height: 20px; @@ -157,13 +166,13 @@ background-color: transparent; } -.error-message { +.add-item .error-message { color: var(--red); font-size: 16px; font-weight: 400; } -.hidden { +.add-item .hidden { display: none; } @@ -178,16 +187,16 @@ margin: 16px 24px; } - .info-section { + .add-item .info-section { gap: 24px; } - .upload-section { + .add-item .upload-section { gap: 10px; } - .upload-button, - .image-uploaded { + .add-item .upload-button, + .add-item .image-uploaded { width: 168px; height: 168px; } diff --git a/src/pages/Home/Home.jsx b/src/pages/Home/Home.jsx index 7aa50e89..c34e18d7 100644 --- a/src/pages/Home/Home.jsx +++ b/src/pages/Home/Home.jsx @@ -8,30 +8,42 @@ import img1 from "../../assets/images/img_home_01.svg"; import img2 from "../../assets/images/img_home_02.svg"; import img3 from "../../assets/images/img_home_03.svg"; +const contents = [ + { + img: img1, + category: "Hot Item", + header: "인기 상품을
확인해 보세요", + content: "가장 Hot한 중고거래 물품을
판다 마켓에서 확인해 보세요", + }, + { + img: img2, + category: "Search", + header: "구매를 원하는
상품을 검색하세요", + content: "구매하고 싶은 물품은 검색해서
쉽게 찾아보세요", + }, + { + img: img3, + category: "Register", + header: "판매를 원하는
상품을 등록하세요", + content: "어떤 물건이든 판매하고 싶은 상품을
쉽게 등록하세요", + }, +]; + const Home = () => { return ( <>
- - - + {contents.map(({ img, category, header, content }) => ( + + ))}