Skip to content

Commit

Permalink
[나지원] sprint11 (#133)
Browse files Browse the repository at this point in the history
* 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
najitwo authored Nov 12, 2024
1 parent f8b22be commit c432a31
Show file tree
Hide file tree
Showing 48 changed files with 1,754 additions and 192 deletions.
8 changes: 4 additions & 4 deletions components/addboard/AddBoardForm.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { ChangeEvent, useState, useEffect, MouseEvent } from "react";
import { useRouter } from "next/router";
import useAuth from "@/hooks/useAuth";
import FileInput from "../ui/FileInput";
import Input from "../ui/Input";
import Textarea from "../ui/Textarea";
import Button from "../ui/Button";
import { fetchData } from "@/lib/fetchData";
import { ARTICLE_URL, IMAGE_URL } from "@/constants/url";
import { useAuth } from "@/contexts/AuthProvider";
import styles from "./AddBoardForm.module.css";

interface Board {
Expand All @@ -26,8 +26,8 @@ const INITIAL_BOARD: Board = {
const AddBoardForm = () => {
const [isDisabled, setIsDisabled] = useState(true);
const [values, setValues] = useState<Board>(INITIAL_BOARD);
const { accessToken } = useAuth();
const router = useRouter();
const { accessToken } = useAuth(true);

const handleChange = (name: BoardField, value: Board[BoardField]): void => {
setValues((prevValues) => {
Expand Down Expand Up @@ -57,7 +57,7 @@ const AddBoardForm = () => {
const formData = new FormData();
formData.append("image", imgFile);

const response = await fetchData(IMAGE_URL, {
const response = await fetchData<Record<string, string>>(IMAGE_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
Expand All @@ -67,7 +67,7 @@ const AddBoardForm = () => {
url = response.url;
}

const { id } = await fetchData(ARTICLE_URL, {
const { id } = await fetchData<Record<string, string>>(ARTICLE_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
Expand Down
95 changes: 95 additions & 0 deletions components/auth/AuthForm.module.css
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;
}
}
99 changes: 99 additions & 0 deletions components/auth/LoginForm.tsx
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;
143 changes: 143 additions & 0 deletions components/auth/SignUpForm.tsx
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;
Loading

0 comments on commit c432a31

Please sign in to comment.