formatting

This commit is contained in:
Виталий Лавшонок
2025-11-04 15:04:59 +03:00
parent 3cd8e14288
commit 4972836164
60 changed files with 3604 additions and 2916 deletions

View File

@@ -1,4 +1,4 @@
import { cn } from "../../../lib/cn";
import { cn } from '../../../lib/cn';
export interface ArticleItemProps {
id: number;
@@ -6,17 +6,17 @@ export interface ArticleItemProps {
tags: string[];
}
const ArticleItem: React.FC<ArticleItemProps> = ({
id, name, tags
}) => {
const ArticleItem: React.FC<ArticleItemProps> = ({ id, name, tags }) => {
return (
<div className={cn("w-full relative rounded-[10px] text-liquid-white mb-[20px]",
// type == "first" ? "bg-liquid-lighter" : "bg-liquid-background",
"gap-[20px] px-[20px] py-[10px] box-border ",
"border-b-[1px] border-b-liquid-lighter",
)}>
<div
className={cn(
'w-full relative rounded-[10px] text-liquid-white mb-[20px]',
// type == "first" ? "bg-liquid-lighter" : "bg-liquid-background",
'gap-[20px] px-[20px] py-[10px] box-border ',
'border-b-[1px] border-b-liquid-lighter',
)}
>
<div className="h-[23px] flex ">
<div className="text-[18px] font-bold w-[60px] mr-[20px] flex items-center">
#{id}
</div>
@@ -25,15 +25,18 @@ const ArticleItem: React.FC<ArticleItemProps> = ({
</div>
</div>
<div className="text-[14px] flex text-liquid-light gap-[10px] mt-[10px]">
{tags.map((v, i) =>
<div key={i} className={cn(
"rounded-full px-[16px] py-[8px] bg-liquid-lighter",
v == "Sertificated" && "text-liquid-green")}>
{tags.map((v, i) => (
<div
key={i}
className={cn(
'rounded-full px-[16px] py-[8px] bg-liquid-lighter',
v == 'Sertificated' && 'text-liquid-green',
)}
>
{v}
</div>
)}
))}
</div>
</div>
);
};

View File

@@ -1,10 +1,9 @@
import { useEffect } from "react";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import { useAppDispatch } from "../../../redux/hooks";
import ArticleItem from "./ArticleItem";
import { setMenuActivePage } from "../../../redux/slices/store";
import { useNavigate } from "react-router-dom";
import { useEffect } from 'react';
import { SecondaryButton } from '../../../components/button/SecondaryButton';
import { useAppDispatch } from '../../../redux/hooks';
import ArticleItem from './ArticleItem';
import { setMenuActivePage } from '../../../redux/slices/store';
import { useNavigate } from 'react-router-dom';
export interface Article {
id: number;
@@ -12,159 +11,152 @@ export interface Article {
tags: string[];
}
const Articles = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const articles: Article[] = [
{
"id": 1,
"name": "Todo List App",
"tags": ["Sertificated", "state", "list"],
id: 1,
name: 'Todo List App',
tags: ['Sertificated', 'state', 'list'],
},
{
"id": 2,
"name": "Search Filter Component",
"tags": ["filter", "props", "hooks"],
id: 2,
name: 'Search Filter Component',
tags: ['filter', 'props', 'hooks'],
},
{
"id": 3,
"name": "User Card List",
"tags": ["components", "props", "array"],
id: 3,
name: 'User Card List',
tags: ['components', 'props', 'array'],
},
{
"id": 4,
"name": "Theme Switcher",
"tags": ["Sertificated", "theme", "hooks"],
id: 4,
name: 'Theme Switcher',
tags: ['Sertificated', 'theme', 'hooks'],
},
{
"id": 2,
"name": "Search Filter Component",
"tags": ["filter", "props", "hooks"],
id: 2,
name: 'Search Filter Component',
tags: ['filter', 'props', 'hooks'],
},
{
"id": 3,
"name": "User Card List",
"tags": ["components", "props", "array"],
id: 3,
name: 'User Card List',
tags: ['components', 'props', 'array'],
},
{
"id": 4,
"name": "Theme Switcher",
"tags": ["Sertificated", "theme", "hooks"],
id: 4,
name: 'Theme Switcher',
tags: ['Sertificated', 'theme', 'hooks'],
},
{
"id": 2,
"name": "Search Filter Component",
"tags": ["filter", "props", "hooks"],
id: 2,
name: 'Search Filter Component',
tags: ['filter', 'props', 'hooks'],
},
{
"id": 3,
"name": "User Card List",
"tags": ["components", "props", "array"],
id: 3,
name: 'User Card List',
tags: ['components', 'props', 'array'],
},
{
"id": 4,
"name": "Theme Switcher",
"tags": ["Sertificated", "theme", "hooks"],
id: 4,
name: 'Theme Switcher',
tags: ['Sertificated', 'theme', 'hooks'],
},
{
"id": 2,
"name": "Search Filter Component",
"tags": ["filter", "props", "hooks"],
id: 2,
name: 'Search Filter Component',
tags: ['filter', 'props', 'hooks'],
},
{
"id": 3,
"name": "User Card List",
"tags": ["components", "props", "array"],
id: 3,
name: 'User Card List',
tags: ['components', 'props', 'array'],
},
{
"id": 4,
"name": "Theme Switcher",
"tags": ["Sertificated", "theme", "hooks"],
id: 4,
name: 'Theme Switcher',
tags: ['Sertificated', 'theme', 'hooks'],
},
{
"id": 2,
"name": "Search Filter Component",
"tags": ["filter", "props", "hooks"],
id: 2,
name: 'Search Filter Component',
tags: ['filter', 'props', 'hooks'],
},
{
"id": 3,
"name": "User Card List",
"tags": ["components", "props", "array"],
id: 3,
name: 'User Card List',
tags: ['components', 'props', 'array'],
},
{
"id": 4,
"name": "Theme Switcher",
"tags": ["Sertificated", "theme", "hooks"],
id: 4,
name: 'Theme Switcher',
tags: ['Sertificated', 'theme', 'hooks'],
},
{
"id": 2,
"name": "Search Filter Component",
"tags": ["filter", "props", "hooks"],
id: 2,
name: 'Search Filter Component',
tags: ['filter', 'props', 'hooks'],
},
{
"id": 3,
"name": "User Card List",
"tags": ["components", "props", "array"],
id: 3,
name: 'User Card List',
tags: ['components', 'props', 'array'],
},
{
"id": 4,
"name": "Theme Switcher",
"tags": ["Sertificated", "theme", "hooks"],
id: 4,
name: 'Theme Switcher',
tags: ['Sertificated', 'theme', 'hooks'],
},
{
"id": 2,
"name": "Search Filter Component",
"tags": ["filter", "props", "hooks"],
id: 2,
name: 'Search Filter Component',
tags: ['filter', 'props', 'hooks'],
},
{
"id": 3,
"name": "User Card List",
"tags": ["components", "props", "array"],
id: 3,
name: 'User Card List',
tags: ['components', 'props', 'array'],
},
{
"id": 4,
"name": "Theme Switcher",
"tags": ["Sertificated", "theme", "hooks"],
}
id: 4,
name: 'Theme Switcher',
tags: ['Sertificated', 'theme', 'hooks'],
},
];
useEffect(() => {
dispatch(setMenuActivePage("articles"))
}, []);
useEffect(() => {
dispatch(setMenuActivePage('articles'));
}, []);
return (
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
<div className="h-full box-border">
<div className="relative flex items-center mb-[20px]">
<div className="h-[50px] text-[40px] font-bold text-liquid-white flex items-center">
Статьи
</div>
<SecondaryButton
onClick={() => {navigate("/article/create")}}
onClick={() => {
navigate('/article/create');
}}
text="Создать статью"
className="absolute right-0"
/>
</div>
<div className="bg-liquid-lighter h-[50px] mb-[20px]">
</div>
<div className="bg-liquid-lighter h-[50px] mb-[20px]"></div>
<div>
{articles.map((v, i) => (
<ArticleItem key={i} {...v} />
))}
</div>
<div>
pages
</div>
<div>pages</div>
</div>
</div>
);

View File

@@ -1,113 +1,133 @@
import { useState, useEffect } from "react";
import { PrimaryButton } from "../../../components/button/PrimaryButton";
import { Input } from "../../../components/input/Input";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import { Link, useNavigate } from "react-router-dom";
import { loginUser } from "../../../redux/slices/auth";
import { useState, useEffect } from 'react';
import { PrimaryButton } from '../../../components/button/PrimaryButton';
import { Input } from '../../../components/input/Input';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import { Link, useNavigate } from 'react-router-dom';
import { loginUser } from '../../../redux/slices/auth';
// import { cn } from "../../../lib/cn";
import { setMenuActivePage } from "../../../redux/slices/store";
import { Balloon } from "../../../assets/icons/auth";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import { googleLogo } from "../../../assets/icons/input";
import { setMenuActivePage } from '../../../redux/slices/store';
import { Balloon } from '../../../assets/icons/auth';
import { SecondaryButton } from '../../../components/button/SecondaryButton';
import { googleLogo } from '../../../assets/icons/input';
const Login = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [username, setUsername] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
const [username, setUsername] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
const { status, jwt } = useAppSelector((state) => state.auth);
const { status, jwt } = useAppSelector((state) => state.auth);
// const [err, setErr] = useState<string>("");
// const [err, setErr] = useState<string>("");
// После успешного логина
useEffect(() => {
dispatch(setMenuActivePage('account'));
console.log(submitClicked);
}, []);
// После успешного логина
useEffect(() => {
dispatch(setMenuActivePage("account"))
console.log(submitClicked);
}, []);
useEffect(() => {
if (jwt) {
navigate('/home/offices'); // или другая страница после входа
}
}, [jwt]);
useEffect(() => {
if (jwt) {
navigate("/home/offices"); // или другая страница после входа
}
}, [jwt]);
const handleLogin = () => {
// setErr(err == "" ? "Неверная почта и/или пароль" : "");
setSubmitClicked(true);
const handleLogin = () => {
// setErr(err == "" ? "Неверная почта и/или пароль" : "");
setSubmitClicked(true);
if (!username || !password) return;
if (!username || !password) return;
dispatch(loginUser({ username, password }));
};
dispatch(loginUser({ username, password }));
};
return (
<div className="h-svh w-svw fixed pointer-events-none top-0 left-0 flex items-center justify-center">
<div className="grid gap-[80px] grid-cols-[400px,384px] box-border relative ">
<div className="flex items-center justify-center ">
<img src={Balloon} />
</div>
<div className=" relative pointer-events-auto">
<div>
<div className="text-[40px] text-liquid-white font-bold h-[50px]">
С возвращением
</div>
<div className="text-[18px] text-liquid-light font-bold h-[23px]">
Вход в аккаунт
</div>
</div>
return (
<div className="h-svh w-svw fixed pointer-events-none top-0 left-0 flex items-center justify-center">
<div className="grid gap-[80px] grid-cols-[400px,384px] box-border relative ">
<div className="flex items-center justify-center ">
<img src={Balloon} />
</div>
<div className=" relative pointer-events-auto">
<div>
<div className="text-[40px] text-liquid-white font-bold h-[50px]">
С возвращением
<Input
name="login"
autocomplete="login"
className="mt-[10px]"
type="text"
label="Логин"
onChange={(v) => {
setUsername(v);
}}
placeholder="login"
/>
<Input
name="password"
autocomplete="password"
className="mt-[10px]"
type="password"
label="Пароль"
onChange={(v) => {
setPassword(v);
}}
placeholder="abCD1234"
/>
<div className="flex justify-end mt-[10px]">
<Link
to={''}
className={
'text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline '
}
>
Забыли пароль?
</Link>
</div>
<div className="mt-[10px]">
<PrimaryButton
className="w-full mb-[8px]"
onClick={handleLogin}
text={status === 'loading' ? 'Вход...' : 'Вход'}
disabled={status === 'loading'}
/>
<SecondaryButton className="w-full" onClick={() => {}}>
<div className="flex items-center">
<img
src={googleLogo}
className="h-[24px] w-[24px] mr-[15px]"
/>
Вход с Google
</div>
</SecondaryButton>
</div>
<div className="flex justify-center mt-[10px]">
<span>
Нет аккаунта?{' '}
<Link
to={'/home/register'}
className={
'text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline '
}
>
Регистрация
</Link>
</span>
</div>
</div>
</div>
<div className="text-[18px] text-liquid-light font-bold h-[23px]">
Вход в аккаунт
</div>
</div>
<Input name="login" autocomplete="login" className="mt-[10px]" type="text" label="Логин" onChange={(v) => { setUsername(v) }} placeholder="login" />
<Input name="password" autocomplete="password" className="mt-[10px]" type="password" label="Пароль" onChange={(v) => { setPassword(v) }} placeholder="abCD1234" />
<div className="flex justify-end mt-[10px]">
<Link
to={""}
className={"text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline "}>
Забыли пароль?
</Link>
</div>
<div className="mt-[10px]">
<PrimaryButton
className="w-full mb-[8px]"
onClick={handleLogin}
text={status === "loading" ? "Вход..." : "Вход"}
disabled={status === "loading"}
/>
<SecondaryButton
className="w-full"
onClick={() => { }}
>
<div className="flex items-center">
<img src={googleLogo} className="h-[24px] w-[24px] mr-[15px]" />
Вход с Google
</div>
</SecondaryButton>
</div>
<div className="flex justify-center mt-[10px]">
<span>
Нет аккаунта? <Link
to={"/home/register"}
className={"text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline "}>
Регистрация
</Link>
</span>
</div>
</div>
</div>
</div>
);
);
};
export default Login;

View File

@@ -1,126 +1,169 @@
import { useState, useEffect } from "react";
import { PrimaryButton } from "../../../components/button/PrimaryButton";
import { Input } from "../../../components/input/Input";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import { useNavigate } from "react-router-dom";
import { registerUser } from "../../../redux/slices/auth";
import { useState, useEffect } from 'react';
import { PrimaryButton } from '../../../components/button/PrimaryButton';
import { Input } from '../../../components/input/Input';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import { useNavigate } from 'react-router-dom';
import { registerUser } from '../../../redux/slices/auth';
// import { cn } from "../../../lib/cn";
import { setMenuActivePage } from "../../../redux/slices/store";
import { Balloon } from "../../../assets/icons/auth";
import { Link } from "react-router-dom";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import { Checkbox } from "../../../components/checkbox/Checkbox";
import { googleLogo } from "../../../assets/icons/input";
import { setMenuActivePage } from '../../../redux/slices/store';
import { Balloon } from '../../../assets/icons/auth';
import { Link } from 'react-router-dom';
import { SecondaryButton } from '../../../components/button/SecondaryButton';
import { Checkbox } from '../../../components/checkbox/Checkbox';
import { googleLogo } from '../../../assets/icons/input';
const Register = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [username, setUsername] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [confirmPassword, setConfirmPassword] = useState<string>("");
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
const [username, setUsername] = useState<string>('');
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [confirmPassword, setConfirmPassword] = useState<string>('');
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
const { status, jwt } = useAppSelector((state) => state.auth);
const { status, jwt } = useAppSelector((state) => state.auth);
// После успешной регистрации — переход в систему
// После успешной регистрации — переход в систему
useEffect(() => {
dispatch(setMenuActivePage("account"))
}, []);
useEffect(() => {
dispatch(setMenuActivePage('account'));
}, []);
useEffect(() => {
if (jwt) {
navigate("/home");
}
console.log(submitClicked);
}, [jwt]);
useEffect(() => {
if (jwt) {
navigate('/home');
}
console.log(submitClicked);
}, [jwt]);
const handleRegister = () => {
setSubmitClicked(true);
const handleRegister = () => {
setSubmitClicked(true);
if (!username || !email || !password || !confirmPassword) return;
if (password !== confirmPassword) return;
if (!username || !email || !password || !confirmPassword) return;
if (password !== confirmPassword) return;
dispatch(registerUser({ username, email, password }));
};
dispatch(registerUser({ username, email, password }));
};
return (
<div className="h-svh w-svw fixed pointer-events-none top-0 left-0 flex items-center justify-center">
<div className="grid gap-[80px] grid-cols-[400px,384px] box-border relative ">
<div className="flex items-center justify-center ">
<img src={Balloon} />
</div>
<div className=" relative pointer-events-auto">
<div>
<div className="text-[40px] text-liquid-white font-bold h-[50px]">
Добро пожаловать
return (
<div className="h-svh w-svw fixed pointer-events-none top-0 left-0 flex items-center justify-center">
<div className="grid gap-[80px] grid-cols-[400px,384px] box-border relative ">
<div className="flex items-center justify-center ">
<img src={Balloon} />
</div>
<div className=" relative pointer-events-auto">
<div>
<div className="text-[40px] text-liquid-white font-bold h-[50px]">
Добро пожаловать
</div>
<div className="text-[18px] text-liquid-light font-bold h-[23px]">
Регистрация
</div>
</div>
<Input
name="email"
autocomplete="email"
className="mt-[10px]"
type="email"
label="Почта"
onChange={(v) => {
setEmail(v);
}}
placeholder="example@gmail.com"
/>
<Input
name="login"
autocomplete="login"
className="mt-[10px]"
type="text"
label="Логин пользователя"
onChange={(v) => {
setUsername(v);
}}
placeholder="login"
/>
<Input
name="password"
autocomplete="password"
className="mt-[10px]"
type="password"
label="Пароль"
onChange={(v) => {
setPassword(v);
}}
placeholder="abCD1234"
/>
<Input
name="confirm-password"
autocomplete="confirm-password"
className="mt-[10px]"
type="password"
label="Повторите пароль"
onChange={(v) => {
setConfirmPassword(v);
}}
placeholder="abCD1234"
/>
<div className=" flex items-center mt-[10px] h-[24px]">
<Checkbox
onChange={(value: boolean) => {
value;
}}
className="p-0 w-fit m-[2.75px]"
size="md"
color="secondary"
variant="default"
/>
<span className="text-[14px] font-medium text-liquid-light h-[18px] ml-[10px]">
Я принимаю{' '}
<Link to={'/home'} className={' underline'}>
политику конфиденциальности
</Link>
</span>
</div>
<div className="mt-[10px]">
<PrimaryButton
className="w-full mb-[8px]"
onClick={() => handleRegister()}
text={
status === 'loading'
? 'Регистрация...'
: 'Регистрация'
}
disabled={status === 'loading'}
/>
<SecondaryButton className="w-full" onClick={() => {}}>
<div className="flex items-center">
<img
src={googleLogo}
className="h-[24px] w-[24px] mr-[15px]"
/>
Регистрация с Google
</div>
</SecondaryButton>
</div>
<div className="flex justify-center mt-[10px]">
<span>
Уже есть аккаунт?{' '}
<Link
to={'/home/login'}
className={
'text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline '
}
>
Авторизация
</Link>
</span>
</div>
</div>
</div>
<div className="text-[18px] text-liquid-light font-bold h-[23px]">
Регистрация
</div>
</div>
<Input name="email" autocomplete="email" className="mt-[10px]" type="email" label="Почта" onChange={(v) => {setEmail(v)}} placeholder="example@gmail.com" />
<Input name="login" autocomplete="login" className="mt-[10px]" type="text" label="Логин пользователя" onChange={(v) => {setUsername(v)}} placeholder="login" />
<Input name="password" autocomplete="password" className="mt-[10px]" type="password" label="Пароль" onChange={(v) => {setPassword(v)}} placeholder="abCD1234" />
<Input name="confirm-password" autocomplete="confirm-password" className="mt-[10px]" type="password" label="Повторите пароль" onChange={(v) => {setConfirmPassword(v)}} placeholder="abCD1234" />
<div className=" flex items-center mt-[10px] h-[24px]">
<Checkbox
onChange={(value: boolean) => { value; }}
className="p-0 w-fit m-[2.75px]"
size="md"
color="secondary"
variant="default" />
<span className="text-[14px] font-medium text-liquid-light h-[18px] ml-[10px]">
Я принимаю <Link
to={"/home"}
className={" underline"}
>
политику конфиденциальности
</Link>
</span>
</div>
<div className="mt-[10px]">
<PrimaryButton
className="w-full mb-[8px]"
onClick={() => handleRegister()}
text={status === "loading" ? "Регистрация..." : "Регистрация"}
disabled={status === "loading"}
/>
<SecondaryButton
className="w-full"
onClick={() => { }}
>
<div className="flex items-center">
<img src={googleLogo} className="h-[24px] w-[24px] mr-[15px]" />
Регистрация с Google
</div>
</SecondaryButton>
</div>
<div className="flex justify-center mt-[10px]">
<span>
Уже есть аккаунт? <Link
to={"/home/login"}
className={"text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline "}>
Авторизация
</Link>
</span>
</div>
</div>
</div>
</div>
);
);
};
export default Register;

View File

@@ -1,65 +1,74 @@
import { cn } from "../../../lib/cn";
import { Account } from "../../../assets/icons/auth";
import { PrimaryButton } from "../../../components/button/PrimaryButton";
import { ReverseButton } from "../../../components/button/ReverseButton";
import { cn } from '../../../lib/cn';
import { Account } from '../../../assets/icons/auth';
import { PrimaryButton } from '../../../components/button/PrimaryButton';
import { ReverseButton } from '../../../components/button/ReverseButton';
export interface ContestItemProps {
name: string;
startAt: string;
duration: number;
members: number;
statusRegister: "reg" | "nonreg";
type: "first" | "second";
statusRegister: 'reg' | 'nonreg';
type: 'first' | 'second';
}
function formatDate(dateString: string): string {
const date = new Date(dateString);
const date = new Date(dateString);
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear();
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${day}/${month}/${year}\n${hours}:${minutes}`;
return `${day}/${month}/${year}\n${hours}:${minutes}`;
}
function formatWaitTime(ms: number): string {
const minutes = Math.floor(ms / 60000);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const minutes = Math.floor(ms / 60000);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
const remainder = days % 10;
let suffix = "дней";
if (remainder === 1 && days !== 11) suffix = "день";
else if (remainder >= 2 && remainder <= 4 && (days < 10 || days > 20)) suffix = "дня";
return `${days} ${suffix}`;
} else if (hours > 0) {
const mins = minutes % 60;
return mins > 0 ? `${hours} ч ${mins} мин` : `${hours} ч`;
} else {
return `${minutes} мин`;
}
if (days > 0) {
const remainder = days % 10;
let suffix = 'дней';
if (remainder === 1 && days !== 11) suffix = 'день';
else if (remainder >= 2 && remainder <= 4 && (days < 10 || days > 20))
suffix = 'дня';
return `${days} ${suffix}`;
} else if (hours > 0) {
const mins = minutes % 60;
return mins > 0 ? `${hours} ч ${mins} мин` : `${hours} ч`;
} else {
return `${minutes} мин`;
}
}
const ContestItem: React.FC<ContestItemProps> = ({
name, startAt, duration, members, statusRegister, type
name,
startAt,
duration,
members,
statusRegister,
type,
}) => {
const now = new Date();
const waitTime = new Date(startAt).getTime() - now.getTime();
return (
<div className={cn("w-full box-border relative rounded-[10px] px-[20px] py-[10px] text-liquid-white text-[16px] leading-[20px]",
waitTime <= 0 ? "grid grid-cols-6" : "grid grid-cols-7",
"items-center font-bold text-liquid-white",
type == "first" ? " bg-liquid-lighter" : " bg-liquid-background"
)}>
<div className="text-left font-bold text-[18px]">
{name}
</div>
<div
className={cn(
'w-full box-border relative rounded-[10px] px-[20px] py-[10px] text-liquid-white text-[16px] leading-[20px]',
waitTime <= 0 ? 'grid grid-cols-6' : 'grid grid-cols-7',
'items-center font-bold text-liquid-white',
type == 'first'
? ' bg-liquid-lighter'
: ' bg-liquid-background',
)}
>
<div className="text-left font-bold text-[18px]">{name}</div>
<div className="text-center text-liquid-brightmain font-normal ">
{/* {authors.map((v, i) => <p key={i}>{v}</p>)} */}
valavshonok
@@ -67,29 +76,29 @@ const ContestItem: React.FC<ContestItemProps> = ({
<div className="text-center text-nowrap whitespace-pre-line">
{formatDate(startAt)}
</div>
<div className="text-center">
{formatWaitTime(duration)}
</div>
{
waitTime > 0 &&
<div className="text-center">{formatWaitTime(duration)}</div>
{waitTime > 0 && (
<div className="text-center whitespace-pre-line ">
{"До начала\n" + formatWaitTime(waitTime)}
{'До начала\n' + formatWaitTime(waitTime)}
</div>
}
)}
<div className="items-center justify-center flex gap-[10px] flex-row w-full">
<div>{members}</div>
<img src={Account} className="h-[24px] w-[24px]"/>
<img src={Account} className="h-[24px] w-[24px]" />
</div>
<div className="flex items-center justify-end">
{
statusRegister == "reg" ?
<> <PrimaryButton onClick={() => {}} text="Регистрация"/></>
:
<> <ReverseButton onClick={() => {}} text="Вы записаны"/></>
}
{statusRegister == 'reg' ? (
<>
{' '}
<PrimaryButton onClick={() => {}} text="Регистрация" />
</>
) : (
<>
{' '}
<ReverseButton onClick={() => {}} text="Вы записаны" />
</>
)}
</div>
</div>
);
};

View File

@@ -1,10 +1,10 @@
import { useEffect } from "react";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import { cn } from "../../../lib/cn";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import ContestsBlock from "./ContestsBlock";
import { setMenuActivePage } from "../../../redux/slices/store";
import { fetchContests } from "../../../redux/slices/contests";
import { useEffect } from 'react';
import { SecondaryButton } from '../../../components/button/SecondaryButton';
import { cn } from '../../../lib/cn';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import ContestsBlock from './ContestsBlock';
import { setMenuActivePage } from '../../../redux/slices/store';
import { fetchContests } from '../../../redux/slices/contests';
const Contests = () => {
const dispatch = useAppDispatch();
@@ -17,12 +17,14 @@ const Contests = () => {
// При загрузке страницы — выставляем активную вкладку и подгружаем контесты
useEffect(() => {
dispatch(setMenuActivePage("contests"));
dispatch(setMenuActivePage('contests'));
dispatch(fetchContests({}));
}, []);
if (loading == "loading") {
return <div className="text-liquid-white p-4">Загрузка контестов...</div>;
if (loading == 'loading') {
return (
<div className="text-liquid-white p-4">Загрузка контестов...</div>
);
}
if (error) {
@@ -33,7 +35,11 @@ const Contests = () => {
<div className="h-full w-[calc(100%+250px)] box-border p-[20px] pt-[20p]">
<div className="h-full box-border">
<div className="relative flex items-center mb-[20px]">
<div className={cn("h-[50px] text-[40px] font-bold text-liquid-white flex items-center")}>
<div
className={cn(
'h-[50px] text-[40px] font-bold text-liquid-white flex items-center',
)}
>
Контесты
</div>
<SecondaryButton
@@ -49,8 +55,7 @@ const Contests = () => {
className="mb-[20px]"
title="Текущие"
contests={contests.filter((contest) => {
const endTime =
new Date(contest.endsAt).getTime()
const endTime = new Date(contest.endsAt).getTime();
return endTime >= now.getTime();
})}
/>
@@ -59,8 +64,7 @@ const Contests = () => {
className="mb-[20px]"
title="Прошедшие"
contests={contests.filter((contest) => {
const endTime =
new Date(contest.endsAt).getTime()
const endTime = new Date(contest.endsAt).getTime();
return endTime < now.getTime();
})}
/>

View File

@@ -1,11 +1,8 @@
import { useState, FC } from "react";
import { cn } from "../../../lib/cn";
import { ChevroneDown } from "../../../assets/icons/groups";
import ContestItem from "./ContestItem";
import { Contest } from "../../../redux/slices/contests";
import { useState, FC } from 'react';
import { cn } from '../../../lib/cn';
import { ChevroneDown } from '../../../assets/icons/groups';
import ContestItem from './ContestItem';
import { Contest } from '../../../redux/slices/contests';
interface ContestsBlockProps {
contests: Contest[];
@@ -13,46 +10,61 @@ interface ContestsBlockProps {
className?: string;
}
const ContestsBlock: FC<ContestsBlockProps> = ({ contests, title, className }) => {
const [active, setActive] = useState<boolean>(title != "Скрытые");
const ContestsBlock: FC<ContestsBlockProps> = ({
contests,
title,
className,
}) => {
const [active, setActive] = useState<boolean>(title != 'Скрытые');
return (
<div className={cn(" border-b-[1px] border-b-liquid-lighter rounded-[10px]",
className
)}>
<div className={cn(" h-[40px] text-[24px] font-bold flex gap-[10px] items-center cursor-pointer border-b-[1px] border-b-transparent transition-all duration-300",
active && "border-b-liquid-lighter"
<div
className={cn(
' border-b-[1px] border-b-liquid-lighter rounded-[10px]',
className,
)}
>
<div
className={cn(
' h-[40px] text-[24px] font-bold flex gap-[10px] items-center cursor-pointer border-b-[1px] border-b-transparent transition-all duration-300',
active && 'border-b-liquid-lighter',
)}
onClick={() => {
setActive(!active)
}}>
setActive(!active);
}}
>
<span>{title}</span>
<img src={ChevroneDown} className={cn("transition-all duration-300",
active && "rotate-180"
)} />
<img
src={ChevroneDown}
className={cn(
'transition-all duration-300',
active && 'rotate-180',
)}
/>
</div>
<div className={cn(" grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300",
active && "grid-rows-[1fr] opacity-100"
)}>
<div
className={cn(
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300',
active && 'grid-rows-[1fr] opacity-100',
)}
>
<div className="overflow-hidden">
<div className="pb-[10px] pt-[20px]">
{
contests.map((v, i) => <ContestItem
key={i}
name={v.name}
startAt={v.startsAt}
statusRegister={"reg"}
duration={new Date(v.endsAt).getTime() - new Date(v.startsAt).getTime()}
members={v.members.length}
type={i % 2 ? "second" : "first"} />)
}
{contests.map((v, i) => (
<ContestItem
key={i}
name={v.name}
startAt={v.startsAt}
statusRegister={'reg'}
duration={
new Date(v.endsAt).getTime() -
new Date(v.startsAt).getTime()
}
members={v.members.length}
type={i % 2 ? 'second' : 'first'}
/>
))}
</div>
</div>
</div>
</div>

View File

@@ -1,26 +1,26 @@
import { FC } from "react";
import { cn } from "../../../lib/cn";
import { useParams, Navigate } from "react-router-dom";
import { FC } from 'react';
import { cn } from '../../../lib/cn';
import { useParams, Navigate } from 'react-router-dom';
interface GroupsBlockProps {}
const Group: FC<GroupsBlockProps> = () => {
const { groupId } = useParams<{ groupId: string }>();
const groupIdNumber = Number(groupId);
const { groupId } = useParams<{ groupId: string }>();
const groupIdNumber = Number(groupId);
if (!groupId || isNaN(groupIdNumber) || !groupIdNumber) {
return <Navigate to="/home/groups" replace />;
}
if (!groupId || isNaN(groupIdNumber) || !groupIdNumber) {
return <Navigate to="/home/groups" replace />;
}
return (
<div
className={cn(
"border-b-[1px] border-b-liquid-lighter rounded-[10px]"
)}
>
{groupIdNumber}
</div>
);
return (
<div
className={cn(
'border-b-[1px] border-b-liquid-lighter rounded-[10px]',
)}
>
{groupIdNumber}
</div>
);
};
export default Group;

View File

@@ -1,11 +1,17 @@
import { cn } from "../../../lib/cn";
import { Book, UserAdd, Edit, EyeClosed, EyeOpen } from "../../../assets/icons/groups";
import { useNavigate } from "react-router-dom";
import { GroupUpdate } from "./Groups";
import { cn } from '../../../lib/cn';
import {
Book,
UserAdd,
Edit,
EyeClosed,
EyeOpen,
} from '../../../assets/icons/groups';
import { useNavigate } from 'react-router-dom';
import { GroupUpdate } from './Groups';
export interface GroupItemProps {
id: number;
role: "menager" | "member" | "owner" | "viewer";
role: 'menager' | 'member' | 'owner' | 'viewer';
visible: boolean;
name: string;
description: string;
@@ -13,61 +19,64 @@ export interface GroupItemProps {
setUpdateGroup: (value: GroupUpdate) => void;
}
interface IconComponentProps {
src: string;
onClick?: () => void;
}
const IconComponent: React.FC<IconComponentProps> = ({
src,
onClick
}) => {
return <img
src={src}
onClick={(e) => {
e.stopPropagation();
if (onClick)
onClick();
}}
className="hover:bg-liquid-light rounded-[5px] cursor-pointer transition-all duration-300"
/>
}
const IconComponent: React.FC<IconComponentProps> = ({ src, onClick }) => {
return (
<img
src={src}
onClick={(e) => {
e.stopPropagation();
if (onClick) onClick();
}}
className="hover:bg-liquid-light rounded-[5px] cursor-pointer transition-all duration-300"
/>
);
};
const GroupItem: React.FC<GroupItemProps> = ({
id, name, visible, role, description, setUpdateGroup, setUpdateActive
id,
name,
visible,
role,
description,
setUpdateGroup,
setUpdateActive,
}) => {
const navigate = useNavigate();
return (
<div className={cn("w-full h-[120px] box-border relative rounded-[10px] p-[10px] text-liquid-white bg-liquid-lighter cursor-pointer",
)}
<div
className={cn(
'w-full h-[120px] box-border relative rounded-[10px] p-[10px] text-liquid-white bg-liquid-lighter cursor-pointer',
)}
onClick={() => navigate(`/group/${id}`)}
>
<div className="grid grid-cols-[100px,1fr] gap-[20px]">
<img src={Book} className="bg-liquid-brightmain rounded-[10px]"/>
<img
src={Book}
className="bg-liquid-brightmain rounded-[10px]"
/>
<div className="grid grid-flow-row grid-rows-[1fr,24px]">
<div className="text-[18px] font-bold">
{name}
</div>
<div className="text-[18px] font-bold">{name}</div>
<div className=" flex gap-[10px]">
{
(role == "menager" || role == "owner") && <IconComponent src={UserAdd}/>
}
{
(role == "menager" || role == "owner") && <IconComponent src={Edit} onClick={() => {
setUpdateGroup({id, name, description });
setUpdateActive(true);
}} />
}
{
visible == false && <IconComponent src={EyeOpen} />
}
{
visible == true && <IconComponent src={EyeClosed} />
}
{(role == 'menager' || role == 'owner') && (
<IconComponent src={UserAdd} />
)}
{(role == 'menager' || role == 'owner') && (
<IconComponent
src={Edit}
onClick={() => {
setUpdateGroup({ id, name, description });
setUpdateActive(true);
}}
/>
)}
{visible == false && <IconComponent src={EyeOpen} />}
{visible == true && <IconComponent src={EyeClosed} />}
</div>
</div>
</div>

View File

@@ -1,12 +1,12 @@
import { useEffect, useMemo, useState } from "react";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import { cn } from "../../../lib/cn";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import GroupsBlock from "./GroupsBlock";
import { setMenuActivePage } from "../../../redux/slices/store";
import { fetchMyGroups } from "../../../redux/slices/groups";
import ModalCreate from "./ModalCreate";
import ModalUpdate from "./ModalUpdate";
import { useEffect, useMemo, useState } from 'react';
import { SecondaryButton } from '../../../components/button/SecondaryButton';
import { cn } from '../../../lib/cn';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import GroupsBlock from './GroupsBlock';
import { setMenuActivePage } from '../../../redux/slices/store';
import { fetchMyGroups } from '../../../redux/slices/groups';
import ModalCreate from './ModalCreate';
import ModalUpdate from './ModalUpdate';
export interface GroupUpdate {
id: number;
@@ -17,11 +17,14 @@ export interface GroupUpdate {
const Groups = () => {
const [modalActive, setModalActive] = useState<boolean>(false);
const [modelUpdateActive, setModalUpdateActive] = useState<boolean>(false);
const [updateGroup, setUpdateGroup] = useState<GroupUpdate>({ id: 0, name: "", description: "" });
const [updateGroup, setUpdateGroup] = useState<GroupUpdate>({
id: 0,
name: '',
description: '',
});
const dispatch = useAppDispatch();
// Берём группы из стора
const groups = useAppSelector((store) => store.groups.groups);
@@ -29,8 +32,8 @@ const Groups = () => {
const currentUserName = useAppSelector((store) => store.auth.username);
useEffect(() => {
dispatch(setMenuActivePage("groups"));
dispatch(fetchMyGroups())
dispatch(setMenuActivePage('groups'));
dispatch(fetchMyGroups());
}, [dispatch]);
// Разделяем группы
@@ -44,17 +47,23 @@ const Groups = () => {
const hidden: typeof groups = []; // пока пустые, без логики
groups.forEach((group) => {
const me = group.members.find((m) => m.username === currentUserName);
const me = group.members.find(
(m) => m.username === currentUserName,
);
if (!me) return;
if (me.role === "Administrator") {
if (me.role === 'Administrator') {
managed.push(group);
} else {
current.push(group);
}
});
return { managedGroups: managed, currentGroups: current, hiddenGroups: hidden };
return {
managedGroups: managed,
currentGroups: current,
hiddenGroups: hidden,
};
}, [groups, currentUserName]);
return (
@@ -63,13 +72,15 @@ const Groups = () => {
<div className="relative flex items-center mb-[20px]">
<div
className={cn(
"h-[50px] text-[40px] font-bold text-liquid-white flex items-center"
'h-[50px] text-[40px] font-bold text-liquid-white flex items-center',
)}
>
Группы
</div>
<SecondaryButton
onClick={() => { setModalActive(true); }}
onClick={() => {
setModalActive(true);
}}
text="Создать группу"
className="absolute right-0"
/>
@@ -83,7 +94,6 @@ const Groups = () => {
groups={managedGroups}
setUpdateActive={setModalUpdateActive}
setUpdateGroup={setUpdateGroup}
/>
<GroupsBlock
className="mb-[20px]"
@@ -101,7 +111,6 @@ const Groups = () => {
/>
</div>
<ModalCreate setActive={setModalActive} active={modalActive} />
<ModalUpdate
setActive={setModalUpdateActive}

View File

@@ -1,9 +1,9 @@
import { useState, FC } from "react";
import GroupItem from "./GroupItem";
import { cn } from "../../../lib/cn";
import { ChevroneDown } from "../../../assets/icons/groups";
import { Group } from "../../../redux/slices/groups";
import { GroupUpdate } from "./Groups";
import { useState, FC } from 'react';
import GroupItem from './GroupItem';
import { cn } from '../../../lib/cn';
import { ChevroneDown } from '../../../assets/icons/groups';
import { Group } from '../../../redux/slices/groups';
import { GroupUpdate } from './Groups';
interface GroupsBlockProps {
groups: Group[];
@@ -13,46 +13,60 @@ interface GroupsBlockProps {
setUpdateGroup: (value: GroupUpdate) => void;
}
const GroupsBlock: FC<GroupsBlockProps> = ({ groups, title, className, setUpdateActive, setUpdateGroup }) => {
const [active, setActive] = useState<boolean>(title != "Скрытые");
const GroupsBlock: FC<GroupsBlockProps> = ({
groups,
title,
className,
setUpdateActive,
setUpdateGroup,
}) => {
const [active, setActive] = useState<boolean>(title != 'Скрытые');
return (
<div className={cn(" border-b-[1px] border-b-liquid-lighter rounded-[10px]",
className
)}>
<div className={cn(" h-[40px] text-[24px] font-bold flex gap-[10px] border-b-[1px] border-b-transparent items-center cursor-pointer transition-all duration-300",
active && " border-b-liquid-lighter"
<div
className={cn(
' border-b-[1px] border-b-liquid-lighter rounded-[10px]',
className,
)}
>
<div
className={cn(
' h-[40px] text-[24px] font-bold flex gap-[10px] border-b-[1px] border-b-transparent items-center cursor-pointer transition-all duration-300',
active && ' border-b-liquid-lighter',
)}
onClick={() => {
setActive(!active)
}}>
setActive(!active);
}}
>
<span>{title}</span>
<img src={ChevroneDown} className={cn("transition-all duration-300",
active && "rotate-180"
)}/>
<img
src={ChevroneDown}
className={cn(
'transition-all duration-300',
active && 'rotate-180',
)}
/>
</div>
<div className={cn(" grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300",
active && "grid-rows-[1fr] opacity-100"
)}>
<div
className={cn(
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300',
active && 'grid-rows-[1fr] opacity-100',
)}
>
<div className="overflow-hidden">
<div className="grid grid-cols-3 gap-[20px] pt-[20px] pb-[20px] box-border">
{
groups.map((v, i) => <GroupItem
key={i}
id={v.id}
visible={true}
description={v.description}
setUpdateActive={setUpdateActive}
setUpdateGroup={setUpdateGroup}
role={"owner"}
name={v.name}/>)
}
{groups.map((v, i) => (
<GroupItem
key={i}
id={v.id}
visible={true}
description={v.description}
setUpdateActive={setUpdateActive}
setUpdateGroup={setUpdateGroup}
role={'owner'}
name={v.name}
/>
))}
</div>
</div>
</div>

View File

@@ -1,10 +1,10 @@
import { FC, useEffect, useState } from "react";
import { Modal } from "../../../components/modal/Modal";
import { PrimaryButton } from "../../../components/button/PrimaryButton";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import { Input } from "../../../components/input/Input";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import { createGroup } from "../../../redux/slices/groups";
import { FC, useEffect, useState } from 'react';
import { Modal } from '../../../components/modal/Modal';
import { PrimaryButton } from '../../../components/button/PrimaryButton';
import { SecondaryButton } from '../../../components/button/SecondaryButton';
import { Input } from '../../../components/input/Input';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import { createGroup } from '../../../redux/slices/groups';
interface ModalCreateProps {
active: boolean;
@@ -12,27 +12,63 @@ interface ModalCreateProps {
}
const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [name, setName] = useState<string>('');
const [description, setDescription] = useState<string>('');
const status = useAppSelector((state) => state.groups.statuses.create);
const dispatch = useAppDispatch();
useEffect(() => {
if (status == "successful") {
if (status == 'successful') {
setActive(false);
}
}, [status]);
return (
<Modal className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white" onOpenChange={setActive} open={active} backdrop="blur" >
<Modal
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
onOpenChange={setActive}
open={active}
backdrop="blur"
>
<div className="w-[500px]">
<div className="font-bold text-[30px]">Создать группу</div>
<Input name="name" autocomplete="name" className="mt-[10px]" type="text" label="Название" onChange={(v) => { setName(v) }} placeholder="login" />
<Input name="description" autocomplete="description" className="mt-[10px]" type="text" label="Описание" onChange={(v) => { setDescription(v) }} placeholder="login" />
<Input
name="name"
autocomplete="name"
className="mt-[10px]"
type="text"
label="Название"
onChange={(v) => {
setName(v);
}}
placeholder="login"
/>
<Input
name="description"
autocomplete="description"
className="mt-[10px]"
type="text"
label="Описание"
onChange={(v) => {
setDescription(v);
}}
placeholder="login"
/>
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
<PrimaryButton onClick={() => { dispatch(createGroup({ name, description })) }} text="Создать" disabled={status == "loading"} />
<SecondaryButton onClick={() => { setActive(false); }} text="Отмена" />
<PrimaryButton
onClick={() => {
dispatch(createGroup({ name, description }));
}}
text="Создать"
disabled={status == 'loading'}
/>
<SecondaryButton
onClick={() => {
setActive(false);
}}
text="Отмена"
/>
</div>
</div>
</Modal>
@@ -40,4 +76,3 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
};
export default ModalCreate;

View File

@@ -1,10 +1,10 @@
import { FC, useEffect, useState } from "react";
import { Modal } from "../../../components/modal/Modal";
import { PrimaryButton } from "../../../components/button/PrimaryButton";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import { Input } from "../../../components/input/Input";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import { deleteGroup, updateGroup } from "../../../redux/slices/groups";
import { FC, useEffect, useState } from 'react';
import { Modal } from '../../../components/modal/Modal';
import { PrimaryButton } from '../../../components/button/PrimaryButton';
import { SecondaryButton } from '../../../components/button/SecondaryButton';
import { Input } from '../../../components/input/Input';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import { deleteGroup, updateGroup } from '../../../redux/slices/groups';
interface ModalUpdateProps {
active: boolean;
@@ -14,36 +14,95 @@ interface ModalUpdateProps {
groupDescription: string;
}
const ModalUpdate: FC<ModalUpdateProps> = ({ active, setActive, groupName, groupId, groupDescription }) => {
const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const statusUpdate = useAppSelector((state) => state.groups.statuses.update);
const statusDelete = useAppSelector((state) => state.groups.statuses.delete);
const ModalUpdate: FC<ModalUpdateProps> = ({
active,
setActive,
groupName,
groupId,
groupDescription,
}) => {
const [name, setName] = useState<string>('');
const [description, setDescription] = useState<string>('');
const statusUpdate = useAppSelector(
(state) => state.groups.statuses.update,
);
const statusDelete = useAppSelector(
(state) => state.groups.statuses.delete,
);
const dispatch = useAppDispatch();
useEffect(() => {
if (statusUpdate == "successful"){
if (statusUpdate == 'successful') {
setActive(false);
}
}, [statusUpdate]);
useEffect(() => {
if (statusDelete == "successful"){
if (statusDelete == 'successful') {
setActive(false);
}
}, [statusDelete]);
return (
<Modal className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white" onOpenChange={setActive} open={active} backdrop="blur" >
<Modal
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
onOpenChange={setActive}
open={active}
backdrop="blur"
>
<div className="w-[500px]">
<div className="font-bold text-[30px]">Изменить группу {groupName} #{groupId}</div>
<Input name="name" autocomplete="name" className="mt-[10px]" type="text" label="Новое название" defaultState={groupName} onChange={(v) => { setName(v)}} placeholder="login"/>
<Input name="description" autocomplete="description" className="mt-[10px]" type="text" label="Описание" onChange={(v) => { setDescription(v)}} placeholder="login" defaultState={groupDescription}/>
<div className="font-bold text-[30px]">
Изменить группу {groupName} #{groupId}
</div>
<Input
name="name"
autocomplete="name"
className="mt-[10px]"
type="text"
label="Новое название"
defaultState={groupName}
onChange={(v) => {
setName(v);
}}
placeholder="login"
/>
<Input
name="description"
autocomplete="description"
className="mt-[10px]"
type="text"
label="Описание"
onChange={(v) => {
setDescription(v);
}}
placeholder="login"
defaultState={groupDescription}
/>
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
<PrimaryButton onClick={() => {dispatch(deleteGroup(groupId))}} text="Удалить" disabled={statusDelete=="loading"} color="error"/>
<PrimaryButton onClick={() => {dispatch(updateGroup({name, description, groupId}))}} text="Обновить" disabled={statusUpdate=="loading"}/>
<SecondaryButton onClick={() => {setActive(false);}} text="Отмена" />
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
<PrimaryButton
onClick={() => {
dispatch(deleteGroup(groupId));
}}
text="Удалить"
disabled={statusDelete == 'loading'}
color="error"
/>
<PrimaryButton
onClick={() => {
dispatch(
updateGroup({ name, description, groupId }),
);
}}
text="Обновить"
disabled={statusUpdate == 'loading'}
/>
<SecondaryButton
onClick={() => {
setActive(false);
}}
text="Отмена"
/>
</div>
</div>
</Modal>
@@ -51,4 +110,3 @@ const ModalUpdate: FC<ModalUpdateProps> = ({ active, setActive, groupName, group
};
export default ModalUpdate;

View File

@@ -1,29 +1,63 @@
import { Logo } from "../../../assets/logos";
import {Account, Clipboard, Cup, Home, Openbook, Users} from "../../../assets/icons/menu";
import MenuItem from "./MenuItem";
import { useAppSelector } from "../../../redux/hooks";
import { Logo } from '../../../assets/logos';
import {
Account,
Clipboard,
Cup,
Home,
Openbook,
Users,
} from '../../../assets/icons/menu';
import MenuItem from './MenuItem';
import { useAppSelector } from '../../../redux/hooks';
const Menu = () => {
const menuItems = [
{text: "Главная", href: "/home", icon: Home, page: "home" },
{text: "Задачи", href: "/home/missions", icon: Clipboard, page: "missions" },
{text: "Статьи", href: "/home/articles", icon: Openbook, page: "articles" },
{text: "Группы", href: "/home/groups", icon: Users, page: "groups" },
{text: "Контесты", href: "/home/contests", icon: Cup, page: "contests" },
{text: "Аккаунт", href: "/home/account", icon: Account, page: "account" },
];
const activePage = useAppSelector((state) => state.store.menu.activePage);
const menuItems = [
{ text: 'Главная', href: '/home', icon: Home, page: 'home' },
{
text: 'Задачи',
href: '/home/missions',
icon: Clipboard,
page: 'missions',
},
{
text: 'Статьи',
href: '/home/articles',
icon: Openbook,
page: 'articles',
},
{ text: 'Группы', href: '/home/groups', icon: Users, page: 'groups' },
{
text: 'Контесты',
href: '/home/contests',
icon: Cup,
page: 'contests',
},
{
text: 'Аккаунт',
href: '/home/account',
icon: Account,
page: 'account',
},
];
const activePage = useAppSelector((state) => state.store.menu.activePage);
return (
<div className="w-[250px] fixed top-0 items-center box-border p-[20px] pt-[35px]">
<img src={Logo} className="w-[173px]" />
<div className="">
{menuItems.map((v, i) => (
<MenuItem key={i} icon={v.icon} text={v.text} href={v.href} active={v.page == activePage} page={v.page}/>
))}
</div>
</div>
);
return (
<div className="w-[250px] fixed top-0 items-center box-border p-[20px] pt-[35px]">
<img src={Logo} className="w-[173px]" />
<div className="">
{menuItems.map((v, i) => (
<MenuItem
key={i}
icon={v.icon}
text={v.text}
href={v.href}
active={v.page == activePage}
page={v.page}
/>
))}
</div>
</div>
);
};
export default Menu;

View File

@@ -1,37 +1,44 @@
import React from "react";
import { Link } from "react-router-dom";
import { useAppDispatch } from "../../../redux/hooks";
import { setMenuActivePage } from "../../../redux/slices/store";
import React from 'react';
import { Link } from 'react-router-dom';
import { useAppDispatch } from '../../../redux/hooks';
import { setMenuActivePage } from '../../../redux/slices/store';
interface MenuItemProps {
icon: string; // SVG или любой JSX
text: string;
href: string;
page: string;
active?: boolean; // необязательный, по умолчанию false
icon: string; // SVG или любой JSX
text: string;
href: string;
page: string;
active?: boolean; // необязательный, по умолчанию false
}
const MenuItem: React.FC<MenuItemProps> = ({ icon, text = "", href = "", active = false, page = "" }) => {
const dispatch = useAppDispatch();
const MenuItem: React.FC<MenuItemProps> = ({
icon,
text = '',
href = '',
active = false,
page = '',
}) => {
const dispatch = useAppDispatch();
return (
<Link
to={href}
className={`
return (
<Link
to={href}
className={`
flex items-center gap-3 p-[16px] rounded-[10px\] h-[40px] text-[18px] font-bold
transition-all duration-300 text-liquid-white mt-[20px]
active:scale-95
${active ? "bg-liquid-darkmain hover:bg-liquid-lighter hover:ring-[1px] hover:ring-liquid-darkmain hover:ring-inset"
: " hover:bg-liquid-lighter"}
${
active
? 'bg-liquid-darkmain hover:bg-liquid-lighter hover:ring-[1px] hover:ring-liquid-darkmain hover:ring-inset'
: ' hover:bg-liquid-lighter'
}
`}
onClick={
() => dispatch(setMenuActivePage(page))
}
>
<img src={icon} />
<span>{text}</span>
</Link>
);
onClick={() => dispatch(setMenuActivePage(page))}
>
<img src={icon} />
<span>{text}</span>
</Link>
);
};
export default MenuItem;

View File

@@ -1,71 +1,78 @@
import { cn } from "../../../lib/cn";
import { IconError, IconSuccess } from "../../../assets/icons/missions";
import { useNavigate } from "react-router-dom";
import { cn } from '../../../lib/cn';
import { IconError, IconSuccess } from '../../../assets/icons/missions';
import { useNavigate } from 'react-router-dom';
export interface MissionItemProps {
id: number;
authorId: number;
name: string;
difficulty: "Easy" | "Medium" | "Hard";
difficulty: 'Easy' | 'Medium' | 'Hard';
tags: string[];
timeLimit: number;
memoryLimit: number;
createdAt: string;
updatedAt: string;
type: "first" | "second";
status: "empty" | "success" | "error";
type: 'first' | 'second';
status: 'empty' | 'success' | 'error';
}
export function formatMilliseconds(ms: number): string {
const rounded = Math.round(ms) / 1000;
const formatted = rounded.toString().replace(/\.?0+$/, '');
return `${formatted} c`;
const rounded = Math.round(ms) / 1000;
const formatted = rounded.toString().replace(/\.?0+$/, '');
return `${formatted} c`;
}
export function formatBytesToMB(bytes: number): string {
const megabytes = Math.floor(bytes / (1024 * 1024));
return `${megabytes} МБ`;
const megabytes = Math.floor(bytes / (1024 * 1024));
return `${megabytes} МБ`;
}
const MissionItem: React.FC<MissionItemProps> = ({
id, name, difficulty, timeLimit, memoryLimit, type, status
id,
name,
difficulty,
timeLimit,
memoryLimit,
type,
status,
}) => {
const navigate = useNavigate();
return (
<div className={cn("h-[44px] w-full relative rounded-[10px] text-liquid-white",
type == "first" ? "bg-liquid-lighter" : "bg-liquid-background",
"grid grid-cols-[80px,1fr,1fr,60px,24px] grid-flow-col gap-[20px] px-[20px] box-border items-center",
status == "error" && "border-l-[11px] border-l-liquid-red pl-[9px]",
status == "success" && "border-l-[11px] border-l-liquid-green pl-[9px]",
"cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300",
)}
onClick={() => {navigate(`/mission/${id}`)}}
<div
className={cn(
'h-[44px] w-full relative rounded-[10px] text-liquid-white',
type == 'first' ? 'bg-liquid-lighter' : 'bg-liquid-background',
'grid grid-cols-[80px,1fr,1fr,60px,24px] grid-flow-col gap-[20px] px-[20px] box-border items-center',
status == 'error' &&
'border-l-[11px] border-l-liquid-red pl-[9px]',
status == 'success' &&
'border-l-[11px] border-l-liquid-green pl-[9px]',
'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300',
)}
onClick={() => {
navigate(`/mission/${id}`);
}}
>
<div className="text-[18px] font-bold">
#{id}
</div>
<div className="text-[18px] font-bold">
{name}
</div>
<div className="text-[18px] font-bold">#{id}</div>
<div className="text-[18px] font-bold">{name}</div>
<div className="text-[12px] text-right">
стандартный ввод/вывод {formatMilliseconds(timeLimit)}, {formatBytesToMB(memoryLimit)}
стандартный ввод/вывод {formatMilliseconds(timeLimit)},{' '}
{formatBytesToMB(memoryLimit)}
</div>
<div className={cn(
"text-center text-[18px]",
difficulty == "Hard" && "text-liquid-red",
difficulty == "Medium" && "text-liquid-orange",
difficulty == "Easy" && "text-liquid-green",
)}>
<div
className={cn(
'text-center text-[18px]',
difficulty == 'Hard' && 'text-liquid-red',
difficulty == 'Medium' && 'text-liquid-orange',
difficulty == 'Easy' && 'text-liquid-green',
)}
>
{difficulty}
</div>
<div className="h-[24px] w-[24px]">
{
status == "error" && <img src={IconError}/>
}
{
status == "success" && <img src={IconSuccess}/>
}
{status == 'error' && <img src={IconError} />}
{status == 'success' && <img src={IconSuccess} />}
</div>
</div>
);

View File

@@ -1,18 +1,17 @@
import MissionItem from "./MissionItem";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import { useEffect, useState } from "react";
import { setMenuActivePage } from "../../../redux/slices/store";
import { useNavigate } from "react-router-dom";
import { fetchMissions } from "../../../redux/slices/missions";
import ModalCreate from "./ModalCreate";
import MissionItem from './MissionItem';
import { SecondaryButton } from '../../../components/button/SecondaryButton';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import { useEffect, useState } from 'react';
import { setMenuActivePage } from '../../../redux/slices/store';
import { useNavigate } from 'react-router-dom';
import { fetchMissions } from '../../../redux/slices/missions';
import ModalCreate from './ModalCreate';
export interface Mission {
id: number;
authorId: number;
name: string;
difficulty: "Easy" | "Medium" | "Hard";
difficulty: 'Easy' | 'Medium' | 'Hard';
tags: string[];
timeLimit: number;
memoryLimit: number;
@@ -21,60 +20,60 @@ export interface Mission {
}
const Missions = () => {
const dispatch = useAppDispatch();
const [modalActive, setModalActive] = useState<boolean>(false);
const missions = useAppSelector((state) => state.missions.missions);
useEffect(() => {
dispatch(setMenuActivePage("missions"))
dispatch(fetchMissions({}))
dispatch(setMenuActivePage('missions'));
dispatch(fetchMissions({}));
}, []);
return (
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
<div className="h-full box-border">
<div className="relative flex items-center mb-[20px]">
<div className="h-[50px] text-[40px] font-bold text-liquid-white flex items-center">
Задачи
</div>
<SecondaryButton
onClick={() => {setModalActive(true)}}
<SecondaryButton
onClick={() => {
setModalActive(true);
}}
text="Добавить задачу"
className="absolute right-0"
/>
</div>
<div className="bg-liquid-lighter h-[50px] mb-[20px]">
</div>
<div className="bg-liquid-lighter h-[50px] mb-[20px]"></div>
<div>
{missions.map((v, i) => (
<MissionItem
key={i}
id={v.id}
authorId={v.authorId}
name={v.name}
difficulty={"Easy"}
tags={v.tags}
timeLimit={1000}
memoryLimit={256 * 1024 * 1024}
createdAt={v.createdAt}
updatedAt={v.updatedAt}
type={i % 2 == 0 ? "first" : "second"}
status={i == 0 || i == 3 || i == 7 ? "success" : (i == 2 || i == 4 || i == 9 ? "error" : "empty")}/>
))}
{missions.map((v, i) => (
<MissionItem
key={i}
id={v.id}
authorId={v.authorId}
name={v.name}
difficulty={'Easy'}
tags={v.tags}
timeLimit={1000}
memoryLimit={256 * 1024 * 1024}
createdAt={v.createdAt}
updatedAt={v.updatedAt}
type={i % 2 == 0 ? 'first' : 'second'}
status={
i == 0 || i == 3 || i == 7
? 'success'
: i == 2 || i == 4 || i == 9
? 'error'
: 'empty'
}
/>
))}
</div>
<div>
pages
</div>
<div>pages</div>
</div>
<ModalCreate setActive={setModalActive} active={modalActive} />

View File

@@ -59,6 +59,10 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
}
}, [status]);
useEffect(() => {
dispatch(setMissionsStatus({ key: 'upload', status: 'idle' }));
}, [active]);
return (
<Modal
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
@@ -152,6 +156,8 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
text="Отмена"
/>
</div>
{status == 'failed' && <div>error</div>}
</div>
</Modal>
);