added next-intl, composed next-auth and next-intl in middleware, fixed some bugs, completed details page, added google auth
No files matched your search
@@ -0,0 +1,145 @@
|
||||
@import "@/shared/ui/variables.scss";
|
||||
|
||||
.confirm-email-form {
|
||||
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-700;
|
||||
}
|
||||
|
||||
&-wrapper {
|
||||
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-300;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#confirm-email-form__input-active {
|
||||
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-500;
|
||||
}
|
||||
|
||||
&__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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
"use client";
|
||||
|
||||
import "./ConfirmEmailForm.scss";
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
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) {
|
||||
setError("Возникла ошибка");
|
||||
} finally {
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = async () => {
|
||||
const data = {
|
||||
email,
|
||||
};
|
||||
const response = await apiInstance.post(
|
||||
"/users/resend_code/",
|
||||
data
|
||||
);
|
||||
|
||||
if (response.status === 200 || response.status === 201) {
|
||||
setMinutes(1);
|
||||
}
|
||||
};
|
||||
|
||||
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="confirm-email-form">
|
||||
<div className="confirm-email-form__inputs">
|
||||
<label>Код подтверждения</label>
|
||||
<div className="confirm-email-form__inputs-wrapper">
|
||||
{otp.map((data, index) => (
|
||||
<input
|
||||
id={data ? "confirm-email-form__input-active" : ""}
|
||||
key={index}
|
||||
onChange={(e) => handleChange(e, index)}
|
||||
value={data}
|
||||
maxLength={1}
|
||||
type="text"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{error ? (
|
||||
<p className="confirm-email-form__error">{error}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
disabled={otp.join("").length === 6 ? false : true}
|
||||
id={`confirm-email-form__send-code${
|
||||
otp.join("").length === 6 ? "_active" : ""
|
||||
}`}
|
||||
type="submit"
|
||||
>
|
||||
{loader ? <Loader /> : "Подтвердить"}
|
||||
</button>
|
||||
|
||||
<p className="confirm-email-form__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={`confirm-email-form__resend-code${
|
||||
minutes === 0 && seconds === 0 ? "_active" : ""
|
||||
}`}
|
||||
>
|
||||
{loader ? <Loader /> : "Отправить код повторно"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmEmailForm;
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import "@/shared/ui/auth-classes.scss";
|
||||
import { useState } from "react";
|
||||
import SendEmail from "./send-email/send-email";
|
||||
import ConfirmCode from "./confirm-code/confirm-code";
|
||||
|
||||
const ForgotPasswordForm = () => {
|
||||
const [changeForm, setChangeForm] = useState<boolean>(false);
|
||||
return changeForm ? (
|
||||
<ConfirmCode setChangeForm={setChangeForm} />
|
||||
) : (
|
||||
<SendEmail setChangeForm={setChangeForm} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPasswordForm;
|
||||
@@ -0,0 +1,32 @@
|
||||
.confirm-code {
|
||||
width: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
|
||||
button {
|
||||
padding: 10px 18px;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||
background: rgb(54, 149, 216);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-code-send-email {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: rgb(54, 149, 216);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.confirm-code {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import Image from "next/image";
|
||||
import key from "./icons/key.svg";
|
||||
import AuthInput from "@/features/AuthInput/AuthInput";
|
||||
import "./confirm-code.scss";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { ITokens } from "@/shared/types/token-type";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IConfirmCodeProps {
|
||||
setChangeForm: (boolean: boolean) => void;
|
||||
}
|
||||
|
||||
const ConfirmCode: React.FC<IConfirmCodeProps> = ({
|
||||
setChangeForm,
|
||||
}: IConfirmCodeProps) => {
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loader, setLoader] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit: React.MouseEventHandler<
|
||||
HTMLFormElement
|
||||
> = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
if (!formData.get("code")) {
|
||||
return setError("Заполните поле");
|
||||
}
|
||||
|
||||
try {
|
||||
setError("");
|
||||
setLoader(true);
|
||||
const res = await apiInstance.post<ITokens>(
|
||||
"/users/password_reset/code/",
|
||||
formData
|
||||
);
|
||||
|
||||
if (res.status === 200 || res.status === 201) {
|
||||
localStorage.setItem(
|
||||
"transitional",
|
||||
JSON.stringify(res.data)
|
||||
);
|
||||
router.push("/sign-in/reset-code");
|
||||
}
|
||||
} catch (error) {
|
||||
setError("An error ocured");
|
||||
} finally {
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="auth-wrapper">
|
||||
<div className="auth-icon2">
|
||||
<Image src={key} alt="Key Icon" />
|
||||
</div>
|
||||
<div className="auth-header">
|
||||
<h2>Введите код</h2>
|
||||
<p>Введите код для сброса и восстановления пароля</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="confirm-code">
|
||||
<AuthInput
|
||||
type="text"
|
||||
name="code"
|
||||
label="Код сброса пароля"
|
||||
error={error}
|
||||
placeholder="Введите код"
|
||||
/>
|
||||
<button type="submit">
|
||||
{loader ? <Loader /> : "Сбросить пароль"}
|
||||
</button>
|
||||
</form>
|
||||
<button
|
||||
onClick={() => setChangeForm(false)}
|
||||
className="confirm-code-send-email"
|
||||
>
|
||||
Получить код
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmCode;
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<desc>
|
||||
Created with Pixso.
|
||||
</desc>
|
||||
<defs>
|
||||
<clipPath id="clip1668_53329">
|
||||
<rect id="key" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="key" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip1668_53329)">
|
||||
<path id="Vector" d="M21 2L19 4L15.5 7.5L11.3906 11.6104L11.3896 11.6113C10.3516 10.6094 8.96289 10.0547 7.52051 10.0674C6.07812 10.0801 4.69922 10.6582 3.67969 11.6777C2.65918 12.6973 2.08105 14.0771 2.06836 15.5195C2.05566 16.9609 2.61035 18.3506 3.6123 19.3877C4.12207 19.9043 4.72852 20.3145 5.39746 20.5957C6.06543 20.877 6.7832 21.0225 7.50879 21.0254C8.23438 21.0273 8.95312 20.8867 9.62402 20.6104C10.2939 20.333 10.9033 19.9268 11.416 19.4141C11.9297 18.9014 12.3359 18.292 12.6123 17.6211C12.8887 16.9502 13.0293 16.2324 13.0273 15.5068C13.0244 14.7812 12.8789 14.0635 12.5977 13.3945C12.3174 12.7256 11.9062 12.1191 11.3906 11.6104M15.5 7.5L18.5 10.5L22 7L19 4" stroke="#489FE1" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -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 |
@@ -0,0 +1,32 @@
|
||||
.send-email {
|
||||
width: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
|
||||
button {
|
||||
padding: 10px 18px;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||
background: rgb(54, 149, 216);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.send-email-confirm-code {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: rgb(54, 149, 216);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.send-email {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import Image from "next/image";
|
||||
import mail from "./icons/mail.svg";
|
||||
import AuthInput from "@/features/AuthInput/AuthInput";
|
||||
import "./send-email.scss";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
|
||||
interface ISendEmailProps {
|
||||
setChangeForm: (boolean: boolean) => void;
|
||||
}
|
||||
|
||||
const SendEmail: React.FC<ISendEmailProps> = ({
|
||||
setChangeForm,
|
||||
}: ISendEmailProps) => {
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loader, setLoader] = useState<boolean>(false);
|
||||
|
||||
const handleSubmit: React.MouseEventHandler<
|
||||
HTMLFormElement
|
||||
> = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
if (!formData.get("email")) {
|
||||
return setError("Заполните поле");
|
||||
}
|
||||
|
||||
try {
|
||||
setError("");
|
||||
setLoader(true);
|
||||
const res = await apiInstance.post(
|
||||
"/users/password_reset/",
|
||||
formData
|
||||
);
|
||||
|
||||
if (res.status === 200 || res.status === 201) {
|
||||
setChangeForm(true);
|
||||
}
|
||||
} catch (error) {
|
||||
setError("An error ocured");
|
||||
} finally {
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="auth-wrapper">
|
||||
<div className="auth-icon2">
|
||||
<Image src={mail} alt="Key Icon" />
|
||||
</div>
|
||||
<div className="auth-header">
|
||||
<h2>Введите email</h2>
|
||||
<p>
|
||||
Введите email и мы отправим код для восстановления пароля
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="send-email">
|
||||
<AuthInput
|
||||
type="text"
|
||||
name="email"
|
||||
label="Email"
|
||||
error={error}
|
||||
placeholder="Введите email"
|
||||
/>
|
||||
<button type="submit">
|
||||
{loader ? <Loader /> : "Отправить код"}
|
||||
</button>
|
||||
</form>
|
||||
<button
|
||||
onClick={() => setChangeForm(true)}
|
||||
className="send-email-confirm-code"
|
||||
>
|
||||
Потвердить код
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SendEmail;
|
||||
@@ -0,0 +1,80 @@
|
||||
.change-password {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 11000;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__error {
|
||||
color: rgb(240, 68, 56);
|
||||
}
|
||||
|
||||
&__success {
|
||||
color: rgb(72, 159, 225);
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 24px;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 9px;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
line-height: 28px;
|
||||
color: rgb(16, 24, 40);
|
||||
}
|
||||
|
||||
a {
|
||||
align-self: flex-end;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 180%;
|
||||
color: rgb(72, 159, 225);
|
||||
}
|
||||
}
|
||||
&__btns {
|
||||
margin-top: 17px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
button {
|
||||
padding: 10px 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
line-height: 24px;
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||
}
|
||||
|
||||
button:first-child {
|
||||
border: 1px solid rgb(72, 159, 225);
|
||||
background-color: rgb(72, 159, 225);
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
border: 1px solid rgb(208, 213, 221);
|
||||
background-color: rgb(255, 255, 255);
|
||||
color: rgb(52, 64, 84);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import "./ChangePassword.scss";
|
||||
import Link from "next/link";
|
||||
import ChangePasswordInput from "./ChangePasswordInput/ChangePasswordInput";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
|
||||
interface IChangePasswordProps {
|
||||
closeWindow: (bool: boolean) => void;
|
||||
}
|
||||
|
||||
const ChangePassword: React.FC<IChangePasswordProps> = ({
|
||||
closeWindow,
|
||||
}: IChangePasswordProps) => {
|
||||
const session = useSession();
|
||||
const [oldPassword, setOldPassword] = useState<string>("");
|
||||
const [newPassword, setNewPassword] = useState<string>("");
|
||||
const [confirmNewPassword, setConfirmNewPassword] =
|
||||
useState<string>("");
|
||||
|
||||
const [warningOldPassword, setWarningOldPassword] =
|
||||
useState<string>("");
|
||||
const [warningNewPassword, setWarningNewPassword] =
|
||||
useState<string>("");
|
||||
const [warningConfirmNewPassword, setWarningConfirmNewPassword] =
|
||||
useState<string>("");
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loader, setLoader] = useState<boolean>(false);
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
|
||||
const changePass = async () => {
|
||||
if (!oldPassword.trim()) {
|
||||
setError("");
|
||||
setWarningNewPassword("");
|
||||
setWarningConfirmNewPassword("");
|
||||
setWarningOldPassword("Пожалуйста введите старый пароль");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newPassword.trim()) {
|
||||
setError("");
|
||||
setWarningConfirmNewPassword("");
|
||||
setWarningOldPassword("");
|
||||
setWarningNewPassword("Пожалуйста введите новый пароль");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirmNewPassword.trim()) {
|
||||
setError("");
|
||||
setWarningNewPassword("");
|
||||
setWarningOldPassword("");
|
||||
setWarningConfirmNewPassword(
|
||||
"Пожалуйста потвердите новый пароль"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirmNewPassword !== newPassword) {
|
||||
setError("");
|
||||
setWarningNewPassword("");
|
||||
setWarningOldPassword("");
|
||||
setWarningConfirmNewPassword("Пароли отличаются");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
old_password: oldPassword,
|
||||
new_password1: newPassword,
|
||||
new_password2: confirmNewPassword,
|
||||
};
|
||||
|
||||
const Authorization = `Bearer ${session.data?.access_token}`;
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
setError("");
|
||||
setWarningNewPassword("");
|
||||
setWarningOldPassword("");
|
||||
setWarningConfirmNewPassword("");
|
||||
setLoader(true);
|
||||
const res = await apiInstance.patch(
|
||||
"/users/change_password/",
|
||||
data,
|
||||
config
|
||||
);
|
||||
|
||||
if ([200, 201].includes(res.status)) return setSuccess(true);
|
||||
} catch (error: any) {
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => closeWindow(false)}
|
||||
className="change-password"
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="change-password__wrapper"
|
||||
>
|
||||
<h4>Изменить пароль</h4>
|
||||
<ChangePasswordInput
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
value={oldPassword}
|
||||
placeholder="Введите старый пароль"
|
||||
label="Старый пароль"
|
||||
error={warningOldPassword}
|
||||
/>
|
||||
<Link href="/sign-in/forgot-password">Забыли пароль?</Link>
|
||||
<ChangePasswordInput
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
value={newPassword}
|
||||
placeholder="Введите новый пароль"
|
||||
label="Новый пароль"
|
||||
error={warningNewPassword}
|
||||
/>
|
||||
<ChangePasswordInput
|
||||
onChange={(e) => setConfirmNewPassword(e.target.value)}
|
||||
value={confirmNewPassword}
|
||||
placeholder="Повторите новый пароль"
|
||||
label="Потвердить новый пароль"
|
||||
error={warningConfirmNewPassword}
|
||||
/>
|
||||
{error ? (
|
||||
<p className="change-password__error">{error}</p>
|
||||
) : null}
|
||||
{success ? (
|
||||
<p className="change-password__success">
|
||||
Вы успешно поменяли пароль!
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="change-password__btns">
|
||||
<button type="button" onClick={changePass}>
|
||||
{loader ? <Loader /> : "Сохранить"}
|
||||
</button>
|
||||
<button type="button" onClick={() => closeWindow(false)}>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePassword;
|
||||
@@ -0,0 +1,43 @@
|
||||
.change-password-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
label {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
line-height: 20px;
|
||||
color: rgb(52, 64, 84);
|
||||
}
|
||||
|
||||
&__field,
|
||||
&__field-with-error {
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border: 1px solid rgb(208, 213, 221);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||
background: rgb(255, 255, 255);
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: rgb(102, 112, 133);
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: rgb(240, 68, 56);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import "./ChangePasswordInput.scss";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import eye_off from "./icons/eye-off.svg";
|
||||
import eye_on from "./icons/eye-on.svg";
|
||||
|
||||
interface IChangePasswordInputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const ChangePasswordInput: React.FC<IChangePasswordInputProps> = ({
|
||||
label,
|
||||
error,
|
||||
placeholder,
|
||||
name,
|
||||
onChange,
|
||||
value,
|
||||
}: IChangePasswordInputProps) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
return (
|
||||
<div className="change-password-input">
|
||||
<label>{label}</label>
|
||||
<div
|
||||
className={`change-password-input__field${
|
||||
error ? "-with-error" : ""
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
type={isOpen ? "text" : "password"}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
type="button"
|
||||
>
|
||||
<Image src={isOpen ? eye_on : eye_off} alt="Eye Icon" />
|
||||
</button>
|
||||
</div>
|
||||
{error ? <p>{error}</p> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePasswordInput;
|
||||
@@ -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_50526">
|
||||
<rect id="eye-off" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="eye-off" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip1668_50526)">
|
||||
<path id="Vector" d="M17.9404 17.9404C16.2305 19.2432 14.1494 19.9648 12 20C5 20 1 12 1 12C2.24414 9.68164 3.96875 7.65625 6.05957 6.05957M9.90039 4.24023C10.5879 4.0791 11.293 3.99805 12 4C19 4 23 12 23 12C22.3926 13.1357 21.6689 14.2051 20.8398 15.1904M14.1201 14.1201C13.8457 14.415 13.5137 14.6514 13.1465 14.8154C12.7783 14.9795 12.3809 15.0674 11.9785 15.0742C11.5752 15.0811 11.1748 15.0078 10.8018 14.8564C10.4277 14.7061 10.0889 14.4814 9.80371 14.1963C9.51855 13.9111 9.29395 13.5723 9.14355 13.1982C8.99219 12.8252 8.91895 12.4248 8.92578 12.0215C8.93262 11.6191 9.02051 11.2217 9.18457 10.8535C9.34863 10.4863 9.58496 10.1543 9.87988 9.87988" stroke="#979797" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
|
||||
<path id="Vector" d="M1 1L23 23" stroke="#979797" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -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 |
@@ -0,0 +1,79 @@
|
||||
.profile-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
|
||||
&__input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
label {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 135%;
|
||||
color: #686b7c;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
input {
|
||||
width: 100%;
|
||||
max-width: 957px;
|
||||
padding: 16px;
|
||||
border: 1px solid rgb(221, 222, 226);
|
||||
border-radius: 8px;
|
||||
background: rgb(255, 255, 255);
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 135%;
|
||||
color: #09090b;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__btn,
|
||||
&__btn_active {
|
||||
margin-top: 50px;
|
||||
width: fit-content;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(158, 167, 175);
|
||||
cursor: auto;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
line-height: 19px;
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
&__btn_active {
|
||||
background-color: rgb(57, 152, 232);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__logout {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.profile-form {
|
||||
&__btn,
|
||||
&__btn_active {
|
||||
align-self: flex-end;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 150px;
|
||||
}
|
||||
|
||||
&__logout {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
"use client";
|
||||
|
||||
import "./ProfileForm.scss";
|
||||
import Image from "next/image";
|
||||
import pen from "./icons/pen.svg";
|
||||
import { useState } from "react";
|
||||
import { apiInstance, authInstanse } from "@/shared/config/apiConfig";
|
||||
import { AxiosError } from "axios";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useSession } from "next-auth/react";
|
||||
import LogoutButton from "@/features/LogoutButton/LogoutButton";
|
||||
import ChangePassword from "./ChangePassword/ChangePassword";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IProfileFormProps {
|
||||
id: number;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
}
|
||||
|
||||
const ProfileForm: React.FC<IProfileFormProps> = ({
|
||||
id,
|
||||
email,
|
||||
first_name,
|
||||
last_name,
|
||||
}: IProfileFormProps) => {
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loader, setLoader] = useState<boolean>(false);
|
||||
|
||||
const [editFirstName, setEditFirstName] = useState<boolean>(true);
|
||||
const [editLastName, setEditLastName] = useState<boolean>(true);
|
||||
|
||||
const [firstName, setFirstName] = useState<string>(first_name);
|
||||
const [lastName, setLastName] = useState<string>(last_name);
|
||||
|
||||
const [openPopup, setOpenPopup] = useState<boolean>(false);
|
||||
|
||||
const thereAreChanges = () => {
|
||||
if (firstName !== first_name || lastName !== last_name)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const updateProfile: React.MouseEventHandler<
|
||||
HTMLFormElement
|
||||
> = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
};
|
||||
|
||||
const Authorization = `Bearer ${session.data?.access_token}`;
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
setLoader(true);
|
||||
const res = await apiInstance.patch(
|
||||
"/users/profile/update/",
|
||||
data,
|
||||
config
|
||||
);
|
||||
if ([200, 201].includes(res.status)) {
|
||||
router.refresh();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
setError(error.message);
|
||||
} else {
|
||||
setError("Возникла непредвиденная ошибка");
|
||||
}
|
||||
} finally {
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={updateProfile} className="profile-form">
|
||||
<div className="profile-form__input">
|
||||
<label>Имя</label>
|
||||
<div>
|
||||
<input
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
disabled={editFirstName}
|
||||
type="text"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditFirstName((prev) => !prev)}
|
||||
>
|
||||
<Image src={pen} alt="Pen Icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-form__input">
|
||||
<label>Фамилия</label>
|
||||
<div>
|
||||
<input
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
disabled={editLastName}
|
||||
type="text"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditLastName((prev) => !prev)}
|
||||
>
|
||||
<Image src={pen} alt="Pen Icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-form__input">
|
||||
<label>Email</label>
|
||||
<div>
|
||||
<input value={email} disabled type="text" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-form__input">
|
||||
<label>Пароль</label>
|
||||
<div>
|
||||
<input
|
||||
value={"*****************************"}
|
||||
disabled={editLastName}
|
||||
type="password"
|
||||
/>
|
||||
<button onClick={() => setOpenPopup(true)} type="button">
|
||||
<Image src={pen} alt="Pen Icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{openPopup && <ChangePassword closeWindow={setOpenPopup} />}
|
||||
|
||||
{error ? <p>{error}</p> : null}
|
||||
|
||||
<button
|
||||
disabled={thereAreChanges()}
|
||||
type="submit"
|
||||
className={`profile-form__btn${
|
||||
thereAreChanges() ? "" : "_active"
|
||||
}`}
|
||||
>
|
||||
{loader ? <Loader /> : "Сохранить изменения"}
|
||||
</button>
|
||||
<LogoutButton className="profile-form__logout" />
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileForm;
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg width="21.908203" height="22.000000" viewBox="0 0 21.9082 22" 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="Vector (Stroke)" d="M14.7539 1.22754C16.3906 -0.40918 19.041 -0.40918 20.6777 1.22754C22.3145 2.86426 22.3145 5.51562 20.6777 7.15234L8.5918 19.2383C8.45508 19.375 8.28516 19.4707 8.09766 19.5166L4.51562 20.3867C4.47852 20.3984 4.44141 20.4072 4.4043 20.4141L2.23047 20.9424C1.875 21.0293 1.5 20.9238 1.24023 20.665C0.982422 20.4062 0.876953 20.0312 0.962891 19.6758L2.38867 13.8076C2.43555 13.6211 2.53125 13.4502 2.66797 13.3145L14.7539 1.22754ZM4.53711 18.2236L7.31641 17.5479L19.1953 5.66895C20.0117 4.85156 20.0117 3.52832 19.1953 2.71094C18.377 1.89355 17.0547 1.89355 16.2363 2.71094L4.35742 14.5889L3.68359 17.3652L4.53711 18.2236Z" fill="#B2B5BE" fill-opacity="1.000000" fill-rule="evenodd"/>
|
||||
<path id="Union" d="M14.5332 4.09473C14.9434 3.68652 15.6074 3.6875 16.0176 4.09766L19.123 7.21191C19.5312 7.62207 19.5312 8.28613 19.1211 8.69531C18.7109 9.10449 18.0469 9.10352 17.6367 8.69336L14.5312 5.57812C14.123 5.16797 14.123 4.50391 14.5332 4.09473ZM8.77734 19.9238C8.77734 19.3447 9.24609 18.875 9.82617 18.875L20.8594 18.875C21.4375 18.875 21.9082 19.3447 21.9082 19.9238C21.9082 20.5029 21.4375 20.9727 20.8594 20.9727L9.82617 20.9727C9.24609 20.9727 8.77734 20.5029 8.77734 19.9238Z" fill="#B2B5BE" fill-opacity="1.000000" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,7 @@
|
||||
import "./CreateReportMap.scss";
|
||||
|
||||
const CreateReportMap = () => {
|
||||
return <div>CreateReportMap</div>;
|
||||
};
|
||||
|
||||
export default CreateReportMap;
|
||||
@@ -0,0 +1,263 @@
|
||||
.report-form {
|
||||
padding: 40px 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
border: 1px solid rgb(213, 213, 213);
|
||||
border-radius: 10px;
|
||||
|
||||
label {
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
line-height: 26px;
|
||||
color: rgb(50, 48, 58);
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100%;
|
||||
max-width: 801px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 22px;
|
||||
|
||||
input,
|
||||
textarea {
|
||||
height: 52px;
|
||||
padding: 16px 20px;
|
||||
width: 100%;
|
||||
border: 1px solid rgb(197, 198, 197);
|
||||
border-radius: 6px;
|
||||
background: rgb(255, 255, 255);
|
||||
font-size: 17px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: rgb(50, 48, 58);
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 276px;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: rgb(197, 198, 197);
|
||||
}
|
||||
|
||||
p {
|
||||
color: rgb(240, 68, 56);
|
||||
}
|
||||
}
|
||||
|
||||
&__error {
|
||||
color: rgb(240, 68, 56);
|
||||
}
|
||||
|
||||
&__map {
|
||||
width: 100%;
|
||||
height: 410px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 286px;
|
||||
border: 1px solid rgb(197, 198, 197);
|
||||
border-radius: 6px;
|
||||
|
||||
.leaflet-container {
|
||||
border-top-left-radius: 6px;
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
padding: 24px;
|
||||
padding-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 11px;
|
||||
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
color: rgb(50, 48, 58);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 140%;
|
||||
color: rgb(50, 48, 58);
|
||||
span {
|
||||
color: rgb(102, 102, 102);
|
||||
}
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-top: 42px;
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
&__add-images {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: rgb(102, 102, 102);
|
||||
}
|
||||
|
||||
&-error {
|
||||
color: rgb(240, 68, 56) !important;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__upload {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: rgb(50, 48, 58);
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
color: rgb(176, 176, 176);
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__images {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
img {
|
||||
width: 187px;
|
||||
height: 187px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
button {
|
||||
width: fit-content;
|
||||
align-self: flex-end;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
text-decoration: underline;
|
||||
color: rgb(50, 48, 58);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
margin-top: 40px;
|
||||
padding: 15px;
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border: 1px solid rgb(72, 159, 225), rgb(72, 159, 225);
|
||||
border-radius: 5px;
|
||||
background-color: rgb(72, 159, 225);
|
||||
color: rgb(255, 255, 255);
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
line-height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.report-form {
|
||||
padding: 36px 20px;
|
||||
gap: 30px;
|
||||
|
||||
label {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&__input {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
&__map {
|
||||
grid-template-columns: 1fr;
|
||||
height: fit-content;
|
||||
|
||||
.leaflet-container {
|
||||
height: 410px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.report-form {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
gap: 26px;
|
||||
|
||||
label {
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__input {
|
||||
gap: 6px;
|
||||
|
||||
textarea {
|
||||
height: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
&__map {
|
||||
.leaflet-container {
|
||||
height: 370px;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
padding: 20px 10px;
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
line-height: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-images {
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
font-size: 19px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
"use client";
|
||||
|
||||
import "./ReportForm.scss";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
MapContainer,
|
||||
Marker,
|
||||
TileLayer,
|
||||
useMapEvents,
|
||||
} from "react-leaflet";
|
||||
import clip from "./icons/clip.svg";
|
||||
import arrow_right from "./icons/arrow-right.svg";
|
||||
import pin_image from "./icons/pin-image.svg";
|
||||
import { ChangeEventHandler, useState } from "react";
|
||||
import { Icon } from "leaflet";
|
||||
import pin_icon from "./icons/pin_icon.svg";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { IDisplayMap } from "@/shared/types/map-type";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface ILatLng {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
|
||||
const ReportForm = () => {
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
const [latLng, setLatLng] = useState<ILatLng[]>([]);
|
||||
const [displayLatLng, setDisplayLatLng] = useState<string[]>([]);
|
||||
const [images, setImages] = useState<File[]>([]);
|
||||
const position = {
|
||||
lat: 42.8746,
|
||||
lng: 74.606,
|
||||
};
|
||||
const [loader, setLoader] = useState<boolean>(false);
|
||||
const [locationWarning, setLocationWarning] = useState<string>("");
|
||||
const [descriptionWarning, setDescriptionWarning] =
|
||||
useState<string>("");
|
||||
const [imageWarning, setImageWarning] = useState<string>("");
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const customIcon = new Icon({
|
||||
iconUrl: pin_icon.src,
|
||||
iconSize: [32, 32],
|
||||
});
|
||||
|
||||
const handleImages: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
if (e.target.files) {
|
||||
const getArrayFromObject = Array.from(e.target.files);
|
||||
setImages(getArrayFromObject);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteImage = (name: string) => {
|
||||
setImages((prev) => prev.filter((img) => img.name !== name));
|
||||
};
|
||||
|
||||
const createReport: React.MouseEventHandler<
|
||||
HTMLFormElement
|
||||
> = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
if (displayLatLng.length === 0) {
|
||||
setDescriptionWarning("");
|
||||
setImageWarning("");
|
||||
setError("");
|
||||
setLocationWarning("Выберите хотя бы одну точку!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.get("description")) {
|
||||
setLocationWarning("");
|
||||
setImageWarning("");
|
||||
setError("");
|
||||
|
||||
setDescriptionWarning("Пожалуйста опишите свое обращение");
|
||||
return;
|
||||
}
|
||||
|
||||
if (images.length === 0) {
|
||||
setLocationWarning("");
|
||||
setDescriptionWarning("");
|
||||
setError("");
|
||||
|
||||
setImageWarning("Загрузите по меньшей мере одну фотографию");
|
||||
return;
|
||||
}
|
||||
|
||||
setLocationWarning("");
|
||||
setDescriptionWarning("");
|
||||
setError("");
|
||||
setImageWarning("");
|
||||
|
||||
const Authorization = `Bearer ${session.data?.access_token}`;
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization,
|
||||
},
|
||||
};
|
||||
|
||||
images.forEach((image) => {
|
||||
formData.append("image", image);
|
||||
});
|
||||
formData.append("latitude1", latLng[0].lat.toString());
|
||||
formData.append("longitude1", latLng[0].lng.toString());
|
||||
formData.append("latitude2", latLng[1].lat.toString());
|
||||
formData.append("longitude2", latLng[1].lng.toString());
|
||||
|
||||
try {
|
||||
setLoader(true);
|
||||
const res = await apiInstance.post(
|
||||
"/report/create/",
|
||||
formData,
|
||||
config
|
||||
);
|
||||
|
||||
if ([200, 201].includes(res.status)) {
|
||||
router.push("/");
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
setError(error.message);
|
||||
} else {
|
||||
setError("Произошла непредвиденная ошибка");
|
||||
}
|
||||
} finally {
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const MapPins = (e: any) => {
|
||||
useMapEvents({
|
||||
click(e) {
|
||||
if (latLng.length < 2) {
|
||||
setLatLng([...latLng, e.latlng]);
|
||||
|
||||
axios
|
||||
.get(
|
||||
`https://nominatim.openstreetmap.org/reverse?lat=${e.latlng.lat}&lon=${e.latlng.lng}&format=json`
|
||||
)
|
||||
.then((res) =>
|
||||
setDisplayLatLng([
|
||||
...displayLatLng,
|
||||
res.data.display_name,
|
||||
])
|
||||
)
|
||||
.catch((error) => console.log(error));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const removeMarker = (latLng: { lat: number; lng: number }) => {
|
||||
setLatLng((prev) => prev.filter((l) => l !== latLng));
|
||||
|
||||
axios
|
||||
.get<IDisplayMap>(
|
||||
`https://nominatim.openstreetmap.org/reverse?lat=${latLng.lat}&lon=${latLng.lng}&format=json`
|
||||
)
|
||||
.then((res) =>
|
||||
setDisplayLatLng((prev) =>
|
||||
prev.filter((loc) => loc !== res.data.display_name)
|
||||
)
|
||||
)
|
||||
.catch((error) => console.log(error));
|
||||
};
|
||||
|
||||
return latLng.map((l) => (
|
||||
<Marker
|
||||
eventHandlers={{
|
||||
click: () => {
|
||||
removeMarker(l);
|
||||
},
|
||||
}}
|
||||
key={l.lat}
|
||||
icon={customIcon}
|
||||
position={l}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={createReport} className="report-form">
|
||||
<div className="report-form__input">
|
||||
<label>Адрес</label>
|
||||
<input
|
||||
value={displayLatLng.join("; ")}
|
||||
disabled
|
||||
placeholder="Отметьте точки на карте"
|
||||
type="text"
|
||||
/>
|
||||
{locationWarning ? <p>{locationWarning}</p> : null}
|
||||
</div>
|
||||
|
||||
<div className="report-form__map">
|
||||
<MapContainer
|
||||
center={position}
|
||||
zoom={14}
|
||||
scrollWheelZoom={false}
|
||||
>
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
<MapPins />
|
||||
</MapContainer>
|
||||
|
||||
<div className="report-form__info">
|
||||
<h4>Как отметить участок дороги? </h4>
|
||||
<Image src={pin_image} alt="Pin Image" />
|
||||
<p>
|
||||
Поставьте булавку и начните рисовать участок дороги{" "}
|
||||
<span>
|
||||
(он может состоять из любого количества ломаных линий)
|
||||
</span>
|
||||
</p>
|
||||
<p>Чтобы удалить отрезок нажмите на точки повторно.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="report-form__input">
|
||||
<label>Добавьте описание проблемы</label>
|
||||
<textarea name="description" placeholder="Введите описание" />
|
||||
{descriptionWarning ? <p>{descriptionWarning}</p> : null}
|
||||
</div>
|
||||
|
||||
<div className="report-form__add-images">
|
||||
<label>Добавьте фотографии</label>
|
||||
<p>
|
||||
Загрузите до 5 фотографии, связанные с дорогой, которую Вы
|
||||
хотите отметить. Фотографии помогут лучше понять проблему.
|
||||
</p>
|
||||
<label
|
||||
className="report-form__upload"
|
||||
htmlFor="report-form-upload"
|
||||
>
|
||||
<Image src={clip} alt="Paper Clip Icon" />
|
||||
Прикрепить файл
|
||||
<span>(до 5 МБ)</span>
|
||||
</label>
|
||||
{imageWarning ? (
|
||||
<p className="report-form__add-images-error">
|
||||
{imageWarning}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<input
|
||||
onChange={handleImages}
|
||||
multiple
|
||||
type="file"
|
||||
id="report-form-upload"
|
||||
/>
|
||||
{images.length ? (
|
||||
<div className="report-form__images">
|
||||
{images.map((image) => (
|
||||
<div key={image.name}>
|
||||
<img
|
||||
src={URL.createObjectURL(image)}
|
||||
alt="Report Image"
|
||||
/>
|
||||
<button onClick={() => deleteImage(image.name)}>
|
||||
удалить
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<button disabled={loader} type="submit">
|
||||
{loader ? <Loader /> : "Отправить на модерацию"}
|
||||
<Image src={arrow_right} alt="Arrow Right Icon" />
|
||||
</button>
|
||||
{error ? <p className="report-form__error">{error}</p> : null}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportForm;
|
||||
@@ -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="clip345_6572">
|
||||
<rect id="arrow-right" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="arrow-right" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip345_6572)">
|
||||
<path id="Vector" d="M5 12L19 12" stroke="#FFFFFF" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path id="Vector" d="M12 5L19 12L12 19" stroke="#FFFFFF" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 804 B |
@@ -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="clip345_6197">
|
||||
<rect id="paperclip" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="paperclip" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip345_6197)">
|
||||
<path id="Vector" d="M21.4404 11.0498L12.25 20.2402C11.124 21.3662 9.59766 21.998 8.00488 21.998C6.41309 21.998 4.88574 21.3662 3.76074 20.2402C2.63477 19.1143 2.00195 17.5869 2.00195 15.9951C2.00195 14.4023 2.63477 12.876 3.76074 11.75L12.9502 2.55957C13.7012 1.80957 14.7188 1.3877 15.7803 1.3877C16.8418 1.3877 17.8594 1.80957 18.6104 2.55957C19.3613 3.31055 19.7822 4.32812 19.7822 5.38965C19.7822 6.45117 19.3613 7.46973 18.6104 8.21973L9.41016 17.4102C9.03516 17.7852 8.52637 17.9961 7.99512 17.9961C7.46484 17.9961 6.95605 17.7852 6.58008 17.4102C6.20508 17.0342 5.99414 16.5254 5.99414 15.9951C5.99414 15.4639 6.20508 14.9551 6.58008 14.5801L15.0703 6.09961" stroke="#32303A" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg width="81.304688" height="64.551758" viewBox="0 0 81.3047 64.5518" 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="clip401_4465">
|
||||
<rect id="pin" width="50.000000" height="50.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="pin" width="50.000000" height="50.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip401_4465)">
|
||||
<path id="Vector" d="M32.8125 9.375C32.8125 7.89746 32.3926 6.44922 31.6035 5.2002C30.8125 3.9502 29.6836 2.95117 28.3496 2.31738C27.0137 1.68359 25.5254 1.44238 24.0586 1.62012C22.5898 1.79883 21.2031 2.38965 20.0586 3.32422C18.9141 4.25977 18.0566 5.5 17.5898 6.90234C17.1211 8.30469 17.0605 9.81055 17.4141 11.2461C17.7676 12.6816 18.5215 13.9863 19.5879 15.0107C20.6543 16.0342 21.9883 16.7354 23.4375 17.0303L23.4375 44.6836C23.4375 45.0986 23.5195 45.5098 23.6816 45.8926L24.6641 48.2363C24.6973 48.2969 24.7461 48.3477 24.8047 48.3828C24.8633 48.417 24.9316 48.4355 25 48.4355C25.0684 48.4355 25.1367 48.417 25.1953 48.3828C25.2539 48.3477 25.3027 48.2969 25.3359 48.2363L26.3203 45.8926C26.4805 45.5098 26.5625 45.0986 26.5625 44.6836L26.5625 17.0303C28.3262 16.668 29.9102 15.709 31.0488 14.3145C32.1875 12.9199 32.8105 11.1758 32.8125 9.375ZM27.3438 9.375C26.8809 9.375 26.4277 9.23828 26.041 8.98047C25.6562 8.72266 25.3555 8.35645 25.1777 7.92871C25.002 7.5 24.9551 7.0293 25.0449 6.57422C25.1348 6.12012 25.3594 5.70215 25.6855 5.37402C26.0137 5.04688 26.4316 4.82324 26.8867 4.73242C27.3418 4.64258 27.8125 4.68848 28.2402 4.86621C28.668 5.04395 29.0352 5.34375 29.293 5.72949C29.5508 6.11523 29.6875 6.56836 29.6875 7.03125C29.6875 7.65332 29.4414 8.24902 29.002 8.68848C28.5605 9.12891 27.9648 9.375 27.3438 9.375Z" fill="#E64452" fill-opacity="1.000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
<circle id="Ellipse 129" cx="25.000000" cy="51.000000" r="5.000000" fill="#FFFFFF" fill-opacity="1.000000"/>
|
||||
<circle id="Ellipse 129" cx="25.000000" cy="51.000000" r="4.750000" stroke="#000000" stroke-opacity="1.000000" stroke-width="0.500000"/>
|
||||
<line id="Line 12" x1="33.294922" y1="53.529297" x2="81.009766" y2="63.081055" stroke="#E64452" stroke-opacity="1.000000" stroke-width="3.000000" stroke-dasharray="6.000000,6.000000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg width="50.000000" height="50.000000" viewBox="0 0 50 50" 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="clip345_6143">
|
||||
<rect id="pin" width="50.000000" height="50.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="pin" width="50.000000" height="50.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip345_6143)">
|
||||
<path id="Vector" d="M32.8125 9.375C32.8125 7.89746 32.3926 6.44922 31.6025 5.2002C30.8125 3.9502 29.6846 2.95117 28.3486 2.31738C27.0127 1.68359 25.5254 1.44238 24.0576 1.62012C22.5908 1.79883 21.2041 2.38965 20.0586 3.32422C18.9141 4.25977 18.0576 5.5 17.5898 6.90234C17.1211 8.30469 17.0605 9.81055 17.415 11.2461C17.7686 12.6816 18.5225 13.9863 19.5879 15.0107C20.6543 16.0342 21.9893 16.7354 23.4375 17.0303L23.4375 44.6836C23.4375 45.0986 23.5205 45.5098 23.6807 45.8926L24.6641 48.2363C24.6973 48.2969 24.7451 48.3477 24.8047 48.3828C24.8643 48.417 24.9316 48.4355 25 48.4355C25.0684 48.4355 25.1357 48.417 25.1953 48.3828C25.2549 48.3477 25.3027 48.2969 25.3359 48.2363L26.3193 45.8926C26.4795 45.5098 26.5625 45.0986 26.5625 44.6836L26.5625 17.0303C28.3262 16.668 29.9102 15.709 31.0488 14.3145C32.1875 12.9199 32.8105 11.1758 32.8125 9.375ZM27.3438 9.375C26.8799 9.375 26.4268 9.23828 26.042 8.98047C25.6562 8.72266 25.3555 8.35645 25.1787 7.92871C25.001 7.5 24.9551 7.0293 25.0449 6.57422C25.1357 6.12012 25.3584 5.70215 25.6865 5.37402C26.0146 5.04688 26.4316 4.82324 26.8867 4.73242C27.3408 4.64258 27.8125 4.68848 28.2402 4.86621C28.6689 5.04395 29.0352 5.34375 29.293 5.72949C29.5498 6.11523 29.6875 6.56836 29.6875 7.03125C29.6875 7.65332 29.4404 8.24902 29.001 8.68848C28.5615 9.12891 27.9658 9.375 27.3438 9.375Z" fill="#E64452" fill-opacity="1.000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,39 @@
|
||||
@import "@/shared/ui/variables.scss";
|
||||
|
||||
.reset-code-form {
|
||||
width: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
&__error {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-top: 32px;
|
||||
padding: 10px 18px;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||
background-color: rgb(54, 149, 216);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.reset-code-form {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import AuthInput from "@/features/AuthInput/AuthInput";
|
||||
import "./ResetCodeForm.scss";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { ITokens } from "@/shared/types/token-type";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
const ResetCodeForm = () => {
|
||||
const [passwordWarning, setPasswordWarning] = useState<string>("");
|
||||
const [passwordConfirmWarning, setPasswordConfirmWarning] =
|
||||
useState<string>("");
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loader, setLoader] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit: React.MouseEventHandler<
|
||||
HTMLFormElement
|
||||
> = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
if (!formData.get("new_password1")) {
|
||||
setError("");
|
||||
setPasswordConfirmWarning("");
|
||||
setPasswordWarning("Заполните поле с новым паролем");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.get("new_password1")) {
|
||||
setError("");
|
||||
setPasswordWarning("");
|
||||
setPasswordConfirmWarning(
|
||||
"Заполните поле с новым паролем потверждения"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError("");
|
||||
setPasswordWarning("");
|
||||
setPasswordConfirmWarning("");
|
||||
setLoader(true);
|
||||
|
||||
const storage = localStorage.getItem("transitional");
|
||||
if (storage === null) return;
|
||||
const transitional: ITokens = JSON.parse(storage);
|
||||
|
||||
const Authorization = `Bearer ${transitional.access_token}`;
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apiInstance.put(
|
||||
"/users/password_reset/confirm/",
|
||||
formData,
|
||||
config
|
||||
);
|
||||
|
||||
if (response.status === 200 || response.status === 201) {
|
||||
localStorage.removeItem("transitional");
|
||||
router.push("/sign-in");
|
||||
}
|
||||
} catch (error) {
|
||||
setError("An error ocured");
|
||||
} finally {
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="reset-code-form">
|
||||
<div className="reset-code-form__inputs">
|
||||
<AuthInput
|
||||
name="new_password1"
|
||||
label="Введите пароль"
|
||||
error={passwordWarning}
|
||||
isPassword
|
||||
placeholder="Введите новый пароль"
|
||||
/>
|
||||
<AuthInput
|
||||
name="new_password2"
|
||||
label="Повторите пароль"
|
||||
error={passwordConfirmWarning}
|
||||
isPassword
|
||||
placeholder="Потвердите новый пароль"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<p className="reset-code-form__error">{error}</p>
|
||||
) : null}
|
||||
<button className="reset-code-form__btn" type="submit">
|
||||
{loader ? <Loader /> : "Сохранить"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetCodeForm;
|
||||
@@ -0,0 +1,55 @@
|
||||
@import "@/shared/ui/variables.scss";
|
||||
|
||||
.sign-in-form {
|
||||
margin-bottom: 8px;
|
||||
width: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__error {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
&__forgot-password {
|
||||
margin: 36px 0;
|
||||
align-self: flex-end;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
&__inputs,
|
||||
&__btns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
&__btns {
|
||||
gap: 16px;
|
||||
|
||||
&_first {
|
||||
padding: 10px 18px;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
color: #fff;
|
||||
background-color: $light-blue;
|
||||
border: 1px solid rgb(72, 159, 225);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||
background: rgb(72, 159, 225);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.sign-in-form {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import "./SignInForm.scss";
|
||||
import AuthInput from "@/features/AuthInput/AuthInput";
|
||||
import GoogleButton from "@/features/GoogleButton/GoogleButton";
|
||||
import { Link, useRouter } from "@/shared/config/navigation";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
|
||||
const SignInForm = () => {
|
||||
const [emailWarning, setEmailWarning] = useState<string>("");
|
||||
const [passwordWarning, setPasswordWarning] = useState<string>("");
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loader, setLoader] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit: React.MouseEventHandler<
|
||||
HTMLFormElement
|
||||
> = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
if (!formData.get("email")?.toString()) {
|
||||
setError("");
|
||||
setPasswordWarning("");
|
||||
setEmailWarning("Заполните поле Email");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.get("password")?.toString()) {
|
||||
setError("");
|
||||
setEmailWarning("");
|
||||
setPasswordWarning("Заполните поле Пароль");
|
||||
return;
|
||||
}
|
||||
|
||||
setError("");
|
||||
setEmailWarning("");
|
||||
setPasswordWarning("");
|
||||
setLoader(true);
|
||||
|
||||
const res = await signIn("credentials", {
|
||||
email: formData.get("email"),
|
||||
password: formData.get("password"),
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
setLoader(false);
|
||||
|
||||
if (res?.ok && !res.error) {
|
||||
router.push("/profile/personal");
|
||||
} else if (res?.status.toString().slice(0, 1) === "4") {
|
||||
setError("Неверный Email или Пароль");
|
||||
} else {
|
||||
setError("Произошла непредвиденная ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="sign-in-form" onSubmit={handleSubmit}>
|
||||
<div className="sign-in-form__inputs">
|
||||
<AuthInput
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="Введите email"
|
||||
error={emailWarning}
|
||||
name="email"
|
||||
/>
|
||||
<AuthInput
|
||||
isPassword
|
||||
label="Пароль"
|
||||
placeholder="Введите пароль"
|
||||
error={passwordWarning}
|
||||
name="password"
|
||||
/>
|
||||
</div>
|
||||
{error ? <p className="sign-in-form__error">{error}</p> : null}
|
||||
<Link
|
||||
href="/sign-in/forgot-password"
|
||||
className="sign-in-form__forgot-password"
|
||||
>
|
||||
Забыли пароль?
|
||||
</Link>
|
||||
|
||||
<div className="sign-in-form__btns">
|
||||
<button className="sign-in-form__btns_first" type="submit">
|
||||
{loader ? <Loader /> : "Войти"}
|
||||
</button>
|
||||
<GoogleButton />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignInForm;
|
||||
@@ -0,0 +1,56 @@
|
||||
@import "@/shared/ui/variables.scss";
|
||||
|
||||
.sign-up-form {
|
||||
margin-bottom: 8px;
|
||||
width: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__error {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
&__forgot-password {
|
||||
margin: 36px 0;
|
||||
align-self: flex-end;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
&__inputs,
|
||||
&__btns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
&__btns {
|
||||
margin-top: 36px;
|
||||
gap: 16px;
|
||||
|
||||
&_first {
|
||||
padding: 10px 18px;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
color: #fff;
|
||||
background-color: $light-blue;
|
||||
border: 1px solid rgb(72, 159, 225);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||
background: rgb(72, 159, 225);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.sign-up-form {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import "./SignUpForm.scss";
|
||||
import { useState } from "react";
|
||||
import AuthInput from "@/features/AuthInput/AuthInput";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import GoogleButton from "@/features/GoogleButton/GoogleButton";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { AxiosError } from "axios";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
const SignUpForm = () => {
|
||||
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 router = useRouter();
|
||||
|
||||
const handleSubmit: React.MouseEventHandler<
|
||||
HTMLFormElement
|
||||
> = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
if (!formData.get("email")?.toString()) {
|
||||
setError("");
|
||||
setPasswordWarning("");
|
||||
setPasswordConfirmWarning("");
|
||||
setEmailWarning("Заполните поле Email");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.get("password")?.toString()) {
|
||||
setError("");
|
||||
setEmailWarning("");
|
||||
setPasswordConfirmWarning("");
|
||||
setPasswordWarning("Заполните поле Пароль");
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
setError("");
|
||||
setEmailWarning("");
|
||||
setPasswordWarning("");
|
||||
setPasswordConfirmWarning("");
|
||||
setLoader(true);
|
||||
|
||||
const res = await apiInstance.post(
|
||||
"/users/register/",
|
||||
formData
|
||||
);
|
||||
|
||||
if ([200, 201].includes(res.status)) {
|
||||
router.push(
|
||||
`/sign-up/confirm-email/?email=${formData.get("email")}`
|
||||
);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
setError("Произошла непредвиденная ошибка");
|
||||
} else {
|
||||
setError("An error ocured");
|
||||
}
|
||||
} finally {
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="sign-up-form" onSubmit={handleSubmit}>
|
||||
<div className="sign-up-form__inputs">
|
||||
<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>
|
||||
{error ? <p className="sign-up-form__error">{error}</p> : null}
|
||||
|
||||
<div className="sign-up-form__btns">
|
||||
<button className="sign-up-form__btns_first" type="submit">
|
||||
{loader ? <Loader /> : "Войти"}
|
||||
</button>
|
||||
<GoogleButton />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignUpForm;
|
||||