diff --git a/package-lock.json b/package-lock.json index a1e590ee6..53262aad3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,10 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "classnames": "^2.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.23.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" } @@ -3241,6 +3243,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -5954,6 +5964,11 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/clean-css": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", @@ -14671,6 +14686,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "dependencies": { + "@remix-run/router": "1.16.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "dependencies": { + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", diff --git a/package.json b/package.json index 7ff0d6b58..48a05b3ce 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,10 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "classnames": "^2.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.23.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index a11777cc4..000000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/images/home/bottom-banner-image.png b/public/images/home/bottom-banner-image.png new file mode 100644 index 000000000..4a5f85b28 Binary files /dev/null and b/public/images/home/bottom-banner-image.png differ diff --git a/public/images/home/feature-search-img.png b/public/images/home/feature-search-img.png new file mode 100644 index 000000000..31e20b979 Binary files /dev/null and b/public/images/home/feature-search-img.png differ diff --git a/public/images/home/feature1-image.png b/public/images/home/feature1-image.png new file mode 100644 index 000000000..4684b9a72 Binary files /dev/null and b/public/images/home/feature1-image.png differ diff --git a/public/images/home/feature3-image.png b/public/images/home/feature3-image.png new file mode 100644 index 000000000..5b8084a77 Binary files /dev/null and b/public/images/home/feature3-image.png differ diff --git a/public/images/home/hero-image.png b/public/images/home/hero-image.png new file mode 100644 index 000000000..d28fb6522 Binary files /dev/null and b/public/images/home/hero-image.png differ diff --git a/public/images/icons/arrow_left.svg b/public/images/icons/arrow_left.svg new file mode 100644 index 000000000..2a9de23a6 --- /dev/null +++ b/public/images/icons/arrow_left.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/icons/arrow_right.svg b/public/images/icons/arrow_right.svg new file mode 100644 index 000000000..daa483c3e --- /dev/null +++ b/public/images/icons/arrow_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/icons/eye-invisible.svg b/public/images/icons/eye-invisible.svg new file mode 100644 index 000000000..92252b05d --- /dev/null +++ b/public/images/icons/eye-invisible.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/images/icons/eye-visible.svg b/public/images/icons/eye-visible.svg new file mode 100644 index 000000000..35a75305e --- /dev/null +++ b/public/images/icons/eye-visible.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/icons/ic_heart.svg b/public/images/icons/ic_heart.svg new file mode 100644 index 000000000..cad016c13 --- /dev/null +++ b/public/images/icons/ic_heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/icons/ic_search.svg b/public/images/icons/ic_search.svg new file mode 100644 index 000000000..52241e6d8 --- /dev/null +++ b/public/images/icons/ic_search.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/icons/ic_sort.svg b/public/images/icons/ic_sort.svg new file mode 100644 index 000000000..657b44f93 --- /dev/null +++ b/public/images/icons/ic_sort.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/logo/favicon.ico b/public/images/logo/favicon.ico new file mode 100644 index 000000000..9fecc692d Binary files /dev/null and b/public/images/logo/favicon.ico differ diff --git a/public/images/logo/logo.svg b/public/images/logo/logo.svg new file mode 100644 index 000000000..d497acbfe --- /dev/null +++ b/public/images/logo/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/logo/panda-market-logo.png b/public/images/logo/panda-market-logo.png new file mode 100644 index 000000000..a1dc1c6a1 Binary files /dev/null and b/public/images/logo/panda-market-logo.png differ diff --git a/public/images/market/img_default.png b/public/images/market/img_default.png new file mode 100644 index 000000000..9a1bd6c32 Binary files /dev/null and b/public/images/market/img_default.png differ diff --git a/public/images/social/facebook-logo.svg b/public/images/social/facebook-logo.svg new file mode 100644 index 000000000..8491c2f83 --- /dev/null +++ b/public/images/social/facebook-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/social/google-logo.png b/public/images/social/google-logo.png new file mode 100644 index 000000000..199f3d628 Binary files /dev/null and b/public/images/social/google-logo.png differ diff --git a/public/images/social/instagram-logo.svg b/public/images/social/instagram-logo.svg new file mode 100644 index 000000000..c83306f84 --- /dev/null +++ b/public/images/social/instagram-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/social/kakao-logo.png b/public/images/social/kakao-logo.png new file mode 100644 index 000000000..bfadc1d35 Binary files /dev/null and b/public/images/social/kakao-logo.png differ diff --git a/public/images/social/twitter-logo.svg b/public/images/social/twitter-logo.svg new file mode 100644 index 000000000..14a6069a1 --- /dev/null +++ b/public/images/social/twitter-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/social/youtube-logo.svg b/public/images/social/youtube-logo.svg new file mode 100644 index 000000000..5fcc0ff34 --- /dev/null +++ b/public/images/social/youtube-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/index.html b/public/index.html index aa069f27c..7ccb20524 100644 --- a/public/index.html +++ b/public/index.html @@ -1,43 +1,32 @@ - + - - - - - - - - - - React App + + + + + + + + + 판다마켓 + + +
- diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fc44b0a37..000000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index a4e47a654..000000000 Binary files a/public/logo512.png and /dev/null differ diff --git a/public/signin/index.html b/public/signin/index.html new file mode 100644 index 000000000..d7ec21315 --- /dev/null +++ b/public/signin/index.html @@ -0,0 +1,94 @@ + + + + + + 판다마켓 - 로그인 + + + + + + +
+ +
+
+ + + 이메일을 입력해d주세요. +
+
+ +
+ + +
+ 비밀번호를 입력해주세요. +
+ +
+
+

간편 로그인하기

+ +
+
+ 판다마켓이 처음이신가요? 회원가입 +
+
+ + + diff --git a/public/signin/signin.js b/public/signin/signin.js new file mode 100644 index 000000000..798f27915 --- /dev/null +++ b/public/signin/signin.js @@ -0,0 +1,107 @@ +document.addEventListener('DOMContentLoaded', () => { + let isValidEmail = false; + let isValidPassword = false; + + const inputEmail = document.getElementById('email'); + const inputPassword = document.getElementById('password'); + const btnTogglePasswordVisibleList = document.querySelectorAll( + '.btn-password-visible' + ); + const btnSignin = document.getElementById('btn-signin'); + const messageErrorEmail = document.getElementById('message-error-email'); + const messageErrorPassword = document.getElementById( + 'message-error-password' + ); + + const validateEmail = () => { + const emailValue = inputEmail.value.trim(); + if (!emailValue) { + showError(inputEmail, messageErrorEmail, '이메일을 입력해주세요.'); + isValidEmail = false; + } else if (!checkEmailRegex(emailValue)) { + showError(inputEmail, messageErrorEmail, '잘못된 이메일 형식입니다.'); + isValidEmail = false; + } else { + clearError(inputEmail, messageErrorEmail); + isValidEmail = true; + } + updateBtnSignin(); + }; + + const validatePassword = () => { + const passwordValue = inputPassword.value.trim(); + if (!passwordValue) { + showError( + inputPassword, + messageErrorPassword, + '비밀번호를 입력해주세요.' + ); + isValidPassword = false; + } else if (passwordValue.length < 8) { + showError( + inputPassword, + messageErrorPassword, + '비밀번호를 8자 이상 입력해주세요' + ); + isValidPassword = false; + } else { + clearError(inputPassword, messageErrorPassword); + isValidPassword = true; + } + updateBtnSignin(); + }; + + const togglePasswordVisible = (e) => { + e.preventDefault(); + const visibleBtn = e.currentTarget; + const targetInput = visibleBtn.parentElement.querySelector('input'); + const visibleImg = visibleBtn.querySelector('.img-password-visible'); + if (targetInput.type === 'password') { + targetInput.type = 'text'; + visibleImg.src = '/images/icons/eye-visible.svg'; + } else { + targetInput.type = 'password'; + visibleImg.src = '/images/icons/eye-invisible.svg'; + } + }; + + const updateBtnSignin = () => { + if (isValidPassword && isValidEmail) { + btnSignin.disabled = false; + } else { + btnSignin.disabled = true; + } + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (btnSignin.disabled) return; + window.location.href = '/items'; + }; + + const checkEmailRegex = (email) => { + const emailRegex = new RegExp('[a-z0-9]+@[a-z]+\\.[a-z]{2,3}'); + return emailRegex.test(email); + }; + + const showError = (input, errorMessageElement, message) => { + input.classList.add('error'); + errorMessageElement.textContent = message; + errorMessageElement.style.display = 'block'; + }; + + const clearError = (input, errorMessageElement) => { + input.classList.remove('error'); + errorMessageElement.textContent = ''; + errorMessageElement.style.display = 'none'; + }; + + inputEmail.addEventListener('focusout', validateEmail); + inputPassword.addEventListener('focusout', validatePassword); + inputEmail.addEventListener('input', validateEmail); + inputPassword.addEventListener('input', validatePassword); + btnSignin.addEventListener('click', handleSubmit); + btnTogglePasswordVisibleList.forEach((button) => { + button.addEventListener('click', togglePasswordVisible); + }); +}); diff --git a/public/signup/index.html b/public/signup/index.html new file mode 100644 index 000000000..f387c5259 --- /dev/null +++ b/public/signup/index.html @@ -0,0 +1,130 @@ + + + + + + 판다마켓 - 회원가입 + + + + + + +
+ +
+
+ + + 이메일을 입력해d주세요. +
+
+ + + 닉네임을 입력해d주세요. +
+
+ +
+ + +
+ 비밀번호를 입력해주세요. +
+
+ +
+ + +
+ 비밀번호를 입력해주세요. +
+ +
+
+

간편 로그인하기

+ +
+
+ 이미 회원이신가요? 로그인 +
+
+ + + diff --git a/public/signup/signup.js b/public/signup/signup.js new file mode 100644 index 000000000..c75081ed0 --- /dev/null +++ b/public/signup/signup.js @@ -0,0 +1,173 @@ +document.addEventListener('DOMContentLoaded', () => { + let isValidEmail = false; + let isValidNickname = false; + let isValidPassword = false; + let isValidPasswordConfirmation = false; + let isPasswordVisible = false; + + const inputEmail = document.getElementById('email'); + const inputNickname = document.getElementById('nickname'); + const inputPassword = document.getElementById('password'); + const inputPasswordConfirmation = document.getElementById( + 'password-confirmation' + ); + const btnTogglePasswordVisibleList = document.querySelectorAll( + '.btn-password-visible' + ); + const btnSignup = document.getElementById('btn-signup'); + const messageErrorEmail = document.getElementById('message-error-email'); + const messageErrorNickname = document.getElementById( + 'message-error-nickname' + ); + const messageErrorPassword = document.getElementById( + 'message-error-password' + ); + const messageErrorPasswordConfirmation = document.getElementById( + 'message-error-password-confirmation' + ); + + const validateEmail = () => { + const emailValue = inputEmail.value.trim(); + if (!emailValue) { + showError(inputEmail, messageErrorEmail, '이메일을 입력해주세요.'); + isValidEmail = false; + } else if (!checkEmailRegex(emailValue)) { + showError(inputEmail, messageErrorEmail, '잘못된 이메일 형식입니다.'); + isValidEmail = false; + } else { + clearError(inputEmail, messageErrorEmail); + isValidEmail = true; + } + updatebtnSignup(); + }; + + const validateNickname = () => { + const nicknameValue = inputNickname.value.trim(); + if (!nicknameValue) { + showError(inputNickname, messageErrorNickname, '닉네임을 입력해주세요.'); + isValidNickname = false; + } else { + clearError(inputNickname, messageErrorNickname); + isValidNickname = true; + } + updatebtnSignup(); + }; + + const validatePassword = () => { + const passwordValue = inputPassword.value.trim(); + if (!passwordValue) { + showError( + inputPassword, + messageErrorPassword, + '비밀번호를 입력해주세요.' + ); + isValidPassword = false; + } else if (passwordValue.length < 8) { + showError( + inputPassword, + messageErrorPassword, + '비밀번호를 8자 이상 입력해주세요' + ); + isValidPassword = false; + } else { + clearError(inputPassword, messageErrorPassword); + isValidPassword = true; + } + validatePasswordConfirmation(); + updatebtnSignup(); + }; + + const validatePasswordConfirmation = () => { + const passwordConfirmationValue = inputPasswordConfirmation.value.trim(); + const passwordValue = inputPassword.value.trim(); + if (!passwordConfirmationValue) { + showError( + inputPasswordConfirmation, + messageErrorPasswordConfirmation, + '비밀번호를 입력해주세요.' + ); + } else if (passwordConfirmationValue !== passwordValue) { + showError( + inputPasswordConfirmation, + messageErrorPasswordConfirmation, + '비밀번호가 일치하지 않습니다.' + ); + isValidPasswordConfirmation = false; + } else { + clearError(inputPasswordConfirmation, messageErrorPasswordConfirmation); + isValidPasswordConfirmation = true; + } + updatebtnSignup(); + }; + + const togglePasswordVisible = (e) => { + e.preventDefault(); + const visibleBtn = e.currentTarget; + const targetInput = visibleBtn.parentElement.querySelector('input'); + const visibleImg = visibleBtn.querySelector('.img-password-visible'); + if (targetInput.type === 'password') { + targetInput.type = 'text'; + visibleImg.src = '/images/icons/eye-visible.svg'; + visibleImg.alt = '비밀번호 표시'; + } else { + targetInput.type = 'password'; + visibleImg.src = '/images/icons/eye-invisible.svg'; + visibleImg.alt = '비밀번호 숨김'; + } + }; + + const updatebtnSignup = () => { + if ( + isValidPassword && + isValidEmail && + isValidNickname && + isValidPasswordConfirmation + ) { + btnSignup.disabled = false; + } else { + btnSignup.disabled = true; + } + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (btnSignup.disabled) return; + window.location.href = '/signin'; + }; + + const checkEmailRegex = (email) => { + const emailRegex = new RegExp('[a-z0-9]+@[a-z]+\\.[a-z]{2,3}'); + return emailRegex.test(email); + }; + + const showError = (input, errorMessageElement, message) => { + input.classList.add('error'); + errorMessageElement.textContent = message; + errorMessageElement.style.display = 'block'; + }; + + const clearError = (input, errorMessageElement) => { + input.classList.remove('error'); + errorMessageElement.textContent = ''; + errorMessageElement.style.display = 'none'; + }; + + inputEmail.addEventListener('focusout', validateEmail); + inputEmail.addEventListener('input', validateEmail); + inputNickname.addEventListener('focusout', validateNickname); + inputNickname.addEventListener('input', validateNickname); + inputPassword.addEventListener('focusout', validatePassword); + inputPassword.addEventListener('input', validatePassword); + inputPasswordConfirmation.addEventListener( + 'focusout', + validatePasswordConfirmation + ); + inputPasswordConfirmation.addEventListener( + 'input', + validatePasswordConfirmation + ); + btnSignup.addEventListener('click', handleSubmit); + btnTogglePasswordVisibleList.forEach((button) => { + button.addEventListener('click', togglePasswordVisible); + }); +}); diff --git a/public/styles/global.css b/public/styles/global.css new file mode 100644 index 000000000..8bb0384c5 --- /dev/null +++ b/public/styles/global.css @@ -0,0 +1,177 @@ +:root { + --gray-900: #1b1d1f; + --gray-800: #26282b; + --gray-600: #454c53; + --gray-500: #72787f; + --gray-400: #9ea4a8; + --gray-200: #e5e7eb; + --gray-100: #e8ebed; + --gray-50: #f7f7f8; + + --blue: #3692ff; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + color: #374151; + word-break: keep-all; + font-family: 'Pretendard', sans-serif; +} + +header { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 70px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 16px; + background-color: #ffffff; + border-bottom: 1px solid #dfdfdf; +} + +main { + margin-top: 70px; +} + +footer { + background-color: #111827; + padding: 32px; + font-size: 16px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 60px; +} + +#copyright { + order: 3; + flex-basis: 100%; + color: #9ca3af; +} + +#footer-menu { + display: flex; + gap: 30px; + color: var(--gray-200); +} + +#social-media { + display: flex; + gap: 12px; +} + +a { + text-decoration: none; + color: inherit; +} + +img { + vertical-align: bottom; +} + +.wrapper { + max-width: 1200px; + margin: 0 auto; + width: 100%; + padding: 0 16px; +} + +button { + background: none; + border: none; + outline: none; + box-shadow: none; + cursor: pointer; + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: inherit; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.button { + background-color: var(--blue); + color: #ffffff; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.button:hover { + background-color: #1967d6; +} + +.button:focus { + background-color: #1251aa; +} + +.button:disabled { + background-color: #9ca3af; + cursor: default; + pointer-events: none; +} + +.pill-button { + font-size: 16px; + font-weight: 600; + border-radius: 999px; + padding: 14.5px 33.5px; +} + +.full-width { + width: 100%; +} + +.break-on-desktop { + display: none; +} + +@media (min-width: 768px) { + header { + padding: 0 24px; + } + + .wrapper { + padding: 0 24px; + } + + .pill-button { + font-size: 20px; + font-weight: 700; + padding: 16px 126px; + } + + footer { + padding: 32px 104px 108px 104px; + } + + #copyright { + flex-basis: auto; + order: 0; + } +} + +@media (min-width: 1280px) { + header { + padding: 0 200px; + } + + .break-on-desktop { + display: inline; + } + + footer { + padding: 32px 200px 108px 200px; + } +} diff --git a/public/styles/sign.css b/public/styles/sign.css new file mode 100644 index 000000000..430ffcabb --- /dev/null +++ b/public/styles/sign.css @@ -0,0 +1,138 @@ +.container-sign { + max-width: 400px; + margin: 0 auto; + padding: 0 16px; +} + +.wrapper-btn-logo { + margin: 0 auto; + display: block; + margin-top: 24px; + margin-bottom: 24px; + width: 198px; +} + +.btn-home-logo { + width: 100%; +} + +.input-item { + margin-bottom: 24px; + display: flex; + flex-direction: column; +} + +.message-error { + display: none; + color: #f74747; + font-weight: 600; + font-size: 15px; + margin-top: 8px; + padding-left: 16px; +} + +label { + display: block; + font-weight: 700; + font-size: 14px; + margin-bottom: 8px; +} + +input { + padding: 16px 24px; + background-color: #f3f4f6; + border: none; + border-radius: 12px; + font-size: 16px; + line-height: 24px; + width: 100%; +} + +input::placeholder { + color: #9ca3af; + font-size: 16px; + line-height: 24px; +} + +input:focus { + outline-color: var(--blue); +} + +input.error { + border: 1px solid #f74747; +} + +.wrapper-input { + position: relative; + display: flex; + align-items: center; +} + +.img-password-toggle { + position: absolute; + right: 24px; + cursor: pointer; +} + +.btn-password-visible { + position: absolute; + right: 24px; + cursor: pointer; +} + +.container-sns-signin { + background-color: #e6f2ff; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 23px; + margin: 24px 0; +} + +.container-sns-signin h3 { + font-weight: 500; + font-size: 16px; + line-height: 24px; +} + +.container-btn-sns-signin { + display: flex; + gap: 16px; +} + +.container-btn-sign { + font-weight: 500; + font-size: 15px; + text-align: center; +} + +.container-btn-sign a { + color: #3182f6; + text-decoration: underline; + text-underline-offset: 2px; +} + +@media (min-width: 768px) { + .container-sign { + max-width: 640px; + } + + .wrapper-btn-logo { + width: 396px; + margin-top: 48px; + margin-bottom: 40px; + } + + label { + font-size: 18px; + margin-bottom: 16px; + } +} + +@media (min-width: 1280px) { + .wrapper-btn-logo { + margin-top: 60px; + margin-bottom: 40px; + } +} diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 74b5e0534..000000000 --- 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 378457572..000000000 --- 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 new file mode 100644 index 000000000..8cd05c49f --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,29 @@ +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import Home from './pages/home/Home'; +// import Signin from './pages/signin/Signin'; +// import Signup from './pages/signup/Signup'; +import Faq from './pages/faq/Faq'; +import Privacy from './pages/privacy/Privacy'; +import Items from './pages/items/Items'; +import AddItems from './pages/addItems/AddItems'; +import NavigationBar from './components/navigationBar/NavigationBar'; +import './styles/global.css'; + +function App() { + return ( + + + + } /> + {/* } /> + } /> */} + } /> + } /> + } /> + } /> + + + ); +} + +export default App; diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index 1f03afeec..000000000 --- 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/images/home/bottom-banner-image.png b/src/assets/images/home/bottom-banner-image.png new file mode 100644 index 000000000..4a5f85b28 Binary files /dev/null and b/src/assets/images/home/bottom-banner-image.png differ diff --git a/src/assets/images/home/feature-search-img.png b/src/assets/images/home/feature-search-img.png new file mode 100644 index 000000000..31e20b979 Binary files /dev/null and b/src/assets/images/home/feature-search-img.png differ diff --git a/src/assets/images/home/feature1-image.png b/src/assets/images/home/feature1-image.png new file mode 100644 index 000000000..4684b9a72 Binary files /dev/null and b/src/assets/images/home/feature1-image.png differ diff --git a/src/assets/images/home/feature3-image.png b/src/assets/images/home/feature3-image.png new file mode 100644 index 000000000..5b8084a77 Binary files /dev/null and b/src/assets/images/home/feature3-image.png differ diff --git a/src/assets/images/home/hero-image.png b/src/assets/images/home/hero-image.png new file mode 100644 index 000000000..d28fb6522 Binary files /dev/null and b/src/assets/images/home/hero-image.png differ diff --git a/src/assets/images/icons/arrow_left.svg b/src/assets/images/icons/arrow_left.svg new file mode 100644 index 000000000..2a9de23a6 --- /dev/null +++ b/src/assets/images/icons/arrow_left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icons/arrow_right.svg b/src/assets/images/icons/arrow_right.svg new file mode 100644 index 000000000..daa483c3e --- /dev/null +++ b/src/assets/images/icons/arrow_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icons/eye-invisible.svg b/src/assets/images/icons/eye-invisible.svg new file mode 100644 index 000000000..92252b05d --- /dev/null +++ b/src/assets/images/icons/eye-invisible.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/images/icons/eye-visible.svg b/src/assets/images/icons/eye-visible.svg new file mode 100644 index 000000000..35a75305e --- /dev/null +++ b/src/assets/images/icons/eye-visible.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icons/ic_heart.svg b/src/assets/images/icons/ic_heart.svg new file mode 100644 index 000000000..cad016c13 --- /dev/null +++ b/src/assets/images/icons/ic_heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icons/ic_search.svg b/src/assets/images/icons/ic_search.svg new file mode 100644 index 000000000..52241e6d8 --- /dev/null +++ b/src/assets/images/icons/ic_search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icons/ic_sort.svg b/src/assets/images/icons/ic_sort.svg new file mode 100644 index 000000000..657b44f93 --- /dev/null +++ b/src/assets/images/icons/ic_sort.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/logo/favicon.ico b/src/assets/images/logo/favicon.ico new file mode 100644 index 000000000..9fecc692d Binary files /dev/null and b/src/assets/images/logo/favicon.ico differ diff --git a/src/assets/images/logo/logo.svg b/src/assets/images/logo/logo.svg new file mode 100644 index 000000000..d497acbfe --- /dev/null +++ b/src/assets/images/logo/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/assets/images/logo/panda-market-logo.png b/src/assets/images/logo/panda-market-logo.png new file mode 100644 index 000000000..a1dc1c6a1 Binary files /dev/null and b/src/assets/images/logo/panda-market-logo.png differ diff --git a/src/assets/images/market/img_default.png b/src/assets/images/market/img_default.png new file mode 100644 index 000000000..9a1bd6c32 Binary files /dev/null and b/src/assets/images/market/img_default.png differ diff --git a/src/assets/images/social/facebook-logo.svg b/src/assets/images/social/facebook-logo.svg new file mode 100644 index 000000000..8491c2f83 --- /dev/null +++ b/src/assets/images/social/facebook-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/social/google-logo.png b/src/assets/images/social/google-logo.png new file mode 100644 index 000000000..199f3d628 Binary files /dev/null and b/src/assets/images/social/google-logo.png differ diff --git a/src/assets/images/social/instagram-logo.svg b/src/assets/images/social/instagram-logo.svg new file mode 100644 index 000000000..c83306f84 --- /dev/null +++ b/src/assets/images/social/instagram-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/social/kakao-logo.png b/src/assets/images/social/kakao-logo.png new file mode 100644 index 000000000..bfadc1d35 Binary files /dev/null and b/src/assets/images/social/kakao-logo.png differ diff --git a/src/assets/images/social/twitter-logo.svg b/src/assets/images/social/twitter-logo.svg new file mode 100644 index 000000000..14a6069a1 --- /dev/null +++ b/src/assets/images/social/twitter-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/social/youtube-logo.svg b/src/assets/images/social/youtube-logo.svg new file mode 100644 index 000000000..5fcc0ff34 --- /dev/null +++ b/src/assets/images/social/youtube-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/components/items/BestItems/BestItems.css b/src/components/items/BestItems/BestItems.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/items/BestItems/BestItems.jsx b/src/components/items/BestItems/BestItems.jsx new file mode 100644 index 000000000..5c1b75bd3 --- /dev/null +++ b/src/components/items/BestItems/BestItems.jsx @@ -0,0 +1,59 @@ +import './BestItems.css'; +import { useEffect, useState } from 'react'; +import Item from '../Item/Item'; +import { getProducts } from '../../../pages/api/Items'; + +const getPageSize = () => { + const width = window.innerWidth; + if (width < 768) { + return 1; + } else if (width < 1280) { + return 2; + } else { + return 4; + } +}; + +function BestItems() { + // 상품 목록 + const [itemList, setItemList] = useState([]); + // 쿼리 + const [order, setOrder] = useState('favorite'); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(4); + const [keyword, setKeyword] = useState(''); + + const fetchItemList = async ({ order, page, pageSize, keyword }) => { + let products = await getProducts({ order, page, pageSize, keyword }); + setItemList(products.list); + }; + + useEffect(() => { + const handleResize = () => { + setPageSize(getPageSize()); + }; + + window.addEventListener('resize', handleResize); + fetchItemList({ order, page, pageSize, keyword }); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [order, page, pageSize, keyword]); + + return ( + <> +
+
베스트 상품
+ +
+ {itemList?.map((item) => ( + + ))} +
+
+ + ); +} + +export default BestItems; diff --git a/src/components/items/Item/Item.css b/src/components/items/Item/Item.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/items/Item/Item.jsx b/src/components/items/Item/Item.jsx new file mode 100644 index 000000000..a60035754 --- /dev/null +++ b/src/components/items/Item/Item.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { ReactComponent as IconHeart } from '../../../assets/images/icons/ic_heart.svg'; +import ImgDefault from '../../../assets/images/market/img_default.png'; + +function Item({ item }) { + const handleErrorImage = (e) => { + e.target.src = ImgDefault; + }; + + return ( +
+ {item.name} +
+

{item.name}

+

{item.price.toLocaleString()}원

+
+ + {item.favoriteCount} +
+
+
+ ); +} + +export default Item; diff --git a/src/components/items/SailItems/SailItems.css b/src/components/items/SailItems/SailItems.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/items/SailItems/SailItems.jsx b/src/components/items/SailItems/SailItems.jsx new file mode 100644 index 000000000..4582f9b6e --- /dev/null +++ b/src/components/items/SailItems/SailItems.jsx @@ -0,0 +1,120 @@ +import './SailItems.css'; +import { ReactComponent as IconSearch } from '../../../assets/images/icons/ic_search.svg'; +import { ReactComponent as IconSort } from '../../../assets/images/icons/ic_sort.svg'; +import { useEffect, useState } from 'react'; +import { getProducts } from '../../../pages/api/Items'; +import Item from '../Item/Item'; + +const getPageSize = () => { + const width = window.innerWidth; + if (width < 768) { + return 4; + } else if (width < 1280) { + return 6; + } else { + return 10; + } +}; + +function SailItems() { + // 상품 + const [itemList, setItemList] = useState([]); + // 쿼리 + const [order, setOrder] = useState('recent'); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [keyword, setKeyword] = useState(''); + // 검색 + const [isDropdownVisible, setIsDropdownVisible] = useState(false); + + const handleClickDropdown = () => { + setIsDropdownVisible(!isDropdownVisible); + }; + const handleClickDropdownItem = (order) => { + setOrder(order); + setIsDropdownVisible(!isDropdownVisible); + }; + const handleKeyupSearchInput = (event) => { + if (event.keyCode === 13) { + event.preventDefault(); + setKeyword(event.target.value); + } + }; + + const fetchItemList = async ({ order, page, pageSize, keyword }) => { + let products = await getProducts({ order, page, pageSize, keyword }); + setItemList(products.list); + }; + + useEffect(() => { + const handleResize = () => { + setPageSize(getPageSize()); + }; + window.addEventListener('resize', handleResize); + fetchItemList({ order, page, pageSize, keyword }); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [order, page, pageSize, keyword]); + + return ( + <> +
+ {/* 판매 중인 상품 헤더 */} +
+
+
판매 중인 상품
+
+
+ + 상품 등록하기 + +
+ + +
+
+ + {isDropdownVisible && ( +
+
handleClickDropdownItem('recent')} + > + 최신순 +
+
handleClickDropdownItem('favorite')} + > + 좋아요순 +
+
+ )} +
+
+
+ {/* 판매 중인 상품 목록 */} +
+ {itemList?.map((item) => ( + + ))} +
+
+ + ); +} + +export default SailItems; diff --git a/src/components/navigationBar/NavigationBar.css b/src/components/navigationBar/NavigationBar.css new file mode 100644 index 000000000..d4f2845ca --- /dev/null +++ b/src/components/navigationBar/NavigationBar.css @@ -0,0 +1,55 @@ +.navbar { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 70px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 16px; + background-color: #ffffff; + border-bottom: 1px solid #dfdfdf; +} + +.navbarLeft { + display: flex; + align-items: center; +} + +.navbarHomeLogo { + width: 153px; +} + +.navbarMenu { + margin: 0 15px; + text-decoration: none; +} + +.navbarMenu ol { + list-style-type: none; + padding-left: 0; +} + +.navbarMenuItem { + display: inline-block; + margin: 10px; + font-size: 18px; + font-weight: bold; +} + +.navbarMenuItem.active { + color: var(--blue); +} + +@media (min-width: 768px) { + .navbar { + padding: 0 24px; + } +} + +@media (min-width: 1280px) { + .navbar { + padding: 0 200px; + } +} diff --git a/src/components/navigationBar/NavigationBar.jsx b/src/components/navigationBar/NavigationBar.jsx new file mode 100644 index 000000000..ccbd95479 --- /dev/null +++ b/src/components/navigationBar/NavigationBar.jsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react'; +import imgPandaMarketLogo from '../../assets/images/logo/panda-market-logo.png'; +import './NavigationBar.css'; + +function NavigationBar() { + return ( +
+
+ + 판다마켓 홈 + + +
+ + 로그인 + +
+ ); +} + +export default NavigationBar; diff --git a/src/index.css b/src/index.css deleted file mode 100644 index ec2585e8c..000000000 --- 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/index.js b/src/index.js index d563c0fb1..593edf121 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import './index.css'; import App from './App'; -import reportWebVitals from './reportWebVitals'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( @@ -10,8 +8,3 @@ root.render( ); - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); diff --git a/src/logo.svg b/src/logo.svg deleted file mode 100644 index 9dfc1c058..000000000 --- a/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/pages/addItems/AddItems.jsx b/src/pages/addItems/AddItems.jsx new file mode 100644 index 000000000..d055b09b3 --- /dev/null +++ b/src/pages/addItems/AddItems.jsx @@ -0,0 +1,9 @@ +function AddItems() { + return ( + <> +

임시 상품등록 페이지

+ + ); +} + +export default AddItems; diff --git a/src/pages/api/Items.js b/src/pages/api/Items.js new file mode 100644 index 000000000..cf7355a3d --- /dev/null +++ b/src/pages/api/Items.js @@ -0,0 +1,41 @@ +/** + * 상품 목록 조회 + * @param {string} order 정렬 기준: recent, favorite + * @param {number} page 페이지 번호 + * @param {number} pageSize 페이지 당 상품 수 + * @param {string} keyword 검색 키워드 + * @returns json. ex) { + "list": [ + { + "id": 22, + "name": "ee", + "description": "eeeeeeeeeeeeeeee", + "price": 4, + "tags": [ + "44" + ], + "images": [ + "https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/210/1718529146905/arrow_left-icon.svg" + ], + "ownerId": 210, + "favoriteCount": 0, + "createdAt": "2024-06-16T09:12:27.057Z", + "updatedAt": "2024-06-16T09:12:27.057Z" + }, ... } + */ +export async function getProducts({ + order, // 정렬 기준: recent, favorite + page, // 페이지 번호 + pageSize, // 페이지 당 상품 수 + keyword, // 검색 키워드 +}) { + const query = `orderBy=${order}&page=${page}&pageSize=${pageSize}&keyword=${keyword}`; + const response = await fetch( + `https://panda-market-api.vercel.app/products?${query}` + ); + if (!response.ok) { + throw new Error('데이터를 불러오는데 실패했습니다'); + } + const body = await response.json(); + return body; +} diff --git a/src/pages/faq/Faq.jsx b/src/pages/faq/Faq.jsx new file mode 100644 index 000000000..2318b6eb1 --- /dev/null +++ b/src/pages/faq/Faq.jsx @@ -0,0 +1,9 @@ +function Faq() { + return ( + <> +

임시 FAQ 페이지

+ + ); +} + +export default Faq; diff --git a/src/pages/home/Home.css b/src/pages/home/Home.css new file mode 100644 index 000000000..0914d215e --- /dev/null +++ b/src/pages/home/Home.css @@ -0,0 +1,172 @@ +.banner { + background-color: #cfe5ff; + height: 60vh; + text-align: center; + background-repeat: no-repeat; + background-position: bottom; + background-size: 130%; +} + +#hero { + background-image: url('../../assets/images/home/hero-image.png'); +} + +.banner h1 { + font-weight: 700; + font-size: 32px; + line-height: 44.8px; + padding-top: 48px; + padding-bottom: 18px; +} + +#bottom-banner { + background-image: url('../../assets/images/home/bottom-banner-image.png'); +} + +/* #login-link-button { + font-size: 16px; + font-weight: 600; + border-radius: 8px; + padding: 14.5px 43px; +} */ + +#features { + padding-top: 51px; +} + +.feature { + margin-bottom: 64px; +} + +.feature img { + width: 100%; + margin-bottom: 20px; +} + +.feature:nth-child(2) { + text-align: right; +} + +.feature-content { + flex: 1; +} + +.feature-content h2 { + color: var(--blue); + font-size: 16px; + line-height: 22.4px; + font-weight: 700; + margin-bottom: 8px; +} + +.feature-content h1 { + font-weight: 700; + font-size: 24px; + line-height: 33.6px; +} + +.feature-content h1 br { + display: none; +} + +.feature-description { + font-weight: 500; + font-size: 16px; + line-height: 19.2px; + letter-spacing: 0.08em; + margin-top: 20px; +} + +@media (min-width: 768px) { + .banner { + height: 90vh; + background-size: 120%; + } + + .banner h1 { + font-size: 40px; + line-height: 56px; + padding-top: 84px; + padding-bottom: 24px; + } + + #hero h1 br { + display: none; + } + + #features { + padding-top: 24px; + padding-bottom: 16px; + } + + .feature-content h2 { + font-size: 18px; + line-height: 25.2px; + margin-bottom: 12px; + } + + .feature-content h1 { + font-size: 32px; + line-height: 44.8px; + } + + .feature-description { + font-size: 18px; + line-height: 21.6px; + } +} + +@media (min-width: 1280px) { + .banner { + text-align: left; + height: 540px; + display: flex; + align-items: center; + background-position: 80% bottom; + background-size: 55%; + } + + .banner h1 { + padding-top: 0; + padding-bottom: 32px; + } + + #hero h1 br { + display: inline; + } + + #features { + padding: 138px 0; + } + + .feature { + margin-bottom: 138px; + display: flex; + align-items: center; + gap: 5%; + } + + .feature:nth-child(2) { + flex-direction: row-reverse; + } + + .feature img { + width: 50%; + margin-bottom: 0; + } + + .feature-content h1 { + font-size: 40px; + line-height: 56px; + } + + .feature-content h1 br { + display: block; + } + + .feature-description { + font-size: 24px; + line-height: 28.8px; + margin-top: 24px; + } +} diff --git a/src/pages/home/Home.jsx b/src/pages/home/Home.jsx new file mode 100644 index 000000000..1dee4e3d6 --- /dev/null +++ b/src/pages/home/Home.jsx @@ -0,0 +1,140 @@ +import './Home.css'; +import imgPandaMarketLogo from '../../assets/images/logo/panda-market-logo.png'; +import imgFeature1 from '../../assets/images/home/feature1-image.png'; +import imgFeatureSearch from '../../assets/images/home/feature-search-img.png'; +import imgFeature3 from '../../assets/images/home/feature3-image.png'; +import imgFacebookLogo from '../../assets/images/social/facebook-logo.svg'; +import imgTwitterLogo from '../../assets/images/social/twitter-logo.svg'; +import imgYoutubeLogo from '../../assets/images/social/youtube-logo.svg'; +import imgInstagramLogo from '../../assets/images/social/instagram-logo.svg'; + +function Home() { + return ( + <> + {/*
+ + 판다마켓 홈 + + + 로그인 + +
*/} +
+
+
+

