This commit is contained in:
Виталий Лавшонок
2025-10-30 20:43:01 +03:00
parent 5ef7933446
commit 99018537c5
21 changed files with 518 additions and 42 deletions

View File

@@ -1,8 +1,4 @@
import { Logo } from "../../../assets/logos";
import { Account, Clipboard, Cup, Home, Openbook, Users } from "../../../assets/icons/menu";
// import MenuItem from "./MenuItem";
import { cn } from "../../../lib/cn";
import { IconError, IconSuccess } from "../../../assets/icons/problems";
export interface ArticleItemProps {
id: number;
@@ -10,17 +6,6 @@ export interface ArticleItemProps {
tags: string[];
}
export function formatMilliseconds(ms: number): string {
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 ArticleItem: React.FC<ArticleItemProps> = ({
id, name, tags
}) => {

View File

@@ -1,23 +1,22 @@
import { useEffect } from "react";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import ArticleItem, { ArticleItemProps } from "./ArticleItem";
import { useAppDispatch } from "../../../redux/hooks";
import ArticleItem from "./ArticleItem";
import { setMenuActivePage } from "../../../redux/slices/store";
export interface Problem {
export interface Article {
id: number;
authorId: number;
name: string;
difficulty: "Easy" | "Medium" | "Hard";
tags: string[];
timeLimit: number;
memoryLimit: number;
createdAt: string;
updatedAt: string;
}
const Articles = () => {
const articles: ArticleItemProps[] = [
const dispatch = useAppDispatch();
const articles: Article[] = [
{
"id": 1,
"name": "Todo List App",
@@ -130,6 +129,9 @@ const Articles = () => {
}
];
useEffect(() => {
dispatch(setMenuActivePage("articles"))
}, []);
return (
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
@@ -137,7 +139,7 @@ const Articles = () => {
<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={() => { }}

View File

@@ -26,6 +26,9 @@ const Login = () => {
// После успешного логина
useEffect(() => {
dispatch(setMenuActivePage("account"))
}, []);
useEffect(() => {
if (jwt) {
navigate("/home/offices"); // или другая страница после входа
}
@@ -58,8 +61,8 @@ const Login = () => {
</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" />
<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
@@ -79,7 +82,7 @@ const Login = () => {
/>
<SecondaryButton
className="w-full"
onClick={() => {}}
onClick={() => { }}
>
<div className="flex items-center">
<img src={googleLogo} className="h-[24px] w-[24px] mr-[15px]" />

View File

@@ -26,8 +26,12 @@ const Register = () => {
const { status, error, jwt } = useAppSelector((state) => state.auth);
// После успешной регистрации — переход в систему
useEffect(() => {
dispatch(setMenuActivePage("account"))
}, []);
useEffect(() => {
dispatch(setMenuActivePage("account"));
if (jwt) {
navigate("/home");
}

View File

@@ -0,0 +1,72 @@
import { cn } from "../../../lib/cn";
export interface ContestItemProps {
id: number;
name: string;
authors: string[];
startAt: string;
registerAt: string;
duration: number;
members: number;
statusRegister: "reg" | "nonreg";
type: "first" | "second";
}
function formatDate(dateString: string): string {
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 hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
return `${day}/${month}/${year}\n${hours}:${minutes}`;
}
const ContestItem: React.FC<ContestItemProps> = ({
id, name, authors, startAt, registerAt, 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",
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">
{name}
</div>
<div className="text-center text-liquid-brightmain font-normal">
{authors.map((v, i) => <p key={i}>{v}</p>)}
</div>
<div className="text-center text-nowrap">
{formatDate(startAt)}
</div>
<div className="text-center">
{duration}
</div>
{
waitTime > 0 &&
<div className="text-center">
{waitTime}
</div>
}
<div className="text-center">
{members}
</div>
<div className="text-center">
{statusRegister}
</div>
</div>
);
};
export default ContestItem;

View File

@@ -0,0 +1,131 @@
import { useEffect } from "react";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import { cn } from "../../../lib/cn";
import { useAppDispatch } from "../../../redux/hooks";
import ContestsBlock from "./ContestsBlock";
import { setMenuActivePage } from "../../../redux/slices/store";
interface Contest {
id: number;
name: string;
authors: string[];
startAt: string;
registerAt: string;
duration: number;
members: number;
statusRegister: "reg" | "nonreg";
}
const Contests = () => {
const dispatch = useAppDispatch();
const now = new Date();
const contests: Contest[] = [
// === Прошедшие контесты ===
{
id: 1,
name: "Code Marathon 2025",
authors: ["tourist", "Petr", "Semen", "Rotar"],
startAt: "2025-09-15T10:00:00.000Z",
registerAt: "2025-09-10T10:00:00.000Z",
duration: 180,
members: 4821,
statusRegister: "reg",
},
{
id: 2,
name: "Autumn Cup 2025",
authors: ["awoo", "Benq"],
startAt: "2025-09-25T17:00:00.000Z",
registerAt: "2025-09-20T17:00:00.000Z",
duration: 150,
members: 3670,
statusRegister: "nonreg",
},
// === Контесты, которые сейчас идут ===
{
id: 3,
name: "Halloween Challenge",
authors: ["Errichto", "Radewoosh"],
startAt: "2025-10-29T10:00:00.000Z", // начался сегодня
registerAt: "2025-10-25T10:00:00.000Z",
duration: 240,
members: 5123,
statusRegister: "reg",
},
{
id: 4,
name: "October Blitz",
authors: ["neal", "Um_nik"],
startAt: "2025-10-29T12:00:00.000Z",
registerAt: "2025-10-24T12:00:00.000Z",
duration: 300,
members: 2890,
statusRegister: "nonreg",
},
// === Контесты, которые еще не начались ===
{
id: 5,
name: "Winter Warmup",
authors: ["tourist", "rng_58"],
startAt: "2025-11-05T18:00:00.000Z",
registerAt: "2025-11-01T18:00:00.000Z",
duration: 180,
members: 2100,
statusRegister: "reg",
},
{
id: 6,
name: "Global Coding Cup",
authors: ["maroonrk", "kostka"],
startAt: "2025-11-12T15:00:00.000Z",
registerAt: "2025-11-08T15:00:00.000Z",
duration: 240,
members: 1520,
statusRegister: "nonreg",
},
];
useEffect(() => {
dispatch(setMenuActivePage("contests"))
}, []);
return (
<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>
<SecondaryButton
onClick={() => { }}
text="Создать группу"
className="absolute right-0"
/>
</div>
<div className="bg-liquid-lighter h-[50px] mb-[20px]">
</div>
<ContestsBlock className="mb-[20px]" title="Текущие" contests={contests.filter(contest => {
const endTime = new Date(contest.startAt).getTime() + contest.duration * 60 * 1000;
return endTime >= now.getTime();
})} />
<ContestsBlock className="mb-[20px]" title="Прошедшие" contests={contests.filter(contest => {
const endTime = new Date(contest.startAt).getTime() + contest.duration * 60 * 1000;
return endTime < now.getTime();
})} />
</div>
</div>
);
};
export default Contests;

View File

@@ -0,0 +1,64 @@
import { useState, FC } from "react";
import { cn } from "../../../lib/cn";
import { ChevroneDown } from "../../../assets/icons/groups";
import ContestItem from "./ContestItem";
interface Contest {
id: number;
name: string;
authors: string[];
startAt: string;
registerAt: string;
duration: number;
members: number;
statusRegister: "reg" | "nonreg";
}
interface GroupsBlockProps {
contests: Contest[];
title: string;
className?: string;
}
const GroupsBlock: FC<GroupsBlockProps> = ({ 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"
)}
onClick={() => {
console.log(active);
setActive(!active)
}}>
<span>{title}</span>
<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="overflow-hidden">
<div className="pb-[10px] pt-[20px]">
{
contests.map((v, i) => <ContestItem key={i} {...v} type={i % 2 ? "second" : "first"} />)
}
</div>
</div>
</div>
</div>
);
};
export default GroupsBlock;

View File

@@ -0,0 +1,59 @@
import { cn } from "../../../lib/cn";
import { Book, UserAdd, Edit, EyeClosed, EyeOpen } from "../../../assets/icons/groups";
export interface GroupItemProps {
id: number;
role: "menager" | "member" | "owner" | "viewer";
visible: boolean;
name: string;
}
interface IconComponentProps {
src: string;
}
const IconComponent: React.FC<IconComponentProps> = ({
src
}) => {
return <img
src={src}
className="hover:bg-liquid-light rounded-[5px] cursor-pointer transition-all duration-300"
/>
}
const GroupItem: React.FC<GroupItemProps> = ({
id, name, visible, role
}) => {
console.log(id);
return (
<div className={cn("w-full h-[120px] box-border relative rounded-[10px] p-[10px] text-liquid-white bg-liquid-lighter",
)}>
<div className="grid grid-cols-[100px,1fr] gap-[20px]">
<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=" flex gap-[10px]">
{
(role == "menager" || role == "owner") && <IconComponent src={UserAdd}/>
}
{
(role == "menager" || role == "owner") && <IconComponent src={Edit}/>
}
{
visible == false && <IconComponent src={EyeOpen} />
}
{
visible == true && <IconComponent src={EyeClosed} />
}
</div>
</div>
</div>
</div>
);
};
export default GroupItem;

View File

@@ -0,0 +1,71 @@
import { useEffect } from "react";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import { cn } from "../../../lib/cn";
import { useAppDispatch } from "../../../redux/hooks";
import GroupsBlock from "./GroupsBlock";
import { setMenuActivePage } from "../../../redux/slices/store";
export interface Group {
id: number;
role: "menager" | "member" | "owner" | "viewer";
visible: boolean;
name: string;
}
const Groups = () => {
const dispatch = useAppDispatch();
const groups: Group[] = [
{ id: 1, role: "owner", name: "Main Administration", visible: true },
{ id: 2, role: "menager", name: "Project Managers", visible: true },
{ id: 3, role: "member", name: "Developers", visible: true },
{ id: 4, role: "viewer", name: "QA Viewers", visible: true },
{ id: 5, role: "member", name: "Design Team", visible: true },
{ id: 6, role: "owner", name: "Executive Board", visible: true },
{ id: 7, role: "menager", name: "HR Managers", visible: true },
{ id: 8, role: "viewer", name: "Marketing Reviewers", visible: false },
{ id: 9, role: "member", name: "Content Creators", visible: false },
{ id: 10, role: "menager", name: "Support Managers", visible: true },
{ id: 11, role: "viewer", name: "External Auditors", visible: false },
{ id: 12, role: "member", name: "Frontend Developers", visible: true },
{ id: 13, role: "member", name: "Backend Developers", visible: true },
{ id: 14, role: "viewer", name: "Guest Access", visible: false },
{ id: 15, role: "menager", name: "Operations", visible: true },
];
useEffect(() => {
dispatch(setMenuActivePage("groups"))
}, []);
return (
<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>
<SecondaryButton
onClick={() => { }}
text="Создать группу"
className="absolute right-0"
/>
</div>
<div className="bg-liquid-lighter h-[50px] mb-[20px]">
</div>
<GroupsBlock className="mb-[20px]" title="Управляемые" groups={groups.filter((v) => v.visible && (v.role == "owner" || v.role == "menager"))} />
<GroupsBlock className="mb-[20px]" title="Текущие" groups={groups.filter((v) => v.visible && (v.role == "member" || v.role == "viewer"))} />
<GroupsBlock className="mb-[20px]" title="Скрытые" groups={groups.filter((v) => v.visible == false)} />
</div>
</div>
);
};
export default Groups;

View File

@@ -0,0 +1,60 @@
import { useState, FC } from "react";
import GroupItem from "./GroupItem";
import { cn } from "../../../lib/cn";
import { ChevroneDown } from "../../../assets/icons/groups";
export interface Group {
id: number;
role: "menager" | "member" | "owner" | "viewer";
visible: boolean;
name: string;
}
interface GroupsBlockProps {
groups: Group[];
title: string;
className?: string;
}
const GroupsBlock: FC<GroupsBlockProps> = ({ groups, 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] border-b-[1px] border-b-transparent items-center cursor-pointer transition-all duration-300",
active && " border-b-liquid-lighter"
)}
onClick={() => {
console.log(active);
setActive(!active)
}}>
<span>{title}</span>
<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="overflow-hidden">
<div className="grid grid-cols-3 gap-[20px] pt-[20px] pb-[20px] box-border">
{
groups.map((v, i) => <GroupItem key={i} {...v} />)
}
</div>
</div>
</div>
</div>
);
};
export default GroupsBlock;

View File

@@ -6,10 +6,10 @@ import { useAppSelector } from "../../../redux/hooks";
const Menu = () => {
const menuItems = [
{text: "Главная", href: "/home", icon: Home, page: "home" },
{text: "Задачи", href: "/home/problems", icon: Clipboard, page: "clipboard" },
{text: "Статьи", href: "/home/articles", icon: Openbook, page: "openbool" },
{text: "Группы", href: "/home", icon: Users, page: "users" },
{text: "Контесты", href: "/home", icon: Cup, page: "cup" },
{text: "Задачи", href: "/home/problems", icon: Clipboard, page: "problems" },
{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);

View File

@@ -1,6 +1,3 @@
import { Logo } from "../../../assets/logos";
import { Account, Clipboard, Cup, Home, Openbook, Users } from "../../../assets/icons/menu";
// import MenuItem from "./MenuItem";
import { cn } from "../../../lib/cn";
import { IconError, IconSuccess } from "../../../assets/icons/problems";
@@ -30,7 +27,7 @@ export function formatBytesToMB(bytes: number): string {
}
const ProblemItem: React.FC<ProblemItemProps> = ({
id, authorId, name, difficulty, tags, timeLimit, memoryLimit, createdAt, updatedAt, type, status
id, name, difficulty, timeLimit, memoryLimit, type, status
}) => {
console.log(id);
return (

View File

@@ -1,9 +1,8 @@
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 ProblemItem from "./ProblemItem";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import { useAppDispatch } from "../../../redux/hooks";
import { useEffect } from "react";
import { setMenuActivePage } from "../../../redux/slices/store";
export interface Problem {
@@ -21,6 +20,8 @@ export interface Problem {
const Problems = () => {
const dispatch = useAppDispatch();
const problems: Problem[] = [
{
"id": 1,
@@ -464,6 +465,9 @@ const Problems = () => {
}
];
useEffect(() => {
dispatch(setMenuActivePage("problems"))
}, []);
return (
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
@@ -471,7 +475,7 @@ const Problems = () => {
<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={() => {}}