diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 74b5e053..00000000 --- a/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/App.js b/src/App.js deleted file mode 100644 index 37845757..00000000 --- a/src/App.js +++ /dev/null @@ -1,25 +0,0 @@ -import logo from './logo.svg'; -import './App.css'; - -function App() { - return ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
-
- ); -} - -export default App; diff --git a/src/App.jsx b/src/App.jsx index c14030ab..a8fb3893 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,10 +1,11 @@ import { BrowserRouter, Routes, Route } from "react-router-dom"; import Home from "./pages/home"; -import SignUp from "./pages/sign/SignUp"; +import Signup from "./pages/sign/Signup"; import Items from "./pages/items"; import GNB from "@/components/GNB"; import { WinSizeProvider } from "./contexts/winSizeContext"; import Login from "./pages/sign/LogIn"; +import AddItem from "./pages/additem"; function App() { return ( @@ -15,10 +16,11 @@ function App() { } /> } /> - } /> + } /> }> {/* } /> */} + } /> diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index 1f03afee..00000000 --- a/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/assets/icons/ico_close.svg b/src/assets/icons/ico_close.svg new file mode 100644 index 00000000..96984c62 --- /dev/null +++ b/src/assets/icons/ico_close.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/ico_plus.svg b/src/assets/icons/ico_plus.svg new file mode 100644 index 00000000..5bb9abf5 --- /dev/null +++ b/src/assets/icons/ico_plus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/Button.jsx b/src/components/Button.jsx index ae9f0a1a..b87581e3 100644 --- a/src/components/Button.jsx +++ b/src/components/Button.jsx @@ -1,55 +1,53 @@ import styled from "styled-components"; import color from "../utils/color"; +import { Link } from "react-router-dom"; -const ButtonContainer = styled.button` - justify-content: center; - align-items: center; - border: none; - background: ${color("primary100")}; - color: ${color("secondary200")}; - font: 500 20px/32px "Pretendard"; - cursor: pointer; - transition: all 0.05s ease-out; - text-align: center; - text-decoration: none; - border-radius: ${(props) => (props.round ? "40px" : "8px")}; - width: ${(props) => props.width}; - height: ${(props) => props.height}; - line-height: ${(props) => props.height}; - display: block; - padding: 0 8px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - font-size: ${(props) => - props.round ? (props.height > "48px" ? "20px" : "18px") : "16px"}; - - .link { +const ButtonContainer = styled.div` + .btn { + justify-content: center; + align-items: center; + border: none; + background: ${color("primary100")}; + color: ${color("secondary200")}; + font: 500 20px/32px "Pretendard"; + cursor: pointer; + transition: all 0.05s ease-out; + text-align: center; text-decoration: none; - color: white; - } + border-radius: ${(props) => (props.round ? "40px" : "8px")}; + width: ${(props) => props.width}; + height: ${(props) => props.height}; + line-height: ${(props) => props.height}; + display: block; + padding: 0 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; - &:hover { - background: #1967d6; - } + font-size: ${(props) => + props.round ? (props.height > "48px" ? "20px" : "18px") : "16px"}; - &:active { - background: ${color("primary300")}; - } + &:hover { + background: #1967d6; + } - &[disabled] { - background: ${color("secondary400")}; - cursor: not-allowed; - } + &:active { + background: ${color("primary300")}; + } - ${(props) => - props.outline && - ` + &[disabled] { + background: ${color("secondary400")}; + cursor: not-allowed; + } + + ${(props) => + props.outline && + ` background: ${color("secondary10")}; color: ${color("primary100")}; border: 1px solid ${color("primary100")}; `} + } `; function Button({ @@ -58,6 +56,8 @@ function Button({ height = "42px", round = false, outline = false, + to, + disabled = false, ...rest }) { return ( @@ -67,8 +67,17 @@ function Button({ height={height} round={round} outline={outline} + to={to} > - {children} + {to ? ( + + {children} + + ) : ( + + )} ); } diff --git a/src/components/GNB.jsx b/src/components/GNB.jsx index 0dd6f2df..c08840e5 100644 --- a/src/components/GNB.jsx +++ b/src/components/GNB.jsx @@ -45,7 +45,7 @@ const GNBContainer = styled.header` &-market { color: ${(props) => - props.currentPath === "/items" + props.currentPath === "/items" || props.currentPath === "/additem" ? color("primary100") : color("secondary600")}; } @@ -94,17 +94,16 @@ function GNB() { 판다마켓 로고 - {location.pathname === "/items" && ( + {(location.pathname === "/items" || + location.pathname === "/additem") && (
자유게시판 중고마켓
)} - - - + ); diff --git a/src/index.css b/src/index.css deleted file mode 100644 index ec2585e8..00000000 --- a/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/src/logo.svg b/src/logo.svg deleted file mode 100644 index 9dfc1c05..00000000 --- a/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/pages/additem/FileInput/index.jsx b/src/pages/additem/FileInput/index.jsx new file mode 100644 index 00000000..86f1e159 --- /dev/null +++ b/src/pages/additem/FileInput/index.jsx @@ -0,0 +1,59 @@ +import { useState } from "react"; + +import IcoClose from "@/assets/icons/ico_close.svg"; +import IcoPlus from "@/assets/icons/ico_plus.svg"; + +import * as ParentS from "../style"; +import * as CurrentS from "./style"; + +function FileInput({ title, ...rest }) { + const [thumbnail, setThumbnail] = useState(""); + const [warning, setWarning] = useState(""); + + function handleFileUpload(e) { + let fileArr = e.target.files; + let image = window.URL.createObjectURL(fileArr[0]); + setThumbnail(image); + } + + function handleOnClick(e) { + if (thumbnail) { + e.preventDefault(); + setWarning("*이미지 등록은 최대 1개까지 가능합니다"); + } + } + + return ( + + {title} + + + + + 이미지 등록 + + {thumbnail && ( + { + e.preventDefault(); + setThumbnail(""); + }} + > + + + + )} + + {warning} + + ); +} + +export default FileInput; diff --git a/src/pages/additem/FileInput/style.jsx b/src/pages/additem/FileInput/style.jsx new file mode 100644 index 00000000..81094ccd --- /dev/null +++ b/src/pages/additem/FileInput/style.jsx @@ -0,0 +1,51 @@ +import styled from "styled-components"; +import color from "@/utils/color"; + +export const File = styled.div` + width: 282px; + height: 282px; + + position: relative; + border-radius: 12px; + background: ${color("secondary200")}; + color: ${color("secondary400")}; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + overflow: hidden; + + font-size: 16px; + font-weight: 400; + transition: all 0.1s ease-out; + + @media (max-width: 1200px) { + width: 168px; + height: 168px; + } + + .thumbnail { + width: 100%; + } + + .close { + position: absolute; + top: 12px; + right: 12px; + } + + &:hover { + filter: brightness(0.95); + } +`; + +export const FileContainer = styled.div` + display: flex; + gap: 24px; + + @media (max-width: 1200px) { + gap: 10px; + } +`; diff --git a/src/pages/additem/index.jsx b/src/pages/additem/index.jsx new file mode 100644 index 00000000..3c046101 --- /dev/null +++ b/src/pages/additem/index.jsx @@ -0,0 +1,105 @@ +import { useState } from "react"; + +import IcoClose from "@/assets/icons/ico_close.svg"; +import Button from "@/components/Button"; +import FileInput from "./FileInput"; + +import * as S from "./style"; + +function Input({ + title, + width = "100%", + height = "56px", + type = "text", + ...rest +}) { + return ( + + {title} + {type === "text" && ( + + )} + {type === "textarea" && ( + + )} + + ); +} + +function AddItem() { + const [tags, setTags] = useState([]); + const [currentTag, setCurrentTag] = useState(""); + const [price, setPrice] = useState(0); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + + function handleKeyDown(e) { + if (e.key === "Enter" && currentTag.trim() !== "") { + e.preventDefault(); + setTags((prev) => [...prev, currentTag.trim()]); + setCurrentTag(""); + } + } + + return ( + <> + + + 상품 등록하기 + + + + setName(e.target.value)} + placeholder="상품명을 입력해주세요" + /> + setDescription(e.target.value)} + /> + + {/* useEffect로 e.target.selectionStart = e.target.value.length - 1; + e.target.selectionEnd = e.target.value.length - 1;로 텍스트 커서 컨트롤하기 */} + + setPrice(Number(e.target.value.replace(/[^0-9]/g, ""))) + } + placeholder="판매 가격을 입력해주세요" + /> + setCurrentTag(e.target.value)} + onKeyDown={handleKeyDown} + /> + + {tags.map((v, i) => ( + + setTags((prev) => prev.filter((_, ii) => ii !== i)) + } + > + #{v} + + ))} + + + + ); +} + +export default AddItem; diff --git a/src/pages/additem/style.jsx b/src/pages/additem/style.jsx new file mode 100644 index 00000000..629c06d1 --- /dev/null +++ b/src/pages/additem/style.jsx @@ -0,0 +1,87 @@ +import styled from "styled-components"; +import color from "../../utils/color"; + +export const Tag = styled.div` + display: flex; + height: 36px; + overflow: hidden; + padding: 6px 12px 6px 16px; + border-radius: 26px; + background: ${color("secondary200")}; + justify-content: center; + align-items: center; + gap: 10px; + cursor: pointer; + transition: all 0.1s ease-out; + + &:hover { + filter: brightness(0.95); + } +`; + +export const TagContainer = styled.div` + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-top: -10px; +`; + +export const SubmitContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + color: ${color("secondary800")}; + font-size: 20px; + font-weight: 700; +`; + +export const InputContainer = styled.form` + max-width: 1200px; + padding: 0 24px; + display: flex; + flex-direction: column; + gap: 32px; + margin: 30px auto 200px; +`; + +export const InputText = styled.input` + width: ${(props) => props.width}; + height: ${(props) => props.height}; + + display: flex; + padding: 16px 24px; + border-radius: 12px; + background: ${color("secondary200")}; + border: none; + box-sizing: border-box; +`; + +export const Textarea = styled.textarea` + width: ${(props) => props.width}; + height: ${(props) => props.height}; + + display: flex; + padding: 16px 24px; + border-radius: 12px; + background: ${color("secondary200")}; + border: none; + box-sizing: border-box; + resize: none; +`; + +export const Label = styled.label` + color: ${color("secondary800")}; + font-size: 18px; + font-weight: 700; + line-height: 26px; + display: flex; + flex-direction: column; + gap: 16px; + + .warning { + color: ${color("errorRed")}; + font-size: 16px; + font-weight: 400; + line-height: 26px; + } +`; diff --git a/src/pages/home/index.jsx b/src/pages/home/index.jsx index bb32b1f5..02c44584 100644 --- a/src/pages/home/index.jsx +++ b/src/pages/home/index.jsx @@ -23,11 +23,15 @@ function Home() { 일상의 모든 물건을
거래해 보세요 - - - + 판다마켓 랜딩 이미지 diff --git a/src/pages/items/BestItems/index.jsx b/src/pages/items/BestItems/index.jsx index 4bdb6c43..755fb0ba 100644 --- a/src/pages/items/BestItems/index.jsx +++ b/src/pages/items/BestItems/index.jsx @@ -1,7 +1,6 @@ import Card from "@/pages/items/Card"; import useArticles from "@/hooks/useArticles"; import styled from "styled-components"; -import { useEffect, useState } from "react"; import { useWinSize } from "../../../contexts/winSizeContext"; const Container = styled.div` @@ -19,20 +18,20 @@ const TitleBar = styled.div` margin-bottom: 16px; `; +const getQuantity = (winSize) => { + if (winSize === "mobile") { + return 1; + } else if (winSize === "tablet") { + return 2; + } else { + return 4; + } +}; + function BestProducts() { - const [quantity, setQuantity] = useState(4); - const { articles, isLoading } = useArticles(1, quantity, "favorite"); const { winSize } = useWinSize(); - - useEffect(() => { - if (winSize === "mobile") { - setQuantity(1); - } else if (winSize === "tablet") { - setQuantity(2); - } else { - setQuantity(4); - } - }, [winSize]); + const quantity = getQuantity(winSize); + const { articles, isLoading } = useArticles(1, quantity, "favorite"); return ( diff --git a/src/pages/items/index.jsx b/src/pages/items/index.jsx index ddda6644..d02867a5 100644 --- a/src/pages/items/index.jsx +++ b/src/pages/items/index.jsx @@ -18,7 +18,7 @@ const ItemsContainer = styled.div` function Items() { return ( - + diff --git a/src/pages/items/products/Pagination/Pagination.jsx b/src/pages/items/products/Pagination/Pagination.jsx new file mode 100644 index 00000000..c52dac00 --- /dev/null +++ b/src/pages/items/products/Pagination/Pagination.jsx @@ -0,0 +1,34 @@ +import * as S from "../style"; +import PageArrow from "@/assets/icons/ico_arrow_right.svg"; + +function Pagination({ pages, count, quantity, pageSection }) { + const [page, setPage] = pages; + + return ( + + + {Array(Math.min(~~(count / quantity) - pageSection + 1, 5)) + .fill("") + .map((_, i) => ( + + ))} + + + ); +} + +export default Pagination; diff --git a/src/pages/items/products/TitleBar/index.jsx b/src/pages/items/products/TitleBar/index.jsx index f63352f4..e0f3e4b8 100644 --- a/src/pages/items/products/TitleBar/index.jsx +++ b/src/pages/items/products/TitleBar/index.jsx @@ -1,4 +1,3 @@ -import { Link } from "react-router-dom"; import { useState } from "react"; import * as S from "./style"; @@ -26,10 +25,8 @@ function TitleBar({ winSize, keywords, pages, orders }) {
전체 상품 -
@@ -70,10 +67,8 @@ function TitleBar({ winSize, keywords, pages, orders }) { style={{ width: "324px" }} /> -
{ + if (winSize === "mobile") { + return 4; + } else if (winSize === "tablet") { + return 6; + } else { + return 10; + } +}; function Products() { //나중에 reducer로 정리하기 const [page, setPage] = useState(1); - const [quantity, setQuantity] = useState(10); const [keyword, setKeyword] = useState(""); - const [order, setOrder] = useState("recent"); const { winSize } = useWinSize(); + const quantity = getQuantity(winSize); let { articles, isLoading, count } = useArticles( page, @@ -24,25 +33,11 @@ function Products() { keyword ); - useEffect(() => { - if (winSize === "mobile") { - setQuantity(4); - } else if (winSize === "tablet") { - setQuantity(6); - } else { - setQuantity(10); - } - }, [winSize]); - - console.log( - `now: ${page} / result: ${count - page * quantity} / total: ${count}` - ); - const pageSection = ~~((page - 1) / 5) * 5; return ( <> - {/* 디바운스 먹이기 */} + {/* 디바운스 추가 */} )} - - - {Array(Math.min(~~(count / quantity) - pageSection + 1, 5)) - .fill("") - .map((_, i) => ( - - ))} - - + ); } diff --git a/src/pages/sign/ShowPassword.jsx b/src/pages/sign/ShowPassword.jsx index cd967a40..a78d4b57 100644 --- a/src/pages/sign/ShowPassword.jsx +++ b/src/pages/sign/ShowPassword.jsx @@ -9,8 +9,8 @@ const Button = styled.button` border: none; right: 24px; left: auto; - top: 64%; - transform: translateY(-65%); + top: 50%; + transform: translateY(-50%); background-image: url(${EyeOpen}); width: 24px; height: 24px; @@ -20,30 +20,27 @@ const Button = styled.button` } `; -function ShowPassword() { +function ShowPassword({ setShowPw }) { const btnRef = useRef(null); btnRef.current?.addEventListener("mousedown", (e) => { - const sibling = e.target.previousElementSibling; e.target.classList.remove("password-hide"); - sibling.type = "text"; + setShowPw(true); }); btnRef.current?.addEventListener("mouseup", (e) => { - const sibling = e.target.previousElementSibling; e.target.classList.add("password-hide"); - sibling.type = "password"; + setShowPw(false); }); btnRef.current?.addEventListener("mouseleave", (e) => { - const sibling = e.target.previousElementSibling; e.target.classList.add("password-hide"); - sibling.type = "password"; + setShowPw(false); }); return ( diff --git a/src/pages/sign/SignInput.jsx b/src/pages/sign/SignInput.jsx index cc210871..65640df9 100644 --- a/src/pages/sign/SignInput.jsx +++ b/src/pages/sign/SignInput.jsx @@ -1,3 +1,4 @@ +import { useRef, useState } from "react"; import ShowPassword from "./ShowPassword"; function SignInput({ @@ -12,24 +13,29 @@ function SignInput({ onChange, ...rest }) { + const sibling = useRef(null); + const [showPw, setShowPw] = useState(false); + return ( <> {inputState.msg && ( diff --git a/src/pages/sign/style.jsx b/src/pages/sign/style.jsx index e6493eaf..8fbc3fb6 100644 --- a/src/pages/sign/style.jsx +++ b/src/pages/sign/style.jsx @@ -8,8 +8,9 @@ export const Container = styled.div` justify-content: center; align-items: center; - .password-container { + .input-container { position: relative; + margin-bottom: 8px; } .sign { @@ -42,7 +43,6 @@ export const Container = styled.div` border: none; border-radius: 12px; background: ${color("secondary200")}; - margin-bottom: 8px; &--error { outline: none;