diff --git a/.env b/.env
new file mode 100644
index 0000000..9a83255
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+NEXT_PUBLIC_BASE_API=https://api.procurement.fishrungames.com/api
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index b967bf0..cc70e83 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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"
},
diff --git a/package.json b/package.json
index 572f60b..034eaf0 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/src/app/[locale]/sign-up/confirm-email/icons/mail.svg b/src/app/[locale]/sign-up/confirm-email/icons/mail.svg
new file mode 100644
index 0000000..dc055d2
--- /dev/null
+++ b/src/app/[locale]/sign-up/confirm-email/icons/mail.svg
@@ -0,0 +1,14 @@
+
diff --git a/src/app/[locale]/sign-up/confirm-email/page.tsx b/src/app/[locale]/sign-up/confirm-email/page.tsx
new file mode 100644
index 0000000..9d58e8d
--- /dev/null
+++ b/src/app/[locale]/sign-up/confirm-email/page.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+ Проверьте свою почту
+
+
+ Мы отправили код на почту {searchParams.email}
+
+
+
+
+
+
+ );
+};
+
+export default ConfirmEmail;
diff --git a/src/features/AuthInput.tsx b/src/features/AuthInput.tsx
index d55c301..9dca71d 100644
--- a/src/features/AuthInput.tsx
+++ b/src/features/AuthInput.tsx
@@ -41,11 +41,11 @@ const AuthInput: React.FC = ({
)}
- {error ? (
+ {error && (
{error}
- ) : null}
+ )}
);
};
diff --git a/src/widgets/forms/ConfirmEmailForm/ConfirmEmailForm.module.scss b/src/widgets/forms/ConfirmEmailForm/ConfirmEmailForm.module.scss
new file mode 100644
index 0000000..93c2f17
--- /dev/null
+++ b/src/widgets/forms/ConfirmEmailForm/ConfirmEmailForm.module.scss
@@ -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%;
+ }
+ }
+ }
+ }
+}
diff --git a/src/widgets/forms/ConfirmEmailForm/ConfirmEmailForm.tsx b/src/widgets/forms/ConfirmEmailForm/ConfirmEmailForm.tsx
new file mode 100644
index 0000000..daccd95
--- /dev/null
+++ b/src/widgets/forms/ConfirmEmailForm/ConfirmEmailForm.tsx
@@ -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 = ({
+ email,
+}: IConfirmEmailFormProps) => {
+ const router = useRouter();
+ const [otp, setOtp] = useState(new Array(6).fill(""));
+ const [seconds, setSeconds] = useState(0);
+ const [minutes, setMinutes] = useState(1);
+ const [error, setError] = useState("");
+ const [loader, setLoader] = useState(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 = 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 (
+
+ );
+};
+
+export default ConfirmEmailForm;
diff --git a/src/widgets/forms/SignUpForm.tsx b/src/widgets/forms/SignUpForm.tsx
index bd2fbe1..a7b5c7c 100644
--- a/src/widgets/forms/SignUpForm.tsx
+++ b/src/widgets/forms/SignUpForm.tsx
@@ -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(false);
- const [emailWarning, setEmailWarning] = useState("");
- const [passwordWarning, setPasswordWarning] = useState("");
- const [passwordConfirmWarning, setPasswordConfirmWarning] =
- useState("");
- const [error, setError] = useState("");
- const [loader, setLoader] = useState(false);
+ const [resError, setResError] = React.useState("");
+ const [loader, setLoader] = React.useState(false);
+ const [showPassword, setShowPassword] = React.useState(false);
+ const [showPasswordTwo, setShowPasswordTwo] = React.useState(false);
const router = useRouter();
- const handleSubmit: React.MouseEventHandler = 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;
- 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({
+ 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 (
-