Add forgot password page
This commit is contained in:
parent
eb81d45008
commit
76af01ee8b
11
src/app/[locale]/sign-in/forgot-password/page.tsx
Normal file
11
src/app/[locale]/sign-in/forgot-password/page.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import ForgotPasswordForm from "../../../../widgets/forms/ForgotPasswordForm/ForgotPasswordForm";
|
||||
|
||||
const ForgotPassword = () => {
|
||||
return (
|
||||
<div className="h-full min-h-[800px] flex justify-center w-full ">
|
||||
<ForgotPasswordForm />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
53
src/features/AuthInput/AuthInput.scss
Normal file
53
src/features/AuthInput/AuthInput.scss
Normal file
@ -0,0 +1,53 @@
|
||||
.auth-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: #344054;
|
||||
}
|
||||
|
||||
&__field,
|
||||
&__field-with-error {
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
align-items: 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);
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: rgb(16, 24, 40);
|
||||
|
||||
::placeholder {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: #667085;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__field-with-error {
|
||||
border: 1px solid rgb(240, 68, 56);
|
||||
}
|
||||
|
||||
p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: rgb(240, 68, 56);
|
||||
}
|
||||
}
|
@ -1,15 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import eye_off from "./icons/eye-off.svg";
|
||||
import eye_on from "./icons/eye-on.svg";
|
||||
import alert from "./icons/alert-circle.svg";
|
||||
import "./AuthInput.scss";
|
||||
import eye_off from "../icons/eye-off.svg";
|
||||
import eye_on from "../icons/eye-on.svg";
|
||||
import alert from "../icons/alert-circle.svg";
|
||||
import { useState } from "react";
|
||||
|
||||
interface IAuthInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
isPassword?: boolean;
|
||||
label: string;
|
||||
error?: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const AuthInput: React.FC<IAuthInputProps> = ({
|
||||
@ -22,17 +23,12 @@ const AuthInput: React.FC<IAuthInputProps> = ({
|
||||
}: IAuthInputProps) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
return (
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<label className="text-[14px] leading-5 text-gray-700">{label}</label>
|
||||
<div
|
||||
className={`px-[10px] py-[14px] flex items-center border border-gray-300 rounded-lg shadow-sm bg-white${
|
||||
error ? "-with-error" : "border border-red-400"
|
||||
}`}
|
||||
>
|
||||
<div className="auth-input">
|
||||
<label>{label}</label>
|
||||
<div className={`auth-input__field${error ? "-with-error" : ""}`}>
|
||||
<input
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
className="w-full text-[16px] leading-6 text-gray-900"
|
||||
type={!isPassword ? type : isOpen ? type : "password"}
|
||||
/>
|
||||
{isPassword && (
|
||||
@ -41,11 +37,11 @@ const AuthInput: React.FC<IAuthInputProps> = ({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{error && (
|
||||
<p className="flex items-center justify-between text-[14px] font-normal leading-5 text-red-500">
|
||||
{error ? (
|
||||
<p>
|
||||
{error} <Image src={alert} alt="Alert Icon" />
|
||||
</p>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: rotation 1s linear infinite;
|
||||
}
|
||||
|
19
src/shared/ui/Loader/Loader.scss
Normal file
19
src/shared/ui/Loader/Loader.scss
Normal file
@ -0,0 +1,19 @@
|
||||
.loader {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
border-top: 3px solid #fff;
|
||||
border-right: 3px solid transparent;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import "./Loader.css";
|
||||
import "./Loader.scss";
|
||||
|
||||
interface ILoader {
|
||||
color?: string;
|
||||
@ -6,10 +6,7 @@ interface ILoader {
|
||||
|
||||
const Loader: React.FC<ILoader> = ({ color }: ILoader) => {
|
||||
return (
|
||||
<span
|
||||
className="w-6 h-6 rounded-full border-t-3 border-r-3 border-t-white border-r-transparent box-border animate-spin"
|
||||
style={{ borderTop: `3px solid ${color}` }}
|
||||
></span>
|
||||
<span className="loader" style={{ borderTop: `3px solid ${color}` }}></span>
|
||||
);
|
||||
};
|
||||
|
||||
|
16
src/widgets/forms/ForgotPasswordForm/ForgotPasswordForm.tsx
Normal file
16
src/widgets/forms/ForgotPasswordForm/ForgotPasswordForm.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import ConfirmCode from "./confirm-code/ConfirmCode";
|
||||
import SendEmail from "./send-email/SendEmail";
|
||||
|
||||
const ForgotPasswordForm = () => {
|
||||
const [changeForm, setChangeForm] = useState<boolean>(false);
|
||||
return changeForm ? (
|
||||
<ConfirmCode setChangeForm={setChangeForm} />
|
||||
) : (
|
||||
<SendEmail setChangeForm={setChangeForm} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPasswordForm;
|
@ -0,0 +1,125 @@
|
||||
import Image from "next/image";
|
||||
import key from "./icons/key.svg";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { ITokens } from "@/shared/types/token-type";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
import { AxiosError } from "axios";
|
||||
import Loader from "@/shared/ui/Loader/Loader";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import alert from "../../icons/alert-circle.svg";
|
||||
|
||||
interface IConfirmCodeProps {
|
||||
setChangeForm: (boolean: boolean) => void;
|
||||
}
|
||||
|
||||
const ConfirmCode: React.FC<IConfirmCodeProps> = ({
|
||||
setChangeForm,
|
||||
}: IConfirmCodeProps) => {
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loader, setLoader] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
|
||||
const confirmCodeScheme = z.object({
|
||||
code: z.string().min(6, "Код подтверждения состоит из 6 цифр"),
|
||||
});
|
||||
type FormFields = z.infer<typeof confirmCodeScheme>;
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<FormFields>({
|
||||
resolver: zodResolver(confirmCodeScheme),
|
||||
});
|
||||
const onSubmit = async (data: FormFields) => {
|
||||
try {
|
||||
setError("");
|
||||
setLoader(true);
|
||||
const res = await apiInstance.post<ITokens>(
|
||||
"/auth/confirm_password/",
|
||||
data
|
||||
);
|
||||
console.log(res.data);
|
||||
if (res.status === 200 || res.status === 201) {
|
||||
localStorage.setItem("transitional", JSON.stringify(res.data));
|
||||
router.push("/sign-in/reset-code");
|
||||
}
|
||||
setLoader(false);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status === 400) {
|
||||
setError("Неверный код");
|
||||
} else {
|
||||
setError("Ошибка на стороне сервера");
|
||||
}
|
||||
} else {
|
||||
setError("Произошла непредвиденная ошибка");
|
||||
}
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex items-center flex-col gap-6">
|
||||
<div className="min-w-[60px] min-h-[60px] flex items-center justify-center border border-light-blue rounded-full">
|
||||
<Image src={key} alt="Key Icon" className="w-[24px] h-[24px]" />
|
||||
</div>
|
||||
<div className="auth-header">
|
||||
<h2 className="text-[24px] font-bold leading-8 text-gray-900 text-center">
|
||||
Введите код
|
||||
</h2>
|
||||
<p className="leading-6 text-gray-500">
|
||||
Введите код для сброса и восстановления пароля
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-[360px] flex flex-col gap-8"
|
||||
>
|
||||
<div>
|
||||
<label className="text-[14px] leading-5 text-gray-700">
|
||||
Код подтверждения
|
||||
</label>
|
||||
<div
|
||||
className={`flex items-center${
|
||||
errors.code?.message && "border border-red-400"
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
placeholder="Код подтверждения"
|
||||
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("code", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
{errors?.code?.message && (
|
||||
<p className="flex items-center justify-between text-[14px] font-normal leading-5 text-red-500">
|
||||
{errors.code.message} <Image src={alert} alt="Alert Icon" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className=" w-full h-[44px] rounded-md shadow-sm bg-light-blue font-bold leading-6 text-white"
|
||||
>
|
||||
{loader ? <Loader /> : "Сбросить пароль"}
|
||||
</button>
|
||||
{error && <div className="text-red-500">{error}</div>}
|
||||
{errors.root && (
|
||||
<div className="text-red-500">{errors.root.message}</div>
|
||||
)}
|
||||
</form>
|
||||
<button
|
||||
onClick={() => setChangeForm(false)}
|
||||
className="text-light-blue leading-6"
|
||||
>
|
||||
Получить код
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmCode;
|
@ -0,0 +1,14 @@
|
||||
<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_53329">
|
||||
<rect id="key" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="key" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip1668_53329)">
|
||||
<path id="Vector" d="M21 2L19 4L15.5 7.5L11.3906 11.6104L11.3896 11.6113C10.3516 10.6094 8.96289 10.0547 7.52051 10.0674C6.07812 10.0801 4.69922 10.6582 3.67969 11.6777C2.65918 12.6973 2.08105 14.0771 2.06836 15.5195C2.05566 16.9609 2.61035 18.3506 3.6123 19.3877C4.12207 19.9043 4.72852 20.3145 5.39746 20.5957C6.06543 20.877 6.7832 21.0225 7.50879 21.0254C8.23438 21.0273 8.95312 20.8867 9.62402 20.6104C10.2939 20.333 10.9033 19.9268 11.416 19.4141C11.9297 18.9014 12.3359 18.292 12.6123 17.6211C12.8887 16.9502 13.0293 16.2324 13.0273 15.5068C13.0244 14.7812 12.8789 14.0635 12.5977 13.3945C12.3174 12.7256 11.9062 12.1191 11.3906 11.6104M15.5 7.5L18.5 10.5L22 7L19 4" stroke="#489FE1" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
117
src/widgets/forms/ForgotPasswordForm/send-email/SendEmail.tsx
Normal file
117
src/widgets/forms/ForgotPasswordForm/send-email/SendEmail.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import Image from "next/image";
|
||||
import mail from "./icons/mail.svg";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { AxiosError } from "axios";
|
||||
import Loader from "@/shared/ui/Loader/Loader";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import alert from "../../icons/alert-circle.svg";
|
||||
|
||||
interface ISendEmailProps {
|
||||
setChangeForm: (boolean: boolean) => void;
|
||||
}
|
||||
|
||||
const SendEmail: React.FC<ISendEmailProps> = ({
|
||||
setChangeForm,
|
||||
}: ISendEmailProps) => {
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loader, setLoader] = useState<boolean>(false);
|
||||
|
||||
const sendEmailFormScheme = z.object({
|
||||
email: z.string().email("Неверный формат email"),
|
||||
});
|
||||
type FormFields = z.infer<typeof sendEmailFormScheme>;
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<FormFields>({
|
||||
resolver: zodResolver(sendEmailFormScheme),
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormFields) => {
|
||||
try {
|
||||
setError("");
|
||||
setLoader(true);
|
||||
const res = await apiInstance.post("/auth/send_mailcode/", data);
|
||||
|
||||
if ([200, 201].includes(res.status)) {
|
||||
console.log(res.data);
|
||||
setChangeForm(true);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
console.log(error);
|
||||
if (error.response?.status === 400) {
|
||||
setError("Пользователь с таким email не найден");
|
||||
} else {
|
||||
setError("Ошибка на стороне сервера");
|
||||
}
|
||||
} else {
|
||||
setError("Произошла непредвиденная ошибка");
|
||||
}
|
||||
} finally {
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex items-center flex-col gap-6">
|
||||
<div className="min-w-[60px] min-h-[60px] flex items-center justify-center border border-light-blue rounded-full">
|
||||
<Image src={mail} alt="Key Icon" className="w-[24px] h-[24px]" />
|
||||
</div>
|
||||
<div className="mb-2 flex flex-col items-center gap-2 text-center">
|
||||
<h2 className="text-[24px] font-bold leading-8 text-gray-900">
|
||||
Введите email
|
||||
</h2>
|
||||
<p className="leading-6 text-gray-500">
|
||||
Введите email и мы отправим код для восстановления пароля
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-[360px] flex flex-col gap-8"
|
||||
>
|
||||
<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>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full h-[44px] rounded-md shadow-sm bg-light-blue font-bold leading-6 text-white"
|
||||
>
|
||||
{loader ? <Loader /> : "Отправить код"}
|
||||
</button>
|
||||
</form>
|
||||
{error && <div className="text-red-500">{error}</div>}
|
||||
{errors.root && <div className="text-red-500">{errors.root.message}</div>}
|
||||
<button
|
||||
onClick={() => setChangeForm(true)}
|
||||
className="text-light-blue leading-6"
|
||||
>
|
||||
Потвердить код
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SendEmail;
|
@ -0,0 +1,14 @@
|
||||
<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="clip55_8430">
|
||||
<rect id="mail" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="mail" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip55_8430)">
|
||||
<path id="Icon" d="M20 4C21.0996 4 22 4.90039 22 6L22 18C22 19.0996 21.0996 20 20 20L4 20C2.90039 20 2 19.0996 2 18L2 6C2 4.90039 2.90039 4 4 4L20 4ZM22 6L12 13L2 6" stroke="#489FE1" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 739 B |
@ -2,18 +2,15 @@
|
||||
import React from "react";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Link from "next/link";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
import { Link, useRouter } from "@/shared/config/navigation";
|
||||
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";
|
||||
import Loader from "@/shared/ui/Loader/Loader";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
const SignInForm = ({}) => {
|
||||
const session = useSession();
|
||||
@ -38,27 +35,6 @@ const SignInForm = ({}) => {
|
||||
resolver: zodResolver(signInFormScheme),
|
||||
});
|
||||
|
||||
// const onSubmit = async (data: FormFields) => {
|
||||
// try {
|
||||
// console.log(data);
|
||||
// const res = await apiInstance.post("/auth/login/", data);
|
||||
// if ([200, 201].includes(res.status)) {
|
||||
// router.push("/");
|
||||
// }
|
||||
// } catch (error: unknown) {
|
||||
// if (error instanceof AxiosError) {
|
||||
// if ([400, 401, 404].includes(error.response?.status as number)) {
|
||||
// setError("Неверный Email или Пароль");
|
||||
// } else if (error.response?.status.toString().slice(0, 1) === "5") {
|
||||
// setError("Ошибка на стороне сервера");
|
||||
// }
|
||||
// } else {
|
||||
// setError("Непредвиденная ошибка");
|
||||
// }
|
||||
// } finally {
|
||||
// setLoader(false);
|
||||
// }
|
||||
// };
|
||||
const onSubmit = async (data: FormFields) => {
|
||||
setLoader(true);
|
||||
const result = await signIn("credentials", {
|
||||
@ -144,7 +120,7 @@ const SignInForm = ({}) => {
|
||||
</div>
|
||||
<div className="text-blue font-semibold pt-2 cursor-pointer">
|
||||
<Link
|
||||
href="/forgot-password-email"
|
||||
href="/sign-in/forgot-password"
|
||||
className="text-blue font-semibold pt-2 cursor-pointer "
|
||||
>
|
||||
{t("passwordForget")}
|
||||
|
Loading…
Reference in New Issue
Block a user