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 (
+
+
+
+
+
+
+
{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 (
+
+
+ {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 (
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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) => (
-
+
))}
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"