diff --git a/src/assets/icons/groups/book.png b/src/assets/icons/groups/book.png new file mode 100644 index 0000000..260af3b Binary files /dev/null and b/src/assets/icons/groups/book.png differ diff --git a/src/assets/icons/groups/chevron-down.svg b/src/assets/icons/groups/chevron-down.svg new file mode 100644 index 0000000..cda1cc2 --- /dev/null +++ b/src/assets/icons/groups/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/groups/edit.svg b/src/assets/icons/groups/edit.svg new file mode 100644 index 0000000..abafac2 --- /dev/null +++ b/src/assets/icons/groups/edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/groups/eye-closed.svg b/src/assets/icons/groups/eye-closed.svg new file mode 100644 index 0000000..a3a57eb --- /dev/null +++ b/src/assets/icons/groups/eye-closed.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/groups/eye-open.png b/src/assets/icons/groups/eye-open.png new file mode 100644 index 0000000..eee5690 Binary files /dev/null and b/src/assets/icons/groups/eye-open.png differ diff --git a/src/assets/icons/groups/index.ts b/src/assets/icons/groups/index.ts new file mode 100644 index 0000000..86bef2e --- /dev/null +++ b/src/assets/icons/groups/index.ts @@ -0,0 +1,8 @@ +import Book from "./book.png" +import EyeClosed from "./eye-closed.svg"; +import EyeOpen from "./eye-open.png"; +import Edit from "./edit.svg"; +import UserAdd from "./user-profile-add.svg"; +import ChevroneDown from "./chevron-down.svg" + +export {Book, Edit, EyeClosed, EyeOpen, UserAdd, ChevroneDown} \ No newline at end of file diff --git a/src/assets/icons/groups/user-profile-add.svg b/src/assets/icons/groups/user-profile-add.svg new file mode 100644 index 0000000..20bbea5 --- /dev/null +++ b/src/assets/icons/groups/user-profile-add.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 448e0d1..1cc8c5a 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -8,6 +8,8 @@ import { useEffect } from "react"; import { fetchWhoAmI } from "../redux/slices/auth"; import Problems from "../views/home/problems/Problems"; import Articles from "../views/home/articles/Articles"; +import Groups from "../views/home/groups/Groups"; +import Contests from "../views/home/contests/Contests"; const Home = () => { const name = useAppSelector((state) => state.auth.username); @@ -31,6 +33,8 @@ const Home = () => { } /> } /> } /> + } /> + } /> diff --git a/src/views/home/articles/ArticleItem.tsx b/src/views/home/articles/ArticleItem.tsx index 680b031..a7dfdc5 100644 --- a/src/views/home/articles/ArticleItem.tsx +++ b/src/views/home/articles/ArticleItem.tsx @@ -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 = ({ id, name, tags }) => { diff --git a/src/views/home/articles/Articles.tsx b/src/views/home/articles/Articles.tsx index b37a8a5..de5f1d4 100644 --- a/src/views/home/articles/Articles.tsx +++ b/src/views/home/articles/Articles.tsx @@ -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 (
@@ -137,7 +139,7 @@ const Articles = () => {
- База статей + Статьи
{ }} diff --git a/src/views/home/auth/Login.tsx b/src/views/home/auth/Login.tsx index 6013949..fc06b64 100644 --- a/src/views/home/auth/Login.tsx +++ b/src/views/home/auth/Login.tsx @@ -26,6 +26,9 @@ const Login = () => { // После успешного логина useEffect(() => { dispatch(setMenuActivePage("account")) + }, []); + + useEffect(() => { if (jwt) { navigate("/home/offices"); // или другая страница после входа } @@ -58,8 +61,8 @@ const Login = () => {
- {setUsername(v)}} placeholder="login"/> - {setPassword(v)}} placeholder="abCD1234" /> + { setUsername(v) }} placeholder="login" /> + { setPassword(v) }} placeholder="abCD1234" />
{ /> {}} + onClick={() => { }} >
diff --git a/src/views/home/auth/Register.tsx b/src/views/home/auth/Register.tsx index 7aae6c7..e2a6e8f 100644 --- a/src/views/home/auth/Register.tsx +++ b/src/views/home/auth/Register.tsx @@ -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"); } diff --git a/src/views/home/contests/ContestItem.tsx b/src/views/home/contests/ContestItem.tsx new file mode 100644 index 0000000..5451ae7 --- /dev/null +++ b/src/views/home/contests/ContestItem.tsx @@ -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 = ({ + id, name, authors, startAt, registerAt, duration, members, statusRegister, type +}) => { + const now = new Date(); + + const waitTime = new Date(startAt).getTime() - now.getTime(); + + return ( +
+
+ {name} +
+
+ {authors.map((v, i) =>

{v}

)} +
+
+ {formatDate(startAt)} +
+
+ {duration} +
+ { + waitTime > 0 && +
+ {waitTime} +
+ } +
+ {members} +
+
+ {statusRegister} +
+ +
+ ); +}; + +export default ContestItem; diff --git a/src/views/home/contests/Contests.tsx b/src/views/home/contests/Contests.tsx new file mode 100644 index 0000000..2d68158 --- /dev/null +++ b/src/views/home/contests/Contests.tsx @@ -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 ( +
+
+ +
+
+ Контесты +
+ { }} + text="Создать группу" + className="absolute right-0" + /> +
+ +
+ +
+ + + { + const endTime = new Date(contest.startAt).getTime() + contest.duration * 60 * 1000; + return endTime >= now.getTime(); + })} /> + { + const endTime = new Date(contest.startAt).getTime() + contest.duration * 60 * 1000; + return endTime < now.getTime(); + })} /> +
+
+ ); +}; + +export default Contests; diff --git a/src/views/home/contests/ContestsBlock.tsx b/src/views/home/contests/ContestsBlock.tsx new file mode 100644 index 0000000..63a5d32 --- /dev/null +++ b/src/views/home/contests/ContestsBlock.tsx @@ -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 = ({ contests, title, className }) => { + + + const [active, setActive] = useState(title != "Скрытые"); + + + return ( + +
+
{ + console.log(active); + setActive(!active) + }}> + {title} + +
+
+
+
+ { + contests.map((v, i) => ) + } +
+ +
+
+
+ ); +}; + +export default GroupsBlock; diff --git a/src/views/home/groups/GroupItem.tsx b/src/views/home/groups/GroupItem.tsx new file mode 100644 index 0000000..eafd491 --- /dev/null +++ b/src/views/home/groups/GroupItem.tsx @@ -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 = ({ + src +}) => { + + return +} + +const GroupItem: React.FC = ({ + id, name, visible, role +}) => { + console.log(id); + return ( +
+
+ +
+
+ {name} +
+
+ { + (role == "menager" || role == "owner") && + } + { + (role == "menager" || role == "owner") && + } + { + visible == false && + } + { + visible == true && + } +
+
+
+
+ ); +}; + +export default GroupItem; diff --git a/src/views/home/groups/Groups.tsx b/src/views/home/groups/Groups.tsx new file mode 100644 index 0000000..34559f6 --- /dev/null +++ b/src/views/home/groups/Groups.tsx @@ -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 ( +
+
+ +
+
+ Группы +
+ { }} + text="Создать группу" + className="absolute right-0" + /> +
+ +
+ +
+ + + v.visible && (v.role == "owner" || v.role == "menager"))} /> + v.visible && (v.role == "member" || v.role == "viewer"))} /> + v.visible == false)} /> +
+
+ ); +}; + +export default Groups; diff --git a/src/views/home/groups/GroupsBlock.tsx b/src/views/home/groups/GroupsBlock.tsx new file mode 100644 index 0000000..fd4e2e1 --- /dev/null +++ b/src/views/home/groups/GroupsBlock.tsx @@ -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 = ({ groups, title, className }) => { + + + const [active, setActive] = useState(title != "Скрытые"); + + + return ( + +
+
{ + console.log(active); + setActive(!active) + }}> + {title} + +
+
+
+ +
+ { + groups.map((v, i) => ) + } +
+
+
+
+ ); +}; + +export default GroupsBlock; diff --git a/src/views/home/menu/Menu.tsx b/src/views/home/menu/Menu.tsx index f2ccfea..63ae749 100644 --- a/src/views/home/menu/Menu.tsx +++ b/src/views/home/menu/Menu.tsx @@ -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); diff --git a/src/views/home/problems/ProblemItem.tsx b/src/views/home/problems/ProblemItem.tsx index 55ab2c5..5410cd5 100644 --- a/src/views/home/problems/ProblemItem.tsx +++ b/src/views/home/problems/ProblemItem.tsx @@ -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 = ({ - id, authorId, name, difficulty, tags, timeLimit, memoryLimit, createdAt, updatedAt, type, status + id, name, difficulty, timeLimit, memoryLimit, type, status }) => { console.log(id); return ( diff --git a/src/views/home/problems/Problems.tsx b/src/views/home/problems/Problems.tsx index 9507a26..5c1ae29 100644 --- a/src/views/home/problems/Problems.tsx +++ b/src/views/home/problems/Problems.tsx @@ -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 (
@@ -471,7 +475,7 @@ const Problems = () => {
- База задач + Задачи
{}}