made fall auth

This commit is contained in:
Alibek 2024-02-09 03:53:32 +06:00
parent 1468fac1cc
commit 45a9d698cd
75 changed files with 2199 additions and 293 deletions

View File

@ -1,4 +1,4 @@
import React from "react"; import "./styles.scss";
const AboutUs = () => { const AboutUs = () => {
return <div>AboutUs</div>; return <div>AboutUs</div>;

View File

@ -0,0 +1,5 @@
.create-report {
h2 {
text-align: start;
}
}

View File

@ -0,0 +1,14 @@
import Typography from "@/shared/ui/components/Typography/Typography";
import "./CreateReport.scss";
const CreateReport = () => {
return (
<div className="create-report page-padding">
<Typography element="h2">Написать обращение</Typography>
<div className="create-report__wrapper"></div>
</div>
);
};
export default CreateReport;

View File

@ -28,3 +28,30 @@ input {
border: none; border: none;
outline: 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;
}
}

View File

@ -1,5 +1,19 @@
"use client";
import { signOut } from "next-auth/react";
import Link from "next/link";
const Profile = () => { const Profile = () => {
return <div>Profile</div>; return (
<div>
<Link
href="#"
onClick={() => signOut({ callbackUrl: "/sign-in" })}
>
Выйти в окно
</Link>
</div>
);
}; };
export default Profile; export default Profile;

View File

View File

@ -0,0 +1,12 @@
import "@/shared/ui/auth-classes.scss";
import ForgotPasswordForm from "@/widgets/ForgotPasswordForm/ForgotPasswordForm";
const ForgotPassword = () => {
return (
<div className="auth-page">
<ForgotPasswordForm />
</div>
);
};
export default ForgotPassword;

View File

@ -0,0 +1,14 @@
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip51_17162">
<rect id="log-in-04" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<rect id="log-in-04" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
<g clip-path="url(#clip51_17162)">
<path id="Icon" d="M12 8L16 12L12 16M3 12L16 12M3.33789 7C5.06641 4.01074 8.29883 2 12 2C17.5225 2 22 6.47754 22 12C22 17.5225 17.5225 22 12 22C8.29883 22 5.06641 19.9893 3.33789 17" stroke="#000000" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 768 B

View File

@ -1,8 +1,29 @@
import React from "react"; import "@/shared/ui/auth-classes.scss";
import classes from "./styles.module.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 = () => { const SignIn = () => {
return <div className={classes.root}>SignIn</div>; return (
<div className="auth-page">
<div className="auth-wrapper">
<div className="auth-icon">
<Image src={sign_in_icon} alt="Sign In Icon" />
</div>
<div className="auth-header">
<h2>Войдите в аккаунт</h2>
<p>Пожалуйста, введите свои данные</p>
</div>
<SignInForm />
<p className="auth-redirect">
Еще нет аккаунта?{" "}
<Link href="/sign-up">Зарегистрируйтесь</Link>
</p>
</div>
</div>
);
}; };
export default SignIn; export default SignIn;

View File

@ -0,0 +1,14 @@
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="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

View File

@ -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 (
<div className="auth-page">
<div className="auth-wrapper">
<div className="auth-icon2">
<Image src={key} alt="Key Icon" />
</div>
<div className="auth-header">
<h2>Введите новый пароль</h2>
<p>
Пароль должен содерждать минимум 8 символов, 1 заглавная
буква и цифра
</p>
</div>
<ResetCodeForm />
</div>
</div>
);
};
export default ResetCode;

View File

@ -0,0 +1,14 @@
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip55_8430">
<rect id="mail" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<rect id="mail" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
<g clip-path="url(#clip55_8430)">
<path id="Icon" d="M20 4C21.0996 4 22 4.90039 22 6L22 18C22 19.0996 21.0996 20 20 20L4 20C2.90039 20 2 19.0996 2 18L2 6C2 4.90039 2.90039 4 4 4L20 4ZM22 6L12 13L2 6" stroke="#489FE1" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 739 B

View File

@ -0,0 +1,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 (
<div className="auth-page">
<div className="auth-wrapper">
<div className="auth-icon2">
<Image src={mail} alt="Mail icon" width={56} height={56} />
</div>
<div className="auth-header">
<h2>Проверьте свою почту</h2>
<p>Мы отправили код на почту {searchParams.email}</p>
</div>
<ConfirmEmailForm email={searchParams.email} />
</div>
</div>
);
};
export default ConfirmEmail;

View File

@ -0,0 +1,7 @@
<svg width="20.317383" height="21.000977" viewBox="0 0 20.3174 21.001" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs/>
<path id="Icon" d="M12.2998 5.22266L18.2529 5.22266C18.7256 5.22266 18.9609 5.22266 19.0996 5.32129C19.2197 5.4082 19.2979 5.54102 19.3154 5.68848C19.335 5.85742 19.2207 6.06445 18.9912 6.47656L17.5703 9.03418C17.4873 9.18359 17.4453 9.25879 17.4297 9.33789C17.415 9.4082 17.415 9.48047 17.4297 9.55078C17.4453 9.62988 17.4873 9.70508 17.5703 9.85449L18.9912 12.4121C19.2207 12.8252 19.335 13.0312 19.3154 13.2002C19.2979 13.3477 19.2197 13.4805 19.0996 13.5674C18.9609 13.667 18.7256 13.667 18.2529 13.667L10.8213 13.667C10.2305 13.667 9.93457 13.667 9.70898 13.5518C9.51074 13.4502 9.34863 13.2891 9.24805 13.0898C9.13281 12.8643 9.13281 12.5693 9.13281 11.9775L9.13281 9.44434M10.6104 1C11.2021 1 11.4971 1 11.7227 1.11523C11.9219 1.21582 12.083 1.37793 12.1846 1.57617C12.2998 1.80176 12.2998 2.09766 12.2998 2.68848L12.2998 7.75586C12.2998 8.34668 12.2998 8.64258 12.1846 8.86816C12.083 9.06641 11.9219 9.22852 11.7227 9.3291C11.4971 9.44434 11.2021 9.44434 10.6104 9.44434L2.7998 9.44434L1.21289 3.09863C1.03418 2.38281 0.944336 2.02539 1.03125 1.74414C1.1084 1.49707 1.27246 1.28711 1.49316 1.15234C1.74512 1 2.11426 1 2.85156 1L10.6104 1ZM5.43848 20L1.21582 3.11133" stroke="#000000" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

30
src/App/sign-up/page.tsx Normal file
View File

@ -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 (
<div className="auth-page">
<div className="auth-wrapper">
<div className="auth-icon">
<Image src={flag} alt="Flag Icon" />
</div>
<div className="auth-header">
<h2>Регистрация</h2>
<p>Пожалуйста, введите свои данные</p>
</div>
<SignUpForm />
<p className="auth-redirect">
Уже есть аккаунт?{" "}
<Link href="/sign-in">Войти в аккаунт</Link>
</p>
</div>
</div>
);
};
export default SignUp;

View File

@ -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;
}
}
}

View File

@ -1,22 +0,0 @@
import "./SectionHeader.scss";
interface ISectionHeaderProps {
title: string;
description?: string;
style?: object;
}
const SectionHeader: React.FC<ISectionHeaderProps> = ({
title,
description,
style,
}: ISectionHeaderProps) => {
return (
<div style={style} className="section-header">
<h3>{title}</h3>
{description && <p>{description}</p>}
</div>
);
};
export default SectionHeader;

View File

View File

@ -0,0 +1,21 @@
<svg width="36.000000" height="16.000000" viewBox="0 0 36 16" 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="clip161_49481">
<rect id="arrow-down" width="16.000000" height="16.000000" fill="white" fill-opacity="0"/>
</clipPath>
<clipPath id="clip183_53161">
<rect id="arrow-down" width="16.000000" height="16.000000" transform="translate(36.000000 16.000000) rotate(180.000000)" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<rect id="arrow-down" width="16.000000" height="16.000000" fill="#FFFFFF" fill-opacity="0"/>
<g clip-path="url(#clip161_49481)">
<path id="Icon" d="M8 3.33301L8 12.666M12.667 8L8 12.666L3.3335 8" stroke="#667085" stroke-opacity="1.000000" stroke-width="1.333333" stroke-linejoin="round"/>
</g>
<rect id="arrow-down" width="16.000000" height="16.000000" transform="translate(36.000000 16.000000) rotate(180.000000)" fill="#FFFFFF" fill-opacity="0"/>
<g clip-path="url(#clip183_53161)">
<path id="Icon" d="M28 12.667L28 3.33398M23.333 8L28 3.33398L32.6665 8" stroke="#667085" stroke-opacity="1.000000" stroke-width="1.333333" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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;
}

View File

@ -0,0 +1,5 @@
export interface ITokens {
refresh_token: string;
access_token: string;
expires_in: string;
}

View File

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

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
import "./Loader.scss";
const Loader = () => {
return <span className="loader"></span>;
};
export default Loader;

View File

@ -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;
}
}

View File

@ -0,0 +1,13 @@
import "./Paragraph.scss";
interface IParagraphProps {
children: React.ReactNode;
}
const Paragraph: React.FC<IParagraphProps> = ({
children,
}: IParagraphProps) => {
return <p className="paragraph">{children}</p>;
};
export default Paragraph;

View File

@ -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;
}
}

View File

@ -0,0 +1,19 @@
import "./Typography.scss";
interface ITypographyProps {
element: string;
children: React.ReactNode;
}
const Typography: React.FC<ITypographyProps> = ({
element,
children,
}: ITypographyProps) => {
const headers: Record<string, React.ReactNode> = {
h2: <h2 className="typography-h2">{children}</h2>,
h3: <h3 className="typography-h3">{children}</h3>,
};
return headers[element];
};
export default Typography;

View File

@ -6,4 +6,5 @@ $light-green: rgb(74, 192, 63);
$gray-300: #d0d5dd; $gray-300: #d0d5dd;
$gray-500: #667085; $gray-500: #667085;
$gray-700: #344054; $gray-700: #344054;
$gray-900: rgb(16, 24, 40);
$black: rgb(50, 48, 58); $black: rgb(50, 48, 58);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,14 @@
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="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

View File

@ -0,0 +1,14 @@
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip55_8430">
<rect id="mail" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<rect id="mail" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
<g clip-path="url(#clip55_8430)">
<path id="Icon" d="M20 4C21.0996 4 22 4.90039 22 6L22 18C22 19.0996 21.0996 20 20 20L4 20C2.90039 20 2 19.0996 2 18L2 6C2 4.90039 2.90039 4 4 4L20 4ZM22 6L12 13L2 6" stroke="#489FE1" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 739 B

View File

@ -0,0 +1,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%;
}
}

View File

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

View File

@ -7,6 +7,7 @@ import {
Marker, Marker,
Popup, Popup,
TileLayer, TileLayer,
useMap,
} from "react-leaflet"; } from "react-leaflet";
import geo_green_icon from "./icons/geo-green.svg"; import geo_green_icon from "./icons/geo-green.svg";
import geo_orange_icon from "./icons/geo-orange.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 { StaticImageData } from "next/image";
import Link from "next/link"; import Link from "next/link";
import { ILocation } from "@/shared/types/report-type"; import { ILocation } from "@/shared/types/report-type";
import { useEffect, useState } from "react";
import L from "leaflet";
interface IData { interface IData {
id: number; id: number;
@ -25,13 +28,24 @@ interface IData {
category: number; category: number;
} }
interface ILatLng {
lat: number;
lng: number;
}
interface IHomeMapProps { interface IHomeMapProps {
data: IData[] | undefined; data: IData[] | undefined;
latLng: ILatLng;
} }
const HomeMap: React.FC<IHomeMapProps> = ({ const HomeMap: React.FC<IHomeMapProps> = ({
data, data,
latLng,
}: IHomeMapProps) => { }: IHomeMapProps) => {
const [position, setPosition] = useState<ILatLng>({
lat: 42.8746,
lng: 74.606,
});
const createCustomIcon = (icon: StaticImageData) => { const createCustomIcon = (icon: StaticImageData) => {
const customIcon = new Icon({ const customIcon = new Icon({
iconUrl: icon.src, iconUrl: icon.src,
@ -50,11 +64,20 @@ const HomeMap: React.FC<IHomeMapProps> = ({
6: createCustomIcon(geo_yellow_icon), 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 ( return (
<MapContainer <MapContainer
center={position as LatLngTuple} center={position}
zoom={14} zoom={14}
scrollWheelZoom={false} scrollWheelZoom={false}
className="home-map" className="home-map"
@ -81,6 +104,7 @@ const HomeMap: React.FC<IHomeMapProps> = ({
</Popup> </Popup>
</Marker> </Marker>
))} ))}
<LocationMark />
</MapContainer> </MapContainer>
); );
}; };

View File

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

View File

@ -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<HTMLInputElement> {
options: IDisplayMap[];
setMapSearch: (string: string) => void;
setLatLng: ({ lat, lng }: { lat: number; lng: number }) => void;
}
const MapSearch: React.FC<IMapSearchProps> = ({
name,
placeholder,
value,
onChange,
options,
setMapSearch,
setLatLng,
}: IMapSearchProps) => {
const handleSubmit = (
display_name: string,
latLng: { lat: number; lng: number }
) => {
setMapSearch(display_name);
setLatLng(latLng);
};
return (
<div className="map-search">
<form
onSubmit={(e) => {
e.preventDefault();
handleSubmit(options[0].display_name, {
lat: +options[0].lat,
lng: +options[0].lon,
});
}}
>
<div className="map-search__input">
<Image src={search} alt="Search Icon" />
<input
onChange={onChange}
value={value}
placeholder={placeholder}
name={name}
type="text"
/>
</div>
<button type="submit">Поиск</button>
</form>
{options.length !== 0 ? (
<div className="map-search__recs">
<ul>
{options.map((opt) => (
<li key={opt.place_id}>
<button
onClick={() => {
setMapSearch(opt.display_name);
setLatLng({
lat: +opt.lat,
lng: +opt.lon,
});
}}
>
{opt.display_name}
</button>
</li>
))}
</ul>
</div>
) : null}
</div>
);
};
export default MapSearch;

View File

Before

Width:  |  Height:  |  Size: 932 B

After

Width:  |  Height:  |  Size: 932 B

View File

@ -4,6 +4,15 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
&__header {
margin-bottom: 50px;
text-align: center;
display: flex;
align-items: center;
flex-direction: column;
gap: 24px;
}
&__categories { &__categories {
margin-bottom: 60px; margin-bottom: 60px;
display: flex; display: flex;
@ -28,9 +37,13 @@
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.map-section { .map-section {
&__header {
margin-bottom: 40px;
gap: 20px;
}
&__categories { &__categories {
margin-bottom: 40px; margin-bottom: 40px;
gap: 25px; gap: 25px;
} }
} }
@ -38,9 +51,13 @@
@media screen and (max-width: 550px) { @media screen and (max-width: 550px) {
.map-section { .map-section {
&__header {
margin-bottom: 25px;
gap: 16px;
}
&__categories { &__categories {
margin-bottom: 25px; margin-bottom: 25px;
padding: 0 16px; padding: 0 16px;
width: 100%; width: 100%;
align-items: flex-start; align-items: flex-start;

View File

@ -1,9 +1,7 @@
"use client"; "use client";
import "./MapSection.scss"; import "./MapSection.scss";
import SearchForm from "@/features/SearchBar/SearchForm";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import SectionHeader from "@/entities/SectionHeader/SectionHeader";
import { import {
ROAD_TYPES, ROAD_TYPES,
ROAD_TYPES_COLORS, ROAD_TYPES_COLORS,
@ -13,34 +11,49 @@ import { useRouter } from "next/navigation";
import { useMapStore } from "./mapSectionStore"; import { useMapStore } from "./mapSectionStore";
import Switch from "./Switch/Switch"; import Switch from "./Switch/Switch";
import { useShallow } from "zustand/react/shallow"; 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 { interface IMapSectionProps {
[key: string]: string; [key: string]: string;
} }
interface ILatLng {
lat: number;
lng: number;
}
const MapSection: React.FC<IMapSectionProps> = ({ const MapSection: React.FC<IMapSectionProps> = ({
categories = "1,2,3,4,5,6", categories = "1,2,3,4,5,6",
queryMap, queryMap,
queryRating, queryRating,
}: IMapSectionProps) => { }: IMapSectionProps) => {
const [mapSearch, setMapSearch] = useState<string>(queryMap || ""); const [mapSearch, setMapSearch] = useState<string>(queryMap || "");
const [latLng, setLatLng] = useState<ILatLng>({
lat: 42.8746,
lng: 74.606,
});
const [query] = useDebounce(mapSearch, 500);
const data = useMapStore(useShallow((state) => state.data)); const data = useMapStore(useShallow((state) => state.data));
const searchedData = useMapStore(
useShallow((state) => state.searchData)
);
const getReports = useMapStore( const getReports = useMapStore(
useShallow((state) => state.getReports) useShallow((state) => state.getReports)
); );
const getLocations = useMapStore(
useShallow((state) => state.getLocations)
);
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
getReports(categories); getReports(categories);
}, [categories]); }, [categories]);
const handleSubmit: React.FormEventHandler< useEffect(() => {
HTMLFormElement
> = async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
setMapSearch(formData.get("map-search") as string);
router.push( router.push(
`/?тип-дороги=${categories}${ `/?тип-дороги=${categories}${
mapSearch ? `&поиск-на-карте=${mapSearch}` : "" mapSearch ? `&поиск-на-карте=${mapSearch}` : ""
@ -49,7 +62,9 @@ const MapSection: React.FC<IMapSectionProps> = ({
scroll: false, scroll: false,
} }
); );
};
getLocations(query);
}, [categories, query, queryRating]);
const setSearchParams = (category: string) => { const setSearchParams = (category: string) => {
const availableCategories = ["1", "2", "3", "4", "5", "6"]; const availableCategories = ["1", "2", "3", "4", "5", "6"];
@ -73,16 +88,22 @@ const MapSection: React.FC<IMapSectionProps> = ({
return ( return (
<section className="map-section"> <section className="map-section">
<SectionHeader <div className="map-section__header">
title="Карта дорог" <Typography element="h3">Карта дорог</Typography>
description="Будьте в курсе последних новостей о дорожном движении, строительствах и мероприятиях!" <Paragraph>
/> Будьте в курсе последних новостей о дорожном движении,
<SearchForm строительствах и мероприятиях!
</Paragraph>
</div>
<MapSearch
setLatLng={setLatLng}
setMapSearch={setMapSearch}
options={searchedData}
onChange={(e) => setMapSearch(e.target.value)} onChange={(e) => setMapSearch(e.target.value)}
name="map-search" name="map-search"
value={mapSearch} value={mapSearch}
placeholder="Введите город, село или регион" placeholder="Введите город, село или регион"
handleSubmit={handleSubmit}
/> />
<div className="map-section__categories"> <div className="map-section__categories">
<ul className="map-section__categories_left"> <ul className="map-section__categories_left">
@ -115,7 +136,7 @@ const MapSection: React.FC<IMapSectionProps> = ({
</ul> </ul>
</div> </div>
<HomeMap data={data?.results} /> <HomeMap data={data?.results} latLng={latLng} />
</section> </section>
); );
}; };

View File

@ -1,7 +1,9 @@
import { apiInstance } from "@/shared/config/apiConfig"; import { apiInstance } from "@/shared/config/apiConfig";
import { IFetch } from "@/shared/types/fetch-type"; import { IFetch } from "@/shared/types/fetch-type";
import { IList } from "@/shared/types/list-type"; import { IList } from "@/shared/types/list-type";
import { IDisplayMap } from "@/shared/types/map-type";
import { IReport } from "@/shared/types/report-type"; import { IReport } from "@/shared/types/report-type";
import axios from "axios";
import { create } from "zustand"; import { create } from "zustand";
interface IFetchReports extends IList { interface IFetchReports extends IList {
@ -10,7 +12,9 @@ interface IFetchReports extends IList {
interface IMapStore extends IFetch { interface IMapStore extends IFetch {
data: IFetchReports; data: IFetchReports;
searchData: IDisplayMap[];
getReports: (categories: string) => Promise<void>; getReports: (categories: string) => Promise<void>;
getLocations: (query: string) => void;
} }
export const useMapStore = create<IMapStore>((set) => ({ export const useMapStore = create<IMapStore>((set) => ({
@ -20,6 +24,7 @@ export const useMapStore = create<IMapStore>((set) => ({
next: null, next: null,
results: [], results: [],
}, },
searchData: [],
isLoading: false, isLoading: false,
error: "", error: "",
getReports: async (categories: string = "1,2,3,4,5,6") => { getReports: async (categories: string = "1,2,3,4,5,6") => {
@ -37,4 +42,22 @@ export const useMapStore = create<IMapStore>((set) => ({
set({ isLoading: false }); set({ isLoading: false });
} }
}, },
getLocations: async (query: string = "") => {
const params: Record<string, any> = {
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<IDisplayMap[]>(url);
const inKG = response.data.filter((location) => {
return location.address.country_code.toLowerCase() === "kg";
});
set({ searchData: inKG });
},
})); }));

View File

@ -1,20 +1,25 @@
import Link from "next/link"; import Link from "next/link";
import "./NavAuth.scss"; import "./NavAuth.scss";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
interface INavAuthProps { interface INavAuthProps {
responsible?: boolean; responsible?: boolean;
setOpenMenu: (boolean: boolean) => void;
} }
const NavAuth: React.FC<INavAuthProps> = ({ const NavAuth: React.FC<INavAuthProps> = ({
responsible, responsible,
setOpenMenu,
}: INavAuthProps) => { }: INavAuthProps) => {
const auth = false; const session = useSession();
const auth = session.status === "authenticated" ? true : false;
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<> <>
{auth ? ( {auth ? (
<Link <Link
onClick={() => setOpenMenu(false)}
href="/profile" href="/profile"
className={`nav-auth-profile-${ className={`nav-auth-profile-${
responsible responsible
@ -26,6 +31,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
</Link> </Link>
) : ( ) : (
<Link <Link
onClick={() => setOpenMenu(false)}
href="/sign-in" href="/sign-in"
className={`nav-auth-signin-${ className={`nav-auth-signin-${
responsible responsible

View File

@ -4,13 +4,20 @@ import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import NavAuth from "../NavAuth/NavAuth"; import NavAuth from "../NavAuth/NavAuth";
const NavMenu = () => { interface INavMenuProps {
setOpenMenu: (boolean: boolean) => void;
}
const NavMenu: React.FC<INavMenuProps> = ({
setOpenMenu,
}: INavMenuProps) => {
const auth = false; const auth = false;
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<nav className="nav-menu"> <nav className="nav-menu">
{LINKS.map((link) => ( {LINKS.map((link) => (
<Link <Link
onClick={() => setOpenMenu(false)}
className={`nav-menu__link${ className={`nav-menu__link${
pathname === link.pathname ? "_active" : "" pathname === link.pathname ? "_active" : ""
}`} }`}
@ -21,7 +28,7 @@ const NavMenu = () => {
</Link> </Link>
))} ))}
<NavAuth responsible /> <NavAuth setOpenMenu={setOpenMenu} responsible />
</nav> </nav>
); );
}; };

View File

@ -5,7 +5,7 @@
width: 100%; width: 100%;
height: 78px; height: 78px;
position: fixed; position: fixed;
z-index: 10000; z-index: 10005;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;

View File

@ -40,7 +40,7 @@ const Navbar = () => {
<div className="navbar__lang-and-auth"> <div className="navbar__lang-and-auth">
<NavLanguage /> <NavLanguage />
<NavAuth /> <NavAuth setOpenMenu={setOpenMenu} />
</div> </div>
<button <button
@ -50,7 +50,7 @@ const Navbar = () => {
<Image src={openMenu ? cross : menu} alt="menu icon" /> <Image src={openMenu ? cross : menu} alt="menu icon" />
</button> </button>
{openMenu && <NavMenu />} {openMenu && <NavMenu setOpenMenu={setOpenMenu} />}
</section> </section>
); );
}; };

View File

@ -4,6 +4,11 @@
padding: 60px 90px 100px 90px; padding: 60px 90px 100px 90px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
h3 {
margin-bottom: 24px;
}
&__list { &__list {
margin-bottom: 40px; margin-bottom: 40px;
display: grid; display: grid;

View File

@ -2,15 +2,15 @@ import "./NewsSection.scss";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { getNews } from "./newsSectionStore"; import { getNews } from "./newsSectionStore";
import SectionHeader from "@/entities/SectionHeader/SectionHeader";
import arrow_icon from "./icons/arrow-right.svg"; import arrow_icon from "./icons/arrow-right.svg";
import NewsCard from "@/entities/NewsCard/NewsCard"; import NewsCard from "@/entities/NewsCard/NewsCard";
import Typography from "@/shared/ui/components/Typography/Typography";
const NewsSection = async () => { const NewsSection = async () => {
const news = await getNews(); const news = await getNews();
return ( return (
<section className="news-section"> <section className="news-section">
<SectionHeader title="Новости" /> <Typography element="h3">Новости</Typography>
<ul className="news-section__list"> <ul className="news-section__list">
{news?.map((article) => ( {news?.map((article) => (
<li key={article.id} className="news-section__card"> <li key={article.id} className="news-section__card">

View File

@ -14,9 +14,9 @@ export const getNews = async () => {
return data.results.slice(0, 4); return data.results.slice(0, 4);
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
alert(error.message); console.log(error.message);
} else { } else {
alert("An error ocured"); console.log("An error ocured");
} }
} }
}; };

View File

@ -6,128 +6,126 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
&__table { &__header {
width: 100%; margin-bottom: 50px;
padding: 15px; text-align: center;
background-color: #fff;
border: 1px solid $gray-300;
border-radius: 5px;
overflow: hidden;
overflow-x: auto;
table {
width: 100%;
border-collapse: collapse;
border-radius: 6px;
thead {
tr {
border-bottom: 1px solid #ddd;
th {
padding: 15px 0;
text-align: left;
color: $gray-500;
font-size: 16px;
font-weight: 500;
gap: 4px;
}
#report-header-date,
#report-header-comment,
#report-header-like {
cursor: pointer;
div {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; flex-direction: column;
gap: 24px;
} }
&__table {
width: 100%;
overflow: hidden;
overflow-x: auto;
border: 1px solid rgb(213, 213, 213);
border-radius: 6px;
&::-webkit-scrollbar {
display: none;
}
table {
thead {
padding: 0 30px;
height: 76px;
border-bottom: 1px solid rgb(241, 244, 249);
tr {
height: 100%;
display: grid;
align-items: center;
grid-template-columns: 120px 200px 210px 380px 165px 92px;
td {
button {
display: flex;
align-items: center;
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 { tbody {
padding: 0 30px;
display: flex;
flex-direction: column;
tr { 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 { td {
padding: 15px 0;
a {
justify-content: flex-start;
}
}
#report-date {
min-width: 150px;
color: $gray-500;
font-weight: 500;
font-size: 16px;
}
#report-location {
min-width: 200px;
overflow: hidden;
a {
text-decoration: underline;
white-space: preserve nowrap;
color: $light-blue;
font-size: 16px;
font-weight: 500;
}
}
#report-type {
min-width: 210px;
}
#report-description {
min-width: 378px;
p {
width: 318px;
color: #32303a;
font-size: 18px; font-size: 18px;
font-weight: 500; font-weight: 500;
line-height: 20px; line-height: 20px;
letter-spacing: 0.1px; }
text-align: justify;
#rating-section-date {
font-size: 16px;
color: rgb(102, 112, 133);
}
#rating-section-link {
a {
text-decoration: underline;
color: $light-blue;
} }
} }
#report-comment, #rating-section-description {
#report-like { color: $black;
div { }
#rating-section-reviews,
#rating-section-likes {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 7px; gap: 7px;
} }
}
#rating-section-reviews {
color: $light-blue;
} }
tr:last-child { #rating-section-likes {
border-bottom: none; color: $light-green;
} }
} }
} }
} }
&-error {
td {
text-align: center;
font-size: 42px;
}
} }
} }
@media screen and (max-width: 1024px) { @media screen and (max-width: 1024px) {
.rating-section { .rating-section {
padding: 0 30px; padding: 0 30px;
&__header {
margin-bottom: 40px;
gap: 20px;
}
} }
} }
@media screen and (max-width: 550px) { @media screen and (max-width: 550px) {
.rating-section { .rating-section {
padding: 0 16px; padding: 0 16px;
&__header {
margin-bottom: 20px;
gap: 16px;
}
} }
} }

View File

@ -3,8 +3,7 @@ import "./RatingSection.scss";
import Image from "next/image"; import Image from "next/image";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import SectionHeader from "@/entities/SectionHeader/SectionHeader"; import SearchForm from "@/features/SearchForm/SearchForm";
import SearchForm from "@/features/SearchBar/SearchForm";
import arrow_down_icon from "./icons/arrow-down.svg"; import arrow_down_icon from "./icons/arrow-down.svg";
import arrow_up_icon from "./icons/arrow-up.svg"; import arrow_up_icon from "./icons/arrow-up.svg";
import like_icon from "./icons/like.svg"; import like_icon from "./icons/like.svg";
@ -17,6 +16,9 @@ import {
ROAD_TYPES_COLORS, ROAD_TYPES_COLORS,
} from "@/shared/variables/road-types"; } from "@/shared/variables/road-types";
import RoadType from "@/entities/RoadType/RoadType"; 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 { interface IRatingSectionProps {
[key: string]: string; [key: string]: string;
@ -81,12 +83,41 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
return description; 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 ( return (
<div className="rating-section"> <section className="rating-section">
<SectionHeader <div className="rating-section__header">
title="Рейтинг" <Typography element="h3">Рейтинг</Typography>
description="Обсуждаем дороги: рейтинг, опыт, комфорт в пути!" <Paragraph>
/> Обсуждаем дороги: рейтинг, опыт, комфорт в пути!
</Paragraph>
</div>
<SearchForm <SearchForm
onChange={(e) => setRatingSearch(e.target.value)} onChange={(e) => setRatingSearch(e.target.value)}
name="rating-search" name="rating-search"
@ -98,77 +129,55 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
<table> <table>
<thead> <thead>
<tr> <tr>
<th id="report-header-date" tabIndex={0}> {params.map((p) => (
<div> <td key={p.id}>
Дата {p.handleClick ? (
<Image src={arrow_down_icon} alt="Arrow Down" /> <button onClick={p.handleClick}>
<Image src={arrow_up_icon} alt="Arrow Up" /> {p.name}
</div> <Image src={arrows} alt="Up and Down Arrows" />
</th> </button>
<th>Адрес</th> ) : (
<th>Статус</th> p.name
<th>Описание</th> )}
<th id="report-header-comment" tabIndex={0}> </td>
<div> ))}
Комментарии
<Image src={arrow_down_icon} alt="Arrow Down" />
<Image src={arrow_up_icon} alt="Arrow Up" />
</div>
</th>
<th id="report-header-like" tabIndex={0}>
<div>
Рейтинг
<Image src={arrow_down_icon} alt="Arrow Down" />
<Image src={arrow_up_icon} alt="Arrow Up" />
</div>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{reports.results.length !== 0 ? ( {reports.results.map((report) => (
reports.results.map((report) => (
<tr key={report.id}> <tr key={report.id}>
<td id="report-date"> <td id="rating-section-date">
{sliceDate(report.created_at)} {sliceDate(report.created_at)}
</td> </td>
<td id="report-location"> <td id="rating-section-link">
<Link href={`/report/${report.location[0].id}`}> <Link href={`/report/${report.id}`}>
{sliceLocation(report.location[0].address)} {sliceLocation(report.location[0].address)}
</Link> </Link>
</td> </td>
<td id="report-type"> <td>
<RoadType <RoadType
color={ROAD_TYPES_COLORS[report.category]} color={ROAD_TYPES_COLORS[report.category]}
> >
{ROAD_TYPES[report.category]} {ROAD_TYPES[report.category]}
</RoadType> </RoadType>
</td> </td>
<td id="report-description"> <td id="rating-section-description">
<p>{sliceDescription(report.description)}</p> {sliceDescription(report.description)}
</td> </td>
<td id="report-comment"> <td id="rating-section-reviews">
<div>
<Image src={message_icon} alt="Message Icon" /> <Image src={message_icon} alt="Message Icon" />
{report.count_reviews} {report.count_reviews}
</div>
</td> </td>
<td id="report-like"> <td id="rating-section-likes">
<div>
<Image src={like_icon} alt="Like Icon" /> <Image src={like_icon} alt="Like Icon" />
{report.total_likes} {report.total_likes}
</div>
</td> </td>
</tr> </tr>
)) ))}
) : (
<tr className="rating-section-error">
<td colSpan={7}>Упс, адрес не найден</td>
</tr>
)}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </section>
); );
}; };

View File

@ -1,14 +0,0 @@
<svg width="16.000000" height="16.000000" viewBox="0 0 16 16" 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="clip183_53161">
<rect id="arrow-down" width="16.000000" height="16.000000" transform="translate(16.000000 16.000000) rotate(180.000000)" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<rect id="arrow-down" width="16.000000" height="16.000000" transform="translate(16.000000 16.000000) rotate(180.000000)" fill="#FFFFFF" fill-opacity="0"/>
<g clip-path="url(#clip183_53161)">
<path id="Icon" d="M8 12.667L8 3.33398M3.33301 8L8 3.33398L12.6665 8" stroke="#667085" stroke-opacity="1.000000" stroke-width="1.333333" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 783 B

View File

@ -1,14 +0,0 @@
<svg width="16.000000" height="16.000000" viewBox="0 0 16 16" 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="clip161_49481">
<rect id="arrow-down" width="16.000000" height="16.000000" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<rect id="arrow-down" width="16.000000" height="16.000000" fill="#FFFFFF" fill-opacity="0"/>
<g clip-path="url(#clip161_49481)">
<path id="Icon" d="M8 3.33301L8 12.666M12.667 8L8 12.666L3.3335 8" stroke="#667085" stroke-opacity="1.000000" stroke-width="1.333333" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 656 B

View File

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

View File

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

View File

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

View File

@ -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<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");
} 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;

View File

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

View File

@ -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<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 (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 (
<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;

View File

@ -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;
}
}

View File

@ -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<HTMLInputElement> {
isPassword?: boolean;
label: string;
error: string;
}
const AuthInput: React.FC<IAuthInputProps> = ({
isPassword,
label,
error,
placeholder,
name,
type,
}: IAuthInputProps) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
return (
<div className="auth-input">
<label>{label}</label>
<div
className={`auth-input__field${error ? "-with-error" : ""}`}
>
<input
name={name}
placeholder={placeholder}
type={!isPassword ? type : isOpen ? type : "password"}
/>
{isPassword && (
<button
onClick={() => setIsOpen((prev) => !prev)}
type="button"
>
<Image src={isOpen ? eye_on : eye_off} alt="Eye Icon" />
</button>
)}
</div>
{error ? (
<p>
{error} <Image src={alert} alt="Alert Icon" />
</p>
) : null}
</div>
);
};
export default AuthInput;

View File

@ -0,0 +1,7 @@
<svg width="14.666992" height="14.666992" viewBox="0 0 14.667 14.667" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs/>
<path id="Icon" d="M7.33398 14C3.65137 14 0.666992 11.0156 0.666992 7.33398C0.666992 3.65137 3.65137 0.666992 7.33398 0.666992C11.0156 0.666992 14 3.65137 14 7.33398C14 11.0156 11.0156 14 7.33398 14ZM7.33398 4.66699L7.33398 7.33398M7.33398 10L7.33984 10" stroke="#F04438" stroke-opacity="1.000000" stroke-width="1.333333" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 566 B

View File

@ -0,0 +1,15 @@
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip1668_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

View File

@ -0,0 +1,15 @@
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip1668_50161">
<rect id="eye" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<rect id="eye" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
<g clip-path="url(#clip1668_50161)">
<path id="Vector" d="M12 4C19 4 23 12 23 12C23 12 19 20 12 20C5 20 1 12 1 12C1 12 5 4 12 4Z" stroke="#979797" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
<path id="Vector" d="M12 15C10.3428 15 9 13.6572 9 12C9 10.3428 10.3428 9 12 9C13.6572 9 15 10.3428 15 12C15 13.6572 13.6572 15 12 15Z" stroke="#979797" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 901 B

View File

@ -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;
}

View File

@ -0,0 +1,14 @@
import Image from "next/image";
import "./GoogleButton.scss";
import google from "./icons/google.svg";
const GoogleButton = () => {
return (
<button className="google-btn">
<Image src={google} alt="Google Icon" />
Войти через Google
</button>
);
};
export default GoogleButton;

View File

@ -0,0 +1,17 @@
<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="clip51_26188">
<rect id="Social icon" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<rect id="Social icon" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
<g clip-path="url(#clip51_26188)">
<path id="Vector" d="M23.7666 12.2764C23.7666 11.4609 23.7002 10.6406 23.5586 9.83789L12.2402 9.83789L12.2402 14.459L18.7217 14.459C18.4531 15.9492 17.5889 17.2676 16.3232 18.1055L16.3232 21.1035L20.1904 21.1035C22.4609 19.0137 23.7666 15.9268 23.7666 12.2764Z" fill="#4285F4" fill-opacity="1.000000" fill-rule="nonzero"/>
<path id="Vector" d="M12.2402 24.001C15.4766 24.001 18.2061 22.9385 20.1943 21.1045L16.3271 18.1064C15.252 18.8379 13.8623 19.2529 12.2441 19.2529C9.11426 19.2529 6.45898 17.1406 5.50684 14.3008L1.5166 14.3008L1.5166 17.3916C3.55371 21.4443 7.70312 24.001 12.2402 24.001Z" fill="#34A853" fill-opacity="1.000000" fill-rule="nonzero"/>
<path id="Vector" d="M5.50293 14.3008C5 12.8105 5 11.1963 5.50293 9.70605L5.50293 6.61523L1.5166 6.61523C-0.185547 10.0059 -0.185547 14.001 1.5166 17.3916L5.50293 14.3008Z" fill="#FBBC04" fill-opacity="1.000000" fill-rule="nonzero"/>
<path id="Vector" d="M12.2402 4.75C13.9512 4.72363 15.6045 5.36719 16.8438 6.54883L20.2695 3.12305C18.1006 1.08594 15.2207 -0.0341797 12.2402 0.000976562C7.70312 0.000976562 3.55371 2.55859 1.5166 6.61523L5.50293 9.70605C6.4502 6.86133 9.10938 4.75 12.2402 4.75Z" fill="#EA4335" fill-opacity="1.000000" fill-rule="nonzero"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -2,12 +2,22 @@
margin-bottom: 32px; margin-bottom: 32px;
height: 52px; height: 52px;
width: 645px; width: 645px;
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
button {
height: 100%;
width: 100px;
border-radius: 0px 6px 6px 0px;
background: rgb(72, 159, 225);
color: white;
}
&__input { &__input {
height: 100%; height: 100%;
width: 100%;
display: flex; display: flex;
flex: 1;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid rgb(197, 198, 197); border: 1px solid rgb(197, 198, 197);
@ -18,22 +28,14 @@
} }
input { input {
width: 100%;
padding: 10px 0; padding: 10px 0;
padding-left: 5px; padding-left: 5px;
width: 100%;
height: 100%; height: 100%;
border: none; border: none;
font-size: 18px; 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) { @media screen and (max-width: 768px) {
@ -41,9 +43,3 @@
width: 90%; width: 90%;
} }
} }
@media screen and (max-width: 550px) {
.search-form {
width: 90%;
}
}

View File

@ -17,7 +17,7 @@ const SearchForm: React.FC<ISearchFormProps> = ({
onChange, onChange,
}: ISearchFormProps) => { }: ISearchFormProps) => {
return ( return (
<form onSubmit={handleSubmit} className="search-form"> <form className="search-form" onSubmit={handleSubmit}>
<div className="search-form__input"> <div className="search-form__input">
<Image src={search} alt="Search Icon" /> <Image src={search} alt="Search Icon" />
<input <input

View File

@ -0,0 +1,15 @@
<svg width="24.000000" height="24.959961" viewBox="0 0 24 24.96" 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="clip18_5000">
<rect id="search" width="24.000000" height="24.959999" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<rect id="search" width="24.000000" height="24.959999" fill="#FFFFFF" fill-opacity="0"/>
<g clip-path="url(#clip18_5000)">
<path id="Vector" d="M11 19.7598C6.58154 19.7598 3 16.0352 3 11.4404C3 6.84473 6.58154 3.12012 11 3.12012C15.4185 3.12012 19 6.84473 19 11.4404C19 16.0352 15.4185 19.7598 11 19.7598Z" stroke="#C5C6C5" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
<path id="Vector" d="M21 21.8408L16.6499 17.3164" stroke="#C5C6C5" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 932 B