-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: add login page - Installed react-hook-form for login form handling - Created useAuth custom hook to manage authentication logic * feat: add signup page * feat: add home page * feat: replace login button with pada logo if access token is present * feat: refresh access token on 401 response status * feat: implement page redirection based on access token presence * refactor: extract SocialLogin component for better readability * feat: implement comment editing
- Loading branch information
Showing
48 changed files
with
1,754 additions
and
192 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
.authForm { | ||
max-width: 400px; | ||
display: flex; | ||
flex-direction: column; | ||
gap: 16px; | ||
} | ||
|
||
.input { | ||
gap: 8px; | ||
} | ||
|
||
.authForm label { | ||
font-size: 0.875rem; | ||
line-height: 1.5rem; | ||
} | ||
|
||
.btnEye { | ||
position: absolute; | ||
width: 24px; | ||
height: 24px; | ||
top: 16px; | ||
right: 16px; | ||
cursor: pointer; | ||
} | ||
|
||
.button { | ||
padding: 12px 145px; | ||
border-radius: 40px; | ||
font-size: 1.25rem; | ||
line-height: 2rem; | ||
cursor: pointer; | ||
margin-bottom: 8px; | ||
} | ||
|
||
.button.active { | ||
background-color: var(--blue); | ||
} | ||
|
||
.validationFocus { | ||
outline: 1px solid var(--red); | ||
} | ||
|
||
.validationMessage { | ||
display: none; | ||
font-size: 0.875rem; | ||
font-weight: 600; | ||
line-height: 1.5rem; | ||
color: var(--red); | ||
padding-left: 16px; | ||
margin-top: 8px; | ||
} | ||
|
||
.authLink { | ||
font-size: 0.875rem; | ||
font-weight: 500; | ||
line-height: 1.5rem; | ||
text-align: center; | ||
color: var(--gary800); | ||
} | ||
|
||
.authLink > a { | ||
color: var(--blue); | ||
text-decoration: underline; | ||
padding-left: 4px; | ||
} | ||
|
||
@media screen and (min-width: 768px) { | ||
.authForm { | ||
max-width: 100%; | ||
width: 640px; | ||
gap: 24px; | ||
} | ||
|
||
.input { | ||
gap: 16px; | ||
} | ||
|
||
.authForm label { | ||
font-size: 1.125rem; | ||
line-height: 1.625rem; | ||
} | ||
|
||
.authForm .authInput { | ||
padding: 16px 24px; | ||
} | ||
|
||
.button { | ||
padding: 16px 124px; | ||
margin-bottom: 0; | ||
} | ||
|
||
.authForm > .otherAccount { | ||
margin-bottom: 0; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { useState } from "react"; | ||
import Link from "next/link"; | ||
import Image from "next/image"; | ||
import { useRouter } from "next/router"; | ||
import { useForm } from "react-hook-form"; | ||
import useAuth from "@/hooks/useAuth"; | ||
import FormInput from "../ui/FormInput"; | ||
import SocialLogin from "./SocialLogin"; | ||
import Button from "../ui/Button"; | ||
import styles from "./AuthForm.module.css"; | ||
import hideIcon from "@/public/btn_hide.svg"; | ||
import showIcon from "@/public/btn_show.svg"; | ||
|
||
interface FormValues extends Record<string, string> { | ||
email: string; | ||
password: string; | ||
} | ||
|
||
const LoginForm = () => { | ||
const [showPassword, setShowPassword] = useState(false); | ||
const { | ||
register, | ||
handleSubmit, | ||
formState: { errors, isValid }, | ||
setError, | ||
} = useForm<FormValues>({ mode: "onChange" }); | ||
const router = useRouter(); | ||
const { login } = useAuth(); | ||
|
||
const onSubmit = async (data: FormValues) => { | ||
try { | ||
await login(data); | ||
router.push("/"); | ||
} catch (err: unknown) { | ||
if (err instanceof Error) { | ||
if (err.message === "존재하지 않는 이메일입니다.") { | ||
setError("email", { type: "manual", message: err.message }); | ||
} | ||
if (err.message === "비밀번호가 일치하지 않습니다.") { | ||
setError("password", { type: "manual", message: err.message }); | ||
} | ||
} | ||
} | ||
}; | ||
|
||
const togglePasswordVisibility = () => { | ||
setShowPassword(!showPassword); | ||
}; | ||
|
||
return ( | ||
<form className={styles.authForm} onSubmit={handleSubmit(onSubmit)}> | ||
<FormInput | ||
className={styles.input} | ||
type="text" | ||
name="email" | ||
label="이메일" | ||
placeholder="이메일을 입력해주세요" | ||
register={register} | ||
required="이메일을 입력해주세요." | ||
pattern={{ | ||
value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, | ||
message: "잘못된 이메일 형식입니다.", | ||
}} | ||
error={errors.email} | ||
/> | ||
<FormInput | ||
className={styles.input} | ||
type={showPassword ? "text" : "password"} | ||
name="password" | ||
label="비밀번호" | ||
placeholder="비밀번호를 입력해주세요" | ||
register={register} | ||
required="비밀번호를 입력해주세요." | ||
minLength={{ | ||
value: 8, | ||
message: "비밀번호를 8자 이상 입력해주세요.", | ||
}} | ||
error={errors.password} | ||
> | ||
<Image | ||
src={showPassword ? showIcon : hideIcon} | ||
className={styles.btnEye} | ||
onClick={togglePasswordVisibility} | ||
alt="비밀번호 표시" | ||
/> | ||
</FormInput> | ||
<Button className={styles.button} type="submit" disabled={!isValid}> | ||
로그인 | ||
</Button> | ||
<SocialLogin /> | ||
<span className={styles.authLink}> | ||
판다마켓이 처음이신가요? | ||
<Link href="/signup">회원가입</Link> | ||
</span> | ||
</form> | ||
); | ||
}; | ||
|
||
export default LoginForm; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { useState, useEffect } from "react"; | ||
import Link from "next/link"; | ||
import Image from "next/image"; | ||
import { useRouter } from "next/router"; | ||
import { useForm } from "react-hook-form"; | ||
import useAuth from "@/hooks/useAuth"; | ||
import FormInput from "../ui/FormInput"; | ||
import SocialLogin from "./SocialLogin"; | ||
import Button from "../ui/Button"; | ||
import styles from "./AuthForm.module.css"; | ||
import hideIcon from "@/public/btn_hide.svg"; | ||
import showIcon from "@/public/btn_show.svg"; | ||
|
||
interface FormValues extends Record<string, string> { | ||
email: string; | ||
nickname: string; | ||
password: string; | ||
passwordConfirmation: string; | ||
} | ||
|
||
const LoginForm = () => { | ||
const [showPassword, setShowPassword] = useState(false); | ||
const [showPasswordConfirmation, setShowPasswordConfirmation] = | ||
useState(false); | ||
const { | ||
register, | ||
handleSubmit, | ||
formState: { errors, isValid }, | ||
watch, | ||
setError, | ||
trigger, | ||
} = useForm<FormValues>({ mode: "onChange" }); | ||
const router = useRouter(); | ||
const { signup } = useAuth(); | ||
const watchedPassword = watch("password"); | ||
|
||
const onSubmit = async (data: FormValues) => { | ||
try { | ||
await signup(data); | ||
router.push("/login"); | ||
} catch (err: unknown) { | ||
if (err instanceof Error) { | ||
if (err.message === "이미 사용중인 이메일입니다.") { | ||
setError("email", { type: "manual", message: err.message }); | ||
} | ||
if (err.message === "이미 사용중인 닉네임입니다.") { | ||
setError("nickname", { type: "manual", message: err.message }); | ||
} | ||
} | ||
} | ||
}; | ||
|
||
const togglePasswordVisibility = () => setShowPassword(!showPassword); | ||
const togglePasswordConfirmationVisibility = () => | ||
setShowPasswordConfirmation(!showPasswordConfirmation); | ||
|
||
useEffect(() => { | ||
if (watchedPassword) { | ||
trigger("passwordConfirmation"); | ||
} | ||
}, [watchedPassword, trigger]); | ||
|
||
return ( | ||
<form className={styles.authForm} onSubmit={handleSubmit(onSubmit)}> | ||
<FormInput | ||
className={styles.input} | ||
type="text" | ||
name="email" | ||
label="이메일" | ||
placeholder="이메일을 입력해주세요" | ||
register={register} | ||
required="이메일을 입력해주세요" | ||
pattern={{ | ||
value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, | ||
message: "잘못된 이메일 형식입니다.", | ||
}} | ||
error={errors.email} | ||
/> | ||
<FormInput | ||
className={styles.input} | ||
type="text" | ||
name="nickname" | ||
label="닉네임" | ||
placeholder="닉네임을 입력해주세요" | ||
register={register} | ||
required="닉네임을 입력해주세요" | ||
error={errors.nickname} | ||
/> | ||
<FormInput | ||
className={styles.input} | ||
type={showPassword ? "text" : "password"} | ||
name="password" | ||
label="비밀번호" | ||
placeholder="비밀번호를 입력해주세요" | ||
register={register} | ||
required="비밀번호를 입력해주세요." | ||
minLength={{ | ||
value: 8, | ||
message: "비밀번호를 8자 이상 입력해주세요.", | ||
}} | ||
error={errors.password} | ||
> | ||
<Image | ||
src={showPassword ? showIcon : hideIcon} | ||
className={styles.btnEye} | ||
onClick={togglePasswordVisibility} | ||
alt="비밀번호 표시" | ||
/> | ||
</FormInput> | ||
<FormInput | ||
className={styles.input} | ||
type={showPasswordConfirmation ? "text" : "password"} | ||
name="passwordConfirmation" | ||
label="비밀번호 확인" | ||
placeholder="비밀번호를 다시 한 번 입력해주세요" | ||
register={register} | ||
validate={{ | ||
matchesPassword: (value) => | ||
value === watchedPassword || "비밀번호가 일치하지 않습니다.", | ||
required: (value) => value !== "" || "비밀번호 확인을 입력해주세요.", | ||
}} | ||
error={errors.passwordConfirmation} | ||
> | ||
<Image | ||
src={showPasswordConfirmation ? showIcon : hideIcon} | ||
className={styles.btnEye} | ||
onClick={togglePasswordConfirmationVisibility} | ||
alt="비밀번호 표시" | ||
/> | ||
</FormInput> | ||
<Button className={styles.button} type="submit" disabled={!isValid}> | ||
회원가입 | ||
</Button> | ||
<SocialLogin /> | ||
<span className={styles.authLink}> | ||
이미 회원이신가요? | ||
<Link href="/login">로그인</Link> | ||
</span> | ||
</form> | ||
); | ||
}; | ||
|
||
export default LoginForm; |
Oops, something went wrong.