made sign-in and sign-up pages

This commit is contained in:
Alibek 2024-01-21 04:16:34 +06:00
parent 3a04c6c1db
commit 301a4ae965
41 changed files with 1015 additions and 17 deletions

View File

@ -0,0 +1,7 @@
import React from "react";
const page = () => {
return <div>page</div>;
};
export default page;

View File

@ -0,0 +1,7 @@
import React from "react";
const page = () => {
return <div>page</div>;
};
export default page;

View File

@ -1,7 +1,3 @@
import React from "react";
import SignInPage from "@/Pages/SignInPage/SignInPage";
const page = () => {
return <div>page</div>;
};
export default page;
export default SignInPage;

View File

@ -1,7 +1,3 @@
import React from "react";
import SignUpPage from "@/Pages/SignUpPage/SignUpPage";
const page = () => {
return <div>page</div>;
};
export default page;
export default SignUpPage;

View File

@ -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",

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

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

View File

@ -0,0 +1,7 @@
@import "../../Shared/variables.scss";
.custom-link {
color: $blue;
font-size: 16px;
font-weight: 400;
}

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

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

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

View 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

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

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

View 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

View 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

View File

@ -36,7 +36,9 @@ const NavMenu = () => {
: "nav-menu__pages-item"
}
>
<Link href={page.path}>{page.page}</Link>
<Link onClick={() => setMenu(false)} href={page.path}>
{page.page}
</Link>
</li>
))}
{auth ? (
@ -47,7 +49,9 @@ const NavMenu = () => {
: "nav-menu__pages-item"
}
>
<Link href="/sign-in">Войти</Link>
<Link onClick={() => setMenu(false)} href="/sign-in">
Войти
</Link>
</li>
) : (
<li
@ -57,7 +61,9 @@ const NavMenu = () => {
: "nav-menu__pages-item"
}
>
<Link href="/profile">Профиль</Link>
<Link onClick={() => setMenu(false)} href="/profile">
Профиль
</Link>
</li>
)}
</ul>

View File

@ -21,6 +21,7 @@
border: 1px solid var(--grey-for-mask, #c5c6c5);
img {
height: 24px;
width: 50px;
}

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

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

View 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 });
},
}));

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

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

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

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

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

View 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

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

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

View 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

View File

@ -0,0 +1,2 @@
export const baseAPI =
"https://api.kgroaduat.fishrungames.com/api/v1";

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

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

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

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

View 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
View File

@ -0,0 +1,6 @@
export interface IFetch {
response?: string;
data?: any;
loading: boolean;
error?: string;
}

View File

@ -1 +1,6 @@
$light-blue: #489fe1;
$blue: #0077b6;
$gray-300: #d0d5dd;
$gray-500: #667085;
$gray-700: #344054;
$red-500: #f04438;

View File

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

View File

@ -38,7 +38,7 @@ const Footer = () => {
</ul>
<div className="footer__nets">
{[youtube, facebook, instagram].map((net) => (
<Image src={net} alt="Net Icon" key={net} />
<Image src={net} alt="Net Icon" key={net.src} />
))}
</div>
</div>

View File

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