From 966e847e6e10264e0d5bebd14316300d6129064c Mon Sep 17 00:00:00 2001 From: "wonsic3686@gmail.com" Date: Fri, 28 Jun 2024 23:41:56 +0900 Subject: [PATCH] =?UTF-8?q?feat(Sprint6):=20additems=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + src/App.jsx | 5 +- src/assets/images/icons/ic_X.svg | 5 + src/assets/images/icons/ic_union.svg | 3 + .../items/SellingItems/SellingItems.jsx | 2 +- .../navigationBar/NavigationBar.jsx | 1 + src/core/ui/buttons/Button/Button.css | 38 +++++ src/core/ui/buttons/Button/Button.jsx | 18 +++ .../inputs/ImageFileInput/ImageFileInput.css | 93 ++++++++++++ .../inputs/ImageFileInput/ImageFileInput.jsx | 84 +++++++++++ src/core/ui/inputs/TagInput/TagInput.css | 39 +++++ src/core/ui/inputs/TagInput/TagInput.jsx | 59 ++++++++ src/pages/addItems/AddItems.css | 71 +++++++++ src/pages/addItems/AddItems.jsx | 142 +++++++++++++++++- src/pages/items/Items.css | 2 +- 15 files changed, 561 insertions(+), 4 deletions(-) create mode 100644 src/assets/images/icons/ic_X.svg create mode 100644 src/assets/images/icons/ic_union.svg create mode 100644 src/core/ui/buttons/Button/Button.css create mode 100644 src/core/ui/buttons/Button/Button.jsx create mode 100644 src/core/ui/inputs/ImageFileInput/ImageFileInput.css create mode 100644 src/core/ui/inputs/ImageFileInput/ImageFileInput.jsx create mode 100644 src/core/ui/inputs/TagInput/TagInput.css create mode 100644 src/core/ui/inputs/TagInput/TagInput.jsx create mode 100644 src/pages/addItems/AddItems.css diff --git a/.gitignore b/.gitignore index 4d29575de..3a894327c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +#file +visualwind.wrapper.tsx + # dependencies /node_modules /.pnp diff --git a/src/App.jsx b/src/App.jsx index 8cd05c49f..d41047767 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -19,7 +19,10 @@ function App() { } /> */} } /> } /> - } /> + + } /> + } /> + } /> diff --git a/src/assets/images/icons/ic_X.svg b/src/assets/images/icons/ic_X.svg new file mode 100644 index 000000000..522eca44f --- /dev/null +++ b/src/assets/images/icons/ic_X.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/icons/ic_union.svg b/src/assets/images/icons/ic_union.svg new file mode 100644 index 000000000..f4ada2f05 --- /dev/null +++ b/src/assets/images/icons/ic_union.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/items/SellingItems/SellingItems.jsx b/src/components/items/SellingItems/SellingItems.jsx index 6fdf236c6..4b063a2bb 100644 --- a/src/components/items/SellingItems/SellingItems.jsx +++ b/src/components/items/SellingItems/SellingItems.jsx @@ -84,7 +84,7 @@ function SellingItems() {
diff --git a/src/components/navigationBar/NavigationBar.jsx b/src/components/navigationBar/NavigationBar.jsx index 9b616613d..c45fe3d5f 100644 --- a/src/components/navigationBar/NavigationBar.jsx +++ b/src/components/navigationBar/NavigationBar.jsx @@ -34,6 +34,7 @@ function NavigationBar() { */} `nav-bar-menu-link ${isActive ? 'active' : ''}` } diff --git a/src/core/ui/buttons/Button/Button.css b/src/core/ui/buttons/Button/Button.css new file mode 100644 index 000000000..0e34edf09 --- /dev/null +++ b/src/core/ui/buttons/Button/Button.css @@ -0,0 +1,38 @@ +.btn-default { + background-color: var(--blue); + color: #ffffff; + font-family: inherit; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 600; + border: none; + outline: none; + box-shadow: none; + cursor: pointer; + line-height: inherit; + border-radius: 8px; + padding: 12px 23px; +} + +.btn-default:hover { + background-color: #1967d6; +} + +.btn-default:focus { + background-color: #1251aa; +} + +.btn-default:disabled { + background-color: #9ca3af; + cursor: default; + pointer-events: none; +} + +.btn-default.pill-button { + font-size: 16px; + font-weight: 600; + border-radius: 999px; + padding: 14.5px 33.5px; +} diff --git a/src/core/ui/buttons/Button/Button.jsx b/src/core/ui/buttons/Button/Button.jsx new file mode 100644 index 000000000..97763771a --- /dev/null +++ b/src/core/ui/buttons/Button/Button.jsx @@ -0,0 +1,18 @@ +import { useEffect, useRef } from 'react'; +import './Button.css'; +function Button({ text = '버튼', onClick = () => {}, isDisabled = false }) { + const ref = useRef(); + + useEffect(() => { + ref.current.disabled = isDisabled ? true : false; + }, [isDisabled]); + + return ( + <> + + + ); +} +export default Button; diff --git a/src/core/ui/inputs/ImageFileInput/ImageFileInput.css b/src/core/ui/inputs/ImageFileInput/ImageFileInput.css new file mode 100644 index 000000000..05688cbbe --- /dev/null +++ b/src/core/ui/inputs/ImageFileInput/ImageFileInput.css @@ -0,0 +1,93 @@ +.wrapper-btn-input-image-file { + display: flex; + justify-content: start; + align-items: center; +} + +.card-input-image-file { + width: 168px; + height: 168px; + margin: 0 8px 0 0; + border-radius: 12px; + background-color: var(--gray-100); + display: inline-flex; + justify-content: center; + align-items: center; +} + +.card-input-image-file:last-child { + margin: 0; +} + +.input-image-file { + display: none; +} + +.wrapper-content-btn-input-image-file { + display: inline-flex; + flex-direction: column; + align-items: center; +} + +.icon-union-input-image-file { + width: 48px; + height: 48px; + margin: 0 0 12px 0; +} + +.text-btn-input-image-file { + color: var(--gray-400); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; +} + +.img-preview-input-image-file { + width: 100%; + height: 100%; + border-radius: 12px; +} + +.btn-delete-input-image-file { + position: relative; + top: -60px; + left: -40px; + opacity: 70%; +} + +.btn-delete-input-image-file:hover { + opacity: 100%; +} + +.btn-delete-input-image-file:focus { + opacity: 100%; +} + +@media (min-width: 768px) { + .card-input-image-file { + width: 162px; + height: 162px; + margin: 0 16px 0 0; + } + + .btn-delete-input-image-file { + position: relative; + top: -60px; + left: -45px; + } +} + +@media (min-width: 1280px) { + .card-input-image-file { + width: 282px; + height: 282px; + margin: 0 24px 0 0; + } + + .btn-delete-input-image-file { + position: relative; + top: -110px; + left: -60px; + } +} diff --git a/src/core/ui/inputs/ImageFileInput/ImageFileInput.jsx b/src/core/ui/inputs/ImageFileInput/ImageFileInput.jsx new file mode 100644 index 000000000..8aaa2e6f6 --- /dev/null +++ b/src/core/ui/inputs/ImageFileInput/ImageFileInput.jsx @@ -0,0 +1,84 @@ +import { useEffect, useRef, useState } from 'react'; +import { ReactComponent as IconUnion } from '../../../../assets/images/icons/ic_union.svg'; +import { ReactComponent as IconX } from '../../../../assets/images/icons/ic_X.svg'; +import ImgDeafault from '../../../../assets/images/market/img_default.png'; +import './ImageFileInput.css'; +function ImageFileInput({ + className = '', + name, + value, + initialPreview, + onChange, +}) { + const [preview, setPreview] = useState(initialPreview); + const inputRef = useRef(); + + 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); + }; + + useEffect(() => { + if (!value) return; + const nextPreview = URL.createObjectURL(value); + setPreview(nextPreview); + + return () => { + setPreview(initialPreview); + URL.revokeObjectURL(nextPreview); + }; + }, [value, initialPreview]); + + return ( +
+ {/* 추가 버튼 */} + + + {/* 미리보기 이미지 */} + {value && ( + <> +
+ 상품 미리보기 +
+ + + )} +
+ ); +} +export default ImageFileInput; diff --git a/src/core/ui/inputs/TagInput/TagInput.css b/src/core/ui/inputs/TagInput/TagInput.css new file mode 100644 index 000000000..0f4603b70 --- /dev/null +++ b/src/core/ui/inputs/TagInput/TagInput.css @@ -0,0 +1,39 @@ +.wrapper-input-tags { + display: flex; + flex-direction: column; + align-items: start; + width: 100%; +} + +.wrapper-list-tag-input-tag { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: flex-start; + width: 100%; + margin: 0; +} + +.tag-input-tag { + display: flex; + align-items: center; + margin: 12px 12px 0 0; + padding: 12px 12px 12px 16px; + border-radius: 26px; + background-color: var(--gray-100); +} + +.tag-input-tag:last-child { + margin: 12px 0 0; +} + +.btn-delete-tag { + color: white; + width: 20px; + height: 20px; + margin: 0 0 0 5px; + background-color: var(--gray-500); + border-radius: 10px; + font-size: 15px; + font-weight: 400; +} diff --git a/src/core/ui/inputs/TagInput/TagInput.jsx b/src/core/ui/inputs/TagInput/TagInput.jsx new file mode 100644 index 000000000..817f7a2f7 --- /dev/null +++ b/src/core/ui/inputs/TagInput/TagInput.jsx @@ -0,0 +1,59 @@ +import { useState } from 'react'; +import './TagInput.css'; + +function TagInput({ className = '', name, tags = [], onAdd, onRemove }) { + const [targetText, setTargetText] = useState(''); + + const handleChange = (e) => { + const { value } = e.target; + if (!value.trim()) return; + setTargetText(value); + }; + + const handlePress = (e) => { + if (e.key === 'Enter' && targetText) { + e.preventDefault(); + onAdd(targetText.trim()); + setTargetText(''); + } + }; + + const handleDeleteTag = (e) => { + onRemove(e.currentTarget.parentNode.dataset.tagName); + }; + + return ( + <> +
+ + {tags.length > 0 && ( +
+ {tags.map((tag, i) => ( +
+ {tag} + +
+ ))} +
+ )} +
+ + ); +} + +export default TagInput; diff --git a/src/pages/addItems/AddItems.css b/src/pages/addItems/AddItems.css new file mode 100644 index 000000000..857de6b74 --- /dev/null +++ b/src/pages/addItems/AddItems.css @@ -0,0 +1,71 @@ +/* AddItems */ +.container-add-item { + max-width: 1200px; + margin: 94px auto; + padding: 0 16px; +} + +.header-add-item { + display: flex; + justify-content: space-between; + align-items: center; +} + +.title-add-item { + margin: 0 0 16px; + font-size: 28px; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +.wrapper-input-form-add-item { + display: flex; + flex-direction: column; + align-items: start; + margin: 0 0 24px; +} + +.title-input-form-add-item { + color: var(--gray-800); + margin: 0 0 12px; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +.input-form-add-item { + height: 56px; + align-self: stretch; + border: none; + border-radius: 12px; + background-color: var(--gray-100); + /* text */ + padding: 16px 24px; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; +} + +/* Chrome, Safari, Edge, Opera */ +.input-form-add-item::-webkit-outer-spin-button, +.input-form-add-item::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.input-form-add-item::placeholder { + color: var(--gray-400); +} + +.input-form-add-item:focus { + outline-color: var(--blue); +} + +.input-textarea { + height: 313px; + resize: none; + text-overflow: scr; +} diff --git a/src/pages/addItems/AddItems.jsx b/src/pages/addItems/AddItems.jsx index d055b09b3..50be45d80 100644 --- a/src/pages/addItems/AddItems.jsx +++ b/src/pages/addItems/AddItems.jsx @@ -1,7 +1,147 @@ +import { useState } from 'react'; +import Button from '../../core/ui/buttons/Button/Button'; +import ImageFileInput from '../../core/ui/inputs/ImageFileInput/ImageFileInput'; +import './AddItems.css'; +import TagInput from '../../core/ui/inputs/TagInput/TagInput'; + +const INITIAL_VALUES = { + imgFile: null, + name: '', + desc: '', + price: 0, + tags: [], +}; + function AddItems() { + const [values, setValues] = useState(INITIAL_VALUES); + const [initialPreview, setInitialPreview] = useState(undefined); + const [isAllValid, setIsAllValid] = useState(false); + + const handleChange = (name, value) => { + setValues((prevValues) => ({ + ...prevValues, + [name]: value, + })); + + checkAllInputValid(); + }; + + const checkAllInputValid = () => { + if (values.name && values.desc && values.price && values.tags.length > 0) { + setIsAllValid(true); + } else { + setIsAllValid(false); + } + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + handleChange(name, value); + }; + + const addTag = (tag) => { + if (!values.tags.includes(tag)) { + handleChange( + 'tags', + values.tags.length > 0 ? [...values.tags, tag] : [tag] + ); + } + }; + + const removeTag = (targetTag) => { + // setValues( + // 'tags', + // values.tags.filter((tag) => tag !== targetTag) + // ); + handleChange( + 'tags', + values.tags.filter((tag) => tag !== targetTag) + ); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + }; + return ( <> -

임시 상품등록 페이지

+
+
+
상품 등록하기
+
+
+
+
+
+
+ +
+ +
+
+
+ + +
+
+ +