Add sign up funtion, confirm email layout
This commit is contained in:
parent
769ea33a46
commit
52c9f9a3b1
1
.env
Normal file
1
.env
Normal file
@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_BASE_API=https://api.procurement.fishrungames.com/api
|
37
package-lock.json
generated
37
package-lock.json
generated
@ -18,6 +18,7 @@
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"sass": "^1.77.8",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
@ -764,7 +765,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
@ -1053,7 +1053,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
@ -1075,7 +1074,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
@ -1201,7 +1199,6 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
@ -1225,7 +1222,6 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
@ -2231,7 +2227,6 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@ -2354,7 +2349,6 @@
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -2670,6 +2664,11 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
|
||||
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw=="
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@ -2800,7 +2799,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
@ -2885,7 +2883,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -2930,7 +2927,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
@ -2966,7 +2962,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
@ -3589,7 +3584,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -3919,7 +3913,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
@ -4243,7 +4236,6 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
@ -4435,6 +4427,22 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.77.8",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
|
||||
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
@ -4935,7 +4943,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
|
@ -19,6 +19,7 @@
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"sass": "^1.77.8",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
|
14
src/app/[locale]/sign-up/confirm-email/icons/mail.svg
Normal file
14
src/app/[locale]/sign-up/confirm-email/icons/mail.svg
Normal file
@ -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 |
34
src/app/[locale]/sign-up/confirm-email/page.tsx
Normal file
34
src/app/[locale]/sign-up/confirm-email/page.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import Image from "next/image";
|
||||
import mail from "./icons/mail.svg";
|
||||
import ConfirmEmailForm from "@/widgets/forms/ConfirmEmailForm/ConfirmEmailForm";
|
||||
|
||||
const ConfirmEmail = ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: {
|
||||
email: string;
|
||||
};
|
||||
}) => {
|
||||
return (
|
||||
<div className="py-[40px] px-[90px]">
|
||||
<div className="h-full min-h-[800px] w-full flex justify-center">
|
||||
<div className="flex items-center flex-col gap-6">
|
||||
<div className="min-w-[60px] min-h-[60px] flex items-center justify-center rounded-full border-2 border-blue">
|
||||
<Image src={mail} alt="Mail icon" width={36} height={36} />
|
||||
</div>
|
||||
<div className="mb-2 flex flex-col items-center gap-2 text-center">
|
||||
<h2 className="text-6 font-bold leading-8 text-gray-900">
|
||||
Проверьте свою почту
|
||||
</h2>
|
||||
<p className="leading-6 text-gray-500">
|
||||
Мы отправили код на почту {searchParams.email}
|
||||
</p>
|
||||
</div>
|
||||
<ConfirmEmailForm email={searchParams.email} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmEmail;
|
@ -41,11 +41,11 @@ const AuthInput: React.FC<IAuthInputProps> = ({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{error ? (
|
||||
{error && (
|
||||
<p className="flex items-center justify-between text-[14px] font-normal leading-5 text-red-500">
|
||||
{error} <Image src={alert} alt="Alert Icon" />
|
||||
</p>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
140
src/widgets/forms/ConfirmEmailForm/ConfirmEmailForm.module.scss
Normal file
140
src/widgets/forms/ConfirmEmailForm/ConfirmEmailForm.module.scss
Normal file
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
147
src/widgets/forms/ConfirmEmailForm/ConfirmEmailForm.tsx
Normal file
147
src/widgets/forms/ConfirmEmailForm/ConfirmEmailForm.tsx
Normal file
@ -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;
|
@ -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>
|
||||
{error ? <p className="text-sm leading-5 text-red-500">{error}</p> : null}
|
||||
{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>
|
||||
{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
src/widgets/forms/icons/alert-circle.svg
Normal file
7
src/widgets/forms/icons/alert-circle.svg
Normal 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
src/widgets/forms/icons/eye-on.svg
Normal file
15
src/widgets/forms/icons/eye-on.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user