diff --git a/app/confirm-email/page.tsx b/app/confirm-email/page.tsx new file mode 100644 index 0000000..a54c709 --- /dev/null +++ b/app/confirm-email/page.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const page = () => { + return
page
; +}; + +export default page; diff --git a/app/forgot-password/page.tsx b/app/forgot-password/page.tsx new file mode 100644 index 0000000..a54c709 --- /dev/null +++ b/app/forgot-password/page.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const page = () => { + return
page
; +}; + +export default page; diff --git a/app/sign-in/page.tsx b/app/sign-in/page.tsx index a54c709..b7c9593 100644 --- a/app/sign-in/page.tsx +++ b/app/sign-in/page.tsx @@ -1,7 +1,3 @@ -import React from "react"; +import SignInPage from "@/Pages/SignInPage/SignInPage"; -const page = () => { - return
page
; -}; - -export default page; +export default SignInPage; diff --git a/app/sign-up/page.tsx b/app/sign-up/page.tsx index a54c709..d6447b2 100644 --- a/app/sign-up/page.tsx +++ b/app/sign-up/page.tsx @@ -1,7 +1,3 @@ -import React from "react"; +import SignUpPage from "@/Pages/SignUpPage/SignUpPage"; -const page = () => { - return
page
; -}; - -export default page; +export default SignUpPage; diff --git a/package.json b/package.json index 8e8148e..f1e64f1 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,12 @@ "lint": "next lint" }, "dependencies": { + "axios": "^1.6.5", "next": "14.1.0", "react": "^18", "react-dom": "^18", - "sass": "^1.70.0" + "sass": "^1.70.0", + "zustand": "^4.5.0" }, "devDependencies": { "@types/node": "^20", diff --git a/src/Entities/AuthHeader/AuthHeader.scss b/src/Entities/AuthHeader/AuthHeader.scss new file mode 100644 index 0000000..af41c98 --- /dev/null +++ b/src/Entities/AuthHeader/AuthHeader.scss @@ -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; + } + } +} diff --git a/src/Entities/AuthHeader/AuthHeader.tsx b/src/Entities/AuthHeader/AuthHeader.tsx new file mode 100644 index 0000000..0e77013 --- /dev/null +++ b/src/Entities/AuthHeader/AuthHeader.tsx @@ -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 = ({ + title, + description, + icon, +}: IAuthHeaderProps) => { + return ( +
+
+ Auth Icon +
+ +
+

{title}

+

{description}

+
+
+ ); +}; + +export default AuthHeader; diff --git a/src/Entities/CustomLink/CustomLink.scss b/src/Entities/CustomLink/CustomLink.scss new file mode 100644 index 0000000..e5ffda4 --- /dev/null +++ b/src/Entities/CustomLink/CustomLink.scss @@ -0,0 +1,7 @@ +@import "../../Shared/variables.scss"; + +.custom-link { + color: $blue; + font-size: 16px; + font-weight: 400; +} diff --git a/src/Entities/CustomLink/CustomLink.tsx b/src/Entities/CustomLink/CustomLink.tsx new file mode 100644 index 0000000..f0ff21b --- /dev/null +++ b/src/Entities/CustomLink/CustomLink.tsx @@ -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 = ({ + children, + path, + style, +}: ICustomLink) => { + return ( + + {children} + + ); +}; + +export default CustomLink; diff --git a/src/Entities/GoogleButton/GoogleButton.scss b/src/Entities/GoogleButton/GoogleButton.scss new file mode 100644 index 0000000..d9aeaa8 --- /dev/null +++ b/src/Entities/GoogleButton/GoogleButton.scss @@ -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; +} diff --git a/src/Entities/GoogleButton/GoogleButton.tsx b/src/Entities/GoogleButton/GoogleButton.tsx new file mode 100644 index 0000000..49b8674 --- /dev/null +++ b/src/Entities/GoogleButton/GoogleButton.tsx @@ -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 ( +
+ Google Icon + {children} +
+ ); +}; + +export default GoogleButton; diff --git a/src/Entities/GoogleButton/icons/google-icon.svg b/src/Entities/GoogleButton/icons/google-icon.svg new file mode 100644 index 0000000..eead66d --- /dev/null +++ b/src/Entities/GoogleButton/icons/google-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/Entities/InputWithLabel/InputWithLabel.scss b/src/Entities/InputWithLabel/InputWithLabel.scss new file mode 100644 index 0000000..a70e20b --- /dev/null +++ b/src/Entities/InputWithLabel/InputWithLabel.scss @@ -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; + } + } +} diff --git a/src/Entities/InputWithLabel/InputWithLabel.tsx b/src/Entities/InputWithLabel/InputWithLabel.tsx new file mode 100644 index 0000000..536ec60 --- /dev/null +++ b/src/Entities/InputWithLabel/InputWithLabel.tsx @@ -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) => void; + name?: string; + secret?: boolean; + error: string; +} + +const InputWithLabel: React.FC = ({ + placeholder, + label, + value, + onChange, + name, + secret, + error, +}: IInputWithLabel) => { + const [show, setShow] = useState(false); + const handleChange = (e: React.ChangeEvent) => { + if (onChange) { + onChange(e); + } + }; + return ( +
+ + +
+ + {secret && ( + + )} +
+ {error ?

{error}

: null} +
+ ); +}; + +export default InputWithLabel; diff --git a/src/Entities/InputWithLabel/icons/eye-icon.svg b/src/Entities/InputWithLabel/icons/eye-icon.svg new file mode 100644 index 0000000..9ae6ea5 --- /dev/null +++ b/src/Entities/InputWithLabel/icons/eye-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Entities/InputWithLabel/icons/eye-off-icon.svg b/src/Entities/InputWithLabel/icons/eye-off-icon.svg new file mode 100644 index 0000000..a1d861d --- /dev/null +++ b/src/Entities/InputWithLabel/icons/eye-off-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/Features/NavMenu/NavMenu.tsx b/src/Features/NavMenu/NavMenu.tsx index e174b2a..1d9b55c 100644 --- a/src/Features/NavMenu/NavMenu.tsx +++ b/src/Features/NavMenu/NavMenu.tsx @@ -36,7 +36,9 @@ const NavMenu = () => { : "nav-menu__pages-item" } > - {page.page} + setMenu(false)} href={page.path}> + {page.page} + ))} {auth ? ( @@ -47,7 +49,9 @@ const NavMenu = () => { : "nav-menu__pages-item" } > - Войти + setMenu(false)} href="/sign-in"> + Войти + ) : (
  • { : "nav-menu__pages-item" } > - Профиль + setMenu(false)} href="/profile"> + Профиль +
  • )} diff --git a/src/Features/SearchBar/SearchBar.scss b/src/Features/SearchBar/SearchBar.scss index f54a2d6..533c9f0 100644 --- a/src/Features/SearchBar/SearchBar.scss +++ b/src/Features/SearchBar/SearchBar.scss @@ -21,6 +21,7 @@ border: 1px solid var(--grey-for-mask, #c5c6c5); img { + height: 24px; width: 50px; } diff --git a/src/Features/SignInForm/SignInForm.scss b/src/Features/SignInForm/SignInForm.scss new file mode 100644 index 0000000..c57f465 --- /dev/null +++ b/src/Features/SignInForm/SignInForm.scss @@ -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%; + } +} diff --git a/src/Features/SignInForm/SignInForm.tsx b/src/Features/SignInForm/SignInForm.tsx new file mode 100644 index 0000000..8862ead --- /dev/null +++ b/src/Features/SignInForm/SignInForm.tsx @@ -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(""); + const [password, setPassword] = useState(""); + + const { + login, + emailError, + passwordError, + error, + loading, + cleanRedirect, + redirect, + } = useSignIn(); + + const router = useRouter(); + + useEffect(() => { + if (redirect) { + router.push("/profile"); + cleanRedirect(); + } + }, [redirect]); + + return ( +
    { + e.preventDefault(); + login(email, password); + }} + > +
    + setEmail(e.target.value)} + placeholder="Введите email" + error={emailError} + /> + {emailError ?

    {emailError}

    : null} + setPassword(e.target.value)} + placeholder="Введите пароль" + secret + error={passwordError} + /> + {error ?

    {error}

    : null} +
    + + + Забыли пароль? + + +
    + + Войти через Google +
    + +
    + Еще нет аккаунта? + + Зарегистрируйтесь +
    +
    + ); +}; + +export default SignInForm; diff --git a/src/Features/SignInForm/sign-in.store.ts b/src/Features/SignInForm/sign-in.store.ts new file mode 100644 index 0000000..c7d7cad --- /dev/null +++ b/src/Features/SignInForm/sign-in.store.ts @@ -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; + cleanRedirect: () => void; + emailError: string; + passwordError: string; + redirect: boolean; +} + +export const useSignIn = create((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 }); + }, +})); diff --git a/src/Features/SignUpForm/SignUpForm.scss b/src/Features/SignUpForm/SignUpForm.scss new file mode 100644 index 0000000..cba189a --- /dev/null +++ b/src/Features/SignUpForm/SignUpForm.scss @@ -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%; + } +} diff --git a/src/Features/SignUpForm/SignUpForm.tsx b/src/Features/SignUpForm/SignUpForm.tsx new file mode 100644 index 0000000..5f2514a --- /dev/null +++ b/src/Features/SignUpForm/SignUpForm.tsx @@ -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(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + const router = useRouter(); + + const { + redirect, + register, + loading, + emailError, + passwordError, + confirmPasswordError, + matchPasswordError, + error, + } = useSignUp(); + + useEffect(() => { + if (redirect) { + router.push("/confirm-email"); + } + }, [redirect]); + + return ( +
    { + e.preventDefault(); + register(email, password, confirmPassword); + }} + > +
    + setEmail(e.target.value)} + error={emailError} + /> + setPassword(e.target.value)} + secret + error={passwordError} + /> + setConfirmPassword(e.target.value)} + secret + error={confirmPasswordError} + /> + {matchPasswordError ?

    {matchPasswordError}

    : null} + {error ?

    {error}

    : null} +
    +
    + + Войти через Google +
    + +
    + Уже есть аккаунт? + Войти в аккаунт +
    +
    + ); +}; + +export default SignUpForm; diff --git a/src/Features/SignUpForm/sign-up.store.ts b/src/Features/SignUpForm/sign-up.store.ts new file mode 100644 index 0000000..01196af --- /dev/null +++ b/src/Features/SignUpForm/sign-up.store.ts @@ -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; + cleanRedirect: () => void; + emailError: string; + passwordError: string; + confirmPasswordError: string; + matchPasswordError: string; + redirect: boolean; +} + +export const useSignUp = create((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; +}; diff --git a/src/Pages/SignInPage/SignInPage.scss b/src/Pages/SignInPage/SignInPage.scss new file mode 100644 index 0000000..536ddd8 --- /dev/null +++ b/src/Pages/SignInPage/SignInPage.scss @@ -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; + } +} diff --git a/src/Pages/SignInPage/SignInPage.tsx b/src/Pages/SignInPage/SignInPage.tsx new file mode 100644 index 0000000..cb3f449 --- /dev/null +++ b/src/Pages/SignInPage/SignInPage.tsx @@ -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 ( +
    + + +
    + ); +}; + +export default SignInPage; diff --git a/src/Pages/SignInPage/icons/sign-in-icon.svg b/src/Pages/SignInPage/icons/sign-in-icon.svg new file mode 100644 index 0000000..bf437a6 --- /dev/null +++ b/src/Pages/SignInPage/icons/sign-in-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Pages/SignUpPage/SignUpPage.scss b/src/Pages/SignUpPage/SignUpPage.scss new file mode 100644 index 0000000..8fe8cae --- /dev/null +++ b/src/Pages/SignUpPage/SignUpPage.scss @@ -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; + } +} diff --git a/src/Pages/SignUpPage/SignUpPage.tsx b/src/Pages/SignUpPage/SignUpPage.tsx new file mode 100644 index 0000000..941f281 --- /dev/null +++ b/src/Pages/SignUpPage/SignUpPage.tsx @@ -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 ( +
    + + +
    + ); +}; + +export default SignUpPage; diff --git a/src/Pages/SignUpPage/icons/flag-icon.svg b/src/Pages/SignUpPage/icons/flag-icon.svg new file mode 100644 index 0000000..4621943 --- /dev/null +++ b/src/Pages/SignUpPage/icons/flag-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Shared/API/baseAPI.ts b/src/Shared/API/baseAPI.ts new file mode 100644 index 0000000..7fe0720 --- /dev/null +++ b/src/Shared/API/baseAPI.ts @@ -0,0 +1,2 @@ +export const baseAPI = + "https://api.kgroaduat.fishrungames.com/api/v1"; diff --git a/src/Shared/UI/Button/Button.scss b/src/Shared/UI/Button/Button.scss new file mode 100644 index 0000000..71d9057 --- /dev/null +++ b/src/Shared/UI/Button/Button.scss @@ -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; +} diff --git a/src/Shared/UI/Button/Button.tsx b/src/Shared/UI/Button/Button.tsx new file mode 100644 index 0000000..8db3a01 --- /dev/null +++ b/src/Shared/UI/Button/Button.tsx @@ -0,0 +1,11 @@ +import "./Button.scss"; + +interface IButton extends React.AllHTMLAttributes { + children: React.ReactNode | null; +} + +const Button: React.FC = ({ children }: IButton) => { + return ; +}; + +export default Button; diff --git a/src/Shared/UI/DefaultLoader/DefaultLoader.scss b/src/Shared/UI/DefaultLoader/DefaultLoader.scss new file mode 100644 index 0000000..3e10b88 --- /dev/null +++ b/src/Shared/UI/DefaultLoader/DefaultLoader.scss @@ -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); + } +} diff --git a/src/Shared/UI/DefaultLoader/DefaultLoader.tsx b/src/Shared/UI/DefaultLoader/DefaultLoader.tsx new file mode 100644 index 0000000..d7a5663 --- /dev/null +++ b/src/Shared/UI/DefaultLoader/DefaultLoader.tsx @@ -0,0 +1,14 @@ +import "./DefaultLoader.scss"; + +const DefaultLoader = () => { + return ( +
    +
    +
    +
    +
    +
    + ); +}; + +export default DefaultLoader; diff --git a/src/Shared/hooks/useUpdateEffect.ts b/src/Shared/hooks/useUpdateEffect.ts new file mode 100644 index 0000000..c5ec648 --- /dev/null +++ b/src/Shared/hooks/useUpdateEffect.ts @@ -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); +} diff --git a/src/Shared/types.ts b/src/Shared/types.ts new file mode 100644 index 0000000..575587b --- /dev/null +++ b/src/Shared/types.ts @@ -0,0 +1,6 @@ +export interface IFetch { + response?: string; + data?: any; + loading: boolean; + error?: string; +} diff --git a/src/Shared/variables.scss b/src/Shared/variables.scss index 56e5e97..36248b4 100644 --- a/src/Shared/variables.scss +++ b/src/Shared/variables.scss @@ -1 +1,6 @@ $light-blue: #489fe1; +$blue: #0077b6; +$gray-300: #d0d5dd; +$gray-500: #667085; +$gray-700: #344054; +$red-500: #f04438; diff --git a/src/Widgets/general/Footer/Footer.scss b/src/Widgets/general/Footer/Footer.scss index ef6768f..4160ee8 100644 --- a/src/Widgets/general/Footer/Footer.scss +++ b/src/Widgets/general/Footer/Footer.scss @@ -1,4 +1,5 @@ .footer { + margin-top: 110px; padding: 48px 90px; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; @@ -43,12 +44,14 @@ @media screen and (max-width: 768px) { .footer { + margin-top: 80px; grid-template-columns: 1fr 1fr 1fr; } } @media screen and (max-width: 550px) { .footer { + margin-top: 70px; grid-template-columns: 1fr; } } diff --git a/src/Widgets/general/Footer/Footer.tsx b/src/Widgets/general/Footer/Footer.tsx index 4bda8ea..2333ffc 100644 --- a/src/Widgets/general/Footer/Footer.tsx +++ b/src/Widgets/general/Footer/Footer.tsx @@ -38,7 +38,7 @@ const Footer = () => {
    {[youtube, facebook, instagram].map((net) => ( - Net Icon + Net Icon ))}
    diff --git a/yarn.lock b/yarn.lock index 00e3b50..8918a8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -414,6 +414,11 @@ asynciterator.prototype@^1.0.0: dependencies: 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: version "1.0.5" 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" 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: version "3.2.1" 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" 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: version "0.0.1" 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" 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: version "2.0.3" 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" 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: version "0.3.3" 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" 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: version "1.0.0" 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" 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: version "9.0.3" 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" 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: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -2251,6 +2308,11 @@ uri-js@^4.2.2: dependencies: 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: version "1.0.2" 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" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 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"