From 52c9f9a3b1abc90d426630e62ca9ad3c8cc27132 Mon Sep 17 00:00:00 2001 From: ariari04 Date: Wed, 21 Aug 2024 12:37:06 +0600 Subject: [PATCH] Add sign up funtion, confirm email layout --- .env | 1 + package-lock.json | 37 +-- package.json | 1 + .../sign-up/confirm-email/icons/mail.svg | 14 ++ .../[locale]/sign-up/confirm-email/page.tsx | 34 +++ src/features/AuthInput.tsx | 4 +- .../ConfirmEmailForm.module.scss | 140 +++++++++++ .../ConfirmEmailForm/ConfirmEmailForm.tsx | 147 +++++++++++ src/widgets/forms/SignUpForm.tsx | 236 ++++++++++-------- src/widgets/forms/icons/alert-circle.svg | 7 + src/widgets/forms/icons/eye-on.svg | 15 ++ 11 files changed, 511 insertions(+), 125 deletions(-) create mode 100644 .env create mode 100644 src/app/[locale]/sign-up/confirm-email/icons/mail.svg create mode 100644 src/app/[locale]/sign-up/confirm-email/page.tsx create mode 100644 src/widgets/forms/ConfirmEmailForm/ConfirmEmailForm.module.scss create mode 100644 src/widgets/forms/ConfirmEmailForm/ConfirmEmailForm.tsx create mode 100644 src/widgets/forms/icons/alert-circle.svg create mode 100644 src/widgets/forms/icons/eye-on.svg 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 @@ + + + Created with Pixso. + + + + + + + + + + + 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 ( +
+
+
+
+ Mail icon +
+
+

+ Проверьте свою почту +

+

+ Мы отправили код на почту {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} Alert Icon

- ) : 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 ( +
+
+ +
+ {otp.map((data, index) => ( + handleChange(e, index)} + value={data} + maxLength={1} + type="text" + /> + ))} +
+ {error ?

{error}

: null} +
+ + +

+ Отправить код повторно через{" "} + + 0{minutes}:{seconds.toString().length === 1 ? `0${seconds}` : seconds} + +

+ + +
+ ); +}; + +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 ( -
+
- - - +
+ +
+ +
+ {errors?.email?.message && ( +

+ {errors.email.message} Alert Icon +

+ )} +
+ +
+ +
+ + +
+ {errors?.password?.message && ( +

+ {errors.password.message} Alert Icon +

+ )} +
+ +
+ +
+ + +
+ {errors?.password_repeat?.message && ( +

+ {errors.password_repeat.message}{" "} + Alert Icon +

+ )} +
- {error ?

{error}

: null} + {errors.root && ( +

{errors.root.message}

+ )} + {resError &&

{resError}

}