diff --git a/src/App/about-us/page.tsx b/src/App/about-us/page.tsx index 4ebdc5c..265d338 100644 --- a/src/App/about-us/page.tsx +++ b/src/App/about-us/page.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import "./styles.scss"; const AboutUs = () => { return
AboutUs
; diff --git a/src/App/sign-in/styles.module.scss b/src/App/about-us/styles.scss similarity index 100% rename from src/App/sign-in/styles.module.scss rename to src/App/about-us/styles.scss diff --git a/src/App/create-report/CreateReport.scss b/src/App/create-report/CreateReport.scss new file mode 100644 index 0000000..b39459a --- /dev/null +++ b/src/App/create-report/CreateReport.scss @@ -0,0 +1,5 @@ +.create-report { + h2 { + text-align: start; + } +} diff --git a/src/App/create-report/page.tsx b/src/App/create-report/page.tsx new file mode 100644 index 0000000..37868c6 --- /dev/null +++ b/src/App/create-report/page.tsx @@ -0,0 +1,14 @@ +import Typography from "@/shared/ui/components/Typography/Typography"; +import "./CreateReport.scss"; + +const CreateReport = () => { + return ( +
+ Написать обращение + +
+
+ ); +}; + +export default CreateReport; diff --git a/src/App/globals.scss b/src/App/globals.scss index 21f6387..1ebd0de 100644 --- a/src/App/globals.scss +++ b/src/App/globals.scss @@ -28,3 +28,30 @@ input { border: none; outline: none; } + +table { + width: 100%; + display: flex; + flex-direction: column; +} + +ul, +ol { + list-style-type: none; +} + +.page-padding { + padding: 40px 90px; +} + +@media screen and (max-width: 1024px) { + .page-padding { + padding: 40px 30px; + } +} + +@media screen and (max-width: 550px) { + .page-padding { + padding: 40px 16px; + } +} diff --git a/src/App/profile/page.tsx b/src/App/profile/page.tsx index 7f6b836..166d4e2 100644 --- a/src/App/profile/page.tsx +++ b/src/App/profile/page.tsx @@ -1,5 +1,19 @@ +"use client"; + +import { signOut } from "next-auth/react"; +import Link from "next/link"; + const Profile = () => { - return
Profile
; + return ( +
+ signOut({ callbackUrl: "/sign-in" })} + > + Выйти в окно + +
+ ); }; export default Profile; diff --git a/src/App/sign-in/SignIn.scss b/src/App/sign-in/SignIn.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/App/sign-in/forgot-password/page.tsx b/src/App/sign-in/forgot-password/page.tsx new file mode 100644 index 0000000..bcd9397 --- /dev/null +++ b/src/App/sign-in/forgot-password/page.tsx @@ -0,0 +1,12 @@ +import "@/shared/ui/auth-classes.scss"; +import ForgotPasswordForm from "@/widgets/ForgotPasswordForm/ForgotPasswordForm"; + +const ForgotPassword = () => { + return ( +
+ +
+ ); +}; + +export default ForgotPassword; diff --git a/src/App/sign-in/icons/sign-in_icon.svg b/src/App/sign-in/icons/sign-in_icon.svg new file mode 100644 index 0000000..64719bd --- /dev/null +++ b/src/App/sign-in/icons/sign-in_icon.svg @@ -0,0 +1,14 @@ + + + Created with Pixso. + + + + + + + + + + + diff --git a/src/App/sign-in/page.tsx b/src/App/sign-in/page.tsx index b65b9d7..837f527 100644 --- a/src/App/sign-in/page.tsx +++ b/src/App/sign-in/page.tsx @@ -1,8 +1,29 @@ -import React from "react"; -import classes from "./styles.module.scss"; +import "@/shared/ui/auth-classes.scss"; +import Image from "next/image"; +import sign_in_icon from "./icons/sign-in_icon.svg"; +import SignInForm from "@/widgets/SignInForm/SignInForm"; +import Link from "next/link"; const SignIn = () => { - return
SignIn
; + return ( +
+
+
+ Sign In Icon +
+
+

Войдите в аккаунт

+

Пожалуйста, введите свои данные

+
+ + +

+ Еще нет аккаунта?{" "} + Зарегистрируйтесь +

+
+
+ ); }; export default SignIn; diff --git a/src/App/sign-in/reset-code/icons/key.svg b/src/App/sign-in/reset-code/icons/key.svg new file mode 100644 index 0000000..88beab1 --- /dev/null +++ b/src/App/sign-in/reset-code/icons/key.svg @@ -0,0 +1,14 @@ + + + Created with Pixso. + + + + + + + + + + + diff --git a/src/App/sign-in/reset-code/page.tsx b/src/App/sign-in/reset-code/page.tsx new file mode 100644 index 0000000..fa29c86 --- /dev/null +++ b/src/App/sign-in/reset-code/page.tsx @@ -0,0 +1,28 @@ +import "@/shared/ui/auth-classes.scss"; +import Image from "next/image"; +import key from "./icons/key.svg"; +import ResetCodeForm from "@/widgets/ResetCodeForm/ResetCodeForm"; + +const ResetCode = () => { + return ( +
+
+
+ Key Icon +
+ +
+

Введите новый пароль

+

+ Пароль должен содерждать минимум 8 символов, 1 заглавная + буква и цифра +

+
+ + +
+
+ ); +}; + +export default ResetCode; diff --git a/src/App/sign-up/confirm-email/icons/mail.svg b/src/App/sign-up/confirm-email/icons/mail.svg new file mode 100644 index 0000000..dc055d2 --- /dev/null +++ b/src/App/sign-up/confirm-email/icons/mail.svg @@ -0,0 +1,14 @@ + + + Created with Pixso. + + + + + + + + + + + diff --git a/src/App/sign-up/confirm-email/page.tsx b/src/App/sign-up/confirm-email/page.tsx new file mode 100644 index 0000000..a287680 --- /dev/null +++ b/src/App/sign-up/confirm-email/page.tsx @@ -0,0 +1,29 @@ +import "@/shared/ui/auth-classes.scss"; +import Image from "next/image"; +import mail from "./icons/mail.svg"; +import ConfirmEmailForm from "@/widgets/ConfirmEmailForm/ConfirmEmailForm"; + +const ConfirmEmail = ({ + searchParams, +}: { + searchParams: { + email: string; + }; +}) => { + return ( +
+
+
+ Mail icon +
+
+

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

+

Мы отправили код на почту {searchParams.email}

+
+ +
+
+ ); +}; + +export default ConfirmEmail; diff --git a/src/App/sign-up/icons/flag.svg b/src/App/sign-up/icons/flag.svg new file mode 100644 index 0000000..4d918fa --- /dev/null +++ b/src/App/sign-up/icons/flag.svg @@ -0,0 +1,7 @@ + + + Created with Pixso. + + + + diff --git a/src/App/sign-up/page.tsx b/src/App/sign-up/page.tsx new file mode 100644 index 0000000..80b3e4d --- /dev/null +++ b/src/App/sign-up/page.tsx @@ -0,0 +1,30 @@ +import "@/shared/ui/auth-classes.scss"; +import Image from "next/image"; +import flag from "./icons/flag.svg"; +import Link from "next/link"; +import SignUpForm from "@/widgets/SignUpForm/SignUpForm"; + +const SignUp = () => { + return ( +
+
+
+ Flag Icon +
+
+

Регистрация

+

Пожалуйста, введите свои данные

+
+ + + +

+ Уже есть аккаунт?{" "} + Войти в аккаунт +

+
+
+ ); +}; + +export default SignUp; diff --git a/src/Entities/SectionHeader/SectionHeader.scss b/src/Entities/SectionHeader/SectionHeader.scss deleted file mode 100644 index f43dced..0000000 --- a/src/Entities/SectionHeader/SectionHeader.scss +++ /dev/null @@ -1,47 +0,0 @@ -.section-header { - margin-bottom: 52px; - display: flex; - flex-direction: column; - align-items: center; - gap: 24px; - text-align: center; - - h3 { - font-size: 42px; - font-weight: 500; - line-height: 50px; - } - - p { - font-size: 24px; - font-weight: 300; - line-height: 29px; - } -} - -@media screen and (max-width: 768px) { - .section-header { - margin-bottom: 42px; - - h3 { - font-size: 36px; - line-height: 43px; - } - } -} - -@media screen and (max-width: 550px) { - .section-header { - margin-bottom: 42px; - - h3 { - font-size: 24px; - line-height: 29px; - } - - p { - font-size: 18px; - line-height: 22px; - } - } -} diff --git a/src/Entities/SectionHeader/SectionHeader.tsx b/src/Entities/SectionHeader/SectionHeader.tsx deleted file mode 100644 index 5e36fb8..0000000 --- a/src/Entities/SectionHeader/SectionHeader.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import "./SectionHeader.scss"; - -interface ISectionHeaderProps { - title: string; - description?: string; - style?: object; -} - -const SectionHeader: React.FC = ({ - title, - description, - style, -}: ISectionHeaderProps) => { - return ( -
-

{title}

- {description &&

{description}

} -
- ); -}; - -export default SectionHeader; diff --git a/src/Shared/hooks/useTimeout.tsx b/src/Shared/hooks/useTimeout.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/Shared/icons/arrows.svg b/src/Shared/icons/arrows.svg new file mode 100644 index 0000000..f53fb73 --- /dev/null +++ b/src/Shared/icons/arrows.svg @@ -0,0 +1,21 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + + diff --git a/src/Shared/types/map-type.ts b/src/Shared/types/map-type.ts new file mode 100644 index 0000000..49c93c2 --- /dev/null +++ b/src/Shared/types/map-type.ts @@ -0,0 +1,16 @@ +export interface IAddress { + city: string; + city_district: string; + country: string; + country_code: string; + road: string; + suburb: string; +} + +export interface IDisplayMap { + lat: string; + lon: string; + address: IAddress; + place_id: number; + display_name: string; +} diff --git a/src/Shared/types/token-type.ts b/src/Shared/types/token-type.ts new file mode 100644 index 0000000..03dc6d2 --- /dev/null +++ b/src/Shared/types/token-type.ts @@ -0,0 +1,5 @@ +export interface ITokens { + refresh_token: string; + access_token: string; + expires_in: string; +} diff --git a/src/Shared/ui/auth-classes.scss b/src/Shared/ui/auth-classes.scss new file mode 100644 index 0000000..406bbfb --- /dev/null +++ b/src/Shared/ui/auth-classes.scss @@ -0,0 +1,99 @@ +@import "./variables.scss"; + +.auth-page { + padding: 96px 32px 48px 32px; + height: 100vh; + min-height: 800px; + width: 100%; + display: flex; + justify-content: center; +} + +.auth-wrapper { + display: flex; + align-items: center; + flex-direction: column; + gap: 24px; +} + +.auth-icon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid rgb(234, 236, 240); + border-radius: 10px; + box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); + background: rgb(255, 255, 255); +} + +.auth-icon2 { + width: 60px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + border: 8px solid rgba(72, 159, 225, 0.15); + border-radius: 50%; + background: rgba(72, 159, 225, 0.3); + + img { + width: 24px; + height: 24px; + } +} + +.auth-header { + margin-bottom: 8px; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + text-align: center; + + h2 { + font-size: 24px; + font-weight: 700; + line-height: 32px; + color: $gray-900; + } + + p { + font-size: 16px; + font-weight: 400; + line-height: 24px; + color: $gray-500; + } +} + +.auth-redirect { + font-size: 14px; + font-weight: 600; + line-height: 20px; + color: $gray-500; + + a { + font-size: 14px; + font-weight: 600; + line-height: 20px; + color: $light-blue; + } +} + +@media screen and (max-width: 768px) { + .auth-page { + padding: 66px 30px; + } +} + +@media screen and (max-width: 550px) { + .auth-page { + padding: 48px 16px; + min-height: 600px; + } + + .auth-wrapper { + width: 100%; + } +} diff --git a/src/Shared/ui/components/Loader/Loader.scss b/src/Shared/ui/components/Loader/Loader.scss new file mode 100644 index 0000000..562a4c2 --- /dev/null +++ b/src/Shared/ui/components/Loader/Loader.scss @@ -0,0 +1,19 @@ +.loader { + width: 24px; + height: 24px; + border-radius: 50%; + display: inline-block; + border-top: 3px solid #fff; + border-right: 3px solid transparent; + box-sizing: border-box; + animation: rotation 1s linear infinite; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/src/Shared/ui/components/Loader/Loader.tsx b/src/Shared/ui/components/Loader/Loader.tsx new file mode 100644 index 0000000..6842803 --- /dev/null +++ b/src/Shared/ui/components/Loader/Loader.tsx @@ -0,0 +1,7 @@ +import "./Loader.scss"; + +const Loader = () => { + return ; +}; + +export default Loader; diff --git a/src/Shared/ui/components/Paragraph/Paragraph.scss b/src/Shared/ui/components/Paragraph/Paragraph.scss new file mode 100644 index 0000000..7c561f0 --- /dev/null +++ b/src/Shared/ui/components/Paragraph/Paragraph.scss @@ -0,0 +1,12 @@ +.paragraph { + font-size: 24px; + font-weight: 300; + line-height: 29px; +} + +@media screen and (max-width: 550px) { + .paragraph { + font-size: 18px; + line-height: 22px; + } +} diff --git a/src/Shared/ui/components/Paragraph/Paragraph.tsx b/src/Shared/ui/components/Paragraph/Paragraph.tsx new file mode 100644 index 0000000..76680f7 --- /dev/null +++ b/src/Shared/ui/components/Paragraph/Paragraph.tsx @@ -0,0 +1,13 @@ +import "./Paragraph.scss"; + +interface IParagraphProps { + children: React.ReactNode; +} + +const Paragraph: React.FC = ({ + children, +}: IParagraphProps) => { + return

{children}

; +}; + +export default Paragraph; diff --git a/src/Shared/ui/components/Typography/Typography.scss b/src/Shared/ui/components/Typography/Typography.scss new file mode 100644 index 0000000..38d9109 --- /dev/null +++ b/src/Shared/ui/components/Typography/Typography.scss @@ -0,0 +1,26 @@ +@import "../../variables.scss"; + +.typography-h2, +.typography-h3 { + text-align: center; + color: $black; + font-size: 42px; + font-weight: 500; + line-height: 50px; +} + +@media screen and (max-width: 768px) { + .typography-h2, + .typography-h3 { + font-size: 36px; + line-height: 43px; + } +} + +@media screen and (max-width: 550px) { + .typography-h2, + .typography-h3 { + font-size: 24px; + line-height: 29px; + } +} diff --git a/src/Shared/ui/components/Typography/Typography.tsx b/src/Shared/ui/components/Typography/Typography.tsx new file mode 100644 index 0000000..d4245a8 --- /dev/null +++ b/src/Shared/ui/components/Typography/Typography.tsx @@ -0,0 +1,19 @@ +import "./Typography.scss"; + +interface ITypographyProps { + element: string; + children: React.ReactNode; +} + +const Typography: React.FC = ({ + element, + children, +}: ITypographyProps) => { + const headers: Record = { + h2:

{children}

, + h3:

{children}

, + }; + return headers[element]; +}; + +export default Typography; diff --git a/src/Shared/ui/variables.scss b/src/Shared/ui/variables.scss index c238237..7f2b288 100644 --- a/src/Shared/ui/variables.scss +++ b/src/Shared/ui/variables.scss @@ -6,4 +6,5 @@ $light-green: rgb(74, 192, 63); $gray-300: #d0d5dd; $gray-500: #667085; $gray-700: #344054; +$gray-900: rgb(16, 24, 40); $black: rgb(50, 48, 58); diff --git a/src/Widgets/ConfirmEmailForm/ConfirmEmailForm.scss b/src/Widgets/ConfirmEmailForm/ConfirmEmailForm.scss new file mode 100644 index 0000000..656dea3 --- /dev/null +++ b/src/Widgets/ConfirmEmailForm/ConfirmEmailForm.scss @@ -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%; + } + } + } + } +} diff --git a/src/Widgets/ConfirmEmailForm/ConfirmEmailForm.tsx b/src/Widgets/ConfirmEmailForm/ConfirmEmailForm.tsx new file mode 100644 index 0000000..362a31b --- /dev/null +++ b/src/Widgets/ConfirmEmailForm/ConfirmEmailForm.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useEffect, useState } from "react"; +import "./ConfirmEmailForm.scss"; +import { apiInstance } from "@/shared/config/apiConfig"; +import { useRouter } from "next/navigation"; +import Loader from "@/shared/ui/components/Loader/Loader"; + +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< + 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 ( +
+
+ +
+ {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/ForgotPasswordForm/ForgotPasswordForm.scss b/src/Widgets/ForgotPasswordForm/ForgotPasswordForm.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/Widgets/ForgotPasswordForm/ForgotPasswordForm.tsx b/src/Widgets/ForgotPasswordForm/ForgotPasswordForm.tsx new file mode 100644 index 0000000..618aab8 --- /dev/null +++ b/src/Widgets/ForgotPasswordForm/ForgotPasswordForm.tsx @@ -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(false); + return changeForm ? ( + + ) : ( + + ); +}; + +export default ForgotPasswordForm; diff --git a/src/Widgets/ForgotPasswordForm/confirm-code/confirm-code.scss b/src/Widgets/ForgotPasswordForm/confirm-code/confirm-code.scss new file mode 100644 index 0000000..95d2d83 --- /dev/null +++ b/src/Widgets/ForgotPasswordForm/confirm-code/confirm-code.scss @@ -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%; + } +} diff --git a/src/Widgets/ForgotPasswordForm/confirm-code/confirm-code.tsx b/src/Widgets/ForgotPasswordForm/confirm-code/confirm-code.tsx new file mode 100644 index 0000000..ef6f9f4 --- /dev/null +++ b/src/Widgets/ForgotPasswordForm/confirm-code/confirm-code.tsx @@ -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 { useRouter } from "next/navigation"; +import { ITokens } from "@/shared/types/token-type"; + +interface IConfirmCodeProps { + setChangeForm: (boolean: boolean) => void; +} + +const ConfirmCode: React.FC = ({ + setChangeForm, +}: IConfirmCodeProps) => { + const [error, setError] = useState(""); + const [loader, setLoader] = useState(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( + "/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 ( +
+
+ Key Icon +
+
+

Введите код

+

Введите код для сброса и восстановления пароля

+
+ +
+ + + + +
+ ); +}; + +export default ConfirmCode; diff --git a/src/Widgets/ForgotPasswordForm/confirm-code/icons/key.svg b/src/Widgets/ForgotPasswordForm/confirm-code/icons/key.svg new file mode 100644 index 0000000..88beab1 --- /dev/null +++ b/src/Widgets/ForgotPasswordForm/confirm-code/icons/key.svg @@ -0,0 +1,14 @@ + + + Created with Pixso. + + + + + + + + + + + diff --git a/src/Widgets/ForgotPasswordForm/send-email/icons/mail.svg b/src/Widgets/ForgotPasswordForm/send-email/icons/mail.svg new file mode 100644 index 0000000..dc055d2 --- /dev/null +++ b/src/Widgets/ForgotPasswordForm/send-email/icons/mail.svg @@ -0,0 +1,14 @@ + + + Created with Pixso. + + + + + + + + + + + diff --git a/src/Widgets/ForgotPasswordForm/send-email/send-email.scss b/src/Widgets/ForgotPasswordForm/send-email/send-email.scss new file mode 100644 index 0000000..3042c7b --- /dev/null +++ b/src/Widgets/ForgotPasswordForm/send-email/send-email.scss @@ -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%; + } +} diff --git a/src/Widgets/ForgotPasswordForm/send-email/send-email.tsx b/src/Widgets/ForgotPasswordForm/send-email/send-email.tsx new file mode 100644 index 0000000..2eb1d2b --- /dev/null +++ b/src/Widgets/ForgotPasswordForm/send-email/send-email.tsx @@ -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 = ({ + setChangeForm, +}: ISendEmailProps) => { + const [error, setError] = useState(""); + const [loader, setLoader] = useState(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 ( +
+
+ Key Icon +
+
+

Введите email

+

+ Введите email и мы отправим код для восстановления пароля +

+
+ +
+ + + + +
+ ); +}; + +export default SendEmail; diff --git a/src/Widgets/MapSection/HomeMap/HomeMap.tsx b/src/Widgets/MapSection/HomeMap/HomeMap.tsx index 82c9aca..6f903b3 100644 --- a/src/Widgets/MapSection/HomeMap/HomeMap.tsx +++ b/src/Widgets/MapSection/HomeMap/HomeMap.tsx @@ -7,6 +7,7 @@ import { Marker, Popup, TileLayer, + useMap, } from "react-leaflet"; import geo_green_icon from "./icons/geo-green.svg"; import geo_orange_icon from "./icons/geo-orange.svg"; @@ -18,6 +19,8 @@ import { DivIcon, Icon, LatLngTuple } from "leaflet"; import { StaticImageData } from "next/image"; import Link from "next/link"; import { ILocation } from "@/shared/types/report-type"; +import { useEffect, useState } from "react"; +import L from "leaflet"; interface IData { id: number; @@ -25,13 +28,24 @@ interface IData { category: number; } +interface ILatLng { + lat: number; + lng: number; +} + interface IHomeMapProps { data: IData[] | undefined; + latLng: ILatLng; } const HomeMap: React.FC = ({ data, + latLng, }: IHomeMapProps) => { + const [position, setPosition] = useState({ + lat: 42.8746, + lng: 74.606, + }); const createCustomIcon = (icon: StaticImageData) => { const customIcon = new Icon({ iconUrl: icon.src, @@ -50,11 +64,20 @@ const HomeMap: React.FC = ({ 6: createCustomIcon(geo_yellow_icon), }; - const position = [42.8746, 74.606]; + const LocationMark = () => { + const map = useMap(); + + map.setView(L.latLng(latLng), map.getZoom(), { animate: true }); + + useEffect(() => { + setPosition(latLng); + }, [latLng]); + return null; + }; return ( = ({ ))} + ); }; diff --git a/src/Widgets/MapSection/MapSearch/MapSearch.scss b/src/Widgets/MapSection/MapSearch/MapSearch.scss new file mode 100644 index 0000000..96c8219 --- /dev/null +++ b/src/Widgets/MapSection/MapSearch/MapSearch.scss @@ -0,0 +1,95 @@ +.map-search { + margin-bottom: 32px; + width: 645px; + position: relative; + + form { + width: 100%; + height: 52px; + display: flex; + align-items: center; + + button { + height: 100%; + width: 100px; + border-radius: 0px 6px 6px 0px; + background: rgb(72, 159, 225); + color: white; + } + } + + &__input { + height: 100%; + display: flex; + flex: 1; + align-items: center; + box-sizing: border-box; + border: 1px solid rgb(197, 198, 197); + border-radius: 6px 0px 0px 6px; + + img { + width: 50px; + } + + input { + padding: 10px 0; + padding-left: 5px; + width: 100%; + height: 100%; + border: none; + font-size: 18px; + } + } + + &__recs { + width: 100%; + max-height: 200px; + position: absolute; + top: 110%; + left: 0; + z-index: 10000; + border-radius: 6px; + + background-color: #fff; + box-shadow: 0px 12px 16px 0px rgba(58, 69, 75, 0.1), + 0px 4px 6px 0px rgba(58, 69, 75, 0.15); + overflow: hidden; + overflow-y: auto; + + ul { + li:first-child { + button { + border-radius: 6px 6px 0px 0px; + } + } + + li:last-child { + button { + border-radius: 0px 0px 6px 6px; + } + } + } + + button { + padding: 20px; + height: 60px; + display: flex; + justify-content: flex-start; + align-items: center; + text-align: start; + width: 100%; + font-size: 18px; + font-weight: 500; + } + + button:hover { + background-color: #f5f5f5; + } + } +} + +@media screen and (max-width: 768px) { + .map-search { + width: 90%; + } +} diff --git a/src/Widgets/MapSection/MapSearch/MapSearch.tsx b/src/Widgets/MapSection/MapSearch/MapSearch.tsx new file mode 100644 index 0000000..266ed53 --- /dev/null +++ b/src/Widgets/MapSection/MapSearch/MapSearch.tsx @@ -0,0 +1,80 @@ +"use client"; + +import "./MapSearch.scss"; +import Image from "next/image"; +import search from "./icons/search.svg"; +import { IDisplayMap } from "@/shared/types/map-type"; + +interface IMapSearchProps + extends React.InputHTMLAttributes { + options: IDisplayMap[]; + setMapSearch: (string: string) => void; + setLatLng: ({ lat, lng }: { lat: number; lng: number }) => void; +} + +const MapSearch: React.FC = ({ + name, + placeholder, + value, + onChange, + options, + setMapSearch, + setLatLng, +}: IMapSearchProps) => { + const handleSubmit = ( + display_name: string, + latLng: { lat: number; lng: number } + ) => { + setMapSearch(display_name); + setLatLng(latLng); + }; + return ( +
+
{ + e.preventDefault(); + handleSubmit(options[0].display_name, { + lat: +options[0].lat, + lng: +options[0].lon, + }); + }} + > +
+ Search Icon + +
+ +
+ + {options.length !== 0 ? ( +
+
    + {options.map((opt) => ( +
  • + +
  • + ))} +
+
+ ) : null} +
+ ); +}; + +export default MapSearch; diff --git a/src/features/SearchBar/icons/search.svg b/src/Widgets/MapSection/MapSearch/icons/search.svg similarity index 100% rename from src/features/SearchBar/icons/search.svg rename to src/Widgets/MapSection/MapSearch/icons/search.svg diff --git a/src/Widgets/MapSection/MapSection.scss b/src/Widgets/MapSection/MapSection.scss index 9f6b18e..0c2328e 100644 --- a/src/Widgets/MapSection/MapSection.scss +++ b/src/Widgets/MapSection/MapSection.scss @@ -4,6 +4,15 @@ flex-direction: column; align-items: center; + &__header { + margin-bottom: 50px; + text-align: center; + display: flex; + align-items: center; + flex-direction: column; + gap: 24px; + } + &__categories { margin-bottom: 60px; display: flex; @@ -28,9 +37,13 @@ @media screen and (max-width: 768px) { .map-section { + &__header { + margin-bottom: 40px; + gap: 20px; + } + &__categories { margin-bottom: 40px; - gap: 25px; } } @@ -38,9 +51,13 @@ @media screen and (max-width: 550px) { .map-section { + &__header { + margin-bottom: 25px; + gap: 16px; + } + &__categories { margin-bottom: 25px; - padding: 0 16px; width: 100%; align-items: flex-start; diff --git a/src/Widgets/MapSection/MapSection.tsx b/src/Widgets/MapSection/MapSection.tsx index 1cd4bc6..f43ccbf 100644 --- a/src/Widgets/MapSection/MapSection.tsx +++ b/src/Widgets/MapSection/MapSection.tsx @@ -1,9 +1,7 @@ "use client"; import "./MapSection.scss"; -import SearchForm from "@/features/SearchBar/SearchForm"; import { useEffect, useState } from "react"; -import SectionHeader from "@/entities/SectionHeader/SectionHeader"; import { ROAD_TYPES, ROAD_TYPES_COLORS, @@ -13,34 +11,49 @@ import { useRouter } from "next/navigation"; import { useMapStore } from "./mapSectionStore"; import Switch from "./Switch/Switch"; import { useShallow } from "zustand/react/shallow"; +import Typography from "@/shared/ui/components/Typography/Typography"; +import Paragraph from "@/shared/ui/components/Paragraph/Paragraph"; +import { useDebounce } from "use-debounce"; +import MapSearch from "./MapSearch/MapSearch"; interface IMapSectionProps { [key: string]: string; } +interface ILatLng { + lat: number; + lng: number; +} + const MapSection: React.FC = ({ categories = "1,2,3,4,5,6", queryMap, queryRating, }: IMapSectionProps) => { const [mapSearch, setMapSearch] = useState(queryMap || ""); + const [latLng, setLatLng] = useState({ + lat: 42.8746, + lng: 74.606, + }); + const [query] = useDebounce(mapSearch, 500); const data = useMapStore(useShallow((state) => state.data)); + const searchedData = useMapStore( + useShallow((state) => state.searchData) + ); + const getReports = useMapStore( useShallow((state) => state.getReports) ); + const getLocations = useMapStore( + useShallow((state) => state.getLocations) + ); const router = useRouter(); useEffect(() => { getReports(categories); }, [categories]); - const handleSubmit: React.FormEventHandler< - HTMLFormElement - > = async (e) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - setMapSearch(formData.get("map-search") as string); - + useEffect(() => { router.push( `/?тип-дороги=${categories}${ mapSearch ? `&поиск-на-карте=${mapSearch}` : "" @@ -49,7 +62,9 @@ const MapSection: React.FC = ({ scroll: false, } ); - }; + + getLocations(query); + }, [categories, query, queryRating]); const setSearchParams = (category: string) => { const availableCategories = ["1", "2", "3", "4", "5", "6"]; @@ -73,16 +88,22 @@ const MapSection: React.FC = ({ return (
- - + Карта дорог + + Будьте в курсе последних новостей о дорожном движении, + строительствах и мероприятиях! + + + + setMapSearch(e.target.value)} name="map-search" value={mapSearch} placeholder="Введите город, село или регион" - handleSubmit={handleSubmit} />
    @@ -115,7 +136,7 @@ const MapSection: React.FC = ({
- +
); }; diff --git a/src/Widgets/MapSection/mapSectionStore.ts b/src/Widgets/MapSection/mapSectionStore.ts index 5b45a60..18d12eb 100644 --- a/src/Widgets/MapSection/mapSectionStore.ts +++ b/src/Widgets/MapSection/mapSectionStore.ts @@ -1,7 +1,9 @@ import { apiInstance } from "@/shared/config/apiConfig"; import { IFetch } from "@/shared/types/fetch-type"; import { IList } from "@/shared/types/list-type"; +import { IDisplayMap } from "@/shared/types/map-type"; import { IReport } from "@/shared/types/report-type"; +import axios from "axios"; import { create } from "zustand"; interface IFetchReports extends IList { @@ -10,7 +12,9 @@ interface IFetchReports extends IList { interface IMapStore extends IFetch { data: IFetchReports; + searchData: IDisplayMap[]; getReports: (categories: string) => Promise; + getLocations: (query: string) => void; } export const useMapStore = create((set) => ({ @@ -20,6 +24,7 @@ export const useMapStore = create((set) => ({ next: null, results: [], }, + searchData: [], isLoading: false, error: "", getReports: async (categories: string = "1,2,3,4,5,6") => { @@ -37,4 +42,22 @@ export const useMapStore = create((set) => ({ set({ isLoading: false }); } }, + getLocations: async (query: string = "") => { + const params: Record = { + q: query, + format: "json", + addressdetails: 1, + polygon_geojson: 0, + }; + const queryString = new URLSearchParams(params).toString(); + const url = `https://nominatim.openstreetmap.org/search?${queryString}`; + + const response = await axios.get(url); + + const inKG = response.data.filter((location) => { + return location.address.country_code.toLowerCase() === "kg"; + }); + + set({ searchData: inKG }); + }, })); diff --git a/src/Widgets/Navbar/NavAuth/NavAuth.tsx b/src/Widgets/Navbar/NavAuth/NavAuth.tsx index 503d932..45a977e 100644 --- a/src/Widgets/Navbar/NavAuth/NavAuth.tsx +++ b/src/Widgets/Navbar/NavAuth/NavAuth.tsx @@ -1,20 +1,25 @@ import Link from "next/link"; import "./NavAuth.scss"; import { usePathname } from "next/navigation"; +import { useSession } from "next-auth/react"; interface INavAuthProps { responsible?: boolean; + setOpenMenu: (boolean: boolean) => void; } const NavAuth: React.FC = ({ responsible, + setOpenMenu, }: INavAuthProps) => { - const auth = false; + const session = useSession(); + const auth = session.status === "authenticated" ? true : false; const pathname = usePathname(); return ( <> {auth ? ( setOpenMenu(false)} href="/profile" className={`nav-auth-profile-${ responsible @@ -26,6 +31,7 @@ const NavAuth: React.FC = ({ ) : ( setOpenMenu(false)} href="/sign-in" className={`nav-auth-signin-${ responsible diff --git a/src/Widgets/Navbar/NavMenu/NavMenu.tsx b/src/Widgets/Navbar/NavMenu/NavMenu.tsx index 988f3ef..a074973 100644 --- a/src/Widgets/Navbar/NavMenu/NavMenu.tsx +++ b/src/Widgets/Navbar/NavMenu/NavMenu.tsx @@ -4,13 +4,20 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import NavAuth from "../NavAuth/NavAuth"; -const NavMenu = () => { +interface INavMenuProps { + setOpenMenu: (boolean: boolean) => void; +} + +const NavMenu: React.FC = ({ + setOpenMenu, +}: INavMenuProps) => { const auth = false; const pathname = usePathname(); return ( ); }; diff --git a/src/Widgets/Navbar/Navbar.scss b/src/Widgets/Navbar/Navbar.scss index 55b65e9..99c0922 100644 --- a/src/Widgets/Navbar/Navbar.scss +++ b/src/Widgets/Navbar/Navbar.scss @@ -5,7 +5,7 @@ width: 100%; height: 78px; position: fixed; - z-index: 10000; + z-index: 10005; display: flex; justify-content: space-between; align-items: center; diff --git a/src/Widgets/Navbar/Navbar.tsx b/src/Widgets/Navbar/Navbar.tsx index 8f2cea6..96d7d82 100644 --- a/src/Widgets/Navbar/Navbar.tsx +++ b/src/Widgets/Navbar/Navbar.tsx @@ -40,7 +40,7 @@ const Navbar = () => {
- +
- {openMenu && } + {openMenu && } ); }; diff --git a/src/Widgets/NewsSection/NewsSection.scss b/src/Widgets/NewsSection/NewsSection.scss index 6247cb2..24e6374 100644 --- a/src/Widgets/NewsSection/NewsSection.scss +++ b/src/Widgets/NewsSection/NewsSection.scss @@ -4,6 +4,11 @@ padding: 60px 90px 100px 90px; display: flex; flex-direction: column; + + h3 { + margin-bottom: 24px; + } + &__list { margin-bottom: 40px; display: grid; diff --git a/src/Widgets/NewsSection/NewsSection.tsx b/src/Widgets/NewsSection/NewsSection.tsx index 0cbf99f..df929ea 100644 --- a/src/Widgets/NewsSection/NewsSection.tsx +++ b/src/Widgets/NewsSection/NewsSection.tsx @@ -2,15 +2,15 @@ import "./NewsSection.scss"; import Image from "next/image"; import Link from "next/link"; import { getNews } from "./newsSectionStore"; -import SectionHeader from "@/entities/SectionHeader/SectionHeader"; import arrow_icon from "./icons/arrow-right.svg"; import NewsCard from "@/entities/NewsCard/NewsCard"; +import Typography from "@/shared/ui/components/Typography/Typography"; const NewsSection = async () => { const news = await getNews(); return (
- + Новости
    {news?.map((article) => (
  • diff --git a/src/Widgets/NewsSection/newsSectionStore.ts b/src/Widgets/NewsSection/newsSectionStore.ts index a4e669a..1dace1b 100644 --- a/src/Widgets/NewsSection/newsSectionStore.ts +++ b/src/Widgets/NewsSection/newsSectionStore.ts @@ -14,9 +14,9 @@ export const getNews = async () => { return data.results.slice(0, 4); } catch (error: unknown) { if (error instanceof AxiosError) { - alert(error.message); + console.log(error.message); } else { - alert("An error ocured"); + console.log("An error ocured"); } } }; diff --git a/src/Widgets/RatingSection/RatingSection.scss b/src/Widgets/RatingSection/RatingSection.scss index 7d57bd3..75f7158 100644 --- a/src/Widgets/RatingSection/RatingSection.scss +++ b/src/Widgets/RatingSection/RatingSection.scss @@ -6,128 +6,126 @@ flex-direction: column; align-items: center; + &__header { + margin-bottom: 50px; + text-align: center; + display: flex; + align-items: center; + flex-direction: column; + gap: 24px; + } + &__table { width: 100%; - padding: 15px; - background-color: #fff; - border: 1px solid $gray-300; - border-radius: 5px; overflow: hidden; overflow-x: auto; + border: 1px solid rgb(213, 213, 213); + border-radius: 6px; + + &::-webkit-scrollbar { + display: none; + } table { - width: 100%; - border-collapse: collapse; - border-radius: 6px; thead { + padding: 0 30px; + height: 76px; + border-bottom: 1px solid rgb(241, 244, 249); tr { - border-bottom: 1px solid #ddd; - th { - padding: 15px 0; - text-align: left; - color: $gray-500; - font-size: 16px; - font-weight: 500; - gap: 4px; - } + height: 100%; + display: grid; + align-items: center; + grid-template-columns: 120px 200px 210px 380px 165px 92px; - #report-header-date, - #report-header-comment, - #report-header-like { - cursor: pointer; - - div { + td { + button { display: flex; align-items: center; - gap: 4px; + color: rgb(102, 112, 133); + font-size: 16px; + font-weight: 400; + line-height: 18px; } + color: rgb(102, 112, 133); + font-size: 16px; + font-weight: 400; + line-height: 18px; } } } tbody { + padding: 0 30px; + display: flex; + flex-direction: column; + tr { - border-bottom: 1px solid #ddd; + height: 90px; + padding: 10px 0; + display: grid; + grid-template-columns: 120px 200px 210px 380px 165px 92px; + align-items: center; td { - padding: 15px 0; - - a { - justify-content: flex-start; - } - } - - #report-date { - min-width: 150px; - color: $gray-500; + font-size: 18px; font-weight: 500; - font-size: 16px; + line-height: 20px; } - #report-location { - min-width: 200px; - overflow: hidden; + #rating-section-date { + font-size: 16px; + color: rgb(102, 112, 133); + } + + #rating-section-link { a { text-decoration: underline; - white-space: preserve nowrap; color: $light-blue; - font-size: 16px; - font-weight: 500; } } - #report-type { - min-width: 210px; + #rating-section-description { + color: $black; } - #report-description { - min-width: 378px; - - p { - width: 318px; - color: #32303a; - font-size: 18px; - font-weight: 500; - line-height: 20px; - letter-spacing: 0.1px; - text-align: justify; - } + #rating-section-reviews, + #rating-section-likes { + display: flex; + align-items: center; + gap: 7px; } - #report-comment, - #report-like { - div { - display: flex; - align-items: center; - gap: 7px; - } + #rating-section-reviews { + color: $light-blue; } - } - tr:last-child { - border-bottom: none; + #rating-section-likes { + color: $light-green; + } } } } } - - &-error { - td { - text-align: center; - font-size: 42px; - } - } } @media screen and (max-width: 1024px) { .rating-section { padding: 0 30px; + + &__header { + margin-bottom: 40px; + gap: 20px; + } } } @media screen and (max-width: 550px) { .rating-section { padding: 0 16px; + &__header { + margin-bottom: 20px; + gap: 16px; + } } } diff --git a/src/Widgets/RatingSection/RatingSection.tsx b/src/Widgets/RatingSection/RatingSection.tsx index 9503d14..460f977 100644 --- a/src/Widgets/RatingSection/RatingSection.tsx +++ b/src/Widgets/RatingSection/RatingSection.tsx @@ -3,8 +3,7 @@ import "./RatingSection.scss"; import Image from "next/image"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; -import SectionHeader from "@/entities/SectionHeader/SectionHeader"; -import SearchForm from "@/features/SearchBar/SearchForm"; +import SearchForm from "@/features/SearchForm/SearchForm"; import arrow_down_icon from "./icons/arrow-down.svg"; import arrow_up_icon from "./icons/arrow-up.svg"; import like_icon from "./icons/like.svg"; @@ -17,6 +16,9 @@ import { ROAD_TYPES_COLORS, } from "@/shared/variables/road-types"; import RoadType from "@/entities/RoadType/RoadType"; +import Typography from "@/shared/ui/components/Typography/Typography"; +import Paragraph from "@/shared/ui/components/Paragraph/Paragraph"; +import arrows from "../../shared/icons/arrows.svg"; interface IRatingSectionProps { [key: string]: string; @@ -81,12 +83,41 @@ const RatingSection: React.FC = ({ return description; }; + const params = [ + { + id: 1, + name: "Дата", + handleClick() { + console.log(this.name); + }, + }, + { id: 2, name: "Адрес" }, + { id: 3, name: "Статус" }, + { id: 4, name: "Описание" }, + { + id: 5, + name: "Комментарии", + handleClick() { + console.log(this.name); + }, + }, + { + id: 6, + name: "Рейтинг", + handleClick() { + console.log(this.name); + }, + }, + ]; + return ( -
    - +
    +
    + Рейтинг + + Обсуждаем дороги: рейтинг, опыт, комфорт в пути! + +
    setRatingSearch(e.target.value)} name="rating-search" @@ -98,77 +129,55 @@ const RatingSection: React.FC = ({ - - - - - - + {params.map((p) => ( + + ))} - {reports.results.length !== 0 ? ( - reports.results.map((report) => ( - - - - - - - - - )) - ) : ( - - + {reports.results.map((report) => ( + + + + + + + - )} + ))}
    -
    - Дата - Arrow Down - Arrow Up -
    -
    АдресСтатусОписание -
    - Комментарии - Arrow Down - Arrow Up -
    -
    -
    - Рейтинг - Arrow Down - Arrow Up -
    -
    + {p.handleClick ? ( + + ) : ( + p.name + )} +
    - {sliceDate(report.created_at)} - - - {sliceLocation(report.location[0].address)} - - - - {ROAD_TYPES[report.category]} - - -

    {sliceDescription(report.description)}

    -
    -
    - Message Icon - {report.count_reviews} -
    -
    -
    - Like Icon - {report.total_likes} -
    -
    Упс, адрес не найден
    + {sliceDate(report.created_at)} + + + {ROAD_TYPES[report.category]} + + + {sliceDescription(report.description)} + + Message Icon + {report.count_reviews} + + Like Icon + {report.total_likes} +
    - +
); }; diff --git a/src/Widgets/RatingSection/icons/arrow-down.svg b/src/Widgets/RatingSection/icons/arrow-down.svg deleted file mode 100644 index a20a1e2..0000000 --- a/src/Widgets/RatingSection/icons/arrow-down.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - Created with Pixso. - - - - - - - - - - - diff --git a/src/Widgets/RatingSection/icons/arrow-up.svg b/src/Widgets/RatingSection/icons/arrow-up.svg deleted file mode 100644 index f29f44d..0000000 --- a/src/Widgets/RatingSection/icons/arrow-up.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - Created with Pixso. - - - - - - - - - - - diff --git a/src/Widgets/ResetCodeForm/ResetCodeForm.scss b/src/Widgets/ResetCodeForm/ResetCodeForm.scss new file mode 100644 index 0000000..b1a394a --- /dev/null +++ b/src/Widgets/ResetCodeForm/ResetCodeForm.scss @@ -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%; + } +} diff --git a/src/Widgets/ResetCodeForm/ResetCodeForm.tsx b/src/Widgets/ResetCodeForm/ResetCodeForm.tsx new file mode 100644 index 0000000..28b30e4 --- /dev/null +++ b/src/Widgets/ResetCodeForm/ResetCodeForm.tsx @@ -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 { useRouter } from "next/navigation"; +import Loader from "@/shared/ui/components/Loader/Loader"; + +const ResetCodeForm = () => { + const [passwordWarning, setPasswordWarning] = useState(""); + const [passwordConfirmWarning, setPasswordConfirmWarning] = + useState(""); + const [error, setError] = useState(""); + const [loader, setLoader] = useState(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 ( +
+
+ + +
+ + {error ? ( +

{error}

+ ) : null} + +
+ ); +}; + +export default ResetCodeForm; diff --git a/src/Widgets/SignInForm/SignInForm.scss b/src/Widgets/SignInForm/SignInForm.scss new file mode 100644 index 0000000..5eb0090 --- /dev/null +++ b/src/Widgets/SignInForm/SignInForm.scss @@ -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%; + } +} diff --git a/src/Widgets/SignInForm/SignInForm.tsx b/src/Widgets/SignInForm/SignInForm.tsx new file mode 100644 index 0000000..e5ee4ea --- /dev/null +++ b/src/Widgets/SignInForm/SignInForm.tsx @@ -0,0 +1,97 @@ +"use client"; + +import "./SignInForm.scss"; +import AuthInput from "@/features/AuthInput/AuthInput"; +import GoogleButton from "@/features/GoogleButton/GoogleButton"; +import Loader from "@/shared/ui/components/Loader/Loader"; +import { signIn } from "next-auth/react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +const SignInForm = () => { + const [emailWarning, setEmailWarning] = useState(""); + const [passwordWarning, setPasswordWarning] = useState(""); + const [error, setError] = useState(""); + const [loader, setLoader] = useState(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"); + } else if (res?.status.toString().slice(0, 1) === "4") { + setError("Неверный Email или Пароль"); + } else { + setError("Произошла непредвиденная ошибка"); + } + }; + + return ( +
+
+ + +
+ {error ?

{error}

: null} + + Забыли пароль? + + +
+ + +
+
+ ); +}; + +export default SignInForm; diff --git a/src/Widgets/SignUpForm/SignUpForm.scss b/src/Widgets/SignUpForm/SignUpForm.scss new file mode 100644 index 0000000..bfb6c3c --- /dev/null +++ b/src/Widgets/SignUpForm/SignUpForm.scss @@ -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%; + } +} diff --git a/src/Widgets/SignUpForm/SignUpForm.tsx b/src/Widgets/SignUpForm/SignUpForm.tsx new file mode 100644 index 0000000..0d45f31 --- /dev/null +++ b/src/Widgets/SignUpForm/SignUpForm.tsx @@ -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 { useRouter } from "next/navigation"; +import { apiInstance } from "@/shared/config/apiConfig"; +import { AxiosError } from "axios"; + +const SignUpForm = () => { + const [emailWarning, setEmailWarning] = useState(""); + const [passwordWarning, setPasswordWarning] = useState(""); + const [passwordConfirmWarning, setPasswordConfirmWarning] = + useState(""); + const [error, setError] = useState(""); + const [loader, setLoader] = useState(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 (res.status === 200 || res.status === 201) { + 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 ( +
+
+ + + +
+ {error ?

{error}

: null} + +
+ + +
+
+ ); +}; + +export default SignUpForm; diff --git a/src/features/AuthInput/AuthInput.scss b/src/features/AuthInput/AuthInput.scss new file mode 100644 index 0000000..e977337 --- /dev/null +++ b/src/features/AuthInput/AuthInput.scss @@ -0,0 +1,55 @@ +@import "@/shared/ui/variables.scss"; + +.auth-input { + display: flex; + flex-direction: column; + gap: 6px; + + label { + font-size: 14px; + font-weight: 400; + line-height: 20px; + color: $gray-700; + } + + &__field, + &__field-with-error { + padding: 10px 14px; + display: flex; + align-items: 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); + + input { + width: 100%; + + font-size: 16px; + font-weight: 400; + line-height: 24px; + color: $gray-900; + + ::placeholder { + font-size: 16px; + font-weight: 400; + line-height: 24px; + color: $gray-500; + } + } + } + + &__field-with-error { + border: 1px solid rgb(240, 68, 56); + } + + p { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 14px; + font-weight: 400; + line-height: 20px; + color: $red-500; + } +} diff --git a/src/features/AuthInput/AuthInput.tsx b/src/features/AuthInput/AuthInput.tsx new file mode 100644 index 0000000..6d91934 --- /dev/null +++ b/src/features/AuthInput/AuthInput.tsx @@ -0,0 +1,55 @@ +"use client"; + +import Image from "next/image"; +import "./AuthInput.scss"; +import eye_off from "./icons/eye-off.svg"; +import eye_on from "./icons/eye-on.svg"; +import alert from "./icons/alert-circle.svg"; +import { useState } from "react"; + +interface IAuthInputProps + extends React.InputHTMLAttributes { + isPassword?: boolean; + label: string; + error: string; +} + +const AuthInput: React.FC = ({ + isPassword, + label, + error, + placeholder, + name, + type, +}: IAuthInputProps) => { + const [isOpen, setIsOpen] = useState(false); + return ( +
+ +
+ + {isPassword && ( + + )} +
+ {error ? ( +

+ {error} Alert Icon +

+ ) : null} +
+ ); +}; + +export default AuthInput; diff --git a/src/features/AuthInput/icons/alert-circle.svg b/src/features/AuthInput/icons/alert-circle.svg new file mode 100644 index 0000000..51cc2b1 --- /dev/null +++ b/src/features/AuthInput/icons/alert-circle.svg @@ -0,0 +1,7 @@ + + + Created with Pixso. + + + + diff --git a/src/features/AuthInput/icons/eye-off.svg b/src/features/AuthInput/icons/eye-off.svg new file mode 100644 index 0000000..99bfa55 --- /dev/null +++ b/src/features/AuthInput/icons/eye-off.svg @@ -0,0 +1,15 @@ + + + Created with Pixso. + + + + + + + + + + + + diff --git a/src/features/AuthInput/icons/eye-on.svg b/src/features/AuthInput/icons/eye-on.svg new file mode 100644 index 0000000..feb18f7 --- /dev/null +++ b/src/features/AuthInput/icons/eye-on.svg @@ -0,0 +1,15 @@ + + + Created with Pixso. + + + + + + + + + + + + diff --git a/src/features/GoogleButton/GoogleButton.scss b/src/features/GoogleButton/GoogleButton.scss new file mode 100644 index 0000000..c924378 --- /dev/null +++ b/src/features/GoogleButton/GoogleButton.scss @@ -0,0 +1,18 @@ +@import "@/shared/ui/variables.scss"; + +.google-btn { + padding: 10px 16px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + 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: 16px; + font-weight: 700; + line-height: 24px; + color: $gray-700; +} diff --git a/src/features/GoogleButton/GoogleButton.tsx b/src/features/GoogleButton/GoogleButton.tsx new file mode 100644 index 0000000..4817ca7 --- /dev/null +++ b/src/features/GoogleButton/GoogleButton.tsx @@ -0,0 +1,14 @@ +import Image from "next/image"; +import "./GoogleButton.scss"; +import google from "./icons/google.svg"; + +const GoogleButton = () => { + return ( + + ); +}; + +export default GoogleButton; diff --git a/src/features/GoogleButton/icons/google.svg b/src/features/GoogleButton/icons/google.svg new file mode 100644 index 0000000..527b35b --- /dev/null +++ b/src/features/GoogleButton/icons/google.svg @@ -0,0 +1,17 @@ + + + Created with Pixso. + + + + + + + + + + + + + + diff --git a/src/features/SearchBar/SearchForm.scss b/src/features/SearchForm/SearchForm.scss similarity index 88% rename from src/features/SearchBar/SearchForm.scss rename to src/features/SearchForm/SearchForm.scss index 91f22a5..661c470 100644 --- a/src/features/SearchBar/SearchForm.scss +++ b/src/features/SearchForm/SearchForm.scss @@ -2,12 +2,22 @@ margin-bottom: 32px; height: 52px; width: 645px; + position: relative; display: flex; align-items: center; + + button { + height: 100%; + width: 100px; + border-radius: 0px 6px 6px 0px; + background: rgb(72, 159, 225); + color: white; + } + &__input { height: 100%; - width: 100%; display: flex; + flex: 1; align-items: center; box-sizing: border-box; border: 1px solid rgb(197, 198, 197); @@ -18,22 +28,14 @@ } input { - width: 100%; padding: 10px 0; padding-left: 5px; + width: 100%; height: 100%; border: none; font-size: 18px; } } - - button { - height: 100%; - width: 100px; - border-radius: 0px 6px 6px 0px; - background: rgb(72, 159, 225); - color: white; - } } @media screen and (max-width: 768px) { @@ -41,9 +43,3 @@ width: 90%; } } - -@media screen and (max-width: 550px) { - .search-form { - width: 90%; - } -} diff --git a/src/features/SearchBar/SearchForm.tsx b/src/features/SearchForm/SearchForm.tsx similarity index 92% rename from src/features/SearchBar/SearchForm.tsx rename to src/features/SearchForm/SearchForm.tsx index cb34050..38e6524 100644 --- a/src/features/SearchBar/SearchForm.tsx +++ b/src/features/SearchForm/SearchForm.tsx @@ -17,7 +17,7 @@ const SearchForm: React.FC = ({ onChange, }: ISearchFormProps) => { return ( -
+
Search Icon + + Created with Pixso. + + + + + + + + + + + +