+ 일상의 모든 물건을 +
+ 거래해 보세요 +

+ + 구경하러 가기 + +
+
+
+
+ 인기 상품 +
+

Hot item

+

+ 인기 상품을 +
+ 확인해 보세요 +

+

+ 가장 HOT한 중고거래 물품을 +
+ 판다마켓에서 확인해 보세요 +

+
+
+
+ 검색 기능 소개 이미지 +
+

Search

+

+ 구매를 원하는 +
+ 상품을 검색하세요 +

+

+ 구매하고 싶은 물품은 검색해서 +
+ 쉽게 찾아보세요 +

+
+
+
+ 판매 상품 등록 +
+

Register

+

+ 판매를 원하는 +
+ 상품을 등록하세요 +

+

+ 어떤 물건이든 판매하고 싶은 상품을 +
+ 쉽게 등록하세요 +

+
+
+
+
+
+

+ 믿을 수 있는 +
+ 판다마켓 중고거래 +

+
+
+
+ + + ); +} + +export default Home; diff --git a/src/pages/items/Items.css b/src/pages/items/Items.css new file mode 100644 index 000000000..1ff75f2ee --- /dev/null +++ b/src/pages/items/Items.css @@ -0,0 +1,174 @@ +/* Items */ +.container-items { + max-width: 1200px; + margin: 94px auto; + padding: 0 16px; +} + +/* Best Items */ +.container-best-items { + padding: 0 0 40px; +} + +.title-best-items { + font-size: 20px; + font-weight: bold; +} + +.list-best-items { +} + +/* Sail Items */ +.container-sail-items { +} + +.header-sail-items { + display: flex; + justify-content: space-between; + padding: 0 0 24px; +} + +.header-sail-items-left { +} + +.title-sail-items { + font-size: 20px; + font-weight: bold; +} + +.header-sail-items-right { + display: flex; + justify-content: space-between; +} + +.btn-add-item { +} + +.wrapper-input-search-item { + display: flex; + background-color: var(--gray-100); + border-radius: 12px; + font-size: 32px; + min-width: 250px; + margin: 0 0 0 8px; + padding: 9px 16px; + align-items: center; +} + +.input-search-item { + border: none; + flex: 1; + background-color: inherit; + margin: 0 0 0 4px; +} + +.input-search-item::placeholder { + color: var(--gray-400); + font-size: 16px; +} + +.wrapper-sort-item { + position: relative; +} + +.btn-sort-item { + border: 1px solid var(--gray-200); + border-radius: 12px; + margin: 0 0 0 8px; + padding: 12px 40px; + display: flex; +} + +.dropdown-sort-item { + position: absolute; + background-color: white; + margin: 5px 0 0 8px; + border-radius: 12px; + border: 1px solid var(--gray-200); + z-index: 99; +} + +.item-dropdown { + padding: 12px 44px; + border-bottom: 1px solid var(--gray-200); + font-size: 16px; + color: #1f2937; + cursor: pointer; +} + +.list-sail-items { + display: grid; + grid-template-columns: repeat(2, 1fr); +} + +/* Item */ +.wrapper-item { + overflow: hidden; + cursor: pointer; +} + +.item-img { + width: 100%; + height: auto; + object-fit: cover; + border-radius: 16px; + overflow: hidden; + aspect-ratio: 1; + margin: 0 0 16px; +} + +.wrapper-item-info { + display: flex; + flex-direction: column; + flex-grow: 1; + line-height: 1.5; +} + +.item-name { + font-size: 16px; + font-weight: 400; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.item-price { + font-size: 16px; + font-weight: bold; +} + +.item-favorite-count { + display: flex; + align-items: center; + color: var(--gray-heart); +} + +@media (min-width: 768px) { + /* Best Items */ + .container-best-items { + } + + .list-best-items { + display: grid; + grid-template-columns: repeat(2, 1fr); + } + + /* Sail Items */ + .list-sail-items { + display: grid; + grid-template-columns: repeat(3, 1fr); + } +} + +@media (min-width: 1280px) { + /* Best Items */ + .list-best-items { + grid-template-columns: repeat(4, 1fr); + } + + /* Sail Items */ + .list-sail-items { + display: grid; + grid-template-columns: repeat(5, 1fr); + } +} diff --git a/src/pages/items/Items.jsx b/src/pages/items/Items.jsx new file mode 100644 index 000000000..0e5615f4e --- /dev/null +++ b/src/pages/items/Items.jsx @@ -0,0 +1,18 @@ +import './Items.css'; +import BestItems from '../../components/items/BestItems/BestItems'; +import SailItems from '../../components/items/SailItems/SailItems'; + +function Items() { + return ( + <> +
+ {/* 베스트 상품 */} + + {/* 판매 중인 상품 */} + +
+ + ); +} + +export default Items; diff --git a/src/pages/privacy/Privacy.jsx b/src/pages/privacy/Privacy.jsx new file mode 100644 index 000000000..c2576ccb3 --- /dev/null +++ b/src/pages/privacy/Privacy.jsx @@ -0,0 +1,9 @@ +function Privacy() { + return ( + <> +

임시 이용약관 페이지

+ + ); +} + +export default Privacy; diff --git a/src/pages/signin/Signin.jsx b/src/pages/signin/Signin.jsx new file mode 100644 index 000000000..fa668fc25 --- /dev/null +++ b/src/pages/signin/Signin.jsx @@ -0,0 +1,79 @@ +import './sign.css'; +import './signinScript'; +import imgPandaLogo from '../../assets/images/logo/logo.svg'; +import iconEyeInvisible from '../../assets/images/icons/eye-invisible.svg'; +import imgGoogleLogo from '../../assets/images/social/google-logo.png'; +import imgKakaoLogo from '../../assets/images/social/kakao-logo.png'; + +function Signin() { + return ( + <> +
+ +
+
+ + + + 이메일을 입력해d주세요. + +
+
+ +
+ + +
+ + 비밀번호를 입력해주세요. + +
+ +
+ +
+ 판다마켓이 처음이신가요? 회원가입 +
+
+ + ); +} + +export default Signin; diff --git a/src/pages/signin/sign.css b/src/pages/signin/sign.css new file mode 100644 index 000000000..430ffcabb --- /dev/null +++ b/src/pages/signin/sign.css @@ -0,0 +1,138 @@ +.container-sign { + max-width: 400px; + margin: 0 auto; + padding: 0 16px; +} + +.wrapper-btn-logo { + margin: 0 auto; + display: block; + margin-top: 24px; + margin-bottom: 24px; + width: 198px; +} + +.btn-home-logo { + width: 100%; +} + +.input-item { + margin-bottom: 24px; + display: flex; + flex-direction: column; +} + +.message-error { + display: none; + color: #f74747; + font-weight: 600; + font-size: 15px; + margin-top: 8px; + padding-left: 16px; +} + +label { + display: block; + font-weight: 700; + font-size: 14px; + margin-bottom: 8px; +} + +input { + padding: 16px 24px; + background-color: #f3f4f6; + border: none; + border-radius: 12px; + font-size: 16px; + line-height: 24px; + width: 100%; +} + +input::placeholder { + color: #9ca3af; + font-size: 16px; + line-height: 24px; +} + +input:focus { + outline-color: var(--blue); +} + +input.error { + border: 1px solid #f74747; +} + +.wrapper-input { + position: relative; + display: flex; + align-items: center; +} + +.img-password-toggle { + position: absolute; + right: 24px; + cursor: pointer; +} + +.btn-password-visible { + position: absolute; + right: 24px; + cursor: pointer; +} + +.container-sns-signin { + background-color: #e6f2ff; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 23px; + margin: 24px 0; +} + +.container-sns-signin h3 { + font-weight: 500; + font-size: 16px; + line-height: 24px; +} + +.container-btn-sns-signin { + display: flex; + gap: 16px; +} + +.container-btn-sign { + font-weight: 500; + font-size: 15px; + text-align: center; +} + +.container-btn-sign a { + color: #3182f6; + text-decoration: underline; + text-underline-offset: 2px; +} + +@media (min-width: 768px) { + .container-sign { + max-width: 640px; + } + + .wrapper-btn-logo { + width: 396px; + margin-top: 48px; + margin-bottom: 40px; + } + + label { + font-size: 18px; + margin-bottom: 16px; + } +} + +@media (min-width: 1280px) { + .wrapper-btn-logo { + margin-top: 60px; + margin-bottom: 40px; + } +} diff --git a/src/pages/signin/signinScript.js b/src/pages/signin/signinScript.js new file mode 100644 index 000000000..798f27915 --- /dev/null +++ b/src/pages/signin/signinScript.js @@ -0,0 +1,107 @@ +document.addEventListener('DOMContentLoaded', () => { + let isValidEmail = false; + let isValidPassword = false; + + const inputEmail = document.getElementById('email'); + const inputPassword = document.getElementById('password'); + const btnTogglePasswordVisibleList = document.querySelectorAll( + '.btn-password-visible' + ); + const btnSignin = document.getElementById('btn-signin'); + const messageErrorEmail = document.getElementById('message-error-email'); + const messageErrorPassword = document.getElementById( + 'message-error-password' + ); + + const validateEmail = () => { + const emailValue = inputEmail.value.trim(); + if (!emailValue) { + showError(inputEmail, messageErrorEmail, '이메일을 입력해주세요.'); + isValidEmail = false; + } else if (!checkEmailRegex(emailValue)) { + showError(inputEmail, messageErrorEmail, '잘못된 이메일 형식입니다.'); + isValidEmail = false; + } else { + clearError(inputEmail, messageErrorEmail); + isValidEmail = true; + } + updateBtnSignin(); + }; + + const validatePassword = () => { + const passwordValue = inputPassword.value.trim(); + if (!passwordValue) { + showError( + inputPassword, + messageErrorPassword, + '비밀번호를 입력해주세요.' + ); + isValidPassword = false; + } else if (passwordValue.length < 8) { + showError( + inputPassword, + messageErrorPassword, + '비밀번호를 8자 이상 입력해주세요' + ); + isValidPassword = false; + } else { + clearError(inputPassword, messageErrorPassword); + isValidPassword = true; + } + updateBtnSignin(); + }; + + const togglePasswordVisible = (e) => { + e.preventDefault(); + const visibleBtn = e.currentTarget; + const targetInput = visibleBtn.parentElement.querySelector('input'); + const visibleImg = visibleBtn.querySelector('.img-password-visible'); + if (targetInput.type === 'password') { + targetInput.type = 'text'; + visibleImg.src = '/images/icons/eye-visible.svg'; + } else { + targetInput.type = 'password'; + visibleImg.src = '/images/icons/eye-invisible.svg'; + } + }; + + const updateBtnSignin = () => { + if (isValidPassword && isValidEmail) { + btnSignin.disabled = false; + } else { + btnSignin.disabled = true; + } + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (btnSignin.disabled) return; + window.location.href = '/items'; + }; + + const checkEmailRegex = (email) => { + const emailRegex = new RegExp('[a-z0-9]+@[a-z]+\\.[a-z]{2,3}'); + return emailRegex.test(email); + }; + + const showError = (input, errorMessageElement, message) => { + input.classList.add('error'); + errorMessageElement.textContent = message; + errorMessageElement.style.display = 'block'; + }; + + const clearError = (input, errorMessageElement) => { + input.classList.remove('error'); + errorMessageElement.textContent = ''; + errorMessageElement.style.display = 'none'; + }; + + inputEmail.addEventListener('focusout', validateEmail); + inputPassword.addEventListener('focusout', validatePassword); + inputEmail.addEventListener('input', validateEmail); + inputPassword.addEventListener('input', validatePassword); + btnSignin.addEventListener('click', handleSubmit); + btnTogglePasswordVisibleList.forEach((button) => { + button.addEventListener('click', togglePasswordVisible); + }); +}); diff --git a/src/pages/signup/Signup.jsx b/src/pages/signup/Signup.jsx new file mode 100644 index 000000000..4bd690f05 --- /dev/null +++ b/src/pages/signup/Signup.jsx @@ -0,0 +1,126 @@ +import './sign.css'; +import './signupScript'; + +function Signup() { + return ( + <> +
+ +
+
+ + + + 이메일을 입력해d주세요. + +
+
+ + + + 닉네임을 입력해d주세요. + +
+
+ +
+ + +
+ + 비밀번호를 입력해주세요. + +
+
+ +
+ + +
+ + 비밀번호를 입력해주세요. + +
+ +
+ +
+ 이미 회원이신가요? 로그인 +
+
+ + ); +} + +export default Signup; diff --git a/src/pages/signup/sign.css b/src/pages/signup/sign.css new file mode 100644 index 000000000..430ffcabb --- /dev/null +++ b/src/pages/signup/sign.css @@ -0,0 +1,138 @@ +.container-sign { + max-width: 400px; + margin: 0 auto; + padding: 0 16px; +} + +.wrapper-btn-logo { + margin: 0 auto; + display: block; + margin-top: 24px; + margin-bottom: 24px; + width: 198px; +} + +.btn-home-logo { + width: 100%; +} + +.input-item { + margin-bottom: 24px; + display: flex; + flex-direction: column; +} + +.message-error { + display: none; + color: #f74747; + font-weight: 600; + font-size: 15px; + margin-top: 8px; + padding-left: 16px; +} + +label { + display: block; + font-weight: 700; + font-size: 14px; + margin-bottom: 8px; +} + +input { + padding: 16px 24px; + background-color: #f3f4f6; + border: none; + border-radius: 12px; + font-size: 16px; + line-height: 24px; + width: 100%; +} + +input::placeholder { + color: #9ca3af; + font-size: 16px; + line-height: 24px; +} + +input:focus { + outline-color: var(--blue); +} + +input.error { + border: 1px solid #f74747; +} + +.wrapper-input { + position: relative; + display: flex; + align-items: center; +} + +.img-password-toggle { + position: absolute; + right: 24px; + cursor: pointer; +} + +.btn-password-visible { + position: absolute; + right: 24px; + cursor: pointer; +} + +.container-sns-signin { + background-color: #e6f2ff; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 23px; + margin: 24px 0; +} + +.container-sns-signin h3 { + font-weight: 500; + font-size: 16px; + line-height: 24px; +} + +.container-btn-sns-signin { + display: flex; + gap: 16px; +} + +.container-btn-sign { + font-weight: 500; + font-size: 15px; + text-align: center; +} + +.container-btn-sign a { + color: #3182f6; + text-decoration: underline; + text-underline-offset: 2px; +} + +@media (min-width: 768px) { + .container-sign { + max-width: 640px; + } + + .wrapper-btn-logo { + width: 396px; + margin-top: 48px; + margin-bottom: 40px; + } + + label { + font-size: 18px; + margin-bottom: 16px; + } +} + +@media (min-width: 1280px) { + .wrapper-btn-logo { + margin-top: 60px; + margin-bottom: 40px; + } +} diff --git a/src/pages/signup/signupScript.js b/src/pages/signup/signupScript.js new file mode 100644 index 000000000..3ee9a6822 --- /dev/null +++ b/src/pages/signup/signupScript.js @@ -0,0 +1,172 @@ +document.addEventListener('DOMContentLoaded', () => { + let isValidEmail = false; + let isValidNickname = false; + let isValidPassword = false; + let isValidPasswordConfirmation = false; + + const inputEmail = document.getElementById('email'); + const inputNickname = document.getElementById('nickname'); + const inputPassword = document.getElementById('password'); + const inputPasswordConfirmation = document.getElementById( + 'password-confirmation' + ); + const btnTogglePasswordVisibleList = document.querySelectorAll( + '.btn-password-visible' + ); + const btnSignup = document.getElementById('btn-signup'); + const messageErrorEmail = document.getElementById('message-error-email'); + const messageErrorNickname = document.getElementById( + 'message-error-nickname' + ); + const messageErrorPassword = document.getElementById( + 'message-error-password' + ); + const messageErrorPasswordConfirmation = document.getElementById( + 'message-error-password-confirmation' + ); + + const validateEmail = () => { + const emailValue = inputEmail.value.trim(); + if (!emailValue) { + showError(inputEmail, messageErrorEmail, '이메일을 입력해주세요.'); + isValidEmail = false; + } else if (!checkEmailRegex(emailValue)) { + showError(inputEmail, messageErrorEmail, '잘못된 이메일 형식입니다.'); + isValidEmail = false; + } else { + clearError(inputEmail, messageErrorEmail); + isValidEmail = true; + } + updatebtnSignup(); + }; + + const validateNickname = () => { + const nicknameValue = inputNickname.value.trim(); + if (!nicknameValue) { + showError(inputNickname, messageErrorNickname, '닉네임을 입력해주세요.'); + isValidNickname = false; + } else { + clearError(inputNickname, messageErrorNickname); + isValidNickname = true; + } + updatebtnSignup(); + }; + + const validatePassword = () => { + const passwordValue = inputPassword.value.trim(); + if (!passwordValue) { + showError( + inputPassword, + messageErrorPassword, + '비밀번호를 입력해주세요.' + ); + isValidPassword = false; + } else if (passwordValue.length < 8) { + showError( + inputPassword, + messageErrorPassword, + '비밀번호를 8자 이상 입력해주세요' + ); + isValidPassword = false; + } else { + clearError(inputPassword, messageErrorPassword); + isValidPassword = true; + } + validatePasswordConfirmation(); + updatebtnSignup(); + }; + + const validatePasswordConfirmation = () => { + const passwordConfirmationValue = inputPasswordConfirmation.value.trim(); + const passwordValue = inputPassword.value.trim(); + if (!passwordConfirmationValue) { + showError( + inputPasswordConfirmation, + messageErrorPasswordConfirmation, + '비밀번호를 입력해주세요.' + ); + } else if (passwordConfirmationValue !== passwordValue) { + showError( + inputPasswordConfirmation, + messageErrorPasswordConfirmation, + '비밀번호가 일치하지 않습니다.' + ); + isValidPasswordConfirmation = false; + } else { + clearError(inputPasswordConfirmation, messageErrorPasswordConfirmation); + isValidPasswordConfirmation = true; + } + updatebtnSignup(); + }; + + const togglePasswordVisible = (e) => { + e.preventDefault(); + const visibleBtn = e.currentTarget; + const targetInput = visibleBtn.parentElement.querySelector('input'); + const visibleImg = visibleBtn.querySelector('.img-password-visible'); + if (targetInput.type === 'password') { + targetInput.type = 'text'; + visibleImg.src = '/images/icons/eye-visible.svg'; + visibleImg.alt = '비밀번호 표시'; + } else { + targetInput.type = 'password'; + visibleImg.src = '/images/icons/eye-invisible.svg'; + visibleImg.alt = '비밀번호 숨김'; + } + }; + + const updatebtnSignup = () => { + if ( + isValidPassword && + isValidEmail && + isValidNickname && + isValidPasswordConfirmation + ) { + btnSignup.disabled = false; + } else { + btnSignup.disabled = true; + } + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (btnSignup.disabled) return; + window.location.href = '/signin'; + }; + + const checkEmailRegex = (email) => { + const emailRegex = new RegExp('[a-z0-9]+@[a-z]+\\.[a-z]{2,3}'); + return emailRegex.test(email); + }; + + const showError = (input, errorMessageElement, message) => { + input.classList.add('error'); + errorMessageElement.textContent = message; + errorMessageElement.style.display = 'block'; + }; + + const clearError = (input, errorMessageElement) => { + input.classList.remove('error'); + errorMessageElement.textContent = ''; + errorMessageElement.style.display = 'none'; + }; + + inputEmail.addEventListener('focusout', validateEmail); + inputEmail.addEventListener('input', validateEmail); + inputNickname.addEventListener('focusout', validateNickname); + inputNickname.addEventListener('input', validateNickname); + inputPassword.addEventListener('focusout', validatePassword); + inputPassword.addEventListener('input', validatePassword); + inputPasswordConfirmation.addEventListener( + 'focusout', + validatePasswordConfirmation + ); + inputPasswordConfirmation.addEventListener( + 'input', + validatePasswordConfirmation + ); + btnSignup.addEventListener('click', handleSubmit); + btnTogglePasswordVisibleList.forEach((button) => { + button.addEventListener('click', togglePasswordVisible); + }); +}); diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js deleted file mode 100644 index 5253d3ad9..000000000 --- a/src/reportWebVitals.js +++ /dev/null @@ -1,13 +0,0 @@ -const reportWebVitals = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/src/setupTests.js b/src/setupTests.js deleted file mode 100644 index 8f2609b7b..000000000 --- a/src/setupTests.js +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 000000000..5369bca56 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,183 @@ +:root { + --gray-900: #1b1d1f; + --gray-800: #26282b; + --gray-600: #454c53; + --gray-500: #72787f; + --gray-400: #9ea4a8; + --gray-200: #e5e7eb; + --gray-100: #e8ebed; + --gray-50: #f7f7f8; + --gray-heart: #4b5563; + + --blue: #3692ff; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + color: #374151; + word-break: keep-all; + font-family: 'Pretendard', sans-serif; +} + +header { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 70px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 16px; + background-color: #ffffff; + border-bottom: 1px solid #dfdfdf; +} + +main { + margin-top: 70px; +} + +footer { + background-color: #111827; + padding: 32px; + font-size: 16px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 60px; +} + +#copyright { + order: 3; + flex-basis: 100%; + color: #9ca3af; +} + +#footer-menu { + display: flex; + gap: 30px; + color: var(--gray-200); +} + +#social-media { + display: flex; + gap: 12px; +} + +a { + text-decoration: none; + color: inherit; +} + +img { + vertical-align: bottom; +} + +.wrapper { + max-width: 1200px; + margin: 0 auto; + width: 100%; + padding: 0 16px; +} + +button { + background: none; + border: none; + outline: none; + box-shadow: none; + cursor: pointer; + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: inherit; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.button { + background-color: var(--blue); + color: #ffffff; + display: inline-flex; + align-items: center; + justify-content: center; + /* 20240621 수정 */ + font-size: 16px; + font-weight: 600; + border-radius: 8px; + padding: 12px 23px; +} + +.button:hover { + background-color: #1967d6; +} + +.button:focus { + background-color: #1251aa; +} + +.button:disabled { + background-color: #9ca3af; + cursor: default; + pointer-events: none; +} + +.pill-button { + font-size: 16px; + font-weight: 600; + border-radius: 999px; + padding: 14.5px 33.5px; +} + +.full-width { + width: 100%; +} + +.break-on-desktop { + display: none; +} + +@media (min-width: 768px) { + header { + padding: 0 24px; + } + + .wrapper { + padding: 0 24px; + } + + .pill-button { + font-size: 20px; + font-weight: 700; + padding: 16px 126px; + } + + footer { + padding: 32px 104px 108px 104px; + } + + #copyright { + flex-basis: auto; + order: 0; + } +} + +@media (min-width: 1280px) { + header { + padding: 0 200px; + } + + .break-on-desktop { + display: inline; + } + + footer { + padding: 32px 200px 108px 200px; + } +} diff --git a/src/utils/items.js b/src/utils/items.js new file mode 100644 index 000000000..e819b567b --- /dev/null +++ b/src/utils/items.js @@ -0,0 +1,10 @@ +// export const getPageSize = () => { +// const width = window.innerWidth; +// if (width < 768) { +// return 1; +// } else if (width < 1280) { +// return 2; +// } else { +// return 4; +// } +// };