diff --git a/src/components/auth/Header.tsx b/src/components/auth/Header.tsx new file mode 100644 index 000000000..e386a9c7c --- /dev/null +++ b/src/components/auth/Header.tsx @@ -0,0 +1,30 @@ +import { Link } from 'react-router-dom'; + +import logoImg from '../../core/assets/images/logo/logo-large@3x.png'; +import styled from 'styled-components'; + +const Wrapper = styled.header` + display: flex; + justify-content: center; + align-items: center; + margin: 3.75rem 0.5rem; +`; + +const Logo = styled.img` + width: 40rem; + @media (375px <= width < 768px) { + width: 30rem; + } +`; + +const Header = () => { + return ( + + + + + + ); +} + +export default Header; diff --git a/src/components/auth/OAuth.tsx b/src/components/auth/OAuth.tsx new file mode 100644 index 000000000..562746222 --- /dev/null +++ b/src/components/auth/OAuth.tsx @@ -0,0 +1,53 @@ +import { Link } from 'react-router-dom'; + +import kakaoIcon from '../../core/assets/icons/sns/kakao/kakao.svg'; +import googleIcon from '../../core/assets/icons/sns/google/google.svg'; +import styled from 'styled-components'; + +const OAuthLoginContainer = styled.div` + display: flex; + background-color: #e6f2ff; + border-radius: 0.8rem; + padding: 0.8rem 3.2rem ; + justify-content: space-between; + align-items: center; +`; + +const OAuthIcons = styled.div` + display: flex; + align-items: center; + gap: 1.6rem; +`; + +const OAuthIcon = styled.img` + cursor: pointer; + width: 4.4rem; + height: 4.4rem; +`; + +const ToggleSign = styled.div` + text-align: center; +`; + +interface OAuthProps { + isLogin: boolean; +} + +const OAuth = ({isLogin}:OAuthProps) => { + return ( + <> + +

간편 로그인하기

+ + + + +
+ +

{isLogin ? '판다마켓이 처음이신가요?' : '이미 회원이신가요?'} {isLogin?'회원가입':'로그인'}

+
+ + ); +} + +export default OAuth; diff --git a/src/components/auth/signIn/SignInForm.tsx b/src/components/auth/signIn/SignInForm.tsx new file mode 100644 index 000000000..773c6d8a7 --- /dev/null +++ b/src/components/auth/signIn/SignInForm.tsx @@ -0,0 +1,41 @@ +import { useNavigate } from 'react-router-dom'; +import { useState } from 'react'; +import usePasswordVisibility from 'lib/hooks/usePasswordVisibility'; + +import BtnLarge from 'core/buttons/BtnLarge'; + +import { Container, ErrorMessage, Form, Icon, Input, InputWrapper, Label } from '../styles'; + +const SignInForm = () => { + const {ref,icon, handlePasswordVisibility} = usePasswordVisibility(); + const navigate = useNavigate(); + const [isValid, setIsValid] = useState(false); + const emailErrorMessage = ''; + const passwordErrorMessage = ''; + + const handleSubmit = (e:React.MouseEvent) => { + e.preventDefault(); + navigate('/items'); + } + + return ( +
+ + + + {emailErrorMessage} + + + + + + + + {passwordErrorMessage} + + 로그인 +
+ ); +} + +export default SignInForm; diff --git a/src/components/auth/signUp/SignUpForm.tsx b/src/components/auth/signUp/SignUpForm.tsx new file mode 100644 index 000000000..70a76a7c6 --- /dev/null +++ b/src/components/auth/signUp/SignUpForm.tsx @@ -0,0 +1,96 @@ +import { useState } from "react"; + +import usePasswordVisibility from "lib/hooks/usePasswordVisibility"; +import useValidate, { InputValue } from "lib/hooks/useValidate"; +import BtnLarge from "core/buttons/BtnLarge"; + +import { Container, ErrorMessage, Form, Input, InputWrapper, Label, Icon } from "../styles"; + +const INITIAL_VALUE = { + email: '', + nickname: '', + password: '', + passwordConfirm: '', +} + +const SignUpForm = () => { + const [value, setValue] = useState(INITIAL_VALUE); + const {ref:passwordRef,icon:passwordIcon, handlePasswordVisibility:handlePasswordView} = usePasswordVisibility(); + const {ref:passwordConfirmRef, icon:passwordConfirmIcon, handlePasswordVisibility:handlePasswordConfirmView} = usePasswordVisibility(); + + const {errorMessage, isValidate} = useValidate({mode: "signup", value}); + + const handleChange = (e:React.ChangeEvent) => { + const target = e.target as HTMLInputElement; + + switch (target.name) { + case 'email': + setValue((prev) => ({ + ...prev, + email: target.value, + })) + break; + case 'nickname': + setValue((prev) => ({ + ...prev, + nickname: target.value, + })) + break; + case 'password': + setValue((prev) => ({ + ...prev, + password: target.value, + })) + break; + case 'passwordConfirm': + setValue((prev) => ({ + ...prev, + passwordConfirm: target.value, + })) + break; + default: + break; + } + } + + const handleSubmit = (e:React.MouseEvent) => { + e.preventDefault(); + if (isValidate) { + + } + } + + return ( +
+ + + + {errorMessage.email} + + + + + {errorMessage.nickname} + + + + + + + + {errorMessage.password} + + + + + + + + {errorMessage.passwordConfirm} + + 회원가입 +
+ ); +} + +export default SignUpForm; diff --git a/src/components/auth/styles.tsx b/src/components/auth/styles.tsx new file mode 100644 index 000000000..3f14a63f7 --- /dev/null +++ b/src/components/auth/styles.tsx @@ -0,0 +1,64 @@ +import { styled } from "styled-components"; + +export const Form = styled.form` + display: flex; + flex-direction: column; + gap: 2.4rem; + margin-bottom: 2rem; + @media (375px <= width < 768px) { + max-width: 40rem; + width: 100%; + } +`; + +export const Label = styled.label` + display: block; + font-weight: 700; + font-size: 1.8rem; + color: var(--gray-800); + line-height: 2.148rem; +`; + +export const InputWrapper = styled.div` + position: relative; +`; + +export const Input = styled.input` + width:100%; + color: var(--gray-400); + background-color: var(--gray-100); + font-weight: 400; + line-height: 2.4rem; + height: 5.6rem; + border-radius: 1.2rem; + border: none; + padding-left: 2.4rem; + :focus { + border: var(--blue); + } +`; + +export const Icon = styled.img` + width: 2.4rem; + height: 2.4rem; + position: absolute; + right: 1.6rem; + bottom: 1.6rem; + margin: auto 0.2rem; + cursor: pointer; +`; + +export const ErrorMessage = styled.p` + visibility: hidden; + margin: 0 0 0 1.6rem; + color: var(--error-red); + font-size: 1.5rem; + font-weight: 600; + line-height: 1.79rem; +`; + +export const Container = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; +`; \ No newline at end of file diff --git a/src/components/nav/LoginBtn.tsx b/src/components/nav/LoginBtn.tsx index 69493e78f..fecb04f09 100644 --- a/src/components/nav/LoginBtn.tsx +++ b/src/components/nav/LoginBtn.tsx @@ -7,7 +7,7 @@ interface LoginBtnProps { const LoginBtn = ({ onClick }: LoginBtnProps) => { return ( - + 로그인 diff --git a/src/core/assets/icons/sns/google/google.svg b/src/core/assets/icons/sns/google/google.svg new file mode 100644 index 000000000..f46c4dda3 --- /dev/null +++ b/src/core/assets/icons/sns/google/google.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/core/assets/icons/sns/kakao/kakao.svg b/src/core/assets/icons/sns/kakao/kakao.svg new file mode 100644 index 000000000..55b14f00b --- /dev/null +++ b/src/core/assets/icons/sns/kakao/kakao.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/core/assets/icons/visibility/disvisibility.svg b/src/core/assets/icons/visibility/disvisibility.svg new file mode 100644 index 000000000..43cfd033b --- /dev/null +++ b/src/core/assets/icons/visibility/disvisibility.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/core/assets/icons/visibility/visibility.svg b/src/core/assets/icons/visibility/visibility.svg new file mode 100644 index 000000000..43a5af172 --- /dev/null +++ b/src/core/assets/icons/visibility/visibility.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/core/buttons/BtnCircle.tsx b/src/core/buttons/BtnCircle.tsx index 7924e2cc9..88fa638e9 100644 --- a/src/core/buttons/BtnCircle.tsx +++ b/src/core/buttons/BtnCircle.tsx @@ -1,10 +1,27 @@ -import "./buttons.css"; +import { styled } from "styled-components"; + +const CircleBtn = styled.button<{isActive:boolean}>` + width: 40px; + height: 40px; + border-radius: 50%; + border: none; + font-size: 16px; + font-weight: 600; + line-height: 19.09px; + background-color: {({isActive}) => isActive ? #3f80ed : white}; + border: 1px solid #e5e7eb; + color: {({isActive}) => isActive ? white : #6b7280}; + display: flex; + align-items: center; + justify-content: center; + +`; interface BtnCircleProps { onClick?: (e: React.MouseEvent) => void; disabled?: boolean; isActive?: boolean; - children?: string | React.ReactNode; + children?: React.ReactNode; } const BtnCircle = ({ @@ -13,14 +30,11 @@ const BtnCircle = ({ isActive = false, children = "", }: BtnCircleProps) => { - const className = `btn-circle ${isActive ? "active" : ""}`; - const handleClick = (e: React.MouseEvent) => { - onClick(e); - }; + return ( - + ); }; diff --git a/src/core/buttons/BtnLarge.tsx b/src/core/buttons/BtnLarge.tsx index 80befa3b6..d25aa5c3e 100644 --- a/src/core/buttons/BtnLarge.tsx +++ b/src/core/buttons/BtnLarge.tsx @@ -21,11 +21,12 @@ interface BtnLargeProps { bgColor: string | null; color: string | null; children: string; + disabled?: boolean; } -const BtnLarge = ({ bgColor, color, children }: BtnLargeProps) => { +const BtnLarge = ({ bgColor, color, disabled=false, children }: BtnLargeProps) => { return ( - + {children} ); diff --git a/src/core/buttons/buttons.css b/src/core/buttons/buttons.css deleted file mode 100644 index 52d295065..000000000 --- a/src/core/buttons/buttons.css +++ /dev/null @@ -1,20 +0,0 @@ -.btn-circle { - width: 40px; - height: 40px; - border-radius: 50%; - border: none; - font-size: 16px; - font-weight: 600; - line-height: 19.09px; - background-color: white; - border: 1px solid #e5e7eb; - color: #6b7280; - display: flex; - align-items: center; - justify-content: center; -} - -.active { - background-color: #2f80ed; - color: white; -} diff --git a/src/lib/hooks/usePasswordVisibility.ts b/src/lib/hooks/usePasswordVisibility.ts new file mode 100644 index 000000000..c908391be --- /dev/null +++ b/src/lib/hooks/usePasswordVisibility.ts @@ -0,0 +1,19 @@ +import { useRef, useState } from "react"; + +import visibilityIcon from '../../core/assets/icons/visibility/visibility.svg'; +import disvisibilityIcon from '../../core/assets/icons/visibility/disvisibility.svg'; + + +const usePasswordVisibility = () => { + const [icon, setIcon] = useState(visibilityIcon); + const ref = useRef(null); + const handlePasswordVisibility = () => { + if (ref.current){ + ref.current.type = ref.current.type === 'text'?'password' : 'text'; + setIcon((prev) => prev === visibilityIcon ? disvisibilityIcon : visibilityIcon); + } + } + return {ref, icon, handlePasswordVisibility}; +} + +export default usePasswordVisibility; \ No newline at end of file diff --git a/src/pages/App.tsx b/src/pages/App.tsx index f877ec343..e7f15a32e 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -5,24 +5,32 @@ import Products from "./products/Products"; import AddItem from "./addItem/AddItem"; import Board from "./board/Board"; import Page404 from "./error/Page404"; -import { GlobalStyles } from "core/styles/GlobalStyles"; import ProductDetail from "./products/ProductDetail"; +import { GlobalStyles } from "core/styles/GlobalStyles"; +import Auth from "./auth/Auth"; + const App = () => { return (
-
); }; diff --git a/src/pages/auth/Auth.tsx b/src/pages/auth/Auth.tsx new file mode 100644 index 000000000..1fa356433 --- /dev/null +++ b/src/pages/auth/Auth.tsx @@ -0,0 +1,51 @@ +import Header from 'components/auth/Header'; +import OAuth from 'components/auth/OAuth'; +import SignInForm from 'components/auth/signIn/SignInForm'; +import SignUpForm from 'components/auth/signUp/SignUpForm'; + +import styled from 'styled-components'; + +const Main = styled.main` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const Article = styled.article` + width: 64rem; + display: flex; + flex-direction: column; + gap: 2.4rem; + @media (375px <= width < 768px) { + max-width: 40rem; + width: 100%; + } +`; + +interface AuthProps { + isLogin: boolean; +} + +const Auth = ({isLogin}:AuthProps) => { + return ( + <> +
+
+
+ + { + isLogin? ( + + ):( + + ) + } + +
+
+ + ); +} + +export default Auth; diff --git a/src/pages/nav/Nav.tsx b/src/pages/nav/Nav.tsx index 8933cf7dc..352bbb38a 100644 --- a/src/pages/nav/Nav.tsx +++ b/src/pages/nav/Nav.tsx @@ -1,4 +1,4 @@ -import { Link } from "react-router-dom"; +import { Link, Outlet } from "react-router-dom"; import LogoImage from "../../core/assets/images/logo/logo-large@3x.png"; import TabList from "../../components/nav/TabList"; @@ -34,6 +34,7 @@ const Nav = () => { setIsSelected(INITIAL_SELECTED); }; return ( + <> + + ); };