Add sign up funtion, confirm email layout

This commit is contained in:
anime committed 2024-08-21 12:37:06 +06:00
1 parent 769ea33a46
commit 52c9f9a3b1
11 files changed
+511 -125

No files matched your search

@@ -0,0 +1,140 @@
.confirmEmailForm {
width: 360px;
display: flex;
flex-direction: column;
align-items: center;
&__inputs {
max-width: 360px;
margin-bottom: 50px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
label {
align-self: flex-start;
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: gray;
}
}
.confirmEmailForm__inputsWrapper {
display: flex;
align-items: center;
gap: 6px;
input {
text-align: center;
padding: 8px;
max-width: 55px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgb(208, 213, 221);
border-radius: 8px;
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
background: rgb(255, 255, 255);
font-size: 48px;
font-weight: 500;
line-height: 60px;
color: rgb(54, 149, 216);
::placeholder {
font-size: 48px;
font-weight: 500;
line-height: 60px;
color: gray;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input:focus {
border: 1px solid rgb(54, 149, 216);
border-radius: 8px;
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
background: rgb(255, 255, 255);
}
}
}
#confirmEmailForm__inputActive {
border: 1px solid rgb(54, 149, 216);
border-radius: 8px;
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
background: rgb(255, 255, 255);
}
button {
padding: 10px 18px;
height: 44px;
width: 100%;
border-radius: 8px;
font-size: 16px;
font-weight: 700;
line-height: 24px;
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
}
#confirm-email-form__send-code,
#confirm-email-form__send-code_active {
background-color: rgb(158, 167, 175);
color: #fff;
}
#confirm-email-form__send-code_active {
background-color: rgb(54, 149, 216);
color: #fff;
}
#confirm-email-form__resend-code,
#confirm-email-form__resend-code_active {
border: 1px solid rgb(158, 167, 175);
background-color: rgb(255, 255, 255);
color: rgb(158, 167, 175);
}
#confirm-email-form__resend-code_active {
background-color: rgb(54, 149, 216);
color: #fff;
}
&__error {
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: red;
}
&__timer {
text-align: center;
margin: 32px 0;
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: rgb(102, 112, 133);
span {
color: rgb(54, 149, 216);
}
}
}
@media screen and (max-width: 550px) {
.confirm-email-form {
width: 100%;
&__inputs {
&-wrapper {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
input {
width: 100%;
}
}
}
}
}
@@ -0,0 +1,147 @@
"use client";
import { useEffect, useState } from "react";
import { apiInstance } from "@/shared/config/apiConfig";
import { useRouter } from "@/shared/config/navigation";
import { AxiosError } from "axios";
import Loader from "@/shared/ui/Loader/Loader";
import s from "./ConfirmEmailForm.module.scss";
interface IConfirmEmailFormProps {
email: string;
}
const ConfirmEmailForm: React.FC<IConfirmEmailFormProps> = ({
email,
}: IConfirmEmailFormProps) => {
const router = useRouter();
const [otp, setOtp] = useState(new Array(6).fill(""));
const [seconds, setSeconds] = useState<number>(0);
const [minutes, setMinutes] = useState<number>(1);
const [error, setError] = useState<string>("");
const [loader, setLoader] = useState<boolean>(false);
const handleChange = (e: any, index: number) => {
if (isNaN(+e.target.value)) return false;
setOtp([...otp.map((data, i) => (i === index ? e.target.value : data))]);
if (e.target.value && e.target.nextSibling) {
e.target.nextSibling.focus();
} else if (!e.target.value && e.target.previousSibling) {
e.target.previousSibling.focus();
}
};
const handleSubmit: React.MouseEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
const code = {
code: +otp.join(""),
};
try {
setLoader(true);
const response = await apiInstance.post("/users/confirm/", code);
if (response.status === 200 || response.status === 201) {
router.push("/sign-in");
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
if ([400, 404].includes(error.response?.status as number)) {
setError("Неверный код подтверждения");
} else {
setError("Ошибка на стороне сервера");
}
} else {
setError("Произошла непредвиденная ошибка");
}
} finally {
setLoader(false);
}
};
const handleClick = async () => {
try {
const data = {
email,
};
const response = await apiInstance.post("/users/resend_code/", data);
if ([200, 201].includes(response.status)) {
setMinutes(1);
}
} catch (error) {
setError(
"Проблема на стороне сервера или вы достигли максимальное количество попыток"
);
}
};
useEffect(() => {
if (minutes === 0 && seconds === 0) return;
const timer = setInterval(() => {
if (seconds === 0) {
setMinutes((prev) => Math.max(0, prev - 1));
setSeconds(59);
} else {
setSeconds((prev) => prev - 1);
}
}, 1000);
return () => clearInterval(timer);
}, [minutes, seconds]);
return (
<form onSubmit={handleSubmit} className={s.confirmEmailForm}>
<div className={s.confirmEmailForm__inputs}>
<label>Код подтверждения</label>
<div className={s.confirmEmailForm__inputsWrapper}>
{otp.map((data, index) => (
<input
id={data ? s.confirmEmailForm__inputActive : ""}
key={index}
onChange={(e) => handleChange(e, index)}
value={data}
maxLength={1}
type="text"
/>
))}
</div>
{error ? <p className={s.confirmEmailForm__error}>{error}</p> : null}
</div>
<button
disabled={otp.join("").length === 6 ? false : true}
id={`${s.confirmEmailForm__sendCode}${
otp.join("").length === 6 ? "__active" : ""
}`}
type="submit"
>
{loader ? <Loader /> : "Подтвердить"}
</button>
<p className={s.confirmEmailForm__timer}>
Отправить код повторно через{" "}
<span>
0{minutes}:{seconds.toString().length === 1 ? `0${seconds}` : seconds}
</span>
</p>
<button
type="button"
onClick={handleClick}
disabled={minutes === 0 && seconds === 0 ? false : true}
id={`${s.confirmEmailForm__resendCode}${
minutes === 0 && seconds === 0 ? "__active" : ""
}`}
>
{loader ? <Loader /> : "Отправить код повторно"}
</button>
</form>
);
};
export default ConfirmEmailForm;
+128 -108
View File
@@ -1,110 +1,66 @@
"use client";
import { useState } from "react";
import { AxiosError } from "axios";
import AuthInput from "@/features/AuthInput";
import Loader from "@/shared/ui/Loader/Loader";
import { apiInstance } from "@/shared/config/apiConfig";
import { Link, useRouter } from "@/shared/config/navigation";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import React from "react";
import alert from "./icons/alert-circle.svg";
import Image from "next/image";
import eye_off from "./icons/eye-off.svg";
import eye_on from "./icons/eye-on.svg";
const SignUpForm = () => {
const [checkbox, setCheckbox] = useState<boolean>(false);
const [emailWarning, setEmailWarning] = useState<string>("");
const [passwordWarning, setPasswordWarning] = useState<string>("");
const [passwordConfirmWarning, setPasswordConfirmWarning] =
useState<string>("");
const [error, setError] = useState<string>("");
const [loader, setLoader] = useState<boolean>(false);
const [resError, setResError] = React.useState<string>("");
const [loader, setLoader] = React.useState<boolean>(false);
const [showPassword, setShowPassword] = React.useState(false);
const [showPasswordTwo, setShowPasswordTwo] = React.useState(false);
const router = useRouter();
const handleSubmit: React.MouseEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const regex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/;
const signUpFormScheme = z
.object({
email: z.string().email("Неверный формат email"),
password: z.string().min(8, "Пароль должен содержать минимум 8 символов"),
password_repeat: z
.string()
.min(8, "Пароль должен содержать минимум 8 символов"),
})
.refine((data) => data.password === data.password_repeat, {
message: "Пароли не совпадают",
path: ["password_repeat"],
});
if (!formData.get("email")?.toString()) {
setError("");
setPasswordWarning("");
setPasswordConfirmWarning("");
setEmailWarning("Заполните поле Email");
return;
}
type FormFields = z.infer<typeof signUpFormScheme>;
if (!formData.get("password")?.toString()) {
setError("");
setEmailWarning("");
setPasswordConfirmWarning("");
setPasswordWarning("Заполните поле Пароль");
return;
}
if ((formData.get("password")?.toString().length as number) < 8) {
setError("");
setEmailWarning("");
setPasswordConfirmWarning("");
setPasswordWarning("Пароль должен содержать минимум 8 символов");
return;
}
if (!regex.test(formData.get("password")?.toString() as string)) {
setError("");
setEmailWarning("");
setPasswordConfirmWarning("");
setPasswordWarning(
"Пароль должен содержать по меньшей мере 1 прописную букву, одну заглавную букву и одну цифру"
);
return;
}
if (!formData.get("password2")?.toString()) {
setError("");
setEmailWarning("");
setPasswordWarning("");
setPasswordConfirmWarning("Заполните поле потверждения");
return;
}
if (
formData.get("password")?.toString() !==
formData.get("password2")?.toString()
) {
setError("");
setEmailWarning("");
setPasswordWarning("");
setPasswordConfirmWarning("Пароли не совпадают");
return;
}
if (!checkbox) {
setEmailWarning("");
setPasswordWarning("");
setPasswordConfirmWarning("");
setError("Необходимо принять политику конфиденциальности");
return;
}
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormFields>({
resolver: zodResolver(signUpFormScheme),
});
const onSubmit = async (data: FormFields) => {
try {
setError("");
setEmailWarning("");
setPasswordWarning("");
setPasswordConfirmWarning("");
setLoader(true);
const res = await apiInstance.post("/users/register/", formData);
const res = await apiInstance.post("/auth/register/", data);
const encodedEmail = encodeURIComponent(data.email);
console.log(encodedEmail);
if ([200, 201].includes(res.status)) {
router.push(`/sign-up/confirm-email/?email=${formData.get("email")}`);
router.push(`sign-up/confirm-email?email=${encodedEmail}`);
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
if ([401, 400].includes(error.response?.status as number)) {
setError("Такой пользователь уже существует");
setResError("Такой пользователь уже существует");
} else if (error.response?.status.toString().slice(0, 1) === "5") {
setError("Ошибка на стороне сервера");
setResError("Ошибка на стороне сервера");
}
} else {
setError("Непредвиденная ошибка");
setResError("Непредвиденная ошибка");
}
} finally {
setLoader(false);
@@ -112,31 +68,95 @@ const SignUpForm = () => {
};
return (
<form className="mb-2 w-[360px] flex flex-col" onSubmit={handleSubmit}>
<form
className="mb-2 w-[360px] flex flex-col"
onSubmit={handleSubmit(onSubmit)}
>
<div className="flex flex-col gap-5">
<AuthInput
type="email"
label="Email"
placeholder="Введите email"
error={emailWarning}
name="email"
/>
<AuthInput
isPassword
label="Пароль"
placeholder="Введите пароль"
error={passwordWarning}
name="password"
/>
<AuthInput
isPassword
label="Пароль потверждения"
placeholder="Повторите пароль"
error={passwordConfirmWarning}
name="password2"
/>
<div>
<label className="text-[14px] leading-5 text-gray-700">Email</label>
<div
className={`flex items-center${
errors.email?.message && "border border-red-400"
}`}
>
<input
placeholder="Email"
className="w-full text-[16px] leading-6 text-gray-900 px-[10px] py-[14px] border border-gray-300 rounded-lg shadow-sm bg-white"
type="text"
{...register("email", { required: true })}
/>
</div>
{errors?.email?.message && (
<p className="flex items-center justify-between text-[14px] font-normal leading-5 text-red-500">
{errors.email.message} <Image src={alert} alt="Alert Icon" />
</p>
)}
</div>
<div>
<label className="text-[14px] leading-5 text-gray-700">Пароль</label>
<div
className={`flex items-center border border-gray-300 rounded-lg shadow-sm bg-white${
errors?.password?.message && "border border-red-400"
}`}
>
<input
placeholder="Пароль"
className="w-full text-[16px] leading-6 text-gray-900 px-[10px] py-[14px]"
type={showPassword ? "text" : "password"}
{...register("password", { required: true })}
/>
<button
onClick={() => setShowPassword((prev) => !prev)}
type="button"
className="pr-2"
>
<Image src={showPassword ? eye_on : eye_off} alt="Eye Icon" />
</button>
</div>
{errors?.password?.message && (
<p className="flex items-center justify-between text-[14px] font-normal leading-5 text-red-500">
{errors.password.message} <Image src={alert} alt="Alert Icon" />
</p>
)}
</div>
<div>
<label className="text-[14px] leading-5 text-gray-700">
Пароль потверждения
</label>
<div
className={`flex items-center border border-gray-300 rounded-lg shadow-sm bg-white${
errors?.password_repeat?.message && "border border-red-400"
}`}
>
<input
placeholder="Пароль потверждения"
className="w-full text-[16px] leading-6 text-gray-900 px-[10px] py-[14px] "
type={showPasswordTwo ? "text" : "password"}
{...register("password_repeat", { required: true })}
/>
<button
onClick={() => setShowPasswordTwo((prev) => !prev)}
type="button"
className="pr-2"
>
<Image src={showPasswordTwo ? eye_on : eye_off} alt="Eye Icon" />
</button>
</div>
{errors?.password_repeat?.message && (
<p className="flex items-center justify-between text-[14px] font-normal leading-5 text-red-500">
{errors.password_repeat.message}{" "}
<Image src={alert} alt="Alert Icon" />
</p>
)}
</div>
</div>
{error ? <p className="text-sm leading-5 text-red-500">{error}</p> : null}
{errors.root && (
<p className="text-sm leading-5 text-red-500">{errors.root.message}</p>
)}
{resError && <p className="text-sm leading-5 text-red-500">{resError}</p>}
<div className="flex flex-col mt-[36px] gap-4">
<button
+7
View File
@@ -0,0 +1,7 @@
<svg width="14.666992" height="14.666992" viewBox="0 0 14.667 14.667" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs/>
<path id="Icon" d="M7.33398 14C3.65137 14 0.666992 11.0156 0.666992 7.33398C0.666992 3.65137 3.65137 0.666992 7.33398 0.666992C11.0156 0.666992 14 3.65137 14 7.33398C14 11.0156 11.0156 14 7.33398 14ZM7.33398 4.66699L7.33398 7.33398M7.33398 10L7.33984 10" stroke="#F04438" stroke-opacity="1.000000" stroke-width="1.333333" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 566 B

+15
View File
@@ -0,0 +1,15 @@
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip1668_50161">
<rect id="eye" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<rect id="eye" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
<g clip-path="url(#clip1668_50161)">
<path id="Vector" d="M12 4C19 4 23 12 23 12C23 12 19 20 12 20C5 20 1 12 1 12C1 12 5 4 12 4Z" stroke="#979797" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
<path id="Vector" d="M12 15C10.3428 15 9 13.6572 9 12C9 10.3428 10.3428 9 12 9C13.6572 9 15 10.3428 15 12C15 13.6572 13.6572 15 12 15Z" stroke="#979797" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 901 B