diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 987202d..f49ed79 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,7 +3,7 @@ name: Deploy Frontend to EC2 (main) on: push: branches: - - ModifyStyle + - RefactorStyle jobs: deploy: diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c3bb799 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "AutoPreview.serverURL": "http://localhost:3000/" +} \ No newline at end of file diff --git a/git b/git new file mode 100644 index 0000000..e658649 --- /dev/null +++ b/git @@ -0,0 +1,30 @@ + origin/AttributesToggle + origin/AutoCompleteWithBackend + origin/Complete + origin/Debug1 + origin/DeployTest + origin/DeployTest3 + origin/Detail + origin/DetailInfo + origin/DetailWindow + origin/Dropdown + origin/Favorites + origin/Feedback + origin/FeedbackPage + origin/ForMerge2 + origin/HEAD -> origin/main + origin/LinkMarkerToDetail + origin/MarkToDetail + origin/MarkerBookmark + origin/ModifyStyle + origin/Refactor + origin/Sign + origin/elaus00-patch-1 + origin/forMerge + origin/gh-pages + origin/main + origin/mapping + origin/mergeForDebug1 + origin/mergeForDebug1-1 + origin/release + origin/release-test diff --git a/package-lock.json b/package-lock.json index a650c8f..2598313 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pureplate", - "version": "0.1.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pureplate", - "version": "0.1.0", + "version": "0.5.0", "license": "ISC", "dependencies": { "@testing-library/jest-dom": "^5.17.0", diff --git a/package.json b/package.json index 4afd715..7546522 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pureplate", - "version": "0.1.0", + "version": "0.5.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^5.17.0", diff --git a/src/Attributes/Button.js b/src/Attributes/Button.js index 13bc9f5..f495d7f 100644 --- a/src/Attributes/Button.js +++ b/src/Attributes/Button.js @@ -1,37 +1,15 @@ import { useAuth } from "../AuthContext"; import styles from "./Button.module.css"; -import halalIcon from "../assets/Icons/flag_halal.svg"; -import veganIcon from "../assets/Icons/flag_vegan.svg"; -import glutenIcon from "../assets/Icons/flag_glutenfree.svg"; -import lactoIcon from "../assets/Icons/flag_loctosfree.svg"; - -import halalIcon1 from "../assets/Icons/flag_halal1.svg"; -import veganIcon1 from "../assets/Icons/flag_vegan1.svg"; -import glutenIcon1 from "../assets/Icons/flag_glutenfree1.svg"; -import lactoIcon1 from "../assets/Icons/flag_loctosfree1.svg"; +import { cssList, icons } from "./iconConfig"; // 분리된 설정 파일 임포트 import { useEffect, useState } from "react"; function Button({ attribute }) { - const [cssList, setCssList] = useState({ - Vegan: "rgba(118, 199, 183, 0.85)", - Halal: "rgba(118, 199, 131, 0.85)", - "Gluten-Free": "rgba(233, 250, 234, 0.9)", - "Lacto-Free": "rgba(254, 246, 176, 0.85) ", - }); - - const [list, setList] = useState({ - Vegan: veganIcon, - Halal: halalIcon, - "Gluten-Free": glutenIcon, - "Lacto-Free": lactoIcon, - }); - - // Icons for different states (default and active) - const [icons, setIcons] = useState({ - Vegan: { default: veganIcon1, active: veganIcon }, - Halal: { default: halalIcon1, active: halalIcon }, - "Gluten-Free": { default: glutenIcon1, active: glutenIcon }, - "Lacto-Free": { default: lactoIcon1, active: lactoIcon }, + const [list, setList] = useState(() => { + const initialList = {}; + Object.keys(icons).forEach((key) => { + initialList[key] = icons[key].default; + }); + return initialList; }); const { dietToggle, setDietToggle } = useAuth(); @@ -102,4 +80,4 @@ function Button({ attribute }) { ); } -export default Button; +export default Button; diff --git a/src/Attributes/Button.module.css b/src/Attributes/Button.module.css index b0546e1..13665cc 100644 --- a/src/Attributes/Button.module.css +++ b/src/Attributes/Button.module.css @@ -4,31 +4,31 @@ /* Default (not selected) */ .selectedFalse { - border-color: #a6b9b1c5; + border-color: #a6b9b100; + background-color: #ffffffd0; } .distance { color: #3e4958; text-align: center; font-family: "AnekBangla-Light", sans-serif; - font-size: 1rem; - line-height: 1.5rem; + font-size: 1.1rem; font-weight: 300; position: relative; - border-radius: 1.875rem; + border-radius: 1.575rem; border-style: solid; border-width: 0.03125rem; padding: 0.625rem 1rem 0.625rem 1rem; display: flex; flex-direction: row; - gap: 0.625rem; align-items: center; justify-content: center; height: 2.875rem; position: relative; cursor: pointer; } + /* 1050px 이하의 화면 크기에서 스타일 적용 */ @media (max-width: 1050px) { .distance { diff --git a/src/Attributes/iconConfig.js b/src/Attributes/iconConfig.js new file mode 100644 index 0000000..ec08915 --- /dev/null +++ b/src/Attributes/iconConfig.js @@ -0,0 +1,23 @@ +import halalIcon from "../assets/Icons/flag_halal.svg"; +import veganIcon from "../assets/Icons/flag_vegan.svg"; +import glutenIcon from "../assets/Icons/flag_glutenfree.svg"; +import lactoIcon from "../assets/Icons/flag_loctosfree.svg"; + +import halalIcon1 from "../assets/Icons/flag_halal1.svg"; +import veganIcon1 from "../assets/Icons/flag_vegan1.svg"; +import glutenIcon1 from "../assets/Icons/flag_glutenfree1.svg"; +import lactoIcon1 from "../assets/Icons/flag_loctosfree1.svg"; + +export const cssList = { + Vegan: "rgba(118, 199, 183, 0.85)", + Halal: "rgba(118, 199, 131, 0.85)", + "Gluten-Free": "rgba(233, 250, 234, 0.9)", + "Lacto-Free": "rgba(254, 246, 176, 0.85)", +}; + +export const icons = { + Vegan: { default: veganIcon1, active: veganIcon }, + Halal: { default: halalIcon1, active: halalIcon }, + "Gluten-Free": { default: glutenIcon1, active: glutenIcon }, + "Lacto-Free": { default: lactoIcon1, active: lactoIcon }, +}; diff --git a/src/Profile/Profile.jsx b/src/Profile/Profile.jsx index ce389ff..03df7b8 100644 --- a/src/Profile/Profile.jsx +++ b/src/Profile/Profile.jsx @@ -2,7 +2,7 @@ import { ChevronDown } from "../ChevronDown/ChevronDown.jsx"; import { useEffect, useState } from "react"; import { useAuth } from "../AuthContext.jsx"; import { Link, useNavigate } from "react-router-dom"; -import Sign from "./Signin/Sign.jsx"; +import SignInMain from "./Signin/SignInMain.jsx"; import LogoutConfirmModal from "./Logout/LogoutConfirmModal.jsx"; import styles from "./Profile.module.css"; import profileIcon from "../assets/Icons/profile.svg"; @@ -72,7 +72,7 @@ function Profile() { onClick={isLoggedIn ? toggleDropdown : openModal} > {!isLoggedIn ? ( - Sign In + Sign In ) : ( <>
{user}
@@ -107,7 +107,7 @@ function Profile() { )} - + ul { - animation: slide-fade-in-dropdown-animation 0.4s ease; -} - -@keyframes slide-fade-out-dropdown-animation { - 0% { - transform: translateY(0); +@media (max-width: 678px) { + .profile { + display: none; } +} - 100% { - transform: translateY(-100%); +@media (max-width: 678px) { + .dropdownMenu { + right: -10px; + width: 100px; + left: auto; } } -.slide-fade-out-dropdown { - overflow: hidden; +@media (max-width: 678px) { + .dropProfile { + display: block; + } } -.slide-fade-out-dropdown > ul { - animation: slide-fade-out-dropdown-animation 0.4s ease; - animation-fill-mode: forwards; +@media (min-width: 678px) { + .profileIcon { + display: none; + width: 35px; + } } - -.components-dropdown > ul { - position: relative; - top: 5px; - margin-top: 0; - margin-bottom: 5px; - padding-left: 0; - list-style: none; -} */ diff --git a/src/Profile/SignUp/SignUp.jsx b/src/Profile/SignUp/SignUp.jsx new file mode 100644 index 0000000..50d11a2 --- /dev/null +++ b/src/Profile/SignUp/SignUp.jsx @@ -0,0 +1,127 @@ +import React, { useState } from "react"; +import SignInInputField from "../Signin/SignInInputField/SignInInputField.jsx"; +import { useAuth } from "../../AuthContext.jsx"; +import styles from "./SignUp.module.css"; +import axios from "axios"; +import { useNavigate } from "react-router-dom"; +import { validateField, validateAllFields } from "./validation"; +import { showSuccessAlert, showErrorAlert } from "./alert"; + +function SignUp({ switchToSignIn, close }) { + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [errors, setErrors] = useState({}); + const navigate = useNavigate(); + const { URL } = useAuth(); + + const handleNameChange = (event) => { + setName(event.target.value); + setErrors((prevErrors) => ({ + ...prevErrors, + ...validateField("name", event.target.value), + })); + }; + + const handleEmailChange = (event) => { + setEmail(event.target.value); + setErrors((prevErrors) => ({ + ...prevErrors, + ...validateField("email", event.target.value), + })); + }; + + const handlePasswordChange = (event) => { + setPassword(event.target.value); + setErrors((prevErrors) => ({ + ...prevErrors, + ...validateField("password", event.target.value), + })); + }; + + const handleConfirmPasswordChange = (event) => { + setConfirmPassword(event.target.value); + setErrors((prevErrors) => ({ + ...prevErrors, + ...validateField("confirmPassword", event.target.value, password), + })); + }; + + const validate = () => { + const tempErrors = validateAllFields( + name, + email, + password, + confirmPassword + ); + setErrors(tempErrors); + return Object.values(tempErrors).every((x) => x === ""); + }; + + const onSubmit = async (event) => { + event.preventDefault(); + if (validate()) { + const data = { + nickname: name, + username: email, + password: password, + }; + + const SIGNUP_URL = `${URL}/api/account/register/`; + try { + const response = await axios.post(SIGNUP_URL, data); + close(); + navigate("/"); + showSuccessAlert(response.data.nickname); + } catch (error) { + if (error.response && error.response.data.username) { + showErrorAlert(); + } + console.error( + "Error during sign up:", + error.response ? error.response.data : error + ); + } + } else { + console.error("Validation failed."); + } + }; + + return ( +
+ + + + + + + ); +} + +export default SignUp; diff --git a/src/Profile/Signin/SignInButton.module.css b/src/Profile/SignUp/SignUp.module.css similarity index 61% rename from src/Profile/Signin/SignInButton.module.css rename to src/Profile/SignUp/SignUp.module.css index 7617e8e..a3dffa2 100644 --- a/src/Profile/Signin/SignInButton.module.css +++ b/src/Profile/SignUp/SignUp.module.css @@ -1,22 +1,25 @@ +.SignUpMain { + display: flex; + flex-direction: column; + position: relative; + padding: 20px; + align-items: center; +} + .signInButton { - margin: -0.1 875rem 0 0 0; - background: var(--button-color, rgba(169, 234, 195, 0.62)); + background: rgba(169, 234, 195, 0.62); border-radius: 0.625rem; display: flex; - flex-direction: row; - gap: 0.625rem; align-items: center; justify-content: center; - flex-shrink: 0; width: 8.75rem; - height: 2.375rem; - position: relative; - cursor: pointer; - margin-bottom: 10px; + height: 2.5rem; + margin-top: 1rem; color: rgba(0, 0, 0, 0.46); text-align: left; font-family: "Alef-Regular", sans-serif; font-size: 1rem; font-weight: 400; + cursor: pointer; } diff --git a/src/Profile/SignUp/alert.js b/src/Profile/SignUp/alert.js new file mode 100644 index 0000000..3805c03 --- /dev/null +++ b/src/Profile/SignUp/alert.js @@ -0,0 +1,18 @@ +import Swal from "sweetalert2"; +import "../../custom-swal.css"; + +export const showSuccessAlert = (username) => { + Swal.fire({ + title: `Welcome to Pureplate, ${username}!`, + text: "Please, sign in!", + icon: "success", + }); +}; + +export const showErrorAlert = () => { + Swal.fire({ + title: "This email is already registered.", + text: "Please choose another email.", + icon: "error", + }); +}; diff --git a/src/Profile/SignUp/validation.js b/src/Profile/SignUp/validation.js new file mode 100644 index 0000000..23a0cd0 --- /dev/null +++ b/src/Profile/SignUp/validation.js @@ -0,0 +1,23 @@ +export const validateField = (key, value, password) => { + let errors = {}; + if (key === "name") { + errors.name = value ? "" : "Enter name."; + } else if (key === "email") { + errors.email = value ? "" : "Enter email."; + } else if (key === "password") { + errors.password = value.length >= 6 ? "" : "Password must be 6+ chars."; + } else if (key === "confirmPassword") { + errors.confirmPassword = password === value ? "" : "Passwords do not match."; + } + return errors; + }; + + export const validateAllFields = (name, email, password, confirmPassword) => { + let errors = {}; + errors.name = name ? "" : "Enter name."; + errors.email = email ? "" : "Enter email."; + errors.password = password.length >= 6 ? "" : "Password must be 6+ chars."; + errors.confirmPassword = password === confirmPassword ? "" : "Passwords do not match."; + return errors; + }; + \ No newline at end of file diff --git a/src/Profile/Signin/Sign.jsx b/src/Profile/Signin/Sign.jsx deleted file mode 100644 index 8dea7a5..0000000 --- a/src/Profile/Signin/Sign.jsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { useState } from "react"; -import SignUp from "./SignUp.jsx"; -import styles from "./Signin.module.css"; -import SignIn from "./SignIn.jsx"; -import xIcon from "../../assets/Icons/x0.svg"; - -function Sign({ isOpen, close }) { - // State to track whether the sign up form is displayed - const [isSignUp, setIsSignUp] = useState(false); - - // Switch to the sign up form - const switchToSignUp = () => setIsSignUp(true); - - // Switch to the sign in form - const switchToSignIn = () => setIsSignUp(false); - - // Handle the close action, reset to sign in form - const handleClose = () => { - setIsSignUp(false); - close(); - }; - - // Render the modal only if isOpen is true - return isOpen ? ( -
-
- {/* Main title showing whether it's sign up or sign in */} -
- {isSignUp ? "Sign Up" : "Sign In"} -
-
- {/* Conditionally render SignUp or SignIn component */} - {isSignUp ? ( - - ) : ( - - )} -
-
-
- {isSignUp - ? "Already have an account?" - : "Don’t you have any account?"} -
- {/* Switch between sign up and sign in on text click */} -
- {isSignUp ? "Sign In" : "Sign Up"} -
-
-
-
- {/* Close button */} - Close -
-
- ) : null; -} - -export default Sign; diff --git a/src/Profile/Signin/SignIn.jsx b/src/Profile/Signin/SignIn.jsx index 8d9afef..0d8c19e 100644 --- a/src/Profile/Signin/SignIn.jsx +++ b/src/Profile/Signin/SignIn.jsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; -import SignInInputField from "./SignInInputField.jsx"; -import SignInButton from "./SignInButton.jsx"; +import SignInInputField from "./SignInInputField/SignInInputField.jsx"; import { useAuth } from "../../AuthContext.jsx"; import styles from "./Signin.module.css"; @@ -26,9 +25,7 @@ function SignIn() { if (validate()) { try { await login(email, password); - // 성공적으로 로그인이 되면, 필요하다면 여기서 추가적인 작업을 수행합니다. } catch (error) { - // 로그인 실패 시, 오류 메시지를 처리합니다. setErrors({ ...errors, form: "Login failed. Please check your email or password.", @@ -40,25 +37,44 @@ function SignIn() { }; return ( -
-
- - - {errors.form &&
{errors.form}
} - - -
+ ); } + +const SignInForm = ({ + email, + password, + errors, + onIdChange, + onPasswordChange, + onSubmit, +}) => ( +
+ + + {errors.form &&
{errors.form}
} + + +); + export default SignIn; diff --git a/src/Profile/Signin/SignInButton.jsx b/src/Profile/Signin/SignInButton.jsx deleted file mode 100644 index ae1a237..0000000 --- a/src/Profile/Signin/SignInButton.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import styles from "./SignInButton.module.css"; - -function SignInButton({ onSubmit, label }) { - return ( - - ); -} - -export default SignInButton; diff --git a/src/Profile/Signin/SignInInputField.jsx b/src/Profile/Signin/SignInInputField/SignInInputField.jsx similarity index 75% rename from src/Profile/Signin/SignInInputField.jsx rename to src/Profile/Signin/SignInInputField/SignInInputField.jsx index 0af9828..a10e427 100644 --- a/src/Profile/Signin/SignInInputField.jsx +++ b/src/Profile/Signin/SignInInputField/SignInInputField.jsx @@ -1,4 +1,4 @@ -import styles from "./Signin.module.css"; +import styles from "./SignInInputField.module.css"; function SignInInputField({ label, @@ -8,8 +8,9 @@ function SignInInputField({ errorMessage, }) { return ( -
-
+
+
{label}
+
-
{label}
); } diff --git a/src/Profile/Signin/SignInInputField/SignInInputField.module.css b/src/Profile/Signin/SignInInputField/SignInInputField.module.css new file mode 100644 index 0000000..bb3ba63 --- /dev/null +++ b/src/Profile/Signin/SignInInputField/SignInInputField.module.css @@ -0,0 +1,75 @@ +.inputFrame { + flex-shrink: 0; + width: 20rem; + height: 4.5rem; + position: relative; + margin-bottom: 0.5rem; +} + +.signInTextBox:focus { + border: none; +} + +.nameLabel { + color: #282828; + text-align: left; + font-family: "Alef-Bold", sans-serif; + font-size: 1rem; + font-weight: 700; + position: relative; + margin-bottom: 0.3rem; +} + +.input { + width: 100%; + height: 2.5rem; + position: relative; + background: #e9e9e9; + border-radius: 0.625rem; +} + +.placeholder { + outline: rgba(54, 54, 54, 0.649); +} + +.placeholder { + color: #979797; + text-align: left; + font-family: "Alef-Regular", sans-serif; + font-size: 0.9375rem; + font-weight: 400; + position: absolute; + padding-left: 0.875rem; + height: 100%; + width: 100%; +} + +/* error */ +.errorInput { + border: 1.5px solid rgba(255, 0, 0, 0.745); + border-radius: 0.625rem; +} + + +@media (max-width: 1024px) { + .inputFrame { + width: 18rem; /* 288px */ + height: 4rem; /* 64px */ + } +} + +@media (max-width: 768px) { + .inputFrame { + width: 16rem; /* 256px */ + height: 3.5rem; /* 56px */ + margin-bottom: 15px; + } +} + +@media (max-width: 480px) { + .inputFrame { + width: 14rem; /* 224px */ + height: 3rem; /* 48px */ + margin-bottom: 23px; + } +} diff --git a/src/Profile/Signin/SignInMain.jsx b/src/Profile/Signin/SignInMain.jsx new file mode 100644 index 0000000..2a48157 --- /dev/null +++ b/src/Profile/Signin/SignInMain.jsx @@ -0,0 +1,63 @@ +import React, { useState } from "react"; +import SignUp from "../SignUp/SignUp.jsx"; +import SignIn from "./SignIn.jsx"; +import xIcon from "../../assets/Icons/x0.svg"; +import styles from "./SignInMain.module.css"; + +const Title = ({ isSignUp }) => ( +
+ {isSignUp ? "Sign Up" : "Sign In"} +
+); + +const SwitchText = ({ isSignUp, switchToSignUp, switchToSignIn }) => ( +
+
+ {isSignUp ? "Already have an account?" : "Don't you have any account?"} +
+
+ {isSignUp ? "Sign In" : "Sign Up"} +
+
+); + +const CloseButton = ({ handleClose }) => ( + Close +); + +function Sign({ isOpen, close }) { + const [isSignUp, setIsSignUp] = useState(false); + + const switchToSignUp = () => setIsSignUp(true); + const switchToSignIn = () => setIsSignUp(false); + const handleClose = () => { + setIsSignUp(false); + close(); + }; + + return isOpen ? ( +
+
+ + <div className={styles.MainFrame}> + {isSignUp ? ( + <SignUp switchToSignIn={switchToSignIn} close={handleClose} /> + ) : ( + <SignIn switchToSignUp={switchToSignUp} close={handleClose} /> + )} + <SwitchText + isSignUp={isSignUp} + switchToSignUp={switchToSignUp} + switchToSignIn={switchToSignIn} + /> + </div> + <CloseButton handleClose={handleClose} /> + </div> + </div> + ) : null; +} + +export default Sign; diff --git a/src/Profile/Signin/SignInMain.module.css b/src/Profile/Signin/SignInMain.module.css new file mode 100644 index 0000000..5e52cf9 --- /dev/null +++ b/src/Profile/Signin/SignInMain.module.css @@ -0,0 +1,99 @@ +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgba(0, 0, 0, 0.6); +} + +.signInTitle { + color: rgba(0, 0, 0, 0.85); + font-family: "Alef-Regular", sans-serif; + font-size: 2rem; + font-weight: 400; + margin-bottom: 1.5rem; + position: relative; + text-align: center; +} + +.signInFrame { + background: white; + border-radius: 0.625rem; + display: flex; + flex-direction: column; + box-shadow: 0 0 0.375rem rgba(0, 0, 0, 0.25); + width: 480px; + min-height: 621px; + background-color: white; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + justify-content: center; + align-items: center; + box-sizing: border-box; +} + +.MainFrame { + display: flex; + flex-direction: column; + justify-content: flex-start; + position: relative; +} + +.close { + flex-shrink: 0; + width: 2rem; + height: 2rem; + position: absolute; + right: 1.5rem; + top: 1.3125rem; + overflow: visible; + cursor: pointer; +} + +/* SwitchText */ +.SwitchFrame { + display: flex; + flex-direction: row; + position: relative; + justify-content: center; +} + +.AskSwitch { + color: rgba(0, 0, 0, 0.8); + font-family: "Alef-Regular", sans-serif; + font-size: 1rem; + font-weight: 400; + padding-right: 0.5rem; +} + +.signUpText { + color: rgba(13, 69, 179, 0.96); + font-family: "Alef-Regular", sans-serif; + font-size: 1rem; + font-weight: 400; + cursor: pointer; +} + + +@media (max-width: 1024px) { + .signInFrame { + width: 25rem; /* 400px */ + min-height: 31.25rem; /* 500px */ + } +} + +@media (max-width: 768px) { + .signInFrame { + width: 25rem; /* 320px */ + min-height: 25rem; /* 400px */ + } +} + +@media (max-width: 480px) { + .signInFrame { + width: 21em; + } +} \ No newline at end of file diff --git a/src/Profile/Signin/SignUp.jsx b/src/Profile/Signin/SignUp.jsx deleted file mode 100644 index 9ddd9a0..0000000 --- a/src/Profile/Signin/SignUp.jsx +++ /dev/null @@ -1,155 +0,0 @@ -import React, { useState } from "react"; -import SignInInputField from "./SignInInputField.jsx"; -import SignInButton from "./SignInButton.jsx"; -import { useAuth } from "../../AuthContext.jsx"; -import styles from "./Signin.module.css"; -import axios from "axios"; -import { useNavigate } from "react-router-dom"; -import Swal from "sweetalert2"; -import "../../custom-swal.css"; - -function SignUp({ switchToSignIn, close }) { - const [name, setName] = useState(""); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const [errors, setErrors] = useState({}); - const navigate = useNavigate(); - const { URL } = useAuth(); - - const validateField = (key, value) => { - let tempErrors = { ...errors }; - if (key === "name") { - tempErrors.name = value ? "" : "Enter name."; - } else if (key === "email") { - tempErrors.email = value ? "" : "Enter email."; - } else if (key === "password") { - tempErrors.password = - value.length >= 6 ? "" : "Password must be 6+ chars."; - } else if (key === "confirmPassword") { - tempErrors.confirmPassword = - password === value ? "" : "Passwords do not match."; - } - - setErrors(tempErrors); - }; - - const handleNameChange = (event) => { - setName(event.target.value); - validateField("name", event.target.value); - }; - const handleEmailChange = (event) => { - setEmail(event.target.value); - validateField("email", event.target.value); - }; - const handlePasswordChange = (event) => { - setPassword(event.target.value); - validateField("password", event.target.value); - }; - const handleConfirmPasswordChange = (event) => { - setConfirmPassword(event.target.value); - validateField("confirmPassword", event.target.value); - }; - - const validate = () => { - let tempErrors = {}; - tempErrors.name = name ? "" : "Enter name."; - tempErrors.email = email ? "" : "Enter email."; - tempErrors.password = - password.length >= 6 ? "" : "Password must be 6+ chars."; - tempErrors.confirmPassword = - password === confirmPassword ? "" : "Passwords do not match."; - - setErrors(tempErrors); - - return Object.values(tempErrors).every((x) => x === ""); - }; - - const showSuccessAlert = (username) => { - Swal.fire({ - title: `Welcome to Pureplate, ${username}!`, // Alert title - text: "Please, sign in!", // Alert contents - icon: "success", // Alert type (success, error, warning, info, question) - }); - }; - - const showErrorAlert = () => { - Swal.fire({ - title: `This email is already registered.`, // Alert title - text: " Please choose another email.", // Alert contents - icon: "error", // Alert type (success, error, warning, info, question) - }); - }; - - const onSubmit = async (event) => { - event.preventDefault(); - if (validate()) { - const data = { - nickname: name, - username: email, - password: password, - // confirmPassword는 서버에서 처리하지 않을 수도 있습니다. - }; - - const SIGNUP_URL = `${URL}/api/account/register/`; - try { - const response = await axios.post(SIGNUP_URL, data); - console.log(response.data); - // 회원가입 성공 후의 로직을 여기에 작성하세요. - // 예: 로그인 페이지로 리다이렉트하기, 성공 메시지 표시하기 등 - close(); - navigate("/"); - showSuccessAlert(response.data.nickname, "success"); - } catch (error) { - const firstKey = Object.keys(error.response.data)[0]; - console.log(firstKey); - if (firstKey === "username") { - showErrorAlert(); - } - console.error( - "Error during sign up:", - error.response ? error.response.data : error - ); - // 오류 처리 로직을 여기에 작성하세요. - // 예: 사용자에게 오류 메시지 표시하기 등 - } - } else { - console.error("Validation failed."); - } - }; - - return ( - <form className={styles.frame33} onSubmit={onSubmit}> - <SignInInputField - label="Name" - value={name} - onChange={handleNameChange} - errorMessage={errors.name} - /> - <SignInInputField - label="Email" - type="email" - value={email} - onChange={handleEmailChange} - errorMessage={errors.email} - /> - <SignInInputField - label="Password" - type="password" - value={password} - onChange={handlePasswordChange} - errorMessage={errors.password} - /> - <SignInInputField - label="Confirm Password" - type="password" - value={confirmPassword} - onChange={handleConfirmPasswordChange} - errorMessage={errors.confirmPassword} - /> - <SignInButton onSubmit={onSubmit} label="Submit" /> - </form> - ); -} - -export default SignUp; diff --git a/src/Profile/Signin/Signin.module.css b/src/Profile/Signin/Signin.module.css index f424def..7b9d5ff 100644 --- a/src/Profile/Signin/Signin.module.css +++ b/src/Profile/Signin/Signin.module.css @@ -1,173 +1,29 @@ -/* General reset for elements */ -a, -button, -input, -select, -h1, -h2, -h3, -h4, -h5, -* { - box-sizing: border-box; - margin: 0; - padding: 0; - border: none; - text-decoration: none; - appearance: none; - background: none; - -webkit-font-smoothing: antialiased; -} - -.signIn, -.signIn * { - box-sizing: border-box; -} - -/* .frame37 { - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-evenly; - flex-shrink: 0; - position: relative; -} */ -.signIn { - background: var(--white, #ffffff); - border-radius: 0.625rem; - /* padding: 1.0625rem 0.3125rem; */ +.SignInInputField { display: flex; flex-direction: column; - gap: 2.3125rem; align-items: center; justify-content: center; - /* height: 25.625rem; */ - box-shadow: 0 0 0.375rem rgba(0, 0, 0, 0.25); - /* */ - width: 480px; - /* height: 621px; */ - - min-height: 621px; - background-color: white; - position: relative; - box-sizing: border-box; - margin: 50px auto; - padding: 20px; -} - -.signInMain { - color: rgba(0, 0, 0, 0.67); - text-align: center; - font-family: "Alef-Regular", sans-serif; - font-size: 2rem; - line-height: 135.02%; - font-weight: 400; - position: relative; - width: 16.8125rem; - height: 2.625rem; - display: flex; - align-items: flex-end; - justify-content: center; -} - -.frame36 { - margin: -0.875rem 0 0 0; - display: flex; - flex-direction: column; - gap: 0.575rem; - align-items: center; - justify-content: flex-start; flex-shrink: 0; position: relative; } -.x { - flex-shrink: 0; - width: 1.5rem; - height: 1.5rem; - position: absolute; - right: 1.5rem; - top: 1.3125rem; - overflow: visible; -} - -/* .SignInArea { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - flex-shrink: 0; - position: relative; -} */ - -.frame33 { +.signInButton { + background: rgba(169, 234, 195, 0.62); + border-radius: 0.625rem; display: flex; - flex-direction: column; - gap: 1.0625rem; align-items: center; justify-content: center; - flex-shrink: 0; - /* height: 13.25rem; */ - position: relative; -} - -.signInTextBox { - flex-shrink: 0; - width: 20rem; - height: 4.375rem; - position: relative; -} - -.frame31 { - width: 20rem; + width: 8.75rem; height: 2.5rem; - position: absolute; - left: 0; - top: 1.875rem; - background: #e9e9e9; - border-radius: 0.625rem; - display: flex; -} + margin-top: 1rem; -.placeholder { - color: #979797; + color: rgba(0, 0, 0, 0.46); text-align: left; font-family: "Alef-Regular", sans-serif; - font-size: 0.9375rem; - font-weight: 400; - position: absolute; - padding-left: 0.875rem; - /* left: 0.875rem; */ - /* top: calc(50% - 0.625rem); */ - height: 100%; - width: 100%; -} - -.nameLabel { - color: #000000; - text-align: left; - font-family: "Alef-Bold", sans-serif; font-size: 1rem; - font-weight: 700; - position: absolute; - left: 0; - top: 0; -} - -.signInButton { - margin: -0.1875rem 0 0 0; - background: var(--button-color, rgba(169, 234, 195, 0.62)); - border-radius: 0.625rem; - display: flex; - flex-direction: row; - gap: 0.625rem; - align-items: center; - justify-content: center; - flex-shrink: 0; - width: 8.75rem; - height: 2.375rem; - position: relative; + font-weight: 400; + cursor: pointer; } .signIn2 { @@ -183,86 +39,6 @@ h5, justify-content: flex-start; } -.frame34 { - display: flex; - flex-direction: column; - gap: 0.625rem; - align-items: center; - justify-content: center; - flex-shrink: 0; - position: relative; -} - -.socialLoginText { - color: rgba(0, 0, 0, 0.67); - text-align: center; - font-family: "Alef-Regular", sans-serif; - font-size: 1rem; - line-height: 135.02%; - font-weight: 400; - position: relative; - display: flex; - align-items: center; - justify-content: center; -} - -.frame32 { - display: flex; - flex-direction: row; - gap: 0.625rem; - align-items: center; - justify-content: center; - flex-shrink: 0; - width: 19.3125rem; - position: relative; -} - -.signUpText { - color: rgba(0, 0, 0, 0.67); - text-align: center; - font-family: "Alef-Regular", sans-serif; - font-size: 1rem; - line-height: 135.02%; - font-weight: 400; - position: relative; - align-self: stretch; - flex: 1; - display: flex; - align-items: center; - justify-content: center; -} - -.signUp { - color: rgba(13, 69, 179, 0.96); - text-align: center; - font-family: "Alef-Regular", sans-serif; - font-size: 1rem; - line-height: 135.02%; - font-weight: 400; - position: relative; - width: 4.125rem; - height: 2rem; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; -} - -/* */ -.modal { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: rgba(0, 0, 0, 0.6); - - /* */ - /* display: flex; - flex-direction: row; - justify-content: center; - align-items: center; */ -} .loginModal { width: 480px; @@ -348,61 +124,6 @@ input::placeholder { border-style: none; } -.socialBox { - margin-bottom: 30px; -} - -.kakao { - background-color: #feec34; - border-color: #feec34; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - box-sizing: border-box; - margin-bottom: 10px; - border-radius: 3px; -} - -.kakaoLogo { - width: 24px; - height: 25px; -} - -.kakaoText { - width: 300px; - font-size: 15px; - text-align: center; - display: inline-block; - box-sizing: border-box; -} - -.facebook { - background-color: #21538a; - border-color: #21538a; - height: 40px; - display: flex; - justify-content: center; - box-sizing: border-box; - color: #fff; - border-radius: 3px; -} - -.facebookText { - padding-top: 12px; - width: 310px; - color: #fff; - font-size: 15px; - text-align: center; - box-sizing: border-box; -} - -.facebookLogo { - padding-top: 7px; - width: 24px; - height: 25px; -} - .loginEnd { text-align: center; font-size: 11px; @@ -423,7 +144,27 @@ input::placeholder { .noUser { text-decoration: underline; } -/* error */ -.errorInput { - border: 2px solid red; /* 오류 경고를 위한 테두리 색상 변경 */ + +.signInButton { + margin: -0.1 875rem 0 0 0; + background: var(--button-color, rgba(169, 234, 195, 0.62)); + border-radius: 0.625rem; + display: flex; + flex-direction: row; + gap: 0.625rem; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 8.75rem; + height: 2.375rem; + position: relative; + cursor: pointer; + margin-top: 0.8rem; + margin-bottom: 10px; + + color: rgba(0, 0, 0, 0.46); + text-align: left; + font-family: "Alef-Regular", sans-serif; + font-size: 1rem; + font-weight: 400; } diff --git a/src/Pureplate/Header.jsx b/src/Pureplate/Header/Header.jsx similarity index 60% rename from src/Pureplate/Header.jsx rename to src/Pureplate/Header/Header.jsx index e309f70..e0daaac 100644 --- a/src/Pureplate/Header.jsx +++ b/src/Pureplate/Header/Header.jsx @@ -1,11 +1,11 @@ import React from "react"; import { Link } from "react-router-dom"; -import styles from "./Pureplate.module.css"; -import SearchBar from "../Search/SearchBar.js"; -import Attributes from "../Attributes/Attributes.js"; -import Profile from "../Profile/Profile.jsx"; -import BookmarkIcon from "../Bookmark/BookmarkIcon.jsx"; -import logo_icon from "../assets/Icons/logo_icon.png"; +import styles from "./Header.module.css"; +import SearchBar from "../../Search/SearchBar.js"; +import Attributes from "../../Attributes/Attributes.js"; +import Profile from "../../Profile/Profile.jsx"; +import BookmarkIcon from "../../Bookmark/BookmarkIcon.jsx"; +import logo_icon from "../../assets/Icons/logo_icon.png"; function Header({ isRestModalOpen, bookmarkToggle }) { return ( diff --git a/src/Pureplate/Header/Header.module.css b/src/Pureplate/Header/Header.module.css new file mode 100644 index 0000000..8d9dcf8 --- /dev/null +++ b/src/Pureplate/Header/Header.module.css @@ -0,0 +1,42 @@ + +.header { + border-style: solid; + border-color: transparent; + border-width: 0.0625rem; + padding: 0.625rem 0.625rem 0.625rem 0.625rem; + display: flex; + flex-direction: row; + gap: 0rem; + align-items: center; + justify-content: flex-start; + width: 100%; + height: 5rem; + padding-left: 1.2rem; + position: absolute; + left: 0rem; + top: 0rem; + box-shadow: 0rem 0.0625rem 1.875rem 0rem rgba(0, 0, 0, 0.06); + } + + @media (max-width: 1000px) { + .header { + padding: 0.625rem 0.625rem 0.625rem 0.625rem; + } + } + + .purePlateIcon { + width: 50px; + height: 50px; + position: relative; + align-items: center; + object-fit: contain; + margin-right: 1rem; + } + + @media (max-width: 600px) { + .purePlateIcon { + min-width: 40px; + min-height: 40px; + } + } + \ No newline at end of file diff --git a/src/Pureplate/Pureplate.jsx b/src/Pureplate/Pureplate.jsx index 38bf2aa..0d5b821 100644 --- a/src/Pureplate/Pureplate.jsx +++ b/src/Pureplate/Pureplate.jsx @@ -3,7 +3,7 @@ import { useParams, useNavigate, Outlet } from "react-router-dom"; import styles from "./Pureplate.module.css"; import MapNaverCur from "../Map/Map.js"; import RestaurantModal from "./RestaurantModal.jsx"; -import Header from "./Header.jsx"; +import Header from "./Header/Header.jsx"; import { useAuth } from "../AuthContext.jsx"; function Pureplate() { @@ -15,7 +15,7 @@ function Pureplate() { const bookmarkToggle = () => { SetBookmarksToggle(!bookmarksToggle); }; - + useEffect(() => { if (id) { setIsRestModalOpen(true); diff --git a/src/Pureplate/Pureplate.module.css b/src/Pureplate/Pureplate.module.css index acce198..8f32c0c 100644 --- a/src/Pureplate/Pureplate.module.css +++ b/src/Pureplate/Pureplate.module.css @@ -1,8 +1,21 @@ -.homeLogedIn { - background: #ffffff; - height: 100%; - position: relative; - overflow: hidden; +a, +button, +input, +select, +h1, +h2, +h3, +h4, +h5, +* { + box-sizing: border-box; + margin: 0; + padding: 0; + border: none; + text-decoration: none; + appearance: none; + background: none; + -webkit-font-smoothing: antialiased; } .mapContainer { @@ -213,42 +226,6 @@ bottom: 47.95%; top: 50.68%; } - -.header { - border-style: solid; - border-color: transparent; - border-width: 0.0625rem; - padding: 0.625rem 0.625rem 0.625rem 0.625rem; - display: flex; - flex-direction: row; - gap: 0rem; - align-items: center; - justify-content: flex-start; - width: 100%; - height: 5rem; - position: absolute; - left: 0rem; - top: 0rem; - box-shadow: 0rem 0.0625rem 1.875rem 0rem rgba(0, 0, 0, 0.06); -} - -@media (max-width: 1000px) { - .header { - padding: 0.625rem 0.625rem 0.625rem 0.625rem; - } -} - -.purePlateIcon { - flex-shrink: 1; - width: 70%; - height: 70%; - min-width: 50%; - min-height: 50%; - position: relative; - align-items: center; - object-fit: contain; -} - .attributes { padding: 0.625rem; display: flex; diff --git a/src/Search/Dropdown.jsx b/src/Search/Dropdown.jsx new file mode 100644 index 0000000..2de64f8 --- /dev/null +++ b/src/Search/Dropdown.jsx @@ -0,0 +1,30 @@ +import React from "react"; +import styles from "./Dropdown.module.css"; +import historySearchIcon from "../assets/Icons/historySearchIcon.svg"; + +function Dropdown({ dropDownList, dropDownItemIndex, clickDropDownItem, setDropDownItemIndex, dropdownWidth }) { + return ( + <ul className={styles.dropDownBox} style={{ width: dropdownWidth }}> + {dropDownList.length === 0 && ( + <li className={styles.dropDownItem}>No matching words found</li> + )} + {dropDownList.map((dropDownItem, dropDownIndex) => ( + <li + key={dropDownIndex} + onClick={() => clickDropDownItem(dropDownItem)} + onMouseOver={() => setDropDownItemIndex(dropDownIndex)} + className={`${styles.dropDownItem} ${ + dropDownItemIndex === dropDownIndex ? styles.selected : "" + }`} + > + <img className={styles.union} src={historySearchIcon} alt="search" /> + <div className={styles.searchHistory}> + <span className={styles.recentSearch12}>{dropDownItem}</span> + </div> + </li> + ))} + </ul> + ); +} + +export default Dropdown; diff --git a/src/Search/Dropdown.module.css b/src/Search/Dropdown.module.css new file mode 100644 index 0000000..d967ba9 --- /dev/null +++ b/src/Search/Dropdown.module.css @@ -0,0 +1,61 @@ +@font-face { + font-family: 'NanumGothic'; + src: url('../assets/fonts/NanumGothic.otf') format('truetype') +} + +.dropDownBox { + background: rgba(255, 255, 255, 0.96); + border-radius: 0 0 0.5rem 0.5rem; + display: flex; + flex-direction: column; + gap: 0.625rem; + box-shadow: 0 0.25rem 0.25rem rgba(0, 0, 0, 0.25); + overflow: hidden; + list-style: none; + width: 17.2rem; + position: absolute; + padding: 0.5rem; + padding-bottom: 1rem; + } + + .dropDownItem { + padding: 0.4rem; + width: 100%; + z-index: 4; + letter-spacing: 2px; + display: flex; + align-items: center; + padding-left: 10px; + color: #616060; + font-family: 'NanumGothic' sans-serif; + } + + .dropDownItem.selected { + background-color: #edf5f5; + } + + .union { + display: flex; + gap: 0.625rem; + align-items: center; + width: 1.4rem; + fill: #AAAAAA; + } + + .recentSearch12 { + color: rgba(98, 98, 98, 0.89); + text-align: left; + font-family: "NanumGothic", sans-serif; + font-size: 1rem; + display: flex; + align-items: center; + } + + .searchHistory { + padding: 0 0.7rem; + display: flex; + gap: 0.3525rem; + align-items: center; + flex: 1; + } + \ No newline at end of file diff --git a/src/Search/SearchBar.js b/src/Search/SearchBar.js index 9b32944..3ea28d4 100644 --- a/src/Search/SearchBar.js +++ b/src/Search/SearchBar.js @@ -1,167 +1,61 @@ -import { useEffect, useState } from "react"; +import React, { useRef, useEffect, useState } from "react"; import styles from "./SearchBar.module.css"; -import axios from "axios"; -import { useAuth } from "../AuthContext.jsx"; import findIcon from "../assets/Icons/searchIcon.svg"; -import historySearchIcon from "../assets/Icons/historySearchIcon.svg"; -import { useNavigate } from "react-router-dom"; +import Dropdown from "./Dropdown"; +import useSearchRestaurants from "./useSearchRestaurants"; function SearchBar() { - // State to store restaurant data - const [restaurants, setRestaurants] = useState([]); - const [restaurantsSet, setRestaurantsSet] = useState(); - const [query, setQuery] = useState(""); - const [isHaveQuery, setIsHaveQuery] = useState(false); - const [dropDownList, setDropDownList] = useState(restaurants); - const [dropDownItemIndex, setDropDownItemIndex] = useState(-1); - const { URL, setRestaurantNameList, restaurantNameList } = useAuth(); - const navigate = useNavigate(); - - // Fetch restaurant data from the API - const fetchRestaurant = async () => { - try { - const response = await axios.get(`${URL}/api/restaurant/list`); - - const restaurantNames = response.data.data.map((element) => element.Name); - const restaurantSet = response.data.data.reduce((acc, element) => { - acc[element.Name] = element.Id; - return acc; - }, {}); - setRestaurantsSet(restaurantSet); - setRestaurants(restaurantNames); - setRestaurantNameList(restaurantNames); - } catch (error) { - if (error.response) { - // The server responded with a status code outside the 2xx range - console.error( - "Server response error:", - error.response.status, - error.response.data - ); - } else if (error.request) { - // The request was made but no response was received - console.error( - "No response from server. This could be a network issue.", - error.request - ); - } else { - // An error occurred in setting up the request - console.error("Error in setting up request:", error.message); - } - } - }; - useEffect(() => { - console.log(restaurantNameList); - }, [restaurantNameList]); + const { + query, + onChange, + onSubmit, + isHaveQuery, + dropDownList, + dropDownItemIndex, + handleDropDownKey, + clickDropDownItem, + } = useSearchRestaurants(); + + const searchFormRef = useRef(null); + const [dropdownWidth, setDropdownWidth] = useState("100%"); + //Dynamically allocate width to Dropbox useEffect(() => { - fetchRestaurant(); - }, []); - - // Handle form submission - const onSubmit = (event) => { - event.preventDefault(); - const link = restaurantsSet[query.trim()]; - if (link) { - navigate(`/${link}`); - setQuery(""); - } - }; - - // Handle input change - const onChange = (event) => { - setQuery(event.target.value); - setIsHaveQuery(true); - }; - - // Update dropdown list based on query - const showDropDownList = () => { - if (query === "") { - setIsHaveQuery(false); - setDropDownList([]); - } else { - const lowerCaseQuery = query.toLowerCase(); - const choosenTextList = restaurants.filter((textItem) => - textItem.toLowerCase().includes(lowerCaseQuery) - ); - setDropDownList(choosenTextList); + if (searchFormRef.current) { + setDropdownWidth(`${searchFormRef.current.offsetWidth}px`); } - }; - - // Handle click on dropdown item - const clickDropDownItem = (clickedItem) => { - setQuery(clickedItem); - setIsHaveQuery(false); - }; - - // Handle keyboard navigation in dropdown - const handleDropDownKey = (event) => { - if ( - event.key === "ArrowDown" && - dropDownList.length - 1 > dropDownItemIndex - ) { - setDropDownItemIndex(dropDownItemIndex + 1); - } - - if (event.key === "ArrowUp" && dropDownItemIndex >= 0) { - setDropDownItemIndex(dropDownItemIndex - 1); - } - - if (event.key === "Enter") { - event.preventDefault(); - if (dropDownItemIndex >= 0) { - clickDropDownItem(dropDownList[dropDownItemIndex]); - } else if (restaurantsSet[query.trim()]) { - onSubmit(event); // Trigger form submission if the query is valid - } else { - } - setDropDownItemIndex(-1); - } - }; - - useEffect(showDropDownList, [query]); + }, [query]); return ( <div className={styles.searchBar}> - <form className={styles.searchForm} onSubmit={onSubmit}> - <input - className={styles.searchInput} - type="text" - name="search" - id="searchInput" - placeholder="Search" - onChange={onChange} - onKeyUp={handleDropDownKey} - value={query} - autoComplete="off" - /> + <div className={styles.searchLayer}> + <form + className={styles.searchForm} + onSubmit={onSubmit} + ref={searchFormRef} + > + <input + className={`${styles.searchInput} ${query ? styles.hasValue : ""}`} + type="text" + name="search" + id="searchInput" + placeholder="Search" + onChange={onChange} + onKeyUp={handleDropDownKey} + value={query} + autoComplete="off" + /> + </form> {isHaveQuery && ( - <ul className={styles.dropDownBox}> - {dropDownList.length === 0 && ( - <li className={styles.dropDownItem}>No matching words found</li> - )} - {dropDownList.map((dropDownItem, dropDownIndex) => ( - <li - key={dropDownIndex} - onClick={() => clickDropDownItem(dropDownItem)} - onMouseOver={() => setDropDownItemIndex(dropDownIndex)} - className={`${styles.dropDownItem} ${ - dropDownItemIndex === dropDownIndex ? styles.selected : "" - }`} - > - <img - className={styles.union} - src={historySearchIcon} - alt="search" - /> - <div className={styles.searchHistory}> - <span className={styles.recentSearch12}>{dropDownItem}</span> - </div> - </li> - ))} - </ul> + <Dropdown + dropDownList={dropDownList} + dropDownItemIndex={dropDownItemIndex} + clickDropDownItem={clickDropDownItem} + setDropDownItemIndex={() => {}} + dropdownWidth={dropdownWidth} + /> )} - </form> + </div> <button className={styles.searchButton} onClick={onSubmit}> <img className={styles.searchIcon} src={findIcon} alt="Search" /> </button> diff --git a/src/Search/SearchBar.module.css b/src/Search/SearchBar.module.css index 7306700..9c56433 100644 --- a/src/Search/SearchBar.module.css +++ b/src/Search/SearchBar.module.css @@ -1,145 +1,57 @@ -/* Entire Search Area*/ +@font-face { + font-family: 'NanumGothic'; + src: url('../assets/fonts/NanumGothic.otf') format('truetype'); +} + .searchBar { display: flex; - flex-direction: row; - gap: 0rem; align-items: center; - justify-content: flex-start; - flex-grow: 0; - flex-shrink: 1; - flex-basis: 24rem; + width: 22rem; min-width: 15rem; height: 3.2rem; - position: relative; padding-right: 15px; } +.searchLayer { + height: 100%; + width: 100%; +} + .searchForm { - background: rgba(255, 255, 255, 0.96); - border-style: solid; - border-color: #d1d1d167; - border-width: 0.0525rem; - display: flex; + background: transparent; flex-direction: column; - gap: 0.625rem; - align-items: flex-start; - justify-content: flex-start; - flex: 1; height: 100%; - position: relative; - color: rgba(0, 0, 0, 0.5); - font-family: "AnekBangla-ExtraLight", sans-serif; - font-size: 1.3rem; - font-weight: 200; - border-radius: 0.5rem 0rem 0rem 0.5rem; } -/* Actual Input occured here */ .searchInput { - color: rgba(0, 0, 0, 0.5); - font-family: "AnekBangla-ExtraLight", sans-serif; - font-size: 1.4rem; - font-weight: 200; - line-height: 149.8%; - position: relative; - width: 100%; + background-color: white; + border-style: solid; + border-color: #d1d1d167; + border-width: 0.0525rem; + border-radius: 0.5rem 0rem 0rem 0.5rem; + color: rgba(0, 0, 0, 0.8); + font-family: "NanumGothic", sans-serif; + font-size: 1.2rem; height: 100%; + width: 100%; display: flex; - align-items: center; justify-content: center; padding-left: 20px; } .searchInput:focus { - border: 0px solid #cfcfcf7a; - outline: none; /* 기본 outline 제거 (선택 사항) */ + outline: none; + border-radius: 0.5rem 0rem 0rem 0rem; } .searchButton { - flex-shrink: 0; width: 3.875rem; height: 100%; - position: relative; - overflow: hidden; background: rgba(74, 124, 89, 0.96); - border-radius: 0rem 0.5rem 0.5rem 0rem; + border-radius: 0rem 0.6rem 0.6rem 0rem; cursor: pointer; } -.dropDownBox { - z-index: 3; - background: rgba(255, 255, 255, 0.96); - border-radius: 0rem 0rem 0.5rem 0.5rem; - display: flex; - flex-direction: column; - gap: 0.625rem; - align-items: center; - justify-content: flex-start; - box-shadow: 0rem 0.25rem 0.25rem 0rem rgba(0, 0, 0, 0.25); - overflow: hidden; - list-style-type: none; - width: 100%; - position: absolute; - top: 104%; /* searchForm의 높이에 맞춰 조정 */ -} - -.dropDownItem { - padding: 10px 8px; - width: 100%; - z-index: 4; - letter-spacing: 2px; - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - align-self: stretch; - flex-shrink: 0; - position: relative; - font-weight: 400; -} - -.dropDownItem.selected { - background-color: #edf5f5; -} - -.union { - display: flex; - flex-direction: row; - gap: 0.625rem; - align-items: center; - justify-content: center; - flex-shrink: 0; - width: 1.4rem; - height: auto; - position: relative; - overflow: visible; - fill: #AAAAAA; -} - -.recentSearch12 { - color: rgba(53, 53, 53, 0.75); - text-align: left; - font-family: "NotoSansKr-Regular", sans-serif; - font-size: 1.1275362968444824rem; - font-weight: 400; - position: relative; - display: flex; - align-items: center; - justify-content: flex-start; -} - -.searchHistory { - padding: 0rem 0.7rem; - display: flex; - flex-direction: row; - gap: 0.3525rem; - align-items: center; - justify-content: flex-start; - align-self: stretch; - flex: 1; - position: relative; -} - .searchIcon { width: 100%; height: 100%; @@ -148,11 +60,21 @@ padding: 13px; } -@media (max-width: 1050px) { - .searchBar { - width: auto; - height: 90%; - } +/* Input 창에 값이 있을 때 달라지는 스타일 */ +.searchInput.hasValue { + border: none; } +@media (max-width: 768px) { + .searchBar{ + width: 60%; + } +} + +@media (max-width: 480px) { + .searchBar{ + width: 60%; + min-width: 100px; + } +} \ No newline at end of file diff --git a/src/Search/useSearchRestaurants.js b/src/Search/useSearchRestaurants.js new file mode 100644 index 0000000..1de88b8 --- /dev/null +++ b/src/Search/useSearchRestaurants.js @@ -0,0 +1,120 @@ +// useSearchRestaurants.js + +import { useState, useEffect } from "react"; +import axios from "axios"; +import { useAuth } from "../AuthContext.jsx"; +import { useNavigate } from "react-router-dom"; + +const useSearchRestaurants = () => { + const [restaurants, setRestaurants] = useState([]); + const [restaurantsSet, setRestaurantsSet] = useState({}); + const [query, setQuery] = useState(""); + const [isHaveQuery, setIsHaveQuery] = useState(false); + const [dropDownList, setDropDownList] = useState([]); + const [dropDownItemIndex, setDropDownItemIndex] = useState(-1); + const { URL } = useAuth(); + const navigate = useNavigate(); + + const fetchRestaurant = async () => { + try { + const response = await axios.get(`${URL}/api/restaurant/list`); + + const restaurantNames = response.data.data.map((element) => element.Name); + const restaurantSet = response.data.data.reduce((acc, element) => { + acc[element.Name] = element.Id; + return acc; + }, {}); + setRestaurantsSet(restaurantSet); + setRestaurants(restaurantNames); + } catch (error) { + if (error.response) { + console.error( + "Server response error:", + error.response.status, + error.response.data + ); + } else if (error.request) { + console.error( + "No response from server. This could be a network issue.", + error.request + ); + } else { + console.error("Error in setting up request:", error.message); + } + } + }; + + useEffect(() => { + fetchRestaurant(); + }, []); + + const onSubmit = (event) => { + event.preventDefault(); + const link = restaurantsSet[query.trim()]; + if (link) { + navigate(`/${link}`); + setQuery(""); + } + }; + + const onChange = (event) => { + setQuery(event.target.value); + setIsHaveQuery(true); + }; + + const showDropDownList = () => { + if (query === "") { + setIsHaveQuery(false); + setDropDownList([]); + } else { + const lowerCaseQuery = query.toLowerCase(); + const choosenTextList = restaurants.filter((textItem) => + textItem.toLowerCase().includes(lowerCaseQuery) + ); + setDropDownList(choosenTextList); + } + }; + + const clickDropDownItem = (clickedItem) => { + setQuery(clickedItem); + setIsHaveQuery(false); + }; + + const handleDropDownKey = (event) => { + if ( + event.key === "ArrowDown" && + dropDownList.length - 1 > dropDownItemIndex + ) { + setDropDownItemIndex(dropDownItemIndex + 1); + } + + if (event.key === "ArrowUp" && dropDownItemIndex >= 0) { + setDropDownItemIndex(dropDownItemIndex - 1); + } + + if (event.key === "Enter") { + event.preventDefault(); + if (dropDownItemIndex >= 0) { + clickDropDownItem(dropDownList[dropDownItemIndex]); + } else if (restaurantsSet[query.trim()]) { + onSubmit(event); + } + setDropDownItemIndex(-1); + } + }; + + useEffect(showDropDownList, [query]); + + return { + query, + onChange, + onSubmit, + isHaveQuery, + dropDownList, + dropDownItemIndex, + handleDropDownKey, + clickDropDownItem, + }; +}; + +export default useSearchRestaurants; diff --git a/src/assets/Icons/flag_glutenfree.svg b/src/assets/Icons/flag_glutenfree.svg index 43ae6e4..bd19ee4 100644 --- a/src/assets/Icons/flag_glutenfree.svg +++ b/src/assets/Icons/flag_glutenfree.svg @@ -1,3 +1,11 @@ -<svg width="24" height="27" viewBox="0 0 24 27" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M11.9537 26.5638C18.5555 26.5638 23.9074 20.6173 23.9074 13.2819C23.9074 5.94651 18.5555 0 11.9537 0C5.35185 0 0 5.94651 0 13.2819C0 20.6173 5.35185 26.5638 11.9537 26.5638Z" fill="#E9FFF3"/> +<svg width="24" height="28" viewBox="0 0 24 28" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_1555_1223)"> +<path d="M11.9537 27.0638C18.5555 27.0638 23.9074 21.1173 23.9074 13.7819C23.9074 6.44651 18.5555 0.5 11.9537 0.5C5.35185 0.5 0 6.44651 0 13.7819C0 21.1173 5.35185 27.0638 11.9537 27.0638Z" fill="#E9FFF3"/> +<path d="M12.7644 20.9368C11.794 20.9368 10.8963 20.7791 10.0714 20.4637C9.25861 20.1362 8.54896 19.6813 7.94242 19.099C7.34801 18.5167 6.88098 17.8313 6.54132 17.0428C6.21379 16.2543 6.05002 15.3931 6.05002 14.459C6.05002 13.5249 6.21379 12.6636 6.54132 11.8751C6.88098 11.0866 7.35408 10.4012 7.96062 9.81897C8.56716 9.23669 9.27681 8.78785 10.0896 8.47245C10.9145 8.14492 11.8121 7.98116 12.7826 7.98116C13.7531 7.98116 14.6386 8.13279 15.4392 8.43606C16.252 8.73933 16.9435 9.2003 17.5136 9.81897L16.6766 10.6742C16.1428 10.1404 15.5545 9.75831 14.9116 9.52783C14.2686 9.29734 13.5772 9.1821 12.8372 9.1821C12.0487 9.1821 11.3209 9.31554 10.6537 9.58242C9.99859 9.83716 9.42238 10.2072 8.92501 10.6924C8.43978 11.1655 8.05766 11.7235 7.77866 12.3664C7.51178 12.9972 7.37834 13.6947 7.37834 14.459C7.37834 15.2111 7.51178 15.9086 7.77866 16.5515C8.05766 17.1945 8.43978 17.7586 8.92501 18.2438C9.42238 18.7169 9.99859 19.0869 10.6537 19.3537C11.3209 19.6085 12.0426 19.7359 12.819 19.7359C13.5468 19.7359 14.2322 19.6267 14.8752 19.4083C15.5302 19.19 16.1307 18.82 16.6766 18.2984L17.4408 19.3174C16.8343 19.8511 16.1246 20.2575 15.3119 20.5365C14.4991 20.8034 13.65 20.9368 12.7644 20.9368ZM16.1489 19.1536V14.459H17.4408V19.3174L16.1489 19.1536Z" fill="black"/> +</g> +<defs> +<clipPath id="clip0_1555_1223"> +<rect width="24" height="27" fill="white" transform="translate(0 0.5)"/> +</clipPath> +</defs> </svg> diff --git a/src/assets/fonts/NanumGothic.otf b/src/assets/fonts/NanumGothic.otf new file mode 100644 index 0000000..a8d8d7b Binary files /dev/null and b/src/assets/fonts/NanumGothic.otf differ