forked from Transparency/kgroad-frontend2
made sign-in and sign-up pages
This commit is contained in:
parent
3a04c6c1db
commit
301a4ae965
7
app/confirm-email/page.tsx
Normal file
7
app/confirm-email/page.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const page = () => {
|
||||||
|
return <div>page</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
7
app/forgot-password/page.tsx
Normal file
7
app/forgot-password/page.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const page = () => {
|
||||||
|
return <div>page</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
@ -1,7 +1,3 @@
|
|||||||
import React from "react";
|
import SignInPage from "@/Pages/SignInPage/SignInPage";
|
||||||
|
|
||||||
const page = () => {
|
export default SignInPage;
|
||||||
return <div>page</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default page;
|
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
import React from "react";
|
import SignUpPage from "@/Pages/SignUpPage/SignUpPage";
|
||||||
|
|
||||||
const page = () => {
|
export default SignUpPage;
|
||||||
return <div>page</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default page;
|
|
||||||
|
@ -9,10 +9,12 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.6.5",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"sass": "^1.70.0"
|
"sass": "^1.70.0",
|
||||||
|
"zustand": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
41
src/Entities/AuthHeader/AuthHeader.scss
Normal file
41
src/Entities/AuthHeader/AuthHeader.scss
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
@import "../../Shared/variables.scss";
|
||||||
|
|
||||||
|
.auth-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
padding: 12px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #eaecf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #101828;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: $gray-500;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
src/Entities/AuthHeader/AuthHeader.tsx
Normal file
29
src/Entities/AuthHeader/AuthHeader.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import Image, { StaticImageData } from "next/image";
|
||||||
|
import "./AuthHeader.scss";
|
||||||
|
|
||||||
|
interface IAuthHeaderProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: StaticImageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthHeader: React.FC<IAuthHeaderProps> = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
}: IAuthHeaderProps) => {
|
||||||
|
return (
|
||||||
|
<div className="auth-header">
|
||||||
|
<div className="auth-header__icon">
|
||||||
|
<Image src={icon} alt="Auth Icon" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="auth-header__text">
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<p>{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthHeader;
|
7
src/Entities/CustomLink/CustomLink.scss
Normal file
7
src/Entities/CustomLink/CustomLink.scss
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
@import "../../Shared/variables.scss";
|
||||||
|
|
||||||
|
.custom-link {
|
||||||
|
color: $blue;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
22
src/Entities/CustomLink/CustomLink.tsx
Normal file
22
src/Entities/CustomLink/CustomLink.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import "./CustomLink.scss";
|
||||||
|
|
||||||
|
interface ICustomLink {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
path: string;
|
||||||
|
style?: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomLink: React.FC<ICustomLink> = ({
|
||||||
|
children,
|
||||||
|
path,
|
||||||
|
style,
|
||||||
|
}: ICustomLink) => {
|
||||||
|
return (
|
||||||
|
<Link href={path} className="custom-link" style={style}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomLink;
|
19
src/Entities/GoogleButton/GoogleButton.scss
Normal file
19
src/Entities/GoogleButton/GoogleButton.scss
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
@import "../../Shared/variables.scss";
|
||||||
|
|
||||||
|
.google-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid $gray-300;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $gray-700;
|
||||||
|
}
|
18
src/Entities/GoogleButton/GoogleButton.tsx
Normal file
18
src/Entities/GoogleButton/GoogleButton.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import "./GoogleButton.scss";
|
||||||
|
import google_icon from "./icons/google-icon.svg";
|
||||||
|
|
||||||
|
interface IGoogleButton {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GoogleButton = ({ children }: IGoogleButton) => {
|
||||||
|
return (
|
||||||
|
<div className="google-btn">
|
||||||
|
<Image src={google_icon} alt="Google Icon" />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GoogleButton;
|
13
src/Entities/GoogleButton/icons/google-icon.svg
Normal file
13
src/Entities/GoogleButton/icons/google-icon.svg
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="Social icon" clip-path="url(#clip0_2134_15985)">
|
||||||
|
<path id="Vector" d="M23.7663 12.2763C23.7663 11.4605 23.7001 10.6404 23.559 9.83789H12.2402V14.4589H18.722C18.453 15.9492 17.5888 17.2676 16.3233 18.1054V21.1037H20.1903C22.4611 19.0137 23.7663 15.9272 23.7663 12.2763Z" fill="#4285F4"/>
|
||||||
|
<path id="Vector_2" d="M12.2401 24.0013C15.4766 24.0013 18.2059 22.9387 20.1945 21.1044L16.3276 18.106C15.2517 18.838 13.8627 19.2525 12.2445 19.2525C9.11388 19.2525 6.45946 17.1404 5.50705 14.3008H1.5166V17.3917C3.55371 21.4439 7.7029 24.0013 12.2401 24.0013Z" fill="#34A853"/>
|
||||||
|
<path id="Vector_3" d="M5.50277 14.3007C5.00011 12.8103 5.00011 11.1965 5.50277 9.70618V6.61523H1.51674C-0.185266 10.006 -0.185266 14.0009 1.51674 17.3916L5.50277 14.3007Z" fill="#FBBC04"/>
|
||||||
|
<path id="Vector_4" d="M12.2401 4.74966C13.9509 4.7232 15.6044 5.36697 16.8434 6.54867L20.2695 3.12262C18.1001 1.0855 15.2208 -0.034466 12.2401 0.000808666C7.7029 0.000808666 3.55371 2.55822 1.5166 6.61481L5.50264 9.70575C6.45064 6.86173 9.10947 4.74966 12.2401 4.74966Z" fill="#EA4335"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2134_15985">
|
||||||
|
<rect width="24" height="24" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
46
src/Entities/InputWithLabel/InputWithLabel.scss
Normal file
46
src/Entities/InputWithLabel/InputWithLabel.scss
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
@import "../../Shared/variables.scss";
|
||||||
|
|
||||||
|
.input-with-label {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
color: $gray-700;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
padding: 10px 14px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid $gray-300;
|
||||||
|
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 18px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
color: $gray-500;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-width: 24px;
|
||||||
|
width: 24;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
62
src/Entities/InputWithLabel/InputWithLabel.tsx
Normal file
62
src/Entities/InputWithLabel/InputWithLabel.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import "./InputWithLabel.scss";
|
||||||
|
import eye_icon from "./icons/eye-icon.svg";
|
||||||
|
import eye_off_icon from "./icons/eye-off-icon.svg";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface IInputWithLabel {
|
||||||
|
value?: string;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
name?: string;
|
||||||
|
secret?: boolean;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputWithLabel: React.FC<IInputWithLabel> = ({
|
||||||
|
placeholder,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
name,
|
||||||
|
secret,
|
||||||
|
error,
|
||||||
|
}: IInputWithLabel) => {
|
||||||
|
const [show, setShow] = useState<boolean>(false);
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="input-with-label">
|
||||||
|
<label>{label}</label>
|
||||||
|
|
||||||
|
<div className="input-with-label__wrapper">
|
||||||
|
<input
|
||||||
|
onChange={handleChange}
|
||||||
|
type={!secret ? "text" : show ? "text" : "password"}
|
||||||
|
value={value}
|
||||||
|
placeholder={placeholder}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
{secret && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShow((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{show ? (
|
||||||
|
<Image src={eye_icon} alt="Eye Opened Icon" />
|
||||||
|
) : (
|
||||||
|
<Image src={eye_off_icon} alt="Eye Closed Icon" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{error ? <p>{error}</p> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputWithLabel;
|
6
src/Entities/InputWithLabel/icons/eye-icon.svg
Normal file
6
src/Entities/InputWithLabel/icons/eye-icon.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="eye">
|
||||||
|
<path id="Vector" d="M1 12C1 12 5 4 12 4C19 4 23 12 23 12C23 12 19 20 12 20C5 20 1 12 1 12Z" stroke="#979797" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path id="Vector_2" d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" stroke="#979797" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 518 B |
11
src/Entities/InputWithLabel/icons/eye-off-icon.svg
Normal file
11
src/Entities/InputWithLabel/icons/eye-off-icon.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="eye-off" clip-path="url(#clip0_1668_50526)">
|
||||||
|
<path id="Vector" d="M17.94 17.94C16.2306 19.243 14.1491 19.9649 12 20C5 20 1 12 1 12C2.24389 9.68192 3.96914 7.65663 6.06 6.06003M9.9 4.24002C10.5883 4.0789 11.2931 3.99836 12 4.00003C19 4.00003 23 12 23 12C22.393 13.1356 21.6691 14.2048 20.84 15.19M14.12 14.12C13.8454 14.4148 13.5141 14.6512 13.1462 14.8151C12.7782 14.9791 12.3809 15.0673 11.9781 15.0744C11.5753 15.0815 11.1752 15.0074 10.8016 14.8565C10.4281 14.7056 10.0887 14.4811 9.80385 14.1962C9.51897 13.9113 9.29439 13.572 9.14351 13.1984C8.99262 12.8249 8.91853 12.4247 8.92563 12.0219C8.93274 11.6191 9.02091 11.2219 9.18488 10.8539C9.34884 10.4859 9.58525 10.1547 9.88 9.88003" stroke="#979797" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path id="Vector_2" d="M1 1L23 23" stroke="#979797" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1668_50526">
|
||||||
|
<rect width="24" height="24" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -36,7 +36,9 @@ const NavMenu = () => {
|
|||||||
: "nav-menu__pages-item"
|
: "nav-menu__pages-item"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Link href={page.path}>{page.page}</Link>
|
<Link onClick={() => setMenu(false)} href={page.path}>
|
||||||
|
{page.page}
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
{auth ? (
|
{auth ? (
|
||||||
@ -47,7 +49,9 @@ const NavMenu = () => {
|
|||||||
: "nav-menu__pages-item"
|
: "nav-menu__pages-item"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Link href="/sign-in">Войти</Link>
|
<Link onClick={() => setMenu(false)} href="/sign-in">
|
||||||
|
Войти
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
) : (
|
) : (
|
||||||
<li
|
<li
|
||||||
@ -57,7 +61,9 @@ const NavMenu = () => {
|
|||||||
: "nav-menu__pages-item"
|
: "nav-menu__pages-item"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Link href="/profile">Профиль</Link>
|
<Link onClick={() => setMenu(false)} href="/profile">
|
||||||
|
Профиль
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
border: 1px solid var(--grey-for-mask, #c5c6c5);
|
border: 1px solid var(--grey-for-mask, #c5c6c5);
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
height: 24px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
44
src/Features/SignInForm/SignInForm.scss
Normal file
44
src/Features/SignInForm/SignInForm.scss
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
@import "../../Shared/variables.scss";
|
||||||
|
|
||||||
|
.sign-in-form {
|
||||||
|
width: 360px;
|
||||||
|
display: grid;
|
||||||
|
gap: 36px;
|
||||||
|
|
||||||
|
&__inputs,
|
||||||
|
&__btns {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: $red-500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btns {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-account {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $gray-500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
.sign-in-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
87
src/Features/SignInForm/SignInForm.tsx
Normal file
87
src/Features/SignInForm/SignInForm.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import "./SignInForm.scss";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import InputWithLabel from "@/Entities/InputWithLabel/InputWithLabel";
|
||||||
|
import Button from "@/Shared/UI/Button/Button";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import CustomLink from "@/Entities/CustomLink/CustomLink";
|
||||||
|
import GoogleButton from "@/Entities/GoogleButton/GoogleButton";
|
||||||
|
import { useSignIn } from "./sign-in.store";
|
||||||
|
import DefaultLoader from "@/Shared/UI/DefaultLoader/DefaultLoader";
|
||||||
|
|
||||||
|
const SignInForm = () => {
|
||||||
|
const [email, setEmail] = useState<string>("");
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
|
||||||
|
const {
|
||||||
|
login,
|
||||||
|
emailError,
|
||||||
|
passwordError,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
cleanRedirect,
|
||||||
|
redirect,
|
||||||
|
} = useSignIn();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (redirect) {
|
||||||
|
router.push("/profile");
|
||||||
|
cleanRedirect();
|
||||||
|
}
|
||||||
|
}, [redirect]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className="sign-in-form"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
login(email, password);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="sign-in-form__inputs">
|
||||||
|
<InputWithLabel
|
||||||
|
label="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="Введите email"
|
||||||
|
error={emailError}
|
||||||
|
/>
|
||||||
|
{emailError ? <p>{emailError}</p> : null}
|
||||||
|
<InputWithLabel
|
||||||
|
label="Пароль"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="Введите пароль"
|
||||||
|
secret
|
||||||
|
error={passwordError}
|
||||||
|
/>
|
||||||
|
{error ? <p>{error}</p> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CustomLink
|
||||||
|
path="/forgot-password"
|
||||||
|
style={{ justifySelf: "flex-end" }}
|
||||||
|
>
|
||||||
|
Забыли пароль?
|
||||||
|
</CustomLink>
|
||||||
|
|
||||||
|
<div className="sign-in-form__btns">
|
||||||
|
<Button type="submit">
|
||||||
|
{!loading ? "Войти" : <DefaultLoader />}
|
||||||
|
</Button>
|
||||||
|
<GoogleButton>Войти через Google</GoogleButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sign-in-form__no-account">
|
||||||
|
<span>Еще нет аккаунта?</span>
|
||||||
|
|
||||||
|
<CustomLink path="/sign-up">Зарегистрируйтесь</CustomLink>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignInForm;
|
63
src/Features/SignInForm/sign-in.store.ts
Normal file
63
src/Features/SignInForm/sign-in.store.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { baseAPI } from "@/Shared/API/baseAPI";
|
||||||
|
import { IFetch } from "@/Shared/types";
|
||||||
|
import axios from "axios";
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface SignInStore extends IFetch {
|
||||||
|
login: (email: string, password: string) => Promise<void>;
|
||||||
|
cleanRedirect: () => void;
|
||||||
|
emailError: string;
|
||||||
|
passwordError: string;
|
||||||
|
redirect: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSignIn = create<SignInStore>((set) => ({
|
||||||
|
loading: false,
|
||||||
|
error: "",
|
||||||
|
emailError: "",
|
||||||
|
passwordError: "",
|
||||||
|
redirect: false,
|
||||||
|
login: async (email: string, password: string) => {
|
||||||
|
if (!email.trim()) {
|
||||||
|
set({ emailError: "Пожалуйста введите почту" });
|
||||||
|
set({ passwordError: "" });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!password.trim()) {
|
||||||
|
set({ passwordError: "Пожалуйста введите пароль" });
|
||||||
|
set({ emailError: "" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
set({ loading: true });
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${baseAPI}/users/login/`,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
localStorage.setItem("user", JSON.stringify(response.data));
|
||||||
|
|
||||||
|
set({ emailError: "" });
|
||||||
|
set({ passwordError: "" });
|
||||||
|
set({ error: "" });
|
||||||
|
set({ redirect: true });
|
||||||
|
} catch (error: any) {
|
||||||
|
set({ emailError: "" });
|
||||||
|
set({ passwordError: "" });
|
||||||
|
set({ error: error.message });
|
||||||
|
} finally {
|
||||||
|
set({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cleanRedirect: () => {
|
||||||
|
set({ redirect: false });
|
||||||
|
},
|
||||||
|
}));
|
44
src/Features/SignUpForm/SignUpForm.scss
Normal file
44
src/Features/SignUpForm/SignUpForm.scss
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
@import "../../Shared/variables.scss";
|
||||||
|
|
||||||
|
.sign-up-form {
|
||||||
|
width: 360px;
|
||||||
|
display: grid;
|
||||||
|
gap: 36px;
|
||||||
|
|
||||||
|
&__inputs,
|
||||||
|
&__btns {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $red-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btns {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__has-account {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $gray-500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
.sign-up-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
90
src/Features/SignUpForm/SignUpForm.tsx
Normal file
90
src/Features/SignUpForm/SignUpForm.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import "./SignUpForm.scss";
|
||||||
|
import InputWithLabel from "@/Entities/InputWithLabel/InputWithLabel";
|
||||||
|
import Button from "@/Shared/UI/Button/Button";
|
||||||
|
import CustomLink from "@/Entities/CustomLink/CustomLink";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import GoogleButton from "@/Entities/GoogleButton/GoogleButton";
|
||||||
|
import { useSignUp } from "./sign-up.store";
|
||||||
|
import DefaultLoader from "@/Shared/UI/DefaultLoader/DefaultLoader";
|
||||||
|
|
||||||
|
const SignUpForm = () => {
|
||||||
|
const [email, setEmail] = useState<string>("");
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState<string>("");
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
redirect,
|
||||||
|
register,
|
||||||
|
loading,
|
||||||
|
emailError,
|
||||||
|
passwordError,
|
||||||
|
confirmPasswordError,
|
||||||
|
matchPasswordError,
|
||||||
|
error,
|
||||||
|
} = useSignUp();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (redirect) {
|
||||||
|
router.push("/confirm-email");
|
||||||
|
}
|
||||||
|
}, [redirect]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className="sign-up-form"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
register(email, password, confirmPassword);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="sign-up-form__inputs">
|
||||||
|
<InputWithLabel
|
||||||
|
label="Email"
|
||||||
|
placeholder="Введите email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
error={emailError}
|
||||||
|
/>
|
||||||
|
<InputWithLabel
|
||||||
|
label="Пароль"
|
||||||
|
placeholder="Введите пароль"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
secret
|
||||||
|
error={passwordError}
|
||||||
|
/>
|
||||||
|
<InputWithLabel
|
||||||
|
label="Пароль Потверждения"
|
||||||
|
placeholder="Повторите пароль"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
secret
|
||||||
|
error={confirmPasswordError}
|
||||||
|
/>
|
||||||
|
{matchPasswordError ? <p>{matchPasswordError}</p> : null}
|
||||||
|
{error ? <p>{error}</p> : null}
|
||||||
|
</div>
|
||||||
|
<div className="sign-up-form__btns">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={() => router.push("/confirm-email")}
|
||||||
|
>
|
||||||
|
{loading ? <DefaultLoader /> : "Зарегистрироваться"}
|
||||||
|
</Button>
|
||||||
|
<GoogleButton>Войти через Google</GoogleButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sign-up-form__has-account">
|
||||||
|
<span>Уже есть аккаунт?</span>
|
||||||
|
<CustomLink path="/sign-in">Войти в аккаунт</CustomLink>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignUpForm;
|
121
src/Features/SignUpForm/sign-up.store.ts
Normal file
121
src/Features/SignUpForm/sign-up.store.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { baseAPI } from "@/Shared/API/baseAPI";
|
||||||
|
import { IFetch } from "@/Shared/types";
|
||||||
|
import axios from "axios";
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface SignUpStore extends IFetch {
|
||||||
|
register: (
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
confirmPassword: string
|
||||||
|
) => Promise<void>;
|
||||||
|
cleanRedirect: () => void;
|
||||||
|
emailError: string;
|
||||||
|
passwordError: string;
|
||||||
|
confirmPasswordError: string;
|
||||||
|
matchPasswordError: string;
|
||||||
|
redirect: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSignUp = create<SignUpStore>((set) => ({
|
||||||
|
loading: false,
|
||||||
|
error: "",
|
||||||
|
emailError: "",
|
||||||
|
passwordError: "",
|
||||||
|
confirmPasswordError: "",
|
||||||
|
matchPasswordError: "",
|
||||||
|
redirect: false,
|
||||||
|
register: async (
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
confirmPassword: string
|
||||||
|
) => {
|
||||||
|
if (!email.trim()) {
|
||||||
|
set({ passwordError: "" });
|
||||||
|
set({ confirmPasswordError: "" });
|
||||||
|
set({ matchPasswordError: "" });
|
||||||
|
set({ emailError: "Пожалуйста введите почту" });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!password.trim()) {
|
||||||
|
set({ emailError: "" });
|
||||||
|
set({ confirmPasswordError: "" });
|
||||||
|
set({ matchPasswordError: "" });
|
||||||
|
set({ passwordError: "Пожалуйста введите пароль" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmPassword.trim()) {
|
||||||
|
set({ emailError: "" });
|
||||||
|
set({ passwordError: "" });
|
||||||
|
set({ matchPasswordError: "" });
|
||||||
|
set({ confirmPasswordError: "Пожалуйста введите пароль" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validatePassword(password)) {
|
||||||
|
set({ emailError: "" });
|
||||||
|
set({ passwordError: "" });
|
||||||
|
set({ matchPasswordError: "" });
|
||||||
|
set({ confirmPasswordError: "" });
|
||||||
|
set({ error: "Минимум 8 символов, 1 заглавная буква и цифра" });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
set({ emailError: "" });
|
||||||
|
set({ confirmPasswordError: "" });
|
||||||
|
set({ passwordError: "" });
|
||||||
|
set({ matchPasswordError: "Пароли не совпадают" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
password2: confirmPassword,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
set({ loading: true });
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${baseAPI}/users/register/`,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
localStorage.setItem("user", JSON.stringify(response.data));
|
||||||
|
|
||||||
|
set({ emailError: "" });
|
||||||
|
set({ passwordError: "" });
|
||||||
|
set({ confirmPasswordError: "" });
|
||||||
|
set({ matchPasswordError: "" });
|
||||||
|
set({ error: "" });
|
||||||
|
set({ redirect: true });
|
||||||
|
} catch (error: any) {
|
||||||
|
set({ emailError: "" });
|
||||||
|
set({ passwordError: "" });
|
||||||
|
set({ confirmPasswordError: "" });
|
||||||
|
set({ matchPasswordError: "" });
|
||||||
|
set({ error: error.message });
|
||||||
|
} finally {
|
||||||
|
set({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cleanRedirect: () => {
|
||||||
|
set({ redirect: false });
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const validatePassword = (password: string) => {
|
||||||
|
const regex = /[A-Z]/;
|
||||||
|
const digitRegex = /\d/;
|
||||||
|
if (password.length < 8) return true;
|
||||||
|
console.log("1");
|
||||||
|
if (!regex.test(password) || !digitRegex.test(password))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
15
src/Pages/SignInPage/SignInPage.scss
Normal file
15
src/Pages/SignInPage/SignInPage.scss
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.sign-in-page {
|
||||||
|
height: 100vh;
|
||||||
|
min-height: 800px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
.sign-in-page {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
}
|
20
src/Pages/SignInPage/SignInPage.tsx
Normal file
20
src/Pages/SignInPage/SignInPage.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import "./SignInPage.scss";
|
||||||
|
import sign_in_icon from "./icons/sign-in-icon.svg";
|
||||||
|
import AuthHeader from "@/Entities/AuthHeader/AuthHeader";
|
||||||
|
import SignInForm from "@/Features/SignInForm/SignInForm";
|
||||||
|
|
||||||
|
const SignInPage = () => {
|
||||||
|
return (
|
||||||
|
<div className="sign-in-page">
|
||||||
|
<AuthHeader
|
||||||
|
title="Войдите в аккаунт"
|
||||||
|
description="Пожалуйста, введите свои данные"
|
||||||
|
icon={sign_in_icon}
|
||||||
|
/>
|
||||||
|
<SignInForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignInPage;
|
5
src/Pages/SignInPage/icons/sign-in-icon.svg
Normal file
5
src/Pages/SignInPage/icons/sign-in-icon.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="log-in-04">
|
||||||
|
<path id="Icon" d="M12 8L16 12M16 12L12 16M16 12H3M3.33782 7C5.06687 4.01099 8.29859 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C8.29859 22 5.06687 19.989 3.33782 17" stroke="#344054" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 395 B |
15
src/Pages/SignUpPage/SignUpPage.scss
Normal file
15
src/Pages/SignUpPage/SignUpPage.scss
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.sign-up-page {
|
||||||
|
height: 100vh;
|
||||||
|
min-height: 800px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
.sign-up-page {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
}
|
19
src/Pages/SignUpPage/SignUpPage.tsx
Normal file
19
src/Pages/SignUpPage/SignUpPage.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import "./SignUpPage.scss";
|
||||||
|
import AuthHeader from "@/Entities/AuthHeader/AuthHeader";
|
||||||
|
import flag_icon from "./icons/flag-icon.svg";
|
||||||
|
import SignUpForm from "@/Features/SignUpForm/SignUpForm";
|
||||||
|
|
||||||
|
const SignUpPage = () => {
|
||||||
|
return (
|
||||||
|
<div className="sign-up-page">
|
||||||
|
<AuthHeader
|
||||||
|
title="Регистрация"
|
||||||
|
description="Пожалуйста, введите свои данные"
|
||||||
|
icon={flag_icon}
|
||||||
|
/>
|
||||||
|
<SignUpForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignUpPage;
|
5
src/Pages/SignUpPage/icons/flag-icon.svg
Normal file
5
src/Pages/SignUpPage/icons/flag-icon.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="flag-05">
|
||||||
|
<path id="Icon" d="M14.0914 6.72222H20.0451C20.5173 6.72222 20.7534 6.72222 20.8914 6.82149C21.0119 6.9081 21.0903 7.04141 21.1075 7.18877C21.1272 7.35767 21.0126 7.56403 20.7833 7.97677L19.3624 10.5343C19.2793 10.684 19.2377 10.7589 19.2214 10.8381C19.207 10.9083 19.207 10.9806 19.2214 11.0508C19.2377 11.13 19.2793 11.2049 19.3624 11.3545L20.7833 13.9121C21.0126 14.3248 21.1272 14.5312 21.1075 14.7001C21.0903 14.8475 21.0119 14.9808 20.8914 15.0674C20.7534 15.1667 20.5173 15.1667 20.0451 15.1667H12.6136C12.0224 15.1667 11.7269 15.1667 11.5011 15.0516C11.3024 14.9504 11.141 14.7889 11.0398 14.5903C10.9247 14.3645 10.9247 14.0689 10.9247 13.4778V10.9444M7.23027 21.5L3.00805 4.61111M4.59143 10.9444H12.4025C12.9937 10.9444 13.2892 10.9444 13.515 10.8294C13.7137 10.7282 13.8751 10.5667 13.9763 10.3681C14.0914 10.1423 14.0914 9.84672 14.0914 9.25556V4.18889C14.0914 3.59772 14.0914 3.30214 13.9763 3.07634C13.8751 2.87773 13.7137 2.71625 13.515 2.61505C13.2892 2.5 12.9937 2.5 12.4025 2.5H4.64335C3.90602 2.5 3.53735 2.5 3.2852 2.65278C3.0642 2.78668 2.89999 2.99699 2.82369 3.24387C2.73663 3.52555 2.82605 3.88321 3.00489 4.59852L4.59143 10.9444Z" stroke="#344054" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
2
src/Shared/API/baseAPI.ts
Normal file
2
src/Shared/API/baseAPI.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const baseAPI =
|
||||||
|
"https://api.kgroaduat.fishrungames.com/api/v1";
|
10
src/Shared/UI/Button/Button.scss
Normal file
10
src/Shared/UI/Button/Button.scss
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
@import "../../variables.scss";
|
||||||
|
|
||||||
|
.ui-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
background-color: $light-blue;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
11
src/Shared/UI/Button/Button.tsx
Normal file
11
src/Shared/UI/Button/Button.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import "./Button.scss";
|
||||||
|
|
||||||
|
interface IButton extends React.AllHTMLAttributes<HTMLButtonElement> {
|
||||||
|
children: React.ReactNode | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button: React.FC<IButton> = ({ children }: IButton) => {
|
||||||
|
return <button className="ui-btn">{children}</button>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
34
src/Shared/UI/DefaultLoader/DefaultLoader.scss
Normal file
34
src/Shared/UI/DefaultLoader/DefaultLoader.scss
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
.lds-ring {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
.lds-ring div {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||||
|
border-color: #fff transparent transparent transparent;
|
||||||
|
}
|
||||||
|
.lds-ring div:nth-child(1) {
|
||||||
|
animation-delay: -0.45s;
|
||||||
|
}
|
||||||
|
.lds-ring div:nth-child(2) {
|
||||||
|
animation-delay: -0.3s;
|
||||||
|
}
|
||||||
|
.lds-ring div:nth-child(3) {
|
||||||
|
animation-delay: -0.15s;
|
||||||
|
}
|
||||||
|
@keyframes lds-ring {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
14
src/Shared/UI/DefaultLoader/DefaultLoader.tsx
Normal file
14
src/Shared/UI/DefaultLoader/DefaultLoader.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import "./DefaultLoader.scss";
|
||||||
|
|
||||||
|
const DefaultLoader = () => {
|
||||||
|
return (
|
||||||
|
<div className="lds-ring">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DefaultLoader;
|
27
src/Shared/hooks/useUpdateEffect.ts
Normal file
27
src/Shared/hooks/useUpdateEffect.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { DependencyList, EffectCallback, useEffect } from "react";
|
||||||
|
|
||||||
|
export function useIsFirstRender(): boolean {
|
||||||
|
const isFirst = useRef(true);
|
||||||
|
|
||||||
|
if (isFirst.current) {
|
||||||
|
isFirst.current = false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isFirst.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateEffect(
|
||||||
|
effect: EffectCallback,
|
||||||
|
deps?: DependencyList
|
||||||
|
) {
|
||||||
|
const isFirst = useIsFirstRender();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isFirst) {
|
||||||
|
return effect();
|
||||||
|
}
|
||||||
|
}, deps);
|
||||||
|
}
|
6
src/Shared/types.ts
Normal file
6
src/Shared/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface IFetch {
|
||||||
|
response?: string;
|
||||||
|
data?: any;
|
||||||
|
loading: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
@ -1 +1,6 @@
|
|||||||
$light-blue: #489fe1;
|
$light-blue: #489fe1;
|
||||||
|
$blue: #0077b6;
|
||||||
|
$gray-300: #d0d5dd;
|
||||||
|
$gray-500: #667085;
|
||||||
|
$gray-700: #344054;
|
||||||
|
$red-500: #f04438;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
.footer {
|
.footer {
|
||||||
|
margin-top: 110px;
|
||||||
padding: 48px 90px;
|
padding: 48px 90px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
@ -43,12 +44,14 @@
|
|||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.footer {
|
.footer {
|
||||||
|
margin-top: 80px;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 550px) {
|
@media screen and (max-width: 550px) {
|
||||||
.footer {
|
.footer {
|
||||||
|
margin-top: 70px;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ const Footer = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
<div className="footer__nets">
|
<div className="footer__nets">
|
||||||
{[youtube, facebook, instagram].map((net) => (
|
{[youtube, facebook, instagram].map((net) => (
|
||||||
<Image src={net} alt="Net Icon" key={net} />
|
<Image src={net} alt="Net Icon" key={net.src} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
69
yarn.lock
69
yarn.lock
@ -414,6 +414,11 @@ asynciterator.prototype@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-symbols "^1.0.3"
|
has-symbols "^1.0.3"
|
||||||
|
|
||||||
|
asynckit@^0.4.0:
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||||
|
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||||
|
|
||||||
available-typed-arrays@^1.0.5:
|
available-typed-arrays@^1.0.5:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
|
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
|
||||||
@ -424,6 +429,15 @@ axe-core@=4.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf"
|
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf"
|
||||||
integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
|
integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
|
||||||
|
|
||||||
|
axios@^1.6.5:
|
||||||
|
version "1.6.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.5.tgz#2c090da14aeeab3770ad30c3a1461bc970fb0cd8"
|
||||||
|
integrity sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.15.4"
|
||||||
|
form-data "^4.0.0"
|
||||||
|
proxy-from-env "^1.1.0"
|
||||||
|
|
||||||
axobject-query@^3.2.1:
|
axobject-query@^3.2.1:
|
||||||
version "3.2.1"
|
version "3.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a"
|
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a"
|
||||||
@ -529,6 +543,13 @@ color-name@~1.1.4:
|
|||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||||
|
|
||||||
|
combined-stream@^1.0.8:
|
||||||
|
version "1.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||||
|
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||||
|
dependencies:
|
||||||
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
@ -590,6 +611,11 @@ define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1:
|
|||||||
has-property-descriptors "^1.0.0"
|
has-property-descriptors "^1.0.0"
|
||||||
object-keys "^1.1.1"
|
object-keys "^1.1.1"
|
||||||
|
|
||||||
|
delayed-stream@~1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||||
|
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||||
|
|
||||||
dequal@^2.0.3:
|
dequal@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||||
@ -1009,6 +1035,11 @@ flatted@^3.2.9:
|
|||||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf"
|
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf"
|
||||||
integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==
|
integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==
|
||||||
|
|
||||||
|
follow-redirects@^1.15.4:
|
||||||
|
version "1.15.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020"
|
||||||
|
integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==
|
||||||
|
|
||||||
for-each@^0.3.3:
|
for-each@^0.3.3:
|
||||||
version "0.3.3"
|
version "0.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
|
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
|
||||||
@ -1024,6 +1055,15 @@ foreground-child@^3.1.0:
|
|||||||
cross-spawn "^7.0.0"
|
cross-spawn "^7.0.0"
|
||||||
signal-exit "^4.0.1"
|
signal-exit "^4.0.1"
|
||||||
|
|
||||||
|
form-data@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
|
||||||
|
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
|
||||||
|
dependencies:
|
||||||
|
asynckit "^0.4.0"
|
||||||
|
combined-stream "^1.0.8"
|
||||||
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
fs.realpath@^1.0.0:
|
fs.realpath@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||||
@ -1566,6 +1606,18 @@ micromatch@^4.0.4:
|
|||||||
braces "^3.0.2"
|
braces "^3.0.2"
|
||||||
picomatch "^2.3.1"
|
picomatch "^2.3.1"
|
||||||
|
|
||||||
|
mime-db@1.52.0:
|
||||||
|
version "1.52.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||||
|
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||||
|
|
||||||
|
mime-types@^2.1.12:
|
||||||
|
version "2.1.35"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||||
|
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||||
|
dependencies:
|
||||||
|
mime-db "1.52.0"
|
||||||
|
|
||||||
minimatch@9.0.3, minimatch@^9.0.1:
|
minimatch@9.0.3, minimatch@^9.0.1:
|
||||||
version "9.0.3"
|
version "9.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
|
||||||
@ -1814,6 +1866,11 @@ prop-types@^15.8.1:
|
|||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
react-is "^16.13.1"
|
react-is "^16.13.1"
|
||||||
|
|
||||||
|
proxy-from-env@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||||
|
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||||
|
|
||||||
punycode@^2.1.0:
|
punycode@^2.1.0:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||||
@ -2251,6 +2308,11 @@ uri-js@^4.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.1.0"
|
punycode "^2.1.0"
|
||||||
|
|
||||||
|
use-sync-external-store@1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
|
|
||||||
which-boxed-primitive@^1.0.2:
|
which-boxed-primitive@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
|
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
|
||||||
@ -2340,3 +2402,10 @@ yocto-queue@^0.1.0:
|
|||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
|
zustand@^4.5.0:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.0.tgz#141354af56f91de378aa6c4b930032ab338f3ef0"
|
||||||
|
integrity sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==
|
||||||
|
dependencies:
|
||||||
|
use-sync-external-store "1.2.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user