Add sign up funtion, confirm email layout

This commit is contained in:
ariari04 2024-08-21 12:37:06 +06:00
parent 769ea33a46
commit 52c9f9a3b1
11 changed files with 511 additions and 125 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
NEXT_PUBLIC_BASE_API=https://api.procurement.fishrungames.com/api

37
package-lock.json generated
View File

@ -18,6 +18,7 @@
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.52.2", "react-hook-form": "^7.52.2",
"sass": "^1.77.8",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.5.2",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
@ -764,7 +765,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": { "dependencies": {
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"picomatch": "^2.0.4" "picomatch": "^2.0.4"
@ -1053,7 +1053,6 @@
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
}, },
@ -1075,7 +1074,6 @@
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": { "dependencies": {
"fill-range": "^7.1.1" "fill-range": "^7.1.1"
}, },
@ -1201,7 +1199,6 @@
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"dependencies": { "dependencies": {
"anymatch": "~3.1.2", "anymatch": "~3.1.2",
"braces": "~3.0.2", "braces": "~3.0.2",
@ -1225,7 +1222,6 @@
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": { "dependencies": {
"is-glob": "^4.0.1" "is-glob": "^4.0.1"
}, },
@ -2231,7 +2227,6 @@
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@ -2354,7 +2349,6 @@
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"os": [ "os": [
@ -2670,6 +2664,11 @@
"node": ">= 4" "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": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -2800,7 +2799,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": { "dependencies": {
"binary-extensions": "^2.0.0" "binary-extensions": "^2.0.0"
}, },
@ -2885,7 +2883,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -2930,7 +2927,6 @@
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": { "dependencies": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
}, },
@ -2966,7 +2962,6 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@ -3589,7 +3584,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -3919,7 +3913,6 @@
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
}, },
@ -4243,7 +4236,6 @@
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": { "dependencies": {
"picomatch": "^2.2.1" "picomatch": "^2.2.1"
}, },
@ -4435,6 +4427,22 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/scheduler": {
"version": "0.23.2", "version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@ -4935,7 +4943,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },

View File

@ -19,6 +19,7 @@
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.52.2", "react-hook-form": "^7.52.2",
"sass": "^1.77.8",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.5.2",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },

View 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

View 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;

View File

@ -41,11 +41,11 @@ const AuthInput: React.FC<IAuthInputProps> = ({
</button> </button>
)} )}
</div> </div>
{error ? ( {error && (
<p className="flex items-center justify-between text-[14px] font-normal leading-5 text-red-500"> <p className="flex items-center justify-between text-[14px] font-normal leading-5 text-red-500">
{error} <Image src={alert} alt="Alert Icon" /> {error} <Image src={alert} alt="Alert Icon" />
</p> </p>
) : null} )}
</div> </div>
); );
}; };

View 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%;
}
}
}
}
}

View 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;

View File

@ -1,110 +1,66 @@
"use client"; "use client";
import { useState } from "react";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import AuthInput from "@/features/AuthInput";
import Loader from "@/shared/ui/Loader/Loader"; import Loader from "@/shared/ui/Loader/Loader";
import { apiInstance } from "@/shared/config/apiConfig"; 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 SignUpForm = () => {
const [checkbox, setCheckbox] = useState<boolean>(false); const [resError, setResError] = React.useState<string>("");
const [emailWarning, setEmailWarning] = useState<string>(""); const [loader, setLoader] = React.useState<boolean>(false);
const [passwordWarning, setPasswordWarning] = useState<string>(""); const [showPassword, setShowPassword] = React.useState(false);
const [passwordConfirmWarning, setPasswordConfirmWarning] = const [showPasswordTwo, setShowPasswordTwo] = React.useState(false);
useState<string>("");
const [error, setError] = useState<string>("");
const [loader, setLoader] = useState<boolean>(false);
const router = useRouter(); const router = useRouter();
const handleSubmit: React.MouseEventHandler<HTMLFormElement> = async (e) => { const signUpFormScheme = z
e.preventDefault(); .object({
const formData = new FormData(e.currentTarget); email: z.string().email("Неверный формат email"),
const regex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/; 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()) { type FormFields = z.infer<typeof signUpFormScheme>;
setError("");
setPasswordWarning("");
setPasswordConfirmWarning("");
setEmailWarning("Заполните поле Email");
return;
}
if (!formData.get("password")?.toString()) { const {
setError(""); register,
setEmailWarning(""); handleSubmit,
setPasswordConfirmWarning(""); formState: { errors, isSubmitting },
setPasswordWarning("Заполните поле Пароль"); } = useForm<FormFields>({
return; resolver: zodResolver(signUpFormScheme),
} });
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 onSubmit = async (data: FormFields) => {
try { try {
setError(""); const res = await apiInstance.post("/auth/register/", data);
setEmailWarning(""); const encodedEmail = encodeURIComponent(data.email);
setPasswordWarning(""); console.log(encodedEmail);
setPasswordConfirmWarning("");
setLoader(true);
const res = await apiInstance.post("/users/register/", formData);
if ([200, 201].includes(res.status)) { 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) { } catch (error: unknown) {
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
if ([401, 400].includes(error.response?.status as number)) { if ([401, 400].includes(error.response?.status as number)) {
setError("Такой пользователь уже существует"); setResError("Такой пользователь уже существует");
} else if (error.response?.status.toString().slice(0, 1) === "5") { } else if (error.response?.status.toString().slice(0, 1) === "5") {
setError("Ошибка на стороне сервера"); setResError("Ошибка на стороне сервера");
} }
} else { } else {
setError("Непредвиденная ошибка"); setResError("Непредвиденная ошибка");
} }
} finally { } finally {
setLoader(false); setLoader(false);
@ -112,31 +68,95 @@ const SignUpForm = () => {
}; };
return ( 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"> <div className="flex flex-col gap-5">
<AuthInput <div>
type="email" <label className="text-[14px] leading-5 text-gray-700">Email</label>
label="Email" <div
placeholder="Введите email" className={`flex items-center${
error={emailWarning} errors.email?.message && "border border-red-400"
name="email" }`}
/> >
<AuthInput <input
isPassword placeholder="Email"
label="Пароль" className="w-full text-[16px] leading-6 text-gray-900 px-[10px] py-[14px] border border-gray-300 rounded-lg shadow-sm bg-white"
placeholder="Введите пароль" type="text"
error={passwordWarning} {...register("email", { required: true })}
name="password"
/>
<AuthInput
isPassword
label="Пароль потверждения"
placeholder="Повторите пароль"
error={passwordConfirmWarning}
name="password2"
/> />
</div> </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"> <div className="flex flex-col mt-[36px] gap-4">
<button <button

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

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