Add sign up funtion, confirm email layout
This commit is contained in:
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
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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 |
Reference in new issue
Block a user