From edd44267808ef55b82c9baf4a76baa41c38fe991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:02:26 +0300 Subject: [PATCH 01/10] statenent tmp --- src/App.tsx | 3 +- src/views/problem/statement/Proble.tsx | 192 ++++++++++++++++++++++ src/views/problem/statement/Statement.tsx | 37 ++++- 3 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 src/views/problem/statement/Proble.tsx diff --git a/src/App.tsx b/src/App.tsx index 3a5d6ff..fd375fb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { Route, Routes } from "react-router-dom"; import Home from "./pages/Home"; import CodeEditor from "./views/problem/codeeditor/CodeEditor"; import Statement from "./views/problem/statement/Statement"; +import ProblemStatement from "./views/problem/statement/Proble"; function App() { return ( @@ -15,7 +16,7 @@ function App() { }/> }/> }/> - }/> + }/> {/* }; + typesetPromise?: (elements?: Element[]) => Promise; + [key: string]: any; + }; + } +} + +export default function ProblemStatement() { + const containerRef = useRef(null); +const legend = "В честь юбилея ректорат ЮФУ решил запустить акцию <<Сто и десять кексов>>. \r\n\r\n $x$, $a_i^2 + b_i^2 \le a_{i+1}^2$ В каждом корпусе университета открылась лавка с кексами, в которой каждый студент может получить бесплатные кексы.\r\n\r\nНе прошло и пары минут после открытия, как к лавкам набежали студенты и образовалось много очередей. Но самая большая очередь образовалась в главном корпусе ЮФУ. Изначально в этой очереди стояло $n$ студентов, но потом в течение следующих $m$ минут какие-то студенты приходили и вставали в очередь, а какие-то уходили.\r\n\r\nЗа каждым студентом закреплен номер его зачетной книжки, будем называть это число номером студента. У каждого студента будет уникальный номер, по которому можно однозначно его идентифицировать. Будем считать, что каждую минуту происходило одно из следующих событий:\r\n\r\n\\begin{enumerate}\r\n \\item Студент с номером $x$ пришел и встал перед студентом с номером $y$;\r\n \\item Студент с номером $x$ пришел и встал в конец очереди;\r\n \\item Студент с номером $x$ ушел из очереди; возможно, он потом вернется.\r\n\\end{enumerate}\r\n\r\nАналитикам стало интересно, а какой будет очередь после $m$ минут? \r\n\r\nПомогите им и сообщите конечное состояние очереди.\r\n\r\n"; + const htmlContent = ` +
+
+
Очередь за кексами
+
+
ограничение по времени на тест
1 секунда +
+
+
ограничение по памяти на тест
256 мегабайт +
+
+
ввод
стандартный ввод +
+
+
вывод
стандартный вывод +
+
+
+

+ \\bf{test} + $$$x$$$, $$$a_i^2 + b_i^2 \\le a_{i+1}^2$$$ + Some complex formula: $$$P(|S - E[S]| \\ge t) \\le 2 \\exp \\left( -\\frac{2 t^2 n^2}{\\sum_{i = 1}^n (b_i - a_i)^2} \\right).$$$ + В честь юбилея ректорат ЮФУ решил запустить акцию «Сто и десять кексов». В каждом корпусе + университета открылась лавка с кексами, в которой каждый студент может получить бесплатные кексы.

+

Не прошло и пары минут после открытия, как к лавкам набежали студенты и образовалось много очередей. Но + самая большая очередь образовалась в главном корпусе ЮФУ. Изначально в этой очереди стояло $$$n$$$ + студентов, но потом в течение следующих $$$m$$$ минут какие-то студенты приходили и вставали в очередь, + а какие-то уходили.

+

За каждым студентом закреплен номер его зачетной книжки, будем называть это число номером студента. У + каждого студента будет уникальный номер, по которому можно однозначно его идентифицировать. Будем + считать, что каждую минуту происходило одно из следующих событий:

+

+
    +
  1. Студент с номером $$$x$$$ пришел и встал перед студентом с номером $$$y$$$;
  2. +
  3. Студент с номером $$$x$$$ пришел и встал в конец очереди;
  4. +
  5. Студент с номером $$$x$$$ ушел из очереди; возможно, он потом вернется.
  6. +
+

+

Аналитикам стало интересно, а какой будет очередь после $$$m$$$ минут?

+

Помогите им и сообщите конечное состояние очереди.

+
+

+

+
+
Входные данные
+

+

В первой строке заданы два целых числа $$$n$$$ и $$$m$$$ $$$(1 \\le n, m \\le 10^5)$$$ — текущее + число студентов в очереди и количество изменений.

+

В следующей строке задается $$$n$$$ целых различных чисел $$$a_1, + a_2, \\cdots , a_n$$$ $$$(1 \\le a_i \\le 10^9)$$$, где $$$a_i$$$ — номер студента, который + стоит на $$$i$$$-й позиции в очереди.

+

В следующих $$$m$$$ строках идет описание запросов изменения очереди.

+

В каждой строке в зависимости от типа запроса задается два или три числа. Первое число $$$t_j$$$ $$$(1 + \\le t_j \\le 3)$$$ — тип события, которое произошло в $$$j$$$-ю минуту.

+

Если $$$t_j = \\textbf{1}$$$, то в строке задается еще 2 числа $$$x$$$ $$$(1 \\le x_j \\le 10^9)$$$ и + $$$y$$$ $$$(1 \\le y_j \\le 10^9)$$$ — номер студента, который пришел, и номер студента, перед + которым он встанет в очереди. Гарантируется, что студент с номером $$$x$$$ ещё не занял очередь, а + студент с номером $$$y$$$ уже стоит в ней.

+

Если $$$t_j = \\textbf{2}$$$, то в строке задается еще 1 число $$$x$$$ $$$(1 \\le x_j \\le + 10^9)$$$ — номер студента, который пришел и встал в конец очереди. Гарантируется, что студент + с номером $$$x$$$ ещё не занял очередь.

+

Если $$$t_j = \\textbf{3}$$$, то в строке задается еще 1 число $$$x$$$ $$$(1 \\le x_j \\le + 10^9)$$$ — номер студента, который ушел из очереди. Гарантируется, что студент с номером + $$$x$$$ стоит в очереди.

+
+

+

+
+
Выходные данные
+

+

В первой строке выведите одно число $$$|a|$$$ — длину очереди после выполнения всех запросов + изменения.

+

В следующей строке выведите $$$|a|$$$ чисел $$$a_1, a_2, \\cdots , a_{|a|}$$$, где $$$a_i$$$ — + номер студента, который стоит на $$$i$$$-й позиции в очереди.

+
+

+

+
+
Пример
+

+

+
+
+
Входные данные
+
+
7 6
1 2 3 4 5 6 7
1 8 3
2 9
3 3
1 3 9
2 10
3 1
+
+
+
Выходные данные
+
+9
+2 8 4 5 6 7 3 9 10 
+
+
+
+
+

+

+
+
Примечание
+

+

Изначально очередь выглядит следующим образом:

+

+

В первую минуту приходит студент с номером 8 и встает перед студентом с номером 3.

+

+

Потом студент с номером 9 встает в конец очереди.

+

+

Студент с номером 3 уходит из очереди.

+

+

Потом он возвращается и становится перед студентом с номером 9.

+

+

После в конец очереди становится студент с номером 10.

+

+

И студент с номером 1 уходит из очереди.

+

+

После $$$m$$$ событий очередь имеет следующий вид:

+

+
+
+

+

+ + `; + + useEffect(() => { + // 1️⃣ Конфигурация MathJax + (window as any).MathJax = { + tex: { + inlineMath: [["$$$", "$$$"]], // наш формат + displayMath: [["$$$$$$", "$$$$$$"]], + processEscapes: true, + }, + options: { + skipHtmlTags: ["script", "noscript", "style", "textarea", "pre", "code"], + }, + startup: { + typeset: false, + }, + }; + + // 2️⃣ Подключаем MathJax + const script = document.createElement("script"); + script.src = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"; + script.async = true; + + script.onload = () => { + if (window.MathJax?.startup?.promise) { + window.MathJax.startup.promise.then(() => renderMath()); + } else { + renderMath(); + } + }; + + document.head.appendChild(script); + + // 3️⃣ Отрисовка формул + const renderMath = () => { + if (containerRef.current && window.MathJax?.typesetPromise) { + window.MathJax.typesetPromise([containerRef.current]).catch(console.error); + } + }; + + return () => { + script.remove(); + }; + }, []); + + return
; +} diff --git a/src/views/problem/statement/Statement.tsx b/src/views/problem/statement/Statement.tsx index 90a4ac6..16cd153 100644 --- a/src/views/problem/statement/Statement.tsx +++ b/src/views/problem/statement/Statement.tsx @@ -1,9 +1,37 @@ import React, { useState } from "react"; import { cn } from "../../../lib/cn"; - +// import FullLatexRenderer from "./FullLatexRenderer"; const Statement: React.FC = () => { +const data = { + "extraResources": { + "example.01.mu": "TVXzATcgNg0KMSAyIDMgNCA1IDYgNw0KMSA4IDMNCjIgOQ0KMyAzDQoxIDMgOQ0KMiAxMA0KMyAxDQo=" + }, + "scoring": null, + "notes": "Изначально очередь выглядит следующим образом:\r\n\r\n\\includegraphics{o1.png}\r\n\r\nВ первую минуту приходит студент с номером 8 и встает перед студентом с номером 3.\r\n\r\n\\includegraphics{o2.png}\r\n\r\nПотом студент с номером 9 встает в конец очереди.\r\n\r\n\\includegraphics{o3.png}\r\n\r\nСтудент с номером 3 уходит из очереди.\r\n\r\n\\includegraphics{o4.png}\r\n\r\nПотом он возвращается и становится перед студентом с номером 9.\r\n\r\n\\includegraphics{o5.png}\r\n\r\nПосле в конец очереди становится студент с номером 10.\r\n\r\n\\includegraphics{o6.png}\r\n\r\nИ студент с номером 1 уходит из очереди.\r\n\r\n\\includegraphics{o7.png}\r\n\r\nПосле $m$ событий очередь имеет следующий вид:\r\n\r\n\\includegraphics{o8.png}", + "legend": "В честь юбилея ректорат ЮФУ решил запустить акцию <<Сто и десять кексов>>. \r\n\r\n $x$, $a_i^2 + b_i^2 \le a_{i+1}^2$ В каждом корпусе университета открылась лавка с кексами, в которой каждый студент может получить бесплатные кексы.\r\n\r\nНе прошло и пары минут после открытия, как к лавкам набежали студенты и образовалось много очередей. Но самая большая очередь образовалась в главном корпусе ЮФУ. Изначально в этой очереди стояло $n$ студентов, но потом в течение следующих $m$ минут какие-то студенты приходили и вставали в очередь, а какие-то уходили.\r\n\r\nЗа каждым студентом закреплен номер его зачетной книжки, будем называть это число номером студента. У каждого студента будет уникальный номер, по которому можно однозначно его идентифицировать. Будем считать, что каждую минуту происходило одно из следующих событий:\r\n\r\n\\begin{enumerate}\r\n \\item Студент с номером $x$ пришел и встал перед студентом с номером $y$;\r\n \\item Студент с номером $x$ пришел и встал в конец очереди;\r\n \\item Студент с номером $x$ ушел из очереди; возможно, он потом вернется.\r\n\\end{enumerate}\r\n\r\nАналитикам стало интересно, а какой будет очередь после $m$ минут? \r\n\r\nПомогите им и сообщите конечное состояние очереди.\r\n\r\n", + "authorLogin": "valavshonok", + "language": "russian", + "timeLimit": 1000, + "output": "В первой строке выведите одно число $|a|$~--- длину очереди после выполнения всех запросов изменения.\r\n\r\nВ следующей строке выведите $|a|$ чисел $a_1, a_2, \\cdots , a_{|a|}$, где $a_i$~--- номер студента, который стоит на $i$-й позиции в очереди.", + "inputFile": "stdin", + "outputFile": "stdout", + "input": "В первой строке заданы два целых числа $n$ и $m$ $(1 \\le n, m \\le 10^5)$~--- текущее число студентов в очереди и количество изменений.\r\n\r\nВ следующей строке задается $n$ целых \\textbf{различных} чисел $a_1, a_2, \\cdots , a_n$ $(1 \\le a_i \\le 10^9)$, где $a_i$~--- номер студента, который стоит на $i$-й позиции в очереди.\r\n\r\nВ следующих $m$ строках идет описание запросов изменения очереди.\r\n\r\nВ каждой строке в зависимости от типа запроса задается два или три числа. Первое число $t_j$ $(1 \\le t_j \\le 3)$~--- тип события, которое произошло в $j$-ю минуту.\r\n\r\nЕсли $t_j = \\textbf{1}$, то в строке задается еще 2 числа $x$ $(1 \\le x_j \\le 10^9)$ и $y$ $(1 \\le y_j \\le 10^9)$~--- номер студента, который пришел, и номер студента, перед которым он встанет в очереди. Гарантируется, что студент с номером $x$ ещё не занял очередь, а студент с номером $y$ уже стоит в ней. \r\n\r\nЕсли $t_j = \\textbf{2}$, то в строке задается еще 1 число $x$ $(1 \\le x_j \\le 10^9)$~--- номер студента, который пришел и встал в конец очереди. Гарантируется, что студент с номером $x$ ещё не занял очередь.\r\n\r\nЕсли $t_j = \\textbf{3}$, то в строке задается еще 1 число $x$ $(1 \\le x_j \\le 10^9)$~--- номер студента, который ушел из очереди. Гарантируется, что студент с номером $x$ стоит в очереди.", + "authorName": "Виталий Лавшонок", + "sampleTests": [ + { + "output": "9\r\n2 8 4 5 6 7 3 9 10 \r\n", + "input": "7 6\r\n1 2 3 4 5 6 7\r\n1 8 3\r\n2 9\r\n3 3\r\n1 3 9\r\n2 10\r\n3 1\r\n", + "inputFile": "example.01", + "outputFile": "example.01.a" + } + ], + "name": "Очередь за кексами", + "interaction": null, + "memoryLimit": 268435456, + "tutorial": "Давайте просто промоделируем все действия.\r\n\r\nЗаведем список элементов, а также сохраним по ключу $x$ указатель на элемент списка. Мы можем это сделать, так как все элементы различны. Например, в С++ можно просто завести коллекцию list, а также map::iterator> или реализовать свой список.\r\n\r\nТеперь мы можем легко обрабатывать все запросы, а в конце просто выведем весь список.\r\n\r\nЗапрос 1-го типа можно обработать так: просто берем по ключу указатель на нужный элемент и вставляем перед ним другой элемент, останется только по ключу $x$ записать указатель на новый элемент.\r\n\r\nЗапрос 2-го типа~--- просто добавить в список элемент в конец и сохранить на него указатель.\r\n\r\nЗапрос 3-го типа~--- удаляем из списка элемент по его указателю.\r\n\r\nВ конце просто выводим массив.\r\n\r\nИтоговая сложность $O(mlog(n))$" +}; return (
@@ -24,14 +52,11 @@ const Statement: React.FC = () => {
- Три друга хотят встретиться друг с другом. Изначально первый друг находится в позиции x = a, второй друг находится в позиции x = b, а третий находится в позиции x = c на координатной оси Ox. -За одну минуту каждый из друзей независимо от других друзей может изменить позицию x на 11 влево или на 11 вправо (то есть присвоить x := x − 1 или x := x + 1), или даже не менять ее. -Введем понятие суммарной попарной дистанции — суммы дистанций между каждой парой друзей. Пусть a′, b′ и c′ — финальные позиции первого, второго и третьего друзей соответственно. Тогда суммарная попарная дистанция
равна |a′ − b′| + |a′ − c′| + |b′ − c′|, где |x| — абсолютная величина (модуль) значения x. -Друзья интересуются, какой минимальной суммарной попарной дистанции они смогут достичь, если они будут двигаться оптимально. Каждый из друзей сдвинется не более одного раза. Таким образом, более формально, они хотят знать минимальную суммарную попарную дистанцию, которой они могут достичь спустя одну минуту. + {/* */}
); }; -export default Statement; +export default Statement; \ No newline at end of file From 71985fa04fa91c1261d369c119d833589a6a3e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:15:29 +0300 Subject: [PATCH 02/10] add problems --- src/App.tsx | 17 +- src/assets/icons/problems/icon-error.svg | 3 + src/assets/icons/problems/icon-success.svg | 3 + src/assets/icons/problems/index.ts | 4 + src/pages/Home.tsx | 8 +- src/styles/index.css | 22 + src/views/home/menu/Menu.tsx | 6 +- src/views/home/problems/ProblemItem.tsx | 72 +++ src/views/home/problems/Problems.tsx | 503 +++++++++++++++++++++ 9 files changed, 625 insertions(+), 13 deletions(-) create mode 100644 src/assets/icons/problems/icon-error.svg create mode 100644 src/assets/icons/problems/icon-success.svg create mode 100644 src/assets/icons/problems/index.ts create mode 100644 src/views/home/problems/ProblemItem.tsx create mode 100644 src/views/home/problems/Problems.tsx diff --git a/src/App.tsx b/src/App.tsx index fd375fb..55f9dea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,13 +11,16 @@ import ProblemStatement from "./views/problem/statement/Proble"; function App() { return ( -
- - }/> -
}/> - }/> - }/> - +
+
+ + } /> +
} /> +
} /> + } /> + + + {/* + + diff --git a/src/assets/icons/problems/icon-success.svg b/src/assets/icons/problems/icon-success.svg new file mode 100644 index 0000000..343b6c1 --- /dev/null +++ b/src/assets/icons/problems/icon-success.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/problems/index.ts b/src/assets/icons/problems/index.ts new file mode 100644 index 0000000..30948a7 --- /dev/null +++ b/src/assets/icons/problems/index.ts @@ -0,0 +1,4 @@ +import IconSuccess from "./icon-success.svg" +import IconError from "./icon-error.svg" + +export {IconError, IconSuccess} \ No newline at end of file diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index cb7eb27..004a00a 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -6,6 +6,7 @@ import Menu from "../views/home/menu/Menu"; import { useAppDispatch, useAppSelector } from "../redux/hooks"; import { useEffect } from "react"; import { fetchWhoAmI } from "../redux/slices/auth"; +import Problems from "../views/home/problems/Problems"; const Home = () => { const name = useAppSelector((state) => state.auth.username); @@ -18,15 +19,16 @@ const Home = () => { return ( -
-
+
+
-
+
} /> } /> } /> + } /> diff --git a/src/styles/index.css b/src/styles/index.css index 6356eee..ecfc61b 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -7,6 +7,7 @@ /* outline: 1px solid green; */ } + :root { color-scheme: light dark; width: 100%; @@ -19,6 +20,7 @@ /* font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; */ font-weight: 400; line-height: 1.5; + background-color: var(--color-liquid-background); color: rgba(255, 255, 255, 0.87); } @@ -73,3 +75,23 @@ body { cursor: pointer; } + + + +html { + scrollbar-gutter: stable; + padding-left: 8px; +} +html::-webkit-scrollbar { + width: 8px; /* ширина вертикального */ +} +/* Трек (фон) */ +html::-webkit-scrollbar-track { + background: transparent; +} +/* Ползунок (thumb) */ +html::-webkit-scrollbar-thumb { + background-color: var(--color-liquid-lighter); + border-radius: 1000px; + cursor: pointer; +} \ No newline at end of file diff --git a/src/views/home/menu/Menu.tsx b/src/views/home/menu/Menu.tsx index 0e505a1..5616e2a 100644 --- a/src/views/home/menu/Menu.tsx +++ b/src/views/home/menu/Menu.tsx @@ -6,7 +6,7 @@ import { useAppSelector } from "../../../redux/hooks"; const Menu = () => { const menuItems = [ {text: "Главная", href: "/home", icon: Home, page: "home" }, - {text: "Задачи", href: "/home", icon: Clipboard, page: "clipboard" }, + {text: "Задачи", href: "/home/problems", icon: Clipboard, page: "clipboard" }, {text: "Статьи", href: "/home", icon: Openbook, page: "openbool" }, {text: "Группы", href: "/home", icon: Users, page: "users" }, {text: "Контесты", href: "/home", icon: Cup, page: "cup" }, @@ -15,8 +15,8 @@ const Menu = () => { const activePage = useAppSelector((state) => state.store.menu.activePage); return ( -
- +
+
{menuItems.map((v, i) => ( diff --git a/src/views/home/problems/ProblemItem.tsx b/src/views/home/problems/ProblemItem.tsx new file mode 100644 index 0000000..55ab2c5 --- /dev/null +++ b/src/views/home/problems/ProblemItem.tsx @@ -0,0 +1,72 @@ +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 ProblemItemProps { + id: number; + authorId: number; + name: string; + difficulty: "Easy" | "Medium" | "Hard"; + tags: string[]; + timeLimit: number; + memoryLimit: number; + createdAt: string; + updatedAt: string; + 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`; +} + +export function formatBytesToMB(bytes: number): string { + const megabytes = Math.floor(bytes / (1024 * 1024)); + return `${megabytes} МБ`; +} + +const ProblemItem: React.FC = ({ + id, authorId, name, difficulty, tags, timeLimit, memoryLimit, createdAt, updatedAt, type, status +}) => { + console.log(id); + return ( +
+
+ #{id} +
+
+ {name} +
+
+ стандартный ввод/вывод {formatMilliseconds(timeLimit)}, {formatBytesToMB(memoryLimit)} +
+
+ {difficulty} +
+
+ { + status == "error" && + } + { + status == "success" && + } +
+
+ ); +}; + +export default ProblemItem; diff --git a/src/views/home/problems/Problems.tsx b/src/views/home/problems/Problems.tsx new file mode 100644 index 0000000..1523049 --- /dev/null +++ b/src/views/home/problems/Problems.tsx @@ -0,0 +1,503 @@ +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"; + + +export interface Problem { + id: number; + authorId: number; + name: string; + difficulty: "Easy" | "Medium" | "Hard"; + tags: string[]; + timeLimit: number; + memoryLimit: number; + createdAt: string; + updatedAt: string; +} + + +const Problems = () => { + + const problems: Problem[] = [ + { + "id": 1, + "authorId": 1, + "name": "Todo List App", + "difficulty": "Easy", + "tags": ["react", "state", "list"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:13.000Z", + "updatedAt": "2025-10-28T13:23:13.000Z" + }, + { + "id": 2, + "authorId": 1, + "name": "Search Filter Component", + "difficulty": "Medium", + "tags": ["filter", "props", "hooks"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:14.000Z", + "updatedAt": "2025-10-28T13:23:14.000Z" + }, + { + "id": 3, + "authorId": 1, + "name": "User Card List", + "difficulty": "Easy", + "tags": ["components", "props", "array"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:15.000Z", + "updatedAt": "2025-10-28T13:23:15.000Z" + }, + { + "id": 4, + "authorId": 1, + "name": "Theme Switcher", + "difficulty": "Medium", + "tags": ["context", "theme", "hooks"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:16.000Z", + "updatedAt": "2025-10-28T13:23:16.000Z" + }, + { + "id": 5, + "authorId": 1, + "name": "Debounced Input", + "difficulty": "Hard", + "tags": ["debounce", "hooks", "events"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:17.000Z", + "updatedAt": "2025-10-28T13:23:17.000Z" + }, + { + "id": 6, + "authorId": 1, + "name": "Pagination Component", + "difficulty": "Medium", + "tags": ["pagination", "array", "state"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:18.000Z", + "updatedAt": "2025-10-28T13:23:18.000Z" + }, + { + "id": 7, + "authorId": 1, + "name": "Modal Window", + "difficulty": "Easy", + "tags": ["ui", "portal", "events"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:19.000Z", + "updatedAt": "2025-10-28T13:23:19.000Z" + }, + { + "id": 8, + "authorId": 1, + "name": "Form Validation", + "difficulty": "Hard", + "tags": ["form", "validation", "hooks"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:20.000Z", + "updatedAt": "2025-10-28T13:23:20.000Z" + }, + { + "id": 9, + "authorId": 1, + "name": "Countdown Timer", + "difficulty": "Medium", + "tags": ["timer", "hooks", "state"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:21.000Z", + "updatedAt": "2025-10-28T13:23:21.000Z" + }, + { + "id": 10, + "authorId": 1, + "name": "Drag And Drop List", + "difficulty": "Hard", + "tags": ["dragdrop", "array", "events"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:22.000Z", + "updatedAt": "2025-10-28T13:23:22.000Z" + }, + { + "id": 11, + "authorId": 1, + "name": "Custom Hook Use Fetch", + "difficulty": "Medium", + "tags": ["hook", "fetch", "async"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:23.000Z", + "updatedAt": "2025-10-28T13:23:23.000Z" + }, + { + "id": 12, + "authorId": 1, + "name": "Infinite Scroll", + "difficulty": "Hard", + "tags": ["scroll", "pagination", "api"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:24.000Z", + "updatedAt": "2025-10-28T13:23:24.000Z" + }, + { + "id": 13, + "authorId": 1, + "name": "Responsive Navbar", + "difficulty": "Easy", + "tags": ["css", "layout", "responsive"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:25.000Z", + "updatedAt": "2025-10-28T13:23:25.000Z" + }, + { + "id": 14, + "authorId": 1, + "name": "Accordion Component", + "difficulty": "Easy", + "tags": ["ui", "state", "events"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:26.000Z", + "updatedAt": "2025-10-28T13:23:26.000Z" + }, + { + "id": 15, + "authorId": 1, + "name": "File Upload Preview", + "difficulty": "Hard", + "tags": ["file", "events", "preview"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:27.000Z", + "updatedAt": "2025-10-28T13:23:27.000Z" + }, + { + "id": 16, + "authorId": 1, + "name": "Dark Mode Toggle", + "difficulty": "Easy", + "tags": ["theme", "context", "localStorage"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:28.000Z", + "updatedAt": "2025-10-28T13:23:28.000Z" + }, + { + "id": 17, + "authorId": 1, + "name": "Realtime Clock", + "difficulty": "Easy", + "tags": ["date", "state", "interval"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:29.000Z", + "updatedAt": "2025-10-28T13:23:29.000Z" + }, + { + "id": 18, + "authorId": 1, + "name": "Chart With Recharts", + "difficulty": "Medium", + "tags": ["chart", "data", "props"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:30.000Z", + "updatedAt": "2025-10-28T13:23:30.000Z" + }, + { + "id": 19, + "authorId": 1, + "name": "Router Navigation", + "difficulty": "Medium", + "tags": ["router", "navigation", "hooks"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:31.000Z", + "updatedAt": "2025-10-28T13:23:31.000Z" + }, + { + "id": 20, + "authorId": 1, + "name": "Data Table Sortable", + "difficulty": "Hard", + "tags": ["table", "sort", "filter"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:32.000Z", + "updatedAt": "2025-10-28T13:23:32.000Z" + }, + { + "id": 1, + "authorId": 1, + "name": "Todo List App", + "difficulty": "Easy", + "tags": ["react", "state", "list"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:13.000Z", + "updatedAt": "2025-10-28T13:23:13.000Z" + }, + { + "id": 2, + "authorId": 1, + "name": "Search Filter Component", + "difficulty": "Medium", + "tags": ["filter", "props", "hooks"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:14.000Z", + "updatedAt": "2025-10-28T13:23:14.000Z" + }, + { + "id": 3, + "authorId": 1, + "name": "User Card List", + "difficulty": "Easy", + "tags": ["components", "props", "array"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:15.000Z", + "updatedAt": "2025-10-28T13:23:15.000Z" + }, + { + "id": 4, + "authorId": 1, + "name": "Theme Switcher", + "difficulty": "Medium", + "tags": ["context", "theme", "hooks"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:16.000Z", + "updatedAt": "2025-10-28T13:23:16.000Z" + }, + { + "id": 5, + "authorId": 1, + "name": "Debounced Input", + "difficulty": "Hard", + "tags": ["debounce", "hooks", "events"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:17.000Z", + "updatedAt": "2025-10-28T13:23:17.000Z" + }, + { + "id": 6, + "authorId": 1, + "name": "Pagination Component", + "difficulty": "Medium", + "tags": ["pagination", "array", "state"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:18.000Z", + "updatedAt": "2025-10-28T13:23:18.000Z" + }, + { + "id": 7, + "authorId": 1, + "name": "Modal Window", + "difficulty": "Easy", + "tags": ["ui", "portal", "events"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:19.000Z", + "updatedAt": "2025-10-28T13:23:19.000Z" + }, + { + "id": 8, + "authorId": 1, + "name": "Form Validation", + "difficulty": "Hard", + "tags": ["form", "validation", "hooks"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:20.000Z", + "updatedAt": "2025-10-28T13:23:20.000Z" + }, + { + "id": 9, + "authorId": 1, + "name": "Countdown Timer", + "difficulty": "Medium", + "tags": ["timer", "hooks", "state"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:21.000Z", + "updatedAt": "2025-10-28T13:23:21.000Z" + }, + { + "id": 10, + "authorId": 1, + "name": "Drag And Drop List", + "difficulty": "Hard", + "tags": ["dragdrop", "array", "events"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:22.000Z", + "updatedAt": "2025-10-28T13:23:22.000Z" + }, + { + "id": 11, + "authorId": 1, + "name": "Custom Hook Use Fetch", + "difficulty": "Medium", + "tags": ["hook", "fetch", "async"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:23.000Z", + "updatedAt": "2025-10-28T13:23:23.000Z" + }, + { + "id": 12, + "authorId": 1, + "name": "Infinite Scroll", + "difficulty": "Hard", + "tags": ["scroll", "pagination", "api"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:24.000Z", + "updatedAt": "2025-10-28T13:23:24.000Z" + }, + { + "id": 13, + "authorId": 1, + "name": "Responsive Navbar", + "difficulty": "Easy", + "tags": ["css", "layout", "responsive"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:25.000Z", + "updatedAt": "2025-10-28T13:23:25.000Z" + }, + { + "id": 14, + "authorId": 1, + "name": "Accordion Component", + "difficulty": "Easy", + "tags": ["ui", "state", "events"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:26.000Z", + "updatedAt": "2025-10-28T13:23:26.000Z" + }, + { + "id": 15, + "authorId": 1, + "name": "File Upload Preview", + "difficulty": "Hard", + "tags": ["file", "events", "preview"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:27.000Z", + "updatedAt": "2025-10-28T13:23:27.000Z" + }, + { + "id": 16, + "authorId": 1, + "name": "Dark Mode Toggle", + "difficulty": "Easy", + "tags": ["theme", "context", "localStorage"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:28.000Z", + "updatedAt": "2025-10-28T13:23:28.000Z" + }, + { + "id": 17, + "authorId": 1, + "name": "Realtime Clock", + "difficulty": "Easy", + "tags": ["date", "state", "interval"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:29.000Z", + "updatedAt": "2025-10-28T13:23:29.000Z" + }, + { + "id": 18, + "authorId": 1, + "name": "Chart With Recharts", + "difficulty": "Medium", + "tags": ["chart", "data", "props"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:30.000Z", + "updatedAt": "2025-10-28T13:23:30.000Z" + }, + { + "id": 19, + "authorId": 1, + "name": "Router Navigation", + "difficulty": "Medium", + "tags": ["router", "navigation", "hooks"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:31.000Z", + "updatedAt": "2025-10-28T13:23:31.000Z" + }, + { + "id": 20, + "authorId": 1, + "name": "Data Table Sortable", + "difficulty": "Hard", + "tags": ["table", "sort", "filter"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:32.000Z", + "updatedAt": "2025-10-28T13:23:32.000Z" + } + ]; + + + return ( +
+
+ +
+
+ База задач +
+ {}} + text="Создать задачу" + className="absolute right-0" + /> +
+ +
+ +
+ +
+ + {problems.map((v, i) => ( + + ))} +
+ + +
+ pages +
+
+
+ ); +}; + +export default Problems; From 11c41985c75b0c3c7783afe346520bba3482a09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:19:01 +0300 Subject: [PATCH 03/10] fix --- src/pages/Home.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 004a00a..f2a5f82 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -19,11 +19,11 @@ const Home = () => { return ( -
-
+
+
-
+
} /> } /> From 34a404f147cb30a9582fff1aba0db8c625a4ecef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:37:26 +0300 Subject: [PATCH 04/10] articles --- src/pages/Home.tsx | 10 ++- src/views/home/articles/ArticleItem.tsx | 50 +++++++++++ src/views/home/articles/Articles.tsx | 107 ++++++++++++++++++++++++ src/views/home/menu/Menu.tsx | 2 +- src/views/home/problems/Problems.tsx | 4 +- 5 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 src/views/home/articles/ArticleItem.tsx create mode 100644 src/views/home/articles/Articles.tsx diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index f2a5f82..448e0d1 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -7,6 +7,7 @@ import { useAppDispatch, useAppSelector } from "../redux/hooks"; import { useEffect } from "react"; import { fetchWhoAmI } from "../redux/slices/auth"; import Problems from "../views/home/problems/Problems"; +import Articles from "../views/home/articles/Articles"; const Home = () => { const name = useAppSelector((state) => state.auth.username); @@ -19,7 +20,7 @@ const Home = () => { return ( -
+
@@ -29,10 +30,15 @@ const Home = () => { } /> } /> } /> + } /> -
+ { + +
} /> +
+ }
); }; diff --git a/src/views/home/articles/ArticleItem.tsx b/src/views/home/articles/ArticleItem.tsx new file mode 100644 index 0000000..838daf2 --- /dev/null +++ b/src/views/home/articles/ArticleItem.tsx @@ -0,0 +1,50 @@ +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; + authorId: number; + name: string; + tags: string[]; + createdAt: string; + updatedAt: 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, authorId, name, tags, createdAt, updatedAt +}) => { + console.log(id); + return ( +
+
+ #{id} +
+
+ {name} +
+
+ {/* стандартный ввод/вывод {formatMilliseconds(timeLimit)}, {formatBytesToMB(memoryLimit)} */} +
+ +
+ ); +}; + +export default ArticleItem; diff --git a/src/views/home/articles/Articles.tsx b/src/views/home/articles/Articles.tsx new file mode 100644 index 0000000..618e678 --- /dev/null +++ b/src/views/home/articles/Articles.tsx @@ -0,0 +1,107 @@ +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"; + + +export interface Problem { + id: number; + authorId: number; + name: string; + difficulty: "Easy" | "Medium" | "Hard"; + tags: string[]; + timeLimit: number; + memoryLimit: number; + createdAt: string; + updatedAt: string; +} + + +const Articles = () => { + + const problems: Problem[] = [ + { + "id": 1, + "authorId": 1, + "name": "Todo List App", + "difficulty": "Easy", + "tags": ["react", "state", "list"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:13.000Z", + "updatedAt": "2025-10-28T13:23:13.000Z" + }, + { + "id": 2, + "authorId": 1, + "name": "Search Filter Component", + "difficulty": "Medium", + "tags": ["filter", "props", "hooks"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:14.000Z", + "updatedAt": "2025-10-28T13:23:14.000Z" + }, + { + "id": 3, + "authorId": 1, + "name": "User Card List", + "difficulty": "Easy", + "tags": ["components", "props", "array"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:15.000Z", + "updatedAt": "2025-10-28T13:23:15.000Z" + }, + { + "id": 4, + "authorId": 1, + "name": "Theme Switcher", + "difficulty": "Medium", + "tags": ["context", "theme", "hooks"], + "timeLimit": 1000, + "memoryLimit": 268435456, + "createdAt": "2025-10-28T13:23:16.000Z", + "updatedAt": "2025-10-28T13:23:16.000Z" + } + ]; + + + return ( +
+
+ +
+
+ База статей +
+ {}} + text="Создать статью" + className="absolute right-0" + /> +
+ +
+ +
+ +
+ + {/* {problems.map((v, i) => ( + + ))} */} +
+ + +
+ pages +
+
+
+ ); +}; + +export default Articles; diff --git a/src/views/home/menu/Menu.tsx b/src/views/home/menu/Menu.tsx index 5616e2a..f2ccfea 100644 --- a/src/views/home/menu/Menu.tsx +++ b/src/views/home/menu/Menu.tsx @@ -7,7 +7,7 @@ const Menu = () => { const menuItems = [ {text: "Главная", href: "/home", icon: Home, page: "home" }, {text: "Задачи", href: "/home/problems", icon: Clipboard, page: "clipboard" }, - {text: "Статьи", href: "/home", icon: Openbook, page: "openbool" }, + {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/account", icon: Account, page: "account" }, diff --git a/src/views/home/problems/Problems.tsx b/src/views/home/problems/Problems.tsx index 1523049..9507a26 100644 --- a/src/views/home/problems/Problems.tsx +++ b/src/views/home/problems/Problems.tsx @@ -466,8 +466,8 @@ const Problems = () => { return ( -
-
+
+
From 5ef7933446d1af3f69de20e23ea36b128277244d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:37:37 +0300 Subject: [PATCH 05/10] Articles --- src/views/home/articles/ArticleItem.tsx | 45 ++++---- src/views/home/articles/Articles.tsx | 138 +++++++++++++++++------- 2 files changed, 126 insertions(+), 57 deletions(-) diff --git a/src/views/home/articles/ArticleItem.tsx b/src/views/home/articles/ArticleItem.tsx index 838daf2..680b031 100644 --- a/src/views/home/articles/ArticleItem.tsx +++ b/src/views/home/articles/ArticleItem.tsx @@ -6,43 +6,50 @@ import { IconError, IconSuccess } from "../../../assets/icons/problems"; export interface ArticleItemProps { id: number; - authorId: number; name: string; tags: string[]; - createdAt: string; - updatedAt: string; } 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 ArticleItem: React.FC = ({ - id, authorId, name, tags, createdAt, updatedAt + id, name, tags }) => { console.log(id); return ( -
-
- #{id} +
+ +
+ #{id} +
+
+ {name} +
-
- {name} +
+ {tags.map((v, i) => +
+ {v} +
+ )}
-
- {/* стандартный ввод/вывод {formatMilliseconds(timeLimit)}, {formatBytesToMB(memoryLimit)} */} -
- +
); }; diff --git a/src/views/home/articles/Articles.tsx b/src/views/home/articles/Articles.tsx index 618e678..b37a8a5 100644 --- a/src/views/home/articles/Articles.tsx +++ b/src/views/home/articles/Articles.tsx @@ -1,9 +1,5 @@ -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 ArticleItem, { ArticleItemProps } from "./ArticleItem"; export interface Problem { @@ -21,50 +17,116 @@ export interface Problem { const Articles = () => { - const problems: Problem[] = [ + const articles: ArticleItemProps[] = [ { "id": 1, - "authorId": 1, "name": "Todo List App", - "difficulty": "Easy", - "tags": ["react", "state", "list"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:13.000Z", - "updatedAt": "2025-10-28T13:23:13.000Z" + "tags": ["Sertificated", "state", "list"], }, { "id": 2, - "authorId": 1, "name": "Search Filter Component", - "difficulty": "Medium", "tags": ["filter", "props", "hooks"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:14.000Z", - "updatedAt": "2025-10-28T13:23:14.000Z" }, { "id": 3, - "authorId": 1, "name": "User Card List", - "difficulty": "Easy", "tags": ["components", "props", "array"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:15.000Z", - "updatedAt": "2025-10-28T13:23:15.000Z" }, { "id": 4, - "authorId": 1, "name": "Theme Switcher", - "difficulty": "Medium", - "tags": ["context", "theme", "hooks"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:16.000Z", - "updatedAt": "2025-10-28T13:23:16.000Z" + "tags": ["Sertificated", "theme", "hooks"], + }, + { + "id": 2, + "name": "Search Filter Component", + "tags": ["filter", "props", "hooks"], + }, + { + "id": 3, + "name": "User Card List", + "tags": ["components", "props", "array"], + }, + { + "id": 4, + "name": "Theme Switcher", + "tags": ["Sertificated", "theme", "hooks"], + }, + { + "id": 2, + "name": "Search Filter Component", + "tags": ["filter", "props", "hooks"], + }, + { + "id": 3, + "name": "User Card List", + "tags": ["components", "props", "array"], + }, + { + "id": 4, + "name": "Theme Switcher", + "tags": ["Sertificated", "theme", "hooks"], + }, + { + "id": 2, + "name": "Search Filter Component", + "tags": ["filter", "props", "hooks"], + }, + { + "id": 3, + "name": "User Card List", + "tags": ["components", "props", "array"], + }, + { + "id": 4, + "name": "Theme Switcher", + "tags": ["Sertificated", "theme", "hooks"], + }, + { + "id": 2, + "name": "Search Filter Component", + "tags": ["filter", "props", "hooks"], + }, + { + "id": 3, + "name": "User Card List", + "tags": ["components", "props", "array"], + }, + { + "id": 4, + "name": "Theme Switcher", + "tags": ["Sertificated", "theme", "hooks"], + }, + { + "id": 2, + "name": "Search Filter Component", + "tags": ["filter", "props", "hooks"], + }, + { + "id": 3, + "name": "User Card List", + "tags": ["components", "props", "array"], + }, + { + "id": 4, + "name": "Theme Switcher", + "tags": ["Sertificated", "theme", "hooks"], + }, + { + "id": 2, + "name": "Search Filter Component", + "tags": ["filter", "props", "hooks"], + }, + { + "id": 3, + "name": "User Card List", + "tags": ["components", "props", "array"], + }, + { + "id": 4, + "name": "Theme Switcher", + "tags": ["Sertificated", "theme", "hooks"], } ]; @@ -77,8 +139,8 @@ const Articles = () => {
База статей
- {}} + { }} text="Создать статью" className="absolute right-0" /> @@ -90,12 +152,12 @@ const Articles = () => {
- {/* {problems.map((v, i) => ( - - ))} */} + {articles.map((v, i) => ( + + ))}
- +
pages
From 99018537c59adba38bb3de261c3c8d7cd4626b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Thu, 30 Oct 2025 20:43:01 +0300 Subject: [PATCH 06/10] pages --- src/assets/icons/groups/book.png | Bin 0 -> 5264 bytes src/assets/icons/groups/chevron-down.svg | 3 + src/assets/icons/groups/edit.svg | 3 + src/assets/icons/groups/eye-closed.svg | 3 + src/assets/icons/groups/eye-open.png | Bin 0 -> 662 bytes src/assets/icons/groups/index.ts | 8 ++ src/assets/icons/groups/user-profile-add.svg | 3 + src/pages/Home.tsx | 4 + src/views/home/articles/ArticleItem.tsx | 15 --- src/views/home/articles/Articles.tsx | 22 ++-- src/views/home/auth/Login.tsx | 9 +- src/views/home/auth/Register.tsx | 6 +- src/views/home/contests/ContestItem.tsx | 72 ++++++++++ src/views/home/contests/Contests.tsx | 131 +++++++++++++++++++ src/views/home/contests/ContestsBlock.tsx | 64 +++++++++ src/views/home/groups/GroupItem.tsx | 59 +++++++++ src/views/home/groups/Groups.tsx | 71 ++++++++++ src/views/home/groups/GroupsBlock.tsx | 60 +++++++++ src/views/home/menu/Menu.tsx | 8 +- src/views/home/problems/ProblemItem.tsx | 5 +- src/views/home/problems/Problems.tsx | 14 +- 21 files changed, 518 insertions(+), 42 deletions(-) create mode 100644 src/assets/icons/groups/book.png create mode 100644 src/assets/icons/groups/chevron-down.svg create mode 100644 src/assets/icons/groups/edit.svg create mode 100644 src/assets/icons/groups/eye-closed.svg create mode 100644 src/assets/icons/groups/eye-open.png create mode 100644 src/assets/icons/groups/index.ts create mode 100644 src/assets/icons/groups/user-profile-add.svg create mode 100644 src/views/home/contests/ContestItem.tsx create mode 100644 src/views/home/contests/Contests.tsx create mode 100644 src/views/home/contests/ContestsBlock.tsx create mode 100644 src/views/home/groups/GroupItem.tsx create mode 100644 src/views/home/groups/Groups.tsx create mode 100644 src/views/home/groups/GroupsBlock.tsx diff --git a/src/assets/icons/groups/book.png b/src/assets/icons/groups/book.png new file mode 100644 index 0000000000000000000000000000000000000000..260af3b44ee9a945c3fb47b7bf32eff6f393616f GIT binary patch literal 5264 zcmeAS@N?(olHy`uVBq!ia0y~yU`PRB4mJh`hJr^^Ll_tsI14-?iy0WWg+Z8+Vb&Z8 z1_lPk;vjb?hIQv;UNSH+u%tWsIx;Y9?C1WI$jZPVqU!157*fIbHiol8By`^Yz2UXx zmt>0^4owu`5R!N*aM1O-V|htTiA%_pR$0mSN16Kr?d@WZUzPo{r%dq3+`nmmL()FF zxU!169bPG;^nD>?k+ajg)w?I#t$KBK^{RIzrIR_%O!t%8_p3I3^1AT#t5)B8|M&fO z1|A86gaZwX%xu^gFPu3ntR1Q|)E}O=Y~K7V_1*0KKW}k9){j&4>=CpKiK~Bf;`r0h zX#1FLZw?;*V=8e@a1L(^$K~bz=69=JhyVL@D$H|%n$|{vg-6<^h)BoeDQ!2oDyO4= z`>$i+$7NHc&Gyu^#1~%^DhcyA;k`a&{ny`@)+sf4gg#v=o#%8wh4Vt(Lq)%vv0(>r35u+#g!-;XYQd`y2uPS%$bEk>(sHyX`T zdG=7*%}q^7y+dI2)y^v++-KA}-p&*LJzJVz_x{e$uk{~uFl{mPkZPJVQ=oK#nEprE zj7+{hQI=Cnoh8jZZ9O?#|9w>c5|HO75v=OodZu5(Uft7d(!@l^C@D_UE9P&sweSAS z^E`R&YW6i9ovZ)leo3D6>G<$w^KA8*y5jZvopWmzurRHfaPX_}bap2{HD^ZABww4$ ze?wOU{yWj*Ah1To_AXQR0nSycZ(n|Q`}e_Qx%x_?ge2_;;HH9jC5S@bXgq^CS3TkjjN^0V?7Z z0v7kH?ycbT-x%O==}HgVt;-9zHmZkMFTB?5h4 zODAb4s+{sD4u120Wpj_z#^-xKL`rfx83`_8Y1|p+{cWq7qnb~xQRMedrDJD#>s!_p z>)eU|IOXY%?1&%LCVnrQ1h&Zwp6Ho;FeFo0RWR+Ky8X8!SNo6JxG|}yMsftk`N)~S zYYd(D%`GpGby07`kx14>&7Mhmmzr0Vw5(VBm7QEYIk;oC7t?eeLk&gc%U$yO@{b?p zkIGDTwzRE@ShG9(+&|aLdX;aKTx?#(si;MAEZlzg+cVciFC@+@@9x;wIgh75q2`RB zrAqAIUG;P4D+gxxY<9kLdeV+y?GJmr6OZ>4iK`>fcT{r_Ig5-;EL z%_{lw^8IE^dovzPZWMSbxkS?VeazK6hwjF>nN0V*b}z6v_VEQ~$u+YJ6|GjUlluMc zNb{>)?tXU{Cb@qJ34N^5X6BC;vcJv92xtu}ou=;3b>Q(wucfIU)<%8~(5wA*>UaHtshM*!}PI&ea!BzsPr5!pi;4E(O-k@;k1_VBwx|F*`JHvXP0pz5vA zBW3$fvZJoE>Bx=Nyq$sRrE5-WzsU88xioRk!PlCZGX>0_{9`akYGHkS?Rw(9Ju)Vy zr&pEm$eh^lM&BW?QRVlo+SJ+C?tau25?K3PUGdSQ@NBzvYd#gG&tL8_C34+9xxnLp zL-M66Kj=C8dCb{ zQ*wH{>t?@?%=PBKx&^HMZc^Xu^K8z{v^d4^=#H@02{Gr(j|yeIW$w7aS1YjM*kcvF zWQpj%7wS5WER7YpR3X&xVqNH)?nj}ll__xsm-@cWo?USKt!74EpwKQQwzR^R71k|H zflAu?*_JzspTDuSXfKM)5~%pf+g~4g|ALZn>YPxcCm)z!UiCZQ@9yFMt!?hKRhM?3 z*dy1pKqa|g8E^IX?T?m8O5Nm|*wmSLJx^}>0o7`o%#!cx#Pgh*mN37NcY4vpD&NC@ z@?E!2$OSd6i_i9@t^eA;M7qT5QS>g>&a-Osc#b`~6EpAl(|Mr^9uwxPCmjF&^~c-& z%NAT!UTHLQs^f=qMcQt?No)3OsrtRI_W$Gyp}Muc9a1cOQbl*4+pQ13wye^GOMTal z<|7jYPX?}d{%EOkTE!R1M|meYHFtPV*qOlQr1YsRa$~Bybl&Xj$FICYHZzB%qe(34ldGnsumbW$}9(E16enouh zoA@djP8H``pD(gk6bfD)sgHY@{aH)-dU9sdi@*2tS`?Q>-I(ffCABiy?&9ua_XDO> zRN0;g2zcOfahKbj>T<1HHjJfwH*$II|CyR}!eI85fZ2}rzca#A-dUt`OpL2o_-5m& z$8-HQcTT!7VpSbMho|7iDp7-~HNINfg%j^q&)+4U z$m!m6F}$VYylwE;u*^(pdFRxNPA2-I+L9ZS7Kt~U5WP4#aI5Qra<(cDrC0ow*9H71 z8q5py^Hly&+Gg-pUiQxAOF8>ax@_F$(PliAgPs4+hbY&hZuYyvyj8?%9XaM2B&OYZ zpm_M*f{<%Xyqb&Ni(WTc^!@SI{dYt;gsysAmhJC+UmVVr?o@N(WqQ9a`{wfFt8S<0 z$!=LKI%}4qq0xfhUYbdZTX*bMs_I|&zGcpuV2?F9D=!_i)jnlwoRqd^hS8Y^6{|%a~>X&&&$f4Qsk30^H2JwZNGLfU0i?B zHGfI_YNd7So~?BaR=;~wP&M~xsL=N{Zla%dJf19druxFs*N^WMK8V|4-TlBnUns`v z>JhI#OX+?89$hLF^j>p=85b3pUrmft{>Br@9|BWFvIQI)pbjcCkyv%dCg;~ zsr6DXbP@A&O_3=RpG^vgdv)-NwvR{o$9RQXUh7tQE!cdxwJl0p^Y6#C(iikd3x((J_3@1{)+=J$*eU!|O|uKN_SHn#eD`HSX`J?dY~6jyay z)ck1e*5LJea%Ro;t`+}9dXIcvE#RNMaZ8@ii94%>D)(vGHa}c`Sa!!dDXz((0n`6R zO}X?&acR|yl&4R_qQXAaId%IlZ@#DI618Q;qo-}h7y_VcjpY^J*u>F|MiA_`YZ;|xd&FT1Vlh)U$&f_dj{oZ!FGhZlP z>o(hb!_V;2!N+`Y>gP{wO1OP4?99%qOXRd2mvl}~EB|~c$E#{t&6+D--``beY${k7 zpX1K5S4&Y+vOg{+=w{2g1+(LdJca+I`e_=mg)Q8-lk4~@>xa4G35#q}!iD`l3Qu$q zo7=*#%y{}_zeM&G&n@RXw{c8{)cSvjwpePZI;j9(w0#Q(MFp8aam z;vX5z>vnk^OPzkRayHBC*2MRAU30BvHtElBx?i9EZo{NW<=Gd`Us||e`)3my{=zp` z4O@%nl`Z-89&Jb7%qu-UTRfD6=WO{BvQgG%uN+qp&k2@qZ|obF&kvdQ z)gYAbvT=$|WXi!8PFnT*SLc0x)?2RYI%&an&O}ks>d5HZozL5OcYgckcyrs9Psc(u zmuHCU`e;98PHo@$$>NUmHpR^w<<{NrJ-k$ScHuwP+znB;F5GzH{+6?Idf-`u*_MuC zxvsmVPHEhpAsF{iE-u7fqEl>D$*V3+Zt;q_Ho9l~-j~lgaQAz}1HY{w%H|y}G7u5f zyl(w;@zk=1?YZ3Q*RJL9D{)?#+T@b+Yf9PMU-lo)f3SXc>*J1-razx%U%vLia{jV+ zbEoK*O_(BZ?cd?AJ60@~b$n{CHBpnxoxSj33xoXBvMV9M?-tz(2z=PE{BGNoDI!aj zb3ce7=L%O1gBge)PW_R2k(Gbamm?Q{DdAIvO(f zJ?F&TXRrRQm*>h9^8J3di(SVq^~#TTejl@)W1TQxhG*qFeeSA_cg#?cLT80PO)Di7ogHx+tg#u#QdJNg#KsZ$(X{L2^YyLOQMRwH9(*p8qr{}~)i zjlE)B@0J|BlCVknz)Sy)$*;F1|M>e@>e=RV&oNwEw{_97bDCGL?P!@gzh~3b zDN|fFPw{MU$?Fb_|Flcwv+$j*jK02K+4shE9~D!|zNNQTPe;MIa|T=g_4E5~PwKqz zOTg3rvr>MA^(XgjhbLZId5fWO`9Cf{PN9axpDrt*DLL!Pa8%)5$t3%cS3NLD-u68V~*yhFFB`$<$O=ZcuSPAeC6jqcTPHA?9ZLclLUAQmOgzr z)kf;jf<(Ev^&b)==JA`Q3j|HLZpaijunYm_ZI!5O|wbf#oj z&aH^1>2W0oo^D%ZcH-%$$CvrnW_+<&dMY_D@ubq#7cc&$H$HJ##JRA3-^XK)|F;!A z<@^_W!!-ZimP1VwPtWbQH)QEx6E?qJvpFF2>+6#{pZD&lf3w1SIro`ancIF?GBzAn zE)Jhso%&Ml$+PN-=k1x7Wo}{DF4tf#H!q{{%Agr&msqJ+j~pgPUl@#M#%+ITGs#r&C;&O+kP0MXvn(B-Ti*HZ1-opHo)%V1?cOk_wva(aW zJGq(JpB0pEXa4{5cnFVF*0E>2@9*y1_hR1miAy}B%HO98DBE_uJt=4O@72cSyxr*m zeg{K#M?UfN$*XQM`dlHhUfYF@|6{@t4*5;&<_V{sG)DZ%Qv2TI8a;-@U z=~so%u{qU=O;*$0k#J#x=k{LBJvH0zPBgu8gvnrb)0yMNVY0GD-|y_$&ZfO%+u?o9 z@%KJ_6)z4`^gJ0*uuWJf*lLc+ORsXjb8F&vT~@zuZj*n;Wcl4!4Qq7V-%Ri~xh*wq z->WljLf*%BwoSgq?mNRE{|B%6nwSqK7Mp&(9{>Ml>WlVJ6}CkR%WtOP9DYL|nLC&G ahgXa7yuZ)%g?$VR3=E#GelF{r5}E)5DDU0? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eee56902b013ac2b35bc2d8edcddd0e9e148884e GIT binary patch literal 662 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4mJh`h6m-gKNuJoI14-?iy0WWg+Z8+Vb&aw z3dZ6fcPEB*=VV?oFfg#BJNh~@Fl_AS{*%bcz`&&K>Eakt!T2`hcH!j!fxCOV=Ke5E zc0R;YzldoHp9|{@J|ET#j!x_Wj0>4AIZoj+5RIrY6tRg7H3twa>%=sJl{>yx>GtDpd zI$1CsQOs$d{o={t&2|5utmpE$-h6lU`kW1$^IpoB&Jmhaz4%JVwXzCzEk$n8eT!?t z-@K2BKYaW7;y!NQA1{wTOBFf)L$UwKcBW8``HXWjndAik`vTSITm`?l-arJ-L26lRE8huaKi6vU-c}}!> zw!=z0@aEk$VGj+jJj`n_b#+k<$`E7`o^RGZTlIX>Qo#(T$gG_r>IM8yVy5t?hdzo3 zZ`t|JXG*LnPx$12hbEVD6wZ`5cFAK>q7{4U9=&xUEOV4D&E~zAcsf;GMw`|BXzXPc zNi)W}pu2nfmHz~XMz7Cql#%mOs@fj0;?Q23Ia8MDBp!e8dY9fuWjVe&W^ + + 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 = () => {
- База задач + Задачи
{}} From 59f89d51135f32da84417b3c7a4a593feedb043b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Sun, 2 Nov 2025 13:15:12 +0300 Subject: [PATCH 07/10] upload mission --- src/App.tsx | 97 +----------- .../{problems => missions}/icon-error.svg | 0 .../{problems => missions}/icon-success.svg | 0 .../icons/{problems => missions}/index.ts | 0 src/pages/Home.tsx | 4 +- src/pages/Mission.tsx | 26 ++++ src/redux/slices/auth.ts | 10 +- src/redux/slices/missions.ts | 146 ++++++++++++++++++ src/redux/store.ts | 2 + src/views/home/auth/Login.tsx | 7 +- src/views/home/auth/Register.tsx | 5 +- src/views/home/menu/Menu.tsx | 3 +- .../MissionItem.tsx} | 8 +- .../Problems.tsx => missions/Missions.tsx} | 16 +- src/views/mission/UploadMissionForm.tsx | 101 ++++++++++++ .../codeeditor/CodeEditor.tsx | 0 .../statement/Mission.tsx} | 2 +- .../statement/Statement.tsx | 0 18 files changed, 312 insertions(+), 115 deletions(-) rename src/assets/icons/{problems => missions}/icon-error.svg (100%) rename src/assets/icons/{problems => missions}/icon-success.svg (100%) rename src/assets/icons/{problems => missions}/index.ts (100%) create mode 100644 src/pages/Mission.tsx create mode 100644 src/redux/slices/missions.ts rename src/views/home/{problems/ProblemItem.tsx => missions/MissionItem.tsx} (92%) rename src/views/home/{problems/Problems.tsx => missions/Missions.tsx} (98%) create mode 100644 src/views/mission/UploadMissionForm.tsx rename src/views/{problem => mission}/codeeditor/CodeEditor.tsx (100%) rename src/views/{problem/statement/Proble.tsx => mission/statement/Mission.tsx} (99%) rename src/views/{problem => mission}/statement/Statement.tsx (100%) diff --git a/src/App.tsx b/src/App.tsx index 55f9dea..474940a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,9 +5,11 @@ import { Route, Routes } from "react-router-dom"; // import { Input } from "./components/input/Input"; // import { Switch } from "./components/switch/Switch"; import Home from "./pages/Home"; -import CodeEditor from "./views/problem/codeeditor/CodeEditor"; -import Statement from "./views/problem/statement/Statement"; -import ProblemStatement from "./views/problem/statement/Proble"; +import CodeEditor from "./views/mission/codeeditor/CodeEditor"; +import Statement from "./views/mission/statement/Statement"; +import MissionStatement from "./views/mission/statement/Mission"; +import Mission from "./pages/Mission"; +import UploadMissionForm from "./views/mission/UploadMissionForm"; function App() { return ( @@ -15,97 +17,14 @@ function App() {
} /> + } /> + }/>
} />
} /> - } /> + } />
- - {/* { - document.documentElement.setAttribute( - "data-theme", - state ? "dark" : "light" - ); - }} - /> -
- { - console.log(state); - }} - /> - { - console.log(state); - }} - /> - { - console.log(state); - }} - /> - - { - console.log(state); - }} - /> - { }} label="test" color="default" defaultState={true}/> - { }} label="test" color="primary" defaultState={true}/> - { }} label="test" color="secondary" defaultState={true}/> - { }} label="test" color="success" defaultState={true}/> - { }} label="test" color="warning" defaultState={true}/> - { }} label="test" color="danger" defaultState={true}/> - { }} color="default" defaultState={true}/> - { }} color="primary" defaultState={true}/> - { }} color="secondary" defaultState={true}/> - { }} color="success" defaultState={true}/> - { }} color="warning" defaultState={true}/> - { }} color="danger" defaultState={true}/> - - -
- - { }} text="Button" className="m-5" /> - { }} text="Button" className="m-5" /> - { }} text="Button" disabled className="m-5" /> - { }} text="Button" className="m-5" /> - { }} text="Button" className="m-5" /> - { }} text="Button" disabled className="m-5" /> -
-
-
*/}
); } diff --git a/src/assets/icons/problems/icon-error.svg b/src/assets/icons/missions/icon-error.svg similarity index 100% rename from src/assets/icons/problems/icon-error.svg rename to src/assets/icons/missions/icon-error.svg diff --git a/src/assets/icons/problems/icon-success.svg b/src/assets/icons/missions/icon-success.svg similarity index 100% rename from src/assets/icons/problems/icon-success.svg rename to src/assets/icons/missions/icon-success.svg diff --git a/src/assets/icons/problems/index.ts b/src/assets/icons/missions/index.ts similarity index 100% rename from src/assets/icons/problems/index.ts rename to src/assets/icons/missions/index.ts diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 1cc8c5a..4f41306 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -6,7 +6,7 @@ import Menu from "../views/home/menu/Menu"; import { useAppDispatch, useAppSelector } from "../redux/hooks"; import { useEffect } from "react"; import { fetchWhoAmI } from "../redux/slices/auth"; -import Problems from "../views/home/problems/Problems"; +import Missions from "../views/home/missions/Missions"; import Articles from "../views/home/articles/Articles"; import Groups from "../views/home/groups/Groups"; import Contests from "../views/home/contests/Contests"; @@ -31,7 +31,7 @@ const Home = () => { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/pages/Mission.tsx b/src/pages/Mission.tsx new file mode 100644 index 0000000..751f89c --- /dev/null +++ b/src/pages/Mission.tsx @@ -0,0 +1,26 @@ +import { useParams, Navigate } from 'react-router-dom'; + + + +const Mission = () => { + + // Получаем параметры из URL + const { missionId } = useParams<{ missionId: string }>(); + + // Если missionId нет, редиректим на /home + if (!missionId) { + return ; + } + + + return ( +
+ +
+ {missionId} +
+
+ ); +}; + +export default Mission; diff --git a/src/redux/slices/auth.ts b/src/redux/slices/auth.ts index ba9e681..cd40719 100644 --- a/src/redux/slices/auth.ts +++ b/src/redux/slices/auth.ts @@ -6,7 +6,7 @@ interface AuthState { jwt: string | null; refreshToken: string | null; username: string | null; - status: "idle" | "loading" | "succeeded" | "failed"; + status: "idle" | "loading" | "successful" | "failed"; error: string | null; } @@ -97,7 +97,7 @@ const authSlice = createSlice({ state.error = null; }); builder.addCase(registerUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => { - state.status = "succeeded"; + state.status = "successful"; axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`; state.jwt = action.payload.jwt; state.refreshToken = action.payload.refreshToken; @@ -113,7 +113,7 @@ const authSlice = createSlice({ state.error = null; }); builder.addCase(loginUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => { - state.status = "succeeded"; + state.status = "successful"; axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`; state.jwt = action.payload.jwt; state.refreshToken = action.payload.refreshToken; @@ -129,7 +129,7 @@ const authSlice = createSlice({ state.error = null; }); builder.addCase(refreshToken.fulfilled, (state, action: PayloadAction<{ username: string }>) => { - state.status = "succeeded"; + state.status = "successful"; state.username = action.payload.username; }); builder.addCase(refreshToken.rejected, (state, action: PayloadAction) => { @@ -143,7 +143,7 @@ const authSlice = createSlice({ state.error = null; }); builder.addCase(fetchWhoAmI.fulfilled, (state, action: PayloadAction<{ username: string }>) => { - state.status = "succeeded"; + state.status = "successful"; state.username = action.payload.username; }); builder.addCase(fetchWhoAmI.rejected, (state, action: PayloadAction) => { diff --git a/src/redux/slices/missions.ts b/src/redux/slices/missions.ts new file mode 100644 index 0000000..571942d --- /dev/null +++ b/src/redux/slices/missions.ts @@ -0,0 +1,146 @@ +import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; +import axios from "../../axios"; + +// Типы данных +interface Statement { + id: number; + language: string; + statementTexts: Record; + mediaFiles?: { id: number; fileName: string; mediaUrl: string }[]; +} + +interface Mission { + id: number; + authorId: number; + name: string; + difficulty: number; + tags: string[]; + createdAt: string; + updatedAt: string; + statements: Statement[] | null; +} + +interface MissionsState { + missions: Mission[]; + currentMission: Mission | null; + hasNextPage: boolean; + status: "idle" | "loading" | "successful" | "failed"; + error: string | null; +} + +// Инициализация состояния +const initialState: MissionsState = { + missions: [], + currentMission: null, + hasNextPage: false, + status: "idle", + error: null, +}; + +// AsyncThunk: Получение списка миссий +export const fetchMissions = createAsyncThunk( + "missions/fetchMissions", + async ( + { page = 0, pageSize = 10, tags }: { page?: number; pageSize?: number; tags?: string[] }, + { rejectWithValue } + ) => { + try { + const params: any = { page, pageSize }; + if (tags) params.tags = tags; + const response = await axios.get("/missions", { params }); + return response.data; // { hasNextPage, missions } + } catch (err: any) { + return rejectWithValue(err.response?.data?.message || "Failed to fetch missions"); + } + } +); + +// AsyncThunk: Получение миссии по id +export const fetchMissionById = createAsyncThunk( + "missions/fetchMissionById", + async (id: number, { rejectWithValue }) => { + try { + const response = await axios.get(`/missions/${id}`); + return response.data; // Mission + } catch (err: any) { + return rejectWithValue(err.response?.data?.message || "Failed to fetch mission"); + } + } +); + +// AsyncThunk: Загрузка миссии +export const uploadMission = createAsyncThunk( + "missions/uploadMission", + async ( + { file, name, difficulty, tags }: { file: File; name: string; difficulty: number; tags: string[] }, + { rejectWithValue } + ) => { + try { + const formData = new FormData(); + formData.append("MissionFile", file); + formData.append("Name", name); + formData.append("Difficulty", difficulty.toString()); + tags.forEach(tag => formData.append("Tags", tag)); + + const response = await axios.post("/missions/upload", formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return response.data; // Mission + } catch (err: any) { + return rejectWithValue(err.response?.data?.message || "Failed to upload mission"); + } + } +); + +// Slice +const missionsSlice = createSlice({ + name: "missions", + initialState, + reducers: {}, + extraReducers: (builder) => { + // fetchMissions + builder.addCase(fetchMissions.pending, (state) => { + state.status = "loading"; + state.error = null; + }); + builder.addCase(fetchMissions.fulfilled, (state, action: PayloadAction<{ missions: Mission[]; hasNextPage: boolean }>) => { + state.status = "successful"; + state.missions = action.payload.missions; + state.hasNextPage = action.payload.hasNextPage; + }); + builder.addCase(fetchMissions.rejected, (state, action: PayloadAction) => { + state.status = "failed"; + state.error = action.payload; + }); + + // fetchMissionById + builder.addCase(fetchMissionById.pending, (state) => { + state.status = "loading"; + state.error = null; + }); + builder.addCase(fetchMissionById.fulfilled, (state, action: PayloadAction) => { + state.status = "successful"; + state.currentMission = action.payload; + }); + builder.addCase(fetchMissionById.rejected, (state, action: PayloadAction) => { + state.status = "failed"; + state.error = action.payload; + }); + + // uploadMission + builder.addCase(uploadMission.pending, (state) => { + state.status = "loading"; + state.error = null; + }); + builder.addCase(uploadMission.fulfilled, (state, action: PayloadAction) => { + state.status = "successful"; + state.missions.unshift(action.payload); // Добавляем новую миссию в начало списка + }); + builder.addCase(uploadMission.rejected, (state, action: PayloadAction) => { + state.status = "failed"; + state.error = action.payload; + }); + }, +}); + +export const missionsReducer = missionsSlice.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index 509071b..834d822 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -1,6 +1,7 @@ import { configureStore } from "@reduxjs/toolkit"; import { authReducer } from "./slices/auth"; import { storeReducer } from "./slices/store"; +import { missionsReducer } from "./slices/missions"; // использование @@ -17,6 +18,7 @@ export const store = configureStore({ //user: userReducer, auth: authReducer, store: storeReducer, + missions: missionsReducer, }, }); diff --git a/src/views/home/auth/Login.tsx b/src/views/home/auth/Login.tsx index fc06b64..7301b12 100644 --- a/src/views/home/auth/Login.tsx +++ b/src/views/home/auth/Login.tsx @@ -4,7 +4,7 @@ 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 { cn } from "../../../lib/cn"; import { setMenuActivePage } from "../../../redux/slices/store"; import { Balloon } from "../../../assets/icons/auth"; import { SecondaryButton } from "../../../components/button/SecondaryButton"; @@ -18,13 +18,14 @@ const Login = () => { const [password, setPassword] = useState(""); const [submitClicked, setSubmitClicked] = useState(false); - const { status, error, jwt } = useAppSelector((state) => state.auth); + const { status, jwt } = useAppSelector((state) => state.auth); - const [err, setErr] = useState(""); + // const [err, setErr] = useState(""); // После успешного логина useEffect(() => { + console.log(submitClicked); dispatch(setMenuActivePage("account")) }, []); diff --git a/src/views/home/auth/Register.tsx b/src/views/home/auth/Register.tsx index e2a6e8f..38f0e12 100644 --- a/src/views/home/auth/Register.tsx +++ b/src/views/home/auth/Register.tsx @@ -4,7 +4,7 @@ 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 { cn } from "../../../lib/cn"; import { setMenuActivePage } from "../../../redux/slices/store"; import { Balloon } from "../../../assets/icons/auth"; import { Link } from "react-router-dom"; @@ -23,11 +23,12 @@ const Register = () => { const [confirmPassword, setConfirmPassword] = useState(""); const [submitClicked, setSubmitClicked] = useState(false); - const { status, error, jwt } = useAppSelector((state) => state.auth); + const { status, jwt } = useAppSelector((state) => state.auth); // После успешной регистрации — переход в систему useEffect(() => { + console.log(submitClicked); dispatch(setMenuActivePage("account")) }, []); diff --git a/src/views/home/menu/Menu.tsx b/src/views/home/menu/Menu.tsx index 63ae749..ddf329b 100644 --- a/src/views/home/menu/Menu.tsx +++ b/src/views/home/menu/Menu.tsx @@ -6,11 +6,12 @@ import { useAppSelector } from "../../../redux/hooks"; const Menu = () => { const menuItems = [ {text: "Главная", href: "/home", icon: Home, page: "home" }, - {text: "Задачи", href: "/home/problems", icon: Clipboard, page: "problems" }, + {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" }, + {text: "Загрузка", href: "/upload", icon: Account, page: "p" }, ]; const activePage = useAppSelector((state) => state.store.menu.activePage); diff --git a/src/views/home/problems/ProblemItem.tsx b/src/views/home/missions/MissionItem.tsx similarity index 92% rename from src/views/home/problems/ProblemItem.tsx rename to src/views/home/missions/MissionItem.tsx index 5410cd5..8c679b2 100644 --- a/src/views/home/problems/ProblemItem.tsx +++ b/src/views/home/missions/MissionItem.tsx @@ -1,7 +1,7 @@ import { cn } from "../../../lib/cn"; -import { IconError, IconSuccess } from "../../../assets/icons/problems"; +import { IconError, IconSuccess } from "../../../assets/icons/missions"; -export interface ProblemItemProps { +export interface MissionItemProps { id: number; authorId: number; name: string; @@ -26,7 +26,7 @@ export function formatBytesToMB(bytes: number): string { return `${megabytes} МБ`; } -const ProblemItem: React.FC = ({ +const MissionItem: React.FC = ({ id, name, difficulty, timeLimit, memoryLimit, type, status }) => { console.log(id); @@ -66,4 +66,4 @@ const ProblemItem: React.FC = ({ ); }; -export default ProblemItem; +export default MissionItem; diff --git a/src/views/home/problems/Problems.tsx b/src/views/home/missions/Missions.tsx similarity index 98% rename from src/views/home/problems/Problems.tsx rename to src/views/home/missions/Missions.tsx index 5c1ae29..5969183 100644 --- a/src/views/home/problems/Problems.tsx +++ b/src/views/home/missions/Missions.tsx @@ -1,11 +1,11 @@ -import ProblemItem from "./ProblemItem"; +import MissionItem from "./MissionItem"; import { SecondaryButton } from "../../../components/button/SecondaryButton"; import { useAppDispatch } from "../../../redux/hooks"; import { useEffect } from "react"; import { setMenuActivePage } from "../../../redux/slices/store"; -export interface Problem { +export interface Mission { id: number; authorId: number; name: string; @@ -18,11 +18,11 @@ export interface Problem { } -const Problems = () => { +const Missions = () => { const dispatch = useAppDispatch(); - const problems: Problem[] = [ + const missions: Mission[] = [ { "id": 1, "authorId": 1, @@ -466,7 +466,7 @@ const Problems = () => { ]; useEffect(() => { - dispatch(setMenuActivePage("problems")) + dispatch(setMenuActivePage("missions")) }, []); return ( @@ -490,8 +490,8 @@ const Problems = () => {
- {problems.map((v, i) => ( - + {missions.map((v, i) => ( + ))}
@@ -504,4 +504,4 @@ const Problems = () => { ); }; -export default Problems; +export default Missions; diff --git a/src/views/mission/UploadMissionForm.tsx b/src/views/mission/UploadMissionForm.tsx new file mode 100644 index 0000000..f5ea22f --- /dev/null +++ b/src/views/mission/UploadMissionForm.tsx @@ -0,0 +1,101 @@ +import React, { useState } from "react"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks"; +import { uploadMission } from "../../redux/slices/missions"; + +const UploadMissionForm: React.FC = () => { + const dispatch = useAppDispatch(); + const { status, error } = useAppSelector(state => state.missions); + + // Локальные состояния формы + const [name, setName] = useState(""); + const [difficulty, setDifficulty] = useState(1); + const [tags, setTags] = useState([]); + const [tagsValue, setTagsValue] = useState(""); + const [file, setFile] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!file) return alert("Выберите файл миссии!"); + + try { + dispatch(uploadMission({ file, name, difficulty, tags })); + + alert("Миссия успешно загружена!"); + setName(""); + setDifficulty(1); + setTags([]); + setFile(null); + } catch (err) { + console.error(err); + alert("Ошибка при загрузке миссии: " + err); + } + }; + + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + setFile(e.target.files[0]); + } + }; + + const handleTagsChange = (e: React.ChangeEvent) => { + setTagsValue(e.target.value); + const value = e.target.value; + const tagsArray = value.split(",").map(tag => tag.trim()).filter(tag => tag); + setTags(tagsArray); + }; + + return ( +
+
+ + setName(e.target.value)} + className="w-full border px-2 py-1" + required + /> +
+ +
+ + setDifficulty(Number(e.target.value))} + className="w-full border px-2 py-1" + required + /> +
+ +
+ + +
+ +
+ + +
+ + + + {status === "failed" && error &&

{error}

} +
+ ); +}; + +export default UploadMissionForm; diff --git a/src/views/problem/codeeditor/CodeEditor.tsx b/src/views/mission/codeeditor/CodeEditor.tsx similarity index 100% rename from src/views/problem/codeeditor/CodeEditor.tsx rename to src/views/mission/codeeditor/CodeEditor.tsx diff --git a/src/views/problem/statement/Proble.tsx b/src/views/mission/statement/Mission.tsx similarity index 99% rename from src/views/problem/statement/Proble.tsx rename to src/views/mission/statement/Mission.tsx index e1a3ce6..e597ad3 100644 --- a/src/views/problem/statement/Proble.tsx +++ b/src/views/mission/statement/Mission.tsx @@ -10,7 +10,7 @@ declare global { } } -export default function ProblemStatement() { +export default function MissionStatement() { const containerRef = useRef(null); const legend = "В честь юбилея ректорат ЮФУ решил запустить акцию <<Сто и десять кексов>>. \r\n\r\n $x$, $a_i^2 + b_i^2 \le a_{i+1}^2$ В каждом корпусе университета открылась лавка с кексами, в которой каждый студент может получить бесплатные кексы.\r\n\r\nНе прошло и пары минут после открытия, как к лавкам набежали студенты и образовалось много очередей. Но самая большая очередь образовалась в главном корпусе ЮФУ. Изначально в этой очереди стояло $n$ студентов, но потом в течение следующих $m$ минут какие-то студенты приходили и вставали в очередь, а какие-то уходили.\r\n\r\nЗа каждым студентом закреплен номер его зачетной книжки, будем называть это число номером студента. У каждого студента будет уникальный номер, по которому можно однозначно его идентифицировать. Будем считать, что каждую минуту происходило одно из следующих событий:\r\n\r\n\\begin{enumerate}\r\n \\item Студент с номером $x$ пришел и встал перед студентом с номером $y$;\r\n \\item Студент с номером $x$ пришел и встал в конец очереди;\r\n \\item Студент с номером $x$ ушел из очереди; возможно, он потом вернется.\r\n\\end{enumerate}\r\n\r\nАналитикам стало интересно, а какой будет очередь после $m$ минут? \r\n\r\nПомогите им и сообщите конечное состояние очереди.\r\n\r\n"; const htmlContent = ` diff --git a/src/views/problem/statement/Statement.tsx b/src/views/mission/statement/Statement.tsx similarity index 100% rename from src/views/problem/statement/Statement.tsx rename to src/views/mission/statement/Statement.tsx From 235b2c16bd463f498c7cd8a2dc2f6b74e964b249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Sun, 2 Nov 2025 15:33:10 +0300 Subject: [PATCH 08/10] statement --- src/App.tsx | 4 +- src/pages/Mission.tsx | 81 ++- src/redux/slices/missions.ts | 3 +- src/redux/slices/submit.ts | 161 ++++++ src/redux/store.ts | 2 + src/views/home/menu/Menu.tsx | 1 - src/views/home/missions/MissionItem.tsx | 9 +- src/views/home/missions/Missions.tsx | 471 +----------------- src/views/mission/codeeditor/CodeEditor.tsx | 16 +- .../mission/statement/LaTextContainer.tsx | 14 + src/views/mission/statement/Statement.tsx | 101 ++-- 11 files changed, 364 insertions(+), 499 deletions(-) create mode 100644 src/redux/slices/submit.ts create mode 100644 src/views/mission/statement/LaTextContainer.tsx diff --git a/src/App.tsx b/src/App.tsx index 474940a..6b269fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ import { Route, Routes } from "react-router-dom"; // import { Input } from "./components/input/Input"; // import { Switch } from "./components/switch/Switch"; import Home from "./pages/Home"; -import CodeEditor from "./views/mission/codeeditor/CodeEditor"; +// import CodeEditor from "./views/mission/codeeditor/CodeEditor"; import Statement from "./views/mission/statement/Statement"; import MissionStatement from "./views/mission/statement/Mission"; import Mission from "./pages/Mission"; @@ -19,7 +19,7 @@ function App() { } /> } /> }/> -
} /> + {/*
} /> */}
} /> } /> diff --git a/src/pages/Mission.tsx b/src/pages/Mission.tsx index 751f89c..64af650 100644 --- a/src/pages/Mission.tsx +++ b/src/pages/Mission.tsx @@ -1,24 +1,93 @@ import { useParams, Navigate } from 'react-router-dom'; - - +import CodeEditor from '../views/mission/codeeditor/CodeEditor'; +import Statement, { StatementData } from '../views/mission/statement/Statement'; +import { PrimaryButton } from '../components/button/PrimaryButton'; +import { useEffect, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { submitMission } from '../redux/slices/submit'; +import { fetchMissionById } from '../redux/slices/missions'; const Mission = () => { + const dispatch = useAppDispatch(); + // Получаем параметры из URL const { missionId } = useParams<{ missionId: string }>(); + const mission = useAppSelector((state) => state.missions.currentMission); + const missionIdNumber = Number(missionId); + + const [code, setCode] = useState(""); + const [language, setLanguage] = useState(""); // Если missionId нет, редиректим на /home - if (!missionId) { + + // Если missionId нет или не число — редиректим + if (!missionId || isNaN(missionIdNumber)) { return ; } + useEffect(() => { + dispatch(fetchMissionById(missionIdNumber)); + }, []); + +if (!mission || !mission.statements || mission.statements.length === 0) { + return
Загрузка или миссия не найдена...
; +} + + + +const statementRaw = mission.statements[0]; + let statementData: StatementData = { id: mission.id }; + + try { + const statementTexts = JSON.parse(statementRaw.statementTexts["problem-properties.json"]); + // console.log(mission); + statementData = { + id: statementRaw.id, + legend: statementTexts.legend, + timeLimit: statementTexts.timeLimit, + output: statementTexts.output, + input: statementTexts.input, + sampleTests: statementTexts.sampleTests, + name: statementTexts.name, + memoryLimit: statementTexts.memoryLimit, + tags: mission.tags, + notes: statementTexts.notes, + }; + } catch (err) { + console.error("Ошибка парсинга statementTexts:", err); + } + return ( -
+
+
- {missionId} + />
+
+
+ { setCode(value); }} + onChangeLanguage={((value: string) => { setLanguage(value); })} + /> +
+
+ { + dispatch(submitMission({ + missionId: missionIdNumber, + language: language, + languageVersion: "latest", + sourceCode: code, + contestId: null, + + })) + }} /> +
+
+
); }; diff --git a/src/redux/slices/missions.ts b/src/redux/slices/missions.ts index 571942d..8beb53d 100644 --- a/src/redux/slices/missions.ts +++ b/src/redux/slices/missions.ts @@ -41,7 +41,7 @@ const initialState: MissionsState = { export const fetchMissions = createAsyncThunk( "missions/fetchMissions", async ( - { page = 0, pageSize = 10, tags }: { page?: number; pageSize?: number; tags?: string[] }, + { page = 0, pageSize = 10, tags = [] }: { page?: number; pageSize?: number; tags?: string[] }, { rejectWithValue } ) => { try { @@ -120,6 +120,7 @@ const missionsSlice = createSlice({ }); builder.addCase(fetchMissionById.fulfilled, (state, action: PayloadAction) => { state.status = "successful"; + console.log(action.payload); state.currentMission = action.payload; }); builder.addCase(fetchMissionById.rejected, (state, action: PayloadAction) => { diff --git a/src/redux/slices/submit.ts b/src/redux/slices/submit.ts new file mode 100644 index 0000000..e970497 --- /dev/null +++ b/src/redux/slices/submit.ts @@ -0,0 +1,161 @@ +import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; +import axios from "../../axios"; + +// Типы данных +export interface Submit { + id?: number; + missionId: number; + language: string; + languageVersion: string; + sourceCode: string; + contestId: number | null; +} + +export interface SubmitStatus { + SubmitId: number; + State: string; + ErrorCode: string; + Message: string; + CurrentTest: number; + AmountOfTests: number; +} + +interface SubmitState { + submits: Submit[]; + currentSubmit?: Submit; + status: "idle" | "loading" | "successful" | "failed"; + error: string | null; +} + +// Начальное состояние +const initialState: SubmitState = { + submits: [], + currentSubmit: undefined, + status: "idle", + error: null, +}; + +// AsyncThunk: Отправка решения +export const submitMission = createAsyncThunk( + "submit/submitMission", + async (submitData: Submit, { rejectWithValue }) => { + try { + const response = await axios.post("/submits", submitData); + return response.data; + } catch (err: any) { + return rejectWithValue(err.response?.data?.message || "Submit failed"); + } + } +); + +// AsyncThunk: Получить все свои отправки +export const fetchMySubmits = createAsyncThunk( + "submit/fetchMySubmits", + async (_, { rejectWithValue }) => { + try { + const response = await axios.get("/submits/my"); + return response.data as Submit[]; + } catch (err: any) { + return rejectWithValue(err.response?.data?.message || "Failed to fetch submits"); + } + } +); + +// AsyncThunk: Получить конкретную отправку по ID +export const fetchSubmitById = createAsyncThunk( + "submit/fetchSubmitById", + async (id: number, { rejectWithValue }) => { + try { + const response = await axios.get(`/submits/${id}`); + return response.data as Submit; + } catch (err: any) { + return rejectWithValue(err.response?.data?.message || "Failed to fetch submit"); + } + } +); + +// AsyncThunk: Получить свои отправки для конкретной миссии +export const fetchMySubmitsByMission = createAsyncThunk( + "submit/fetchMySubmitsByMission", + async (missionId: number, { rejectWithValue }) => { + try { + const response = await axios.get(`/submits/my/mission/${missionId}`); + return response.data as Submit[]; + } catch (err: any) { + return rejectWithValue(err.response?.data?.message || "Failed to fetch mission submits"); + } + } +); + +// Slice +const submitSlice = createSlice({ + name: "submit", + initialState, + reducers: { + clearCurrentSubmit: (state) => { + state.currentSubmit = undefined; + state.status = "idle"; + state.error = null; + }, + }, + extraReducers: (builder) => { + // Отправка решения + builder.addCase(submitMission.pending, (state) => { + state.status = "loading"; + state.error = null; + }); + builder.addCase(submitMission.fulfilled, (state, action: PayloadAction) => { + state.status = "successful"; + state.submits.push(action.payload); + }); + builder.addCase(submitMission.rejected, (state, action: PayloadAction) => { + state.status = "failed"; + state.error = action.payload; + }); + + // Получить все свои отправки + builder.addCase(fetchMySubmits.pending, (state) => { + state.status = "loading"; + state.error = null; + }); + builder.addCase(fetchMySubmits.fulfilled, (state, action: PayloadAction) => { + state.status = "successful"; + state.submits = action.payload; + }); + builder.addCase(fetchMySubmits.rejected, (state, action: PayloadAction) => { + state.status = "failed"; + state.error = action.payload; + }); + + // Получить отправку по ID + builder.addCase(fetchSubmitById.pending, (state) => { + state.status = "loading"; + state.error = null; + }); + builder.addCase(fetchSubmitById.fulfilled, (state, action: PayloadAction) => { + state.status = "successful"; + state.currentSubmit = action.payload; + }); + builder.addCase(fetchSubmitById.rejected, (state, action: PayloadAction) => { + state.status = "failed"; + state.error = action.payload; + }); + + // Получить отправки по миссии + builder.addCase(fetchMySubmitsByMission.pending, (state) => { + state.status = "loading"; + state.error = null; + }); + builder.addCase(fetchMySubmitsByMission.fulfilled, (state, action: PayloadAction) => { + state.status = "successful"; + state.submits = action.payload; + }); + builder.addCase(fetchMySubmitsByMission.rejected, (state, action: PayloadAction) => { + state.status = "failed"; + state.error = action.payload; + }); + }, +}); + +export const { clearCurrentSubmit } = submitSlice.actions; +export const submitReducer = submitSlice.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index 834d822..6ea89a8 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -2,6 +2,7 @@ import { configureStore } from "@reduxjs/toolkit"; import { authReducer } from "./slices/auth"; import { storeReducer } from "./slices/store"; import { missionsReducer } from "./slices/missions"; +import { submitReducer } from "./slices/submit"; // использование @@ -19,6 +20,7 @@ export const store = configureStore({ auth: authReducer, store: storeReducer, missions: missionsReducer, + submin: submitReducer, }, }); diff --git a/src/views/home/menu/Menu.tsx b/src/views/home/menu/Menu.tsx index ddf329b..0e5cab3 100644 --- a/src/views/home/menu/Menu.tsx +++ b/src/views/home/menu/Menu.tsx @@ -11,7 +11,6 @@ const Menu = () => { {text: "Группы", href: "/home/groups", icon: Users, page: "groups" }, {text: "Контесты", href: "/home/contests", icon: Cup, page: "contests" }, {text: "Аккаунт", href: "/home/account", icon: Account, page: "account" }, - {text: "Загрузка", href: "/upload", icon: Account, page: "p" }, ]; const activePage = useAppSelector((state) => state.store.menu.activePage); diff --git a/src/views/home/missions/MissionItem.tsx b/src/views/home/missions/MissionItem.tsx index 8c679b2..2bdf6f8 100644 --- a/src/views/home/missions/MissionItem.tsx +++ b/src/views/home/missions/MissionItem.tsx @@ -1,5 +1,6 @@ import { cn } from "../../../lib/cn"; import { IconError, IconSuccess } from "../../../assets/icons/missions"; +import { useNavigate } from "react-router-dom"; export interface MissionItemProps { id: number; @@ -29,14 +30,18 @@ export function formatBytesToMB(bytes: number): string { const MissionItem: React.FC = ({ id, name, difficulty, timeLimit, memoryLimit, type, status }) => { - console.log(id); + const navigate = useNavigate(); + return (
+ "cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300", + )} + onClick={() => {navigate(`/mission/${id}`)}} + >
#{id}
diff --git a/src/views/home/missions/Missions.tsx b/src/views/home/missions/Missions.tsx index 5969183..c62b0db 100644 --- a/src/views/home/missions/Missions.tsx +++ b/src/views/home/missions/Missions.tsx @@ -1,8 +1,10 @@ import MissionItem from "./MissionItem"; import { SecondaryButton } from "../../../components/button/SecondaryButton"; -import { useAppDispatch } from "../../../redux/hooks"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; import { useEffect } from "react"; import { setMenuActivePage } from "../../../redux/slices/store"; +import { useNavigate } from "react-router-dom"; +import { fetchMissions } from "../../../redux/slices/missions"; export interface Mission { @@ -17,457 +19,18 @@ export interface Mission { updatedAt: string; } - const Missions = () => { const dispatch = useAppDispatch(); + const naivgate = useNavigate(); - const missions: Mission[] = [ - { - "id": 1, - "authorId": 1, - "name": "Todo List App", - "difficulty": "Easy", - "tags": ["react", "state", "list"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:13.000Z", - "updatedAt": "2025-10-28T13:23:13.000Z" - }, - { - "id": 2, - "authorId": 1, - "name": "Search Filter Component", - "difficulty": "Medium", - "tags": ["filter", "props", "hooks"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:14.000Z", - "updatedAt": "2025-10-28T13:23:14.000Z" - }, - { - "id": 3, - "authorId": 1, - "name": "User Card List", - "difficulty": "Easy", - "tags": ["components", "props", "array"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:15.000Z", - "updatedAt": "2025-10-28T13:23:15.000Z" - }, - { - "id": 4, - "authorId": 1, - "name": "Theme Switcher", - "difficulty": "Medium", - "tags": ["context", "theme", "hooks"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:16.000Z", - "updatedAt": "2025-10-28T13:23:16.000Z" - }, - { - "id": 5, - "authorId": 1, - "name": "Debounced Input", - "difficulty": "Hard", - "tags": ["debounce", "hooks", "events"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:17.000Z", - "updatedAt": "2025-10-28T13:23:17.000Z" - }, - { - "id": 6, - "authorId": 1, - "name": "Pagination Component", - "difficulty": "Medium", - "tags": ["pagination", "array", "state"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:18.000Z", - "updatedAt": "2025-10-28T13:23:18.000Z" - }, - { - "id": 7, - "authorId": 1, - "name": "Modal Window", - "difficulty": "Easy", - "tags": ["ui", "portal", "events"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:19.000Z", - "updatedAt": "2025-10-28T13:23:19.000Z" - }, - { - "id": 8, - "authorId": 1, - "name": "Form Validation", - "difficulty": "Hard", - "tags": ["form", "validation", "hooks"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:20.000Z", - "updatedAt": "2025-10-28T13:23:20.000Z" - }, - { - "id": 9, - "authorId": 1, - "name": "Countdown Timer", - "difficulty": "Medium", - "tags": ["timer", "hooks", "state"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:21.000Z", - "updatedAt": "2025-10-28T13:23:21.000Z" - }, - { - "id": 10, - "authorId": 1, - "name": "Drag And Drop List", - "difficulty": "Hard", - "tags": ["dragdrop", "array", "events"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:22.000Z", - "updatedAt": "2025-10-28T13:23:22.000Z" - }, - { - "id": 11, - "authorId": 1, - "name": "Custom Hook Use Fetch", - "difficulty": "Medium", - "tags": ["hook", "fetch", "async"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:23.000Z", - "updatedAt": "2025-10-28T13:23:23.000Z" - }, - { - "id": 12, - "authorId": 1, - "name": "Infinite Scroll", - "difficulty": "Hard", - "tags": ["scroll", "pagination", "api"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:24.000Z", - "updatedAt": "2025-10-28T13:23:24.000Z" - }, - { - "id": 13, - "authorId": 1, - "name": "Responsive Navbar", - "difficulty": "Easy", - "tags": ["css", "layout", "responsive"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:25.000Z", - "updatedAt": "2025-10-28T13:23:25.000Z" - }, - { - "id": 14, - "authorId": 1, - "name": "Accordion Component", - "difficulty": "Easy", - "tags": ["ui", "state", "events"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:26.000Z", - "updatedAt": "2025-10-28T13:23:26.000Z" - }, - { - "id": 15, - "authorId": 1, - "name": "File Upload Preview", - "difficulty": "Hard", - "tags": ["file", "events", "preview"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:27.000Z", - "updatedAt": "2025-10-28T13:23:27.000Z" - }, - { - "id": 16, - "authorId": 1, - "name": "Dark Mode Toggle", - "difficulty": "Easy", - "tags": ["theme", "context", "localStorage"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:28.000Z", - "updatedAt": "2025-10-28T13:23:28.000Z" - }, - { - "id": 17, - "authorId": 1, - "name": "Realtime Clock", - "difficulty": "Easy", - "tags": ["date", "state", "interval"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:29.000Z", - "updatedAt": "2025-10-28T13:23:29.000Z" - }, - { - "id": 18, - "authorId": 1, - "name": "Chart With Recharts", - "difficulty": "Medium", - "tags": ["chart", "data", "props"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:30.000Z", - "updatedAt": "2025-10-28T13:23:30.000Z" - }, - { - "id": 19, - "authorId": 1, - "name": "Router Navigation", - "difficulty": "Medium", - "tags": ["router", "navigation", "hooks"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:31.000Z", - "updatedAt": "2025-10-28T13:23:31.000Z" - }, - { - "id": 20, - "authorId": 1, - "name": "Data Table Sortable", - "difficulty": "Hard", - "tags": ["table", "sort", "filter"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:32.000Z", - "updatedAt": "2025-10-28T13:23:32.000Z" - }, - { - "id": 1, - "authorId": 1, - "name": "Todo List App", - "difficulty": "Easy", - "tags": ["react", "state", "list"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:13.000Z", - "updatedAt": "2025-10-28T13:23:13.000Z" - }, - { - "id": 2, - "authorId": 1, - "name": "Search Filter Component", - "difficulty": "Medium", - "tags": ["filter", "props", "hooks"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:14.000Z", - "updatedAt": "2025-10-28T13:23:14.000Z" - }, - { - "id": 3, - "authorId": 1, - "name": "User Card List", - "difficulty": "Easy", - "tags": ["components", "props", "array"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:15.000Z", - "updatedAt": "2025-10-28T13:23:15.000Z" - }, - { - "id": 4, - "authorId": 1, - "name": "Theme Switcher", - "difficulty": "Medium", - "tags": ["context", "theme", "hooks"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:16.000Z", - "updatedAt": "2025-10-28T13:23:16.000Z" - }, - { - "id": 5, - "authorId": 1, - "name": "Debounced Input", - "difficulty": "Hard", - "tags": ["debounce", "hooks", "events"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:17.000Z", - "updatedAt": "2025-10-28T13:23:17.000Z" - }, - { - "id": 6, - "authorId": 1, - "name": "Pagination Component", - "difficulty": "Medium", - "tags": ["pagination", "array", "state"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:18.000Z", - "updatedAt": "2025-10-28T13:23:18.000Z" - }, - { - "id": 7, - "authorId": 1, - "name": "Modal Window", - "difficulty": "Easy", - "tags": ["ui", "portal", "events"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:19.000Z", - "updatedAt": "2025-10-28T13:23:19.000Z" - }, - { - "id": 8, - "authorId": 1, - "name": "Form Validation", - "difficulty": "Hard", - "tags": ["form", "validation", "hooks"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:20.000Z", - "updatedAt": "2025-10-28T13:23:20.000Z" - }, - { - "id": 9, - "authorId": 1, - "name": "Countdown Timer", - "difficulty": "Medium", - "tags": ["timer", "hooks", "state"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:21.000Z", - "updatedAt": "2025-10-28T13:23:21.000Z" - }, - { - "id": 10, - "authorId": 1, - "name": "Drag And Drop List", - "difficulty": "Hard", - "tags": ["dragdrop", "array", "events"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:22.000Z", - "updatedAt": "2025-10-28T13:23:22.000Z" - }, - { - "id": 11, - "authorId": 1, - "name": "Custom Hook Use Fetch", - "difficulty": "Medium", - "tags": ["hook", "fetch", "async"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:23.000Z", - "updatedAt": "2025-10-28T13:23:23.000Z" - }, - { - "id": 12, - "authorId": 1, - "name": "Infinite Scroll", - "difficulty": "Hard", - "tags": ["scroll", "pagination", "api"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:24.000Z", - "updatedAt": "2025-10-28T13:23:24.000Z" - }, - { - "id": 13, - "authorId": 1, - "name": "Responsive Navbar", - "difficulty": "Easy", - "tags": ["css", "layout", "responsive"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:25.000Z", - "updatedAt": "2025-10-28T13:23:25.000Z" - }, - { - "id": 14, - "authorId": 1, - "name": "Accordion Component", - "difficulty": "Easy", - "tags": ["ui", "state", "events"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:26.000Z", - "updatedAt": "2025-10-28T13:23:26.000Z" - }, - { - "id": 15, - "authorId": 1, - "name": "File Upload Preview", - "difficulty": "Hard", - "tags": ["file", "events", "preview"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:27.000Z", - "updatedAt": "2025-10-28T13:23:27.000Z" - }, - { - "id": 16, - "authorId": 1, - "name": "Dark Mode Toggle", - "difficulty": "Easy", - "tags": ["theme", "context", "localStorage"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:28.000Z", - "updatedAt": "2025-10-28T13:23:28.000Z" - }, - { - "id": 17, - "authorId": 1, - "name": "Realtime Clock", - "difficulty": "Easy", - "tags": ["date", "state", "interval"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:29.000Z", - "updatedAt": "2025-10-28T13:23:29.000Z" - }, - { - "id": 18, - "authorId": 1, - "name": "Chart With Recharts", - "difficulty": "Medium", - "tags": ["chart", "data", "props"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:30.000Z", - "updatedAt": "2025-10-28T13:23:30.000Z" - }, - { - "id": 19, - "authorId": 1, - "name": "Router Navigation", - "difficulty": "Medium", - "tags": ["router", "navigation", "hooks"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:31.000Z", - "updatedAt": "2025-10-28T13:23:31.000Z" - }, - { - "id": 20, - "authorId": 1, - "name": "Data Table Sortable", - "difficulty": "Hard", - "tags": ["table", "sort", "filter"], - "timeLimit": 1000, - "memoryLimit": 268435456, - "createdAt": "2025-10-28T13:23:32.000Z", - "updatedAt": "2025-10-28T13:23:32.000Z" - } - ]; + const missions = useAppSelector((state) => state.missions.missions); - useEffect(() => { + useEffect(() => { dispatch(setMenuActivePage("missions")) - }, []); + dispatch(fetchMissions({})) + }, []); + return (
@@ -478,7 +41,7 @@ const Missions = () => { Задачи
{}} + onClick={() => {naivgate("/upload")}} text="Создать задачу" className="absolute right-0" /> @@ -491,7 +54,19 @@ const Missions = () => {
{missions.map((v, i) => ( - + ))}
diff --git a/src/views/mission/codeeditor/CodeEditor.tsx b/src/views/mission/codeeditor/CodeEditor.tsx index 94974cd..cd048c4 100644 --- a/src/views/mission/codeeditor/CodeEditor.tsx +++ b/src/views/mission/codeeditor/CodeEditor.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import Editor from "@monaco-editor/react"; import { upload } from "../../../assets/icons/input"; import { cn } from "../../../lib/cn"; @@ -14,7 +14,12 @@ const languageMap: Record = { csharp: "csharp" }; -const CodeEditor: React.FC = () => { +export interface CodeEditorProps { + onChange: (value: string) => void; + onChangeLanguage: (value: string) => void; +} + +const CodeEditor: React.FC = ({onChange, onChangeLanguage}) => { const [language, setLanguage] = useState("cpp"); const [code, setCode] = useState(""); const [isDragging, setIsDragging] = useState(false); @@ -30,6 +35,13 @@ const CodeEditor: React.FC = () => { { value: "csharp", text: "C#" }, ]; + useEffect(() => { + onChange(code); + }, [code]) + useEffect(() => { + onChangeLanguage(language); + }, [language]) + const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; diff --git a/src/views/mission/statement/LaTextContainer.tsx b/src/views/mission/statement/LaTextContainer.tsx new file mode 100644 index 0000000..f36d00a --- /dev/null +++ b/src/views/mission/statement/LaTextContainer.tsx @@ -0,0 +1,14 @@ +import React, { useEffect, useRef } from "react"; + +interface LaTextContainerProps { + content: string; +} + +const LaTextContainer: React.FC = ({ content }) => { + + return
+ {content} +
; +}; + +export default LaTextContainer; diff --git a/src/views/mission/statement/Statement.tsx b/src/views/mission/statement/Statement.tsx index 16cd153..63588ad 100644 --- a/src/views/mission/statement/Statement.tsx +++ b/src/views/mission/statement/Statement.tsx @@ -1,58 +1,85 @@ import React, { useState } from "react"; import { cn } from "../../../lib/cn"; +import LaTextContainer from "./LaTextContainer"; // import FullLatexRenderer from "./FullLatexRenderer"; -const Statement: React.FC = () => { +export interface StatementData { + id?: number; + name?: string; + tags?: string[]; + timeLimit?: number; + memoryLimit?: number; + legend?: string; + input?: string; + output?: string; + sampleTests?: { input: string; output: string }[]; + notes?: string; +} + + + +const Statement: React.FC = ({ + id, + name, + tags, + timeLimit = 1000, + memoryLimit = 256 * 1024 * 1024, + legend = "", + input = "", + output = "", + sampleTests = [], + notes = "", +}) => { -const data = { - "extraResources": { - "example.01.mu": "TVXzATcgNg0KMSAyIDMgNCA1IDYgNw0KMSA4IDMNCjIgOQ0KMyAzDQoxIDMgOQ0KMiAxMA0KMyAxDQo=" - }, - "scoring": null, - "notes": "Изначально очередь выглядит следующим образом:\r\n\r\n\\includegraphics{o1.png}\r\n\r\nВ первую минуту приходит студент с номером 8 и встает перед студентом с номером 3.\r\n\r\n\\includegraphics{o2.png}\r\n\r\nПотом студент с номером 9 встает в конец очереди.\r\n\r\n\\includegraphics{o3.png}\r\n\r\nСтудент с номером 3 уходит из очереди.\r\n\r\n\\includegraphics{o4.png}\r\n\r\nПотом он возвращается и становится перед студентом с номером 9.\r\n\r\n\\includegraphics{o5.png}\r\n\r\nПосле в конец очереди становится студент с номером 10.\r\n\r\n\\includegraphics{o6.png}\r\n\r\nИ студент с номером 1 уходит из очереди.\r\n\r\n\\includegraphics{o7.png}\r\n\r\nПосле $m$ событий очередь имеет следующий вид:\r\n\r\n\\includegraphics{o8.png}", - "legend": "В честь юбилея ректорат ЮФУ решил запустить акцию <<Сто и десять кексов>>. \r\n\r\n $x$, $a_i^2 + b_i^2 \le a_{i+1}^2$ В каждом корпусе университета открылась лавка с кексами, в которой каждый студент может получить бесплатные кексы.\r\n\r\nНе прошло и пары минут после открытия, как к лавкам набежали студенты и образовалось много очередей. Но самая большая очередь образовалась в главном корпусе ЮФУ. Изначально в этой очереди стояло $n$ студентов, но потом в течение следующих $m$ минут какие-то студенты приходили и вставали в очередь, а какие-то уходили.\r\n\r\nЗа каждым студентом закреплен номер его зачетной книжки, будем называть это число номером студента. У каждого студента будет уникальный номер, по которому можно однозначно его идентифицировать. Будем считать, что каждую минуту происходило одно из следующих событий:\r\n\r\n\\begin{enumerate}\r\n \\item Студент с номером $x$ пришел и встал перед студентом с номером $y$;\r\n \\item Студент с номером $x$ пришел и встал в конец очереди;\r\n \\item Студент с номером $x$ ушел из очереди; возможно, он потом вернется.\r\n\\end{enumerate}\r\n\r\nАналитикам стало интересно, а какой будет очередь после $m$ минут? \r\n\r\nПомогите им и сообщите конечное состояние очереди.\r\n\r\n", - "authorLogin": "valavshonok", - "language": "russian", - "timeLimit": 1000, - "output": "В первой строке выведите одно число $|a|$~--- длину очереди после выполнения всех запросов изменения.\r\n\r\nВ следующей строке выведите $|a|$ чисел $a_1, a_2, \\cdots , a_{|a|}$, где $a_i$~--- номер студента, который стоит на $i$-й позиции в очереди.", - "inputFile": "stdin", - "outputFile": "stdout", - "input": "В первой строке заданы два целых числа $n$ и $m$ $(1 \\le n, m \\le 10^5)$~--- текущее число студентов в очереди и количество изменений.\r\n\r\nВ следующей строке задается $n$ целых \\textbf{различных} чисел $a_1, a_2, \\cdots , a_n$ $(1 \\le a_i \\le 10^9)$, где $a_i$~--- номер студента, который стоит на $i$-й позиции в очереди.\r\n\r\nВ следующих $m$ строках идет описание запросов изменения очереди.\r\n\r\nВ каждой строке в зависимости от типа запроса задается два или три числа. Первое число $t_j$ $(1 \\le t_j \\le 3)$~--- тип события, которое произошло в $j$-ю минуту.\r\n\r\nЕсли $t_j = \\textbf{1}$, то в строке задается еще 2 числа $x$ $(1 \\le x_j \\le 10^9)$ и $y$ $(1 \\le y_j \\le 10^9)$~--- номер студента, который пришел, и номер студента, перед которым он встанет в очереди. Гарантируется, что студент с номером $x$ ещё не занял очередь, а студент с номером $y$ уже стоит в ней. \r\n\r\nЕсли $t_j = \\textbf{2}$, то в строке задается еще 1 число $x$ $(1 \\le x_j \\le 10^9)$~--- номер студента, который пришел и встал в конец очереди. Гарантируется, что студент с номером $x$ ещё не занял очередь.\r\n\r\nЕсли $t_j = \\textbf{3}$, то в строке задается еще 1 число $x$ $(1 \\le x_j \\le 10^9)$~--- номер студента, который ушел из очереди. Гарантируется, что студент с номером $x$ стоит в очереди.", - "authorName": "Виталий Лавшонок", - "sampleTests": [ - { - "output": "9\r\n2 8 4 5 6 7 3 9 10 \r\n", - "input": "7 6\r\n1 2 3 4 5 6 7\r\n1 8 3\r\n2 9\r\n3 3\r\n1 3 9\r\n2 10\r\n3 1\r\n", - "inputFile": "example.01", - "outputFile": "example.01.a" - } - ], - "name": "Очередь за кексами", - "interaction": null, - "memoryLimit": 268435456, - "tutorial": "Давайте просто промоделируем все действия.\r\n\r\nЗаведем список элементов, а также сохраним по ключу $x$ указатель на элемент списка. Мы можем это сделать, так как все элементы различны. Например, в С++ можно просто завести коллекцию list, а также map::iterator> или реализовать свой список.\r\n\r\nТеперь мы можем легко обрабатывать все запросы, а в конце просто выведем весь список.\r\n\r\nЗапрос 1-го типа можно обработать так: просто берем по ключу указатель на нужный элемент и вставляем перед ним другой элемент, останется только по ключу $x$ записать указатель на новый элемент.\r\n\r\nЗапрос 2-го типа~--- просто добавить в список элемент в конец и сохранить на него указатель.\r\n\r\nЗапрос 3-го типа~--- удаляем из списка элемент по его указателю.\r\n\r\nВ конце просто выводим массив.\r\n\r\nИтоговая сложность $O(mlog(n))$" -}; return (
-

Грод на 2700

-

Задача #1234

+

{name}

+

Задача #{id}

-
- tags +
+ {tags && tags.map((v, i) =>
{v}
)}
-
-

ограничение по времени на тест: 1 секунда

-

ограничение по памяти на тест: 256 мегабайт

+
+

ограничение по времени на тест: {timeLimit / 1000} секунда

+

ограничение по памяти на тест: {memoryLimit / 1024 / 1024} мегабайт

ввод: стандартный ввод

вывод: стандартный вывод

+
+ +
+
+
Входные данные
+ +
+
+
Выходные данные
+ +
+
- {/* */} +
{sampleTests.length == 1 ? "Пример" : "Примеры"}
+
+ + {sampleTests.map((v, i) => +
+
Входные данные
+
{v.input}
+
Выходные данные
+
{v.output}
+
+ )} +
+
+
+
Примечание
+ +
Автор: Jacks
From f6c681c03843b84d64ffb974c605a44b3a7fc9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Sun, 2 Nov 2025 23:41:23 +0300 Subject: [PATCH 09/10] submissions --- src/App.tsx | 7 +- src/assets/icons/header/arrow-left-sm.svg | 3 + src/assets/icons/header/chevron-left.svg | 3 + src/assets/icons/header/chevron-right.svg | 3 + src/assets/icons/header/index.ts | 5 + src/axios.ts | 14 ++ src/pages/Home.tsx | 5 +- src/pages/Mission.tsx | 169 +++++++++++---- src/redux/slices/auth.ts | 36 +++- src/redux/slices/missions.ts | 3 +- src/redux/slices/submit.ts | 53 +++-- src/styles/index.css | 24 ++- src/styles/latex-container.css | 26 +++ src/views/home/articles/ArticleItem.tsx | 1 - src/views/home/auth/Login.tsx | 2 - src/views/home/auth/Register.tsx | 3 +- src/views/home/contests/ContestsBlock.tsx | 1 - src/views/home/groups/GroupItem.tsx | 1 - src/views/home/groups/GroupsBlock.tsx | 1 - src/views/mission/codeeditor/CodeEditor.tsx | 6 +- src/views/mission/statement/Header.tsx | 30 +++ .../mission/statement/LaTextContainer.tsx | 113 ++++++++++- src/views/mission/statement/Mission.tsx | 192 ------------------ .../mission/statement/MissionSubmissions.tsx | 61 ++++++ src/views/mission/statement/Statement.tsx | 46 +++-- .../mission/statement/SubmissionItem.tsx | 79 +++++++ 26 files changed, 589 insertions(+), 298 deletions(-) create mode 100644 src/assets/icons/header/arrow-left-sm.svg create mode 100644 src/assets/icons/header/chevron-left.svg create mode 100644 src/assets/icons/header/chevron-right.svg create mode 100644 src/assets/icons/header/index.ts create mode 100644 src/styles/latex-container.css create mode 100644 src/views/mission/statement/Header.tsx delete mode 100644 src/views/mission/statement/Mission.tsx create mode 100644 src/views/mission/statement/MissionSubmissions.tsx create mode 100644 src/views/mission/statement/SubmissionItem.tsx diff --git a/src/App.tsx b/src/App.tsx index 6b269fc..eab18e1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,9 +5,6 @@ import { Route, Routes } from "react-router-dom"; // import { Input } from "./components/input/Input"; // import { Switch } from "./components/switch/Switch"; import Home from "./pages/Home"; -// import CodeEditor from "./views/mission/codeeditor/CodeEditor"; -import Statement from "./views/mission/statement/Statement"; -import MissionStatement from "./views/mission/statement/Mission"; import Mission from "./pages/Mission"; import UploadMissionForm from "./views/mission/UploadMissionForm"; @@ -19,9 +16,7 @@ function App() { } /> } /> }/> - {/*
} /> */} -
} /> - } /> + } />
diff --git a/src/assets/icons/header/arrow-left-sm.svg b/src/assets/icons/header/arrow-left-sm.svg new file mode 100644 index 0000000..a78e6db --- /dev/null +++ b/src/assets/icons/header/arrow-left-sm.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/header/chevron-left.svg b/src/assets/icons/header/chevron-left.svg new file mode 100644 index 0000000..77ecb49 --- /dev/null +++ b/src/assets/icons/header/chevron-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/header/chevron-right.svg b/src/assets/icons/header/chevron-right.svg new file mode 100644 index 0000000..5df23ae --- /dev/null +++ b/src/assets/icons/header/chevron-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/header/index.ts b/src/assets/icons/header/index.ts new file mode 100644 index 0000000..5ea2a4b --- /dev/null +++ b/src/assets/icons/header/index.ts @@ -0,0 +1,5 @@ +import arrowLeft from "./arrow-left-sm.svg"; +import chevroneLeft from "./chevron-left.svg" +import chevroneRight from "./chevron-right.svg" + +export {arrowLeft, chevroneLeft, chevroneRight} \ No newline at end of file diff --git a/src/axios.ts b/src/axios.ts index 1dc691d..3da69b2 100644 --- a/src/axios.ts +++ b/src/axios.ts @@ -7,4 +7,18 @@ const instance = axios.create({ }, }); +// Request interceptor: автоматически подставляет JWT, если есть +instance.interceptors.request.use( + (config) => { + const token = localStorage.getItem("jwt"); // или можно брать из Redux через store.getState() + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + export default instance; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 4f41306..4058f4a 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -5,11 +5,12 @@ import Register from "../views/home/auth/Register"; import Menu from "../views/home/menu/Menu"; import { useAppDispatch, useAppSelector } from "../redux/hooks"; import { useEffect } from "react"; -import { fetchWhoAmI } from "../redux/slices/auth"; +import { fetchWhoAmI, logout } from "../redux/slices/auth"; import Missions from "../views/home/missions/Missions"; import Articles from "../views/home/articles/Articles"; import Groups from "../views/home/groups/Groups"; import Contests from "../views/home/contests/Contests"; +import { PrimaryButton } from "../components/button/PrimaryButton"; const Home = () => { const name = useAppSelector((state) => state.auth.username); @@ -35,7 +36,7 @@ const Home = () => { } /> } /> } /> - + {name} {dispatch(logout())}}>выйти} />
{ diff --git a/src/pages/Mission.tsx b/src/pages/Mission.tsx index 64af650..01814da 100644 --- a/src/pages/Mission.tsx +++ b/src/pages/Mission.tsx @@ -2,10 +2,12 @@ import { useParams, Navigate } from 'react-router-dom'; import CodeEditor from '../views/mission/codeeditor/CodeEditor'; import Statement, { StatementData } from '../views/mission/statement/Statement'; import { PrimaryButton } from '../components/button/PrimaryButton'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { submitMission } from '../redux/slices/submit'; +import { fetchMySubmitsByMission, submitMission } from '../redux/slices/submit'; import { fetchMissionById } from '../redux/slices/missions'; +import Header from '../views/mission/statement/Header'; +import MissionSubmissions from '../views/mission/statement/MissionSubmissions'; const Mission = () => { @@ -15,35 +17,86 @@ const Mission = () => { const { missionId } = useParams<{ missionId: string }>(); const mission = useAppSelector((state) => state.missions.currentMission); const missionIdNumber = Number(missionId); - - const [code, setCode] = useState(""); - const [language, setLanguage] = useState(""); - - // Если missionId нет, редиректим на /home - - // Если missionId нет или не число — редиректим if (!missionId || isNaN(missionIdNumber)) { return ; } + const [code, setCode] = useState(""); + const [language, setLanguage] = useState(""); + + const pollingRef = useRef(null); + const submissions = useAppSelector((state) => state.submin.submitsById[missionIdNumber] || []); + useEffect(() => { dispatch(fetchMissionById(missionIdNumber)); + dispatch(fetchMySubmitsByMission(missionIdNumber)); + }, [missionIdNumber]); + + useEffect(() => { + return () => { + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + }; }, []); -if (!mission || !mission.statements || mission.statements.length === 0) { - return
Загрузка или миссия не найдена...
; -} + + useEffect(() => { + if (submissions.length === 0) return; + + const hasWaiting = submissions.some( + s => s.solution.status === "Waiting" || s.solution.testerState === "Waiting" + ); + + if (hasWaiting) { + startPolling(); + } + }, [submissions]); + + + if (!mission || !mission.statements || mission.statements.length === 0) { + return
Загрузка...
; + } -const statementRaw = mission.statements[0]; + interface StatementData { + id: number; + legend?: string; + timeLimit?: number; + output?: string; + input?: string; + sampleTests?: any[]; + name?: string; + memoryLimit?: number; + tags?: string[]; + notes?: string; + html?: string; + mediaFiles?: any[]; + } + let statementData: StatementData = { id: mission.id }; try { - const statementTexts = JSON.parse(statementRaw.statementTexts["problem-properties.json"]); - // console.log(mission); + // 1. Берём первый statement с форматом Latex и языком russian + const latexStatement = mission.statements.find( + (stmt: any) => stmt && stmt.language === "russian" && stmt.format === "Latex" + ); + + // 2. Берём первый statement с форматом Html и языком russian + const htmlStatement = mission.statements.find( + (stmt: any) => stmt && stmt.language === "russian" && stmt.format === "Html" + ); + + if (!latexStatement) throw new Error("Не найден блок Latex на русском"); + if (!htmlStatement) throw new Error("Не найден блок Html на русском"); + + // 3. Парсим данные из problem-properties.json + const statementTexts = JSON.parse(latexStatement.statementTexts["problem-properties.json"]); + statementData = { - id: statementRaw.id, + id: missionIdNumber, legend: statementTexts.legend, timeLimit: statementTexts.timeLimit, output: statementTexts.output, @@ -53,41 +106,81 @@ const statementRaw = mission.statements[0]; memoryLimit: statementTexts.memoryLimit, tags: mission.tags, notes: statementTexts.notes, + html: htmlStatement.statementTexts["problem.html"], + mediaFiles: latexStatement.mediaFiles }; } catch (err) { console.error("Ошибка парсинга statementTexts:", err); } - + + + + + const startPolling = () => { + if (pollingRef.current) + return; + + pollingRef.current = setInterval(async () => { + dispatch(fetchMySubmitsByMission(missionIdNumber)); + + const hasWaiting = submissions.some( + (s: any) => s.solution.status == "Waiting" || s.solution.testerState === "Waiting" + ); + if (!hasWaiting) { + // Всё проверено — стоп + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + } + }, 5000); // 10 секунд + }; + + return ( -
-
+
+
+
-
-
- { setCode(value); }} - onChangeLanguage={((value: string) => { setLanguage(value); })} + +
+
+
-
- { - dispatch(submitMission({ - missionId: missionIdNumber, - language: language, - languageVersion: "latest", - sourceCode: code, - contestId: null, - })) - }} /> +
+
+
+ { setCode(value); }} + onChangeLanguage={((value: string) => { setLanguage(value); })} + /> +
+
+ { + await dispatch(submitMission({ + missionId: missionIdNumber, + language: language, + languageVersion: "latest", + sourceCode: code, + contestId: null, + + })).unwrap(); + dispatch(fetchMySubmitsByMission(missionIdNumber)); + }} /> +
+ +
+ +
+
-
); }; diff --git a/src/redux/slices/auth.ts b/src/redux/slices/auth.ts index cd40719..41a31ce 100644 --- a/src/redux/slices/auth.ts +++ b/src/redux/slices/auth.ts @@ -77,6 +77,22 @@ export const fetchWhoAmI = createAsyncThunk( } ); +// AsyncThunk: Загрузка токенов из localStorage +export const loadTokensFromLocalStorage = createAsyncThunk( + "auth/loadTokens", + async (_, { dispatch }) => { + const jwt = localStorage.getItem("jwt"); + const refreshToken = localStorage.getItem("refreshToken"); + + if (jwt && refreshToken) { + axios.defaults.headers.common['Authorization'] = `Bearer ${jwt}`; + return { jwt, refreshToken }; + } else { + return { jwt: null, refreshToken: null }; + } + } +); + // Slice const authSlice = createSlice({ name: "auth", @@ -88,6 +104,9 @@ const authSlice = createSlice({ state.username = null; state.status = "idle"; state.error = null; + localStorage.removeItem("jwt"); + localStorage.removeItem("refreshToken"); + delete axios.defaults.headers.common['Authorization']; }, }, extraReducers: (builder) => { @@ -98,9 +117,11 @@ const authSlice = createSlice({ }); builder.addCase(registerUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => { state.status = "successful"; - axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`; state.jwt = action.payload.jwt; state.refreshToken = action.payload.refreshToken; + axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`; + localStorage.setItem("jwt", action.payload.jwt); + localStorage.setItem("refreshToken", action.payload.refreshToken); }); builder.addCase(registerUser.rejected, (state, action: PayloadAction) => { state.status = "failed"; @@ -114,9 +135,11 @@ const authSlice = createSlice({ }); builder.addCase(loginUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => { state.status = "successful"; - axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`; state.jwt = action.payload.jwt; state.refreshToken = action.payload.refreshToken; + axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`; + localStorage.setItem("jwt", action.payload.jwt); + localStorage.setItem("refreshToken", action.payload.refreshToken); }); builder.addCase(loginUser.rejected, (state, action: PayloadAction) => { state.status = "failed"; @@ -150,6 +173,15 @@ const authSlice = createSlice({ state.status = "failed"; state.error = action.payload; }); + + // Загрузка токенов из localStorage + builder.addCase(loadTokensFromLocalStorage.fulfilled, (state, action: PayloadAction<{ jwt: string | null; refreshToken: string | null }>) => { + state.jwt = action.payload.jwt; + state.refreshToken = action.payload.refreshToken; + if (action.payload.jwt) { + axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`; + } + }); }, }); diff --git a/src/redux/slices/missions.ts b/src/redux/slices/missions.ts index 8beb53d..ee1bcb8 100644 --- a/src/redux/slices/missions.ts +++ b/src/redux/slices/missions.ts @@ -17,7 +17,7 @@ interface Mission { tags: string[]; createdAt: string; updatedAt: string; - statements: Statement[] | null; + statements?: Statement[]; } interface MissionsState { @@ -120,7 +120,6 @@ const missionsSlice = createSlice({ }); builder.addCase(fetchMissionById.fulfilled, (state, action: PayloadAction) => { state.status = "successful"; - console.log(action.payload); state.currentMission = action.payload; }); builder.addCase(fetchMissionById.rejected, (state, action: PayloadAction) => { diff --git a/src/redux/slices/submit.ts b/src/redux/slices/submit.ts index e970497..fbe3265 100644 --- a/src/redux/slices/submit.ts +++ b/src/redux/slices/submit.ts @@ -11,17 +11,33 @@ export interface Submit { contestId: number | null; } -export interface SubmitStatus { - SubmitId: number; - State: string; - ErrorCode: string; - Message: string; - CurrentTest: number; - AmountOfTests: number; +export interface Solution { + id: number; + missionId: number; + language: string; + languageVersion: string; + sourceCode: string; + status: string; + time: string; + testerState: string; + testerErrorCode: string; + testerMessage: string; + currentTest: number; + amountOfTests: number; +} + +export interface MissionSubmit { + id: number; + userId: number; + solution: Solution; + contestId: number | null; + contestName: string | null; + sourceType: string; } interface SubmitState { submits: Submit[]; + submitsById: Record; // ✅ добавлено currentSubmit?: Submit; status: "idle" | "loading" | "successful" | "failed"; error: string | null; @@ -30,6 +46,7 @@ interface SubmitState { // Начальное состояние const initialState: SubmitState = { submits: [], + submitsById: {}, // ✅ инициализация currentSubmit: undefined, status: "idle", error: null, @@ -74,13 +91,13 @@ export const fetchSubmitById = createAsyncThunk( } ); -// AsyncThunk: Получить свои отправки для конкретной миссии +// ✅ AsyncThunk: Получить отправки для конкретной миссии (новая структура) export const fetchMySubmitsByMission = createAsyncThunk( "submit/fetchMySubmitsByMission", async (missionId: number, { rejectWithValue }) => { try { const response = await axios.get(`/submits/my/mission/${missionId}`); - return response.data as Submit[]; + return { missionId, data: response.data as MissionSubmit[] }; } catch (err: any) { return rejectWithValue(err.response?.data?.message || "Failed to fetch mission submits"); } @@ -97,6 +114,9 @@ const submitSlice = createSlice({ state.status = "idle"; state.error = null; }, + clearSubmitsByMission: (state, action: PayloadAction) => { + delete state.submitsById[action.payload]; + }, }, extraReducers: (builder) => { // Отправка решения @@ -141,15 +161,18 @@ const submitSlice = createSlice({ state.error = action.payload; }); - // Получить отправки по миссии + // ✅ Получить отправки по миссии builder.addCase(fetchMySubmitsByMission.pending, (state) => { state.status = "loading"; state.error = null; }); - builder.addCase(fetchMySubmitsByMission.fulfilled, (state, action: PayloadAction) => { - state.status = "successful"; - state.submits = action.payload; - }); + builder.addCase( + fetchMySubmitsByMission.fulfilled, + (state, action: PayloadAction<{ missionId: number; data: MissionSubmit[] }>) => { + state.status = "successful"; + state.submitsById[action.payload.missionId] = action.payload.data; + } + ); builder.addCase(fetchMySubmitsByMission.rejected, (state, action: PayloadAction) => { state.status = "failed"; state.error = action.payload; @@ -157,5 +180,5 @@ const submitSlice = createSlice({ }, }); -export const { clearCurrentSubmit } = submitSlice.actions; +export const { clearCurrentSubmit, clearSubmitsByMission } = submitSlice.actions; export const submitReducer = submitSlice.reducer; diff --git a/src/styles/index.css b/src/styles/index.css index ecfc61b..2063dc2 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -2,6 +2,8 @@ @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; +@import "./latex-container.css"; + * { -webkit-tap-highlight-color: transparent; /* Отключаем выделение синим при тапе на телефоне*/ /* outline: 1px solid green; */ @@ -26,7 +28,7 @@ #root { width: 100%; - height: 100%; + height: 100vh; } body { @@ -39,7 +41,6 @@ body { } - /* Общий контейнер полосы прокрутки */ .thin-scrollbar::-webkit-scrollbar { width: 4px; /* ширина вертикального */ @@ -77,6 +78,25 @@ body { +/* Общий контейнер полосы прокрутки */ +.thin-dark-scrollbar::-webkit-scrollbar { + width: 4px; /* ширина вертикального */ +} + +/* Трек (фон) */ +.thin-dark-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +/* Ползунок (thumb) */ +.thin-dark-scrollbar::-webkit-scrollbar-thumb { + background: var(--color-liquid-lighter); + border-radius: 1000px; + cursor: pointer; +} + + + html { scrollbar-gutter: stable; diff --git a/src/styles/latex-container.css b/src/styles/latex-container.css new file mode 100644 index 0000000..600b047 --- /dev/null +++ b/src/styles/latex-container.css @@ -0,0 +1,26 @@ + +.latex-container p { + text-align: justify; /* выравнивание по ширине */ + text-justify: inter-word; + margin-bottom: 0.8em; /* небольшой отступ между абзацами */ + line-height: 1.2; + /* text-indent: 1em; */ +} + +.latex-container ol { + padding-left: 1.5em; /* отступ для нумерации */ + margin: 0.5em 0; /* небольшой отступ сверху и снизу */ + line-height: 1.5; /* удобный межстрочный интервал */ + font-family: "Inter", sans-serif; + font-size: 1rem; +} + +.latex-container ol li { + margin-bottom: 0.4em; /* расстояние между пунктами */ +} + +.latex-container .section-title{ + font-size: 16px; + font-weight: bold; +} + diff --git a/src/views/home/articles/ArticleItem.tsx b/src/views/home/articles/ArticleItem.tsx index a7dfdc5..9676fab 100644 --- a/src/views/home/articles/ArticleItem.tsx +++ b/src/views/home/articles/ArticleItem.tsx @@ -9,7 +9,6 @@ export interface ArticleItemProps { const ArticleItem: React.FC = ({ id, name, tags }) => { - console.log(id); return (
{ // После успешного логина useEffect(() => { - console.log(submitClicked); dispatch(setMenuActivePage("account")) }, []); @@ -37,7 +36,6 @@ const Login = () => { const handleLogin = () => { // setErr(err == "" ? "Неверная почта и/или пароль" : ""); - // console.log(123); setSubmitClicked(true); if (!username || !password) return; diff --git a/src/views/home/auth/Register.tsx b/src/views/home/auth/Register.tsx index 38f0e12..4f39ef7 100644 --- a/src/views/home/auth/Register.tsx +++ b/src/views/home/auth/Register.tsx @@ -28,7 +28,6 @@ const Register = () => { // После успешной регистрации — переход в систему useEffect(() => { - console.log(submitClicked); dispatch(setMenuActivePage("account")) }, []); @@ -71,7 +70,7 @@ const Register = () => {
{ console.log(value) }} + onChange={(value: boolean) => { value; }} className="p-0 w-fit m-[2.75px]" size="md" color="secondary" diff --git a/src/views/home/contests/ContestsBlock.tsx b/src/views/home/contests/ContestsBlock.tsx index 63a5d32..ec14e55 100644 --- a/src/views/home/contests/ContestsBlock.tsx +++ b/src/views/home/contests/ContestsBlock.tsx @@ -37,7 +37,6 @@ const GroupsBlock: FC = ({ contests, title, className }) => { active && "border-b-liquid-lighter" )} onClick={() => { - console.log(active); setActive(!active) }}> {title} diff --git a/src/views/home/groups/GroupItem.tsx b/src/views/home/groups/GroupItem.tsx index eafd491..d4821d4 100644 --- a/src/views/home/groups/GroupItem.tsx +++ b/src/views/home/groups/GroupItem.tsx @@ -26,7 +26,6 @@ const IconComponent: React.FC = ({ const GroupItem: React.FC = ({ id, name, visible, role }) => { - console.log(id); return (
diff --git a/src/views/home/groups/GroupsBlock.tsx b/src/views/home/groups/GroupsBlock.tsx index fd4e2e1..05e7e76 100644 --- a/src/views/home/groups/GroupsBlock.tsx +++ b/src/views/home/groups/GroupsBlock.tsx @@ -33,7 +33,6 @@ const GroupsBlock: FC = ({ groups, title, className }) => { active && " border-b-liquid-lighter" )} onClick={() => { - console.log(active); setActive(!active) }}> {title} diff --git a/src/views/mission/codeeditor/CodeEditor.tsx b/src/views/mission/codeeditor/CodeEditor.tsx index cd048c4..a9133d9 100644 --- a/src/views/mission/codeeditor/CodeEditor.tsx +++ b/src/views/mission/codeeditor/CodeEditor.tsx @@ -20,14 +20,14 @@ export interface CodeEditorProps { } const CodeEditor: React.FC = ({onChange, onChangeLanguage}) => { - const [language, setLanguage] = useState("cpp"); + const [language, setLanguage] = useState("C++"); const [code, setCode] = useState(""); const [isDragging, setIsDragging] = useState(false); const items = [ { value: "c", text: "C" }, - { value: "cpp", text: "C++" }, + { value: "C++", text: "C++" }, { value: "java", text: "Java" }, { value: "python", text: "Python" }, { value: "pascal", text: "Pascal" }, @@ -88,7 +88,7 @@ const CodeEditor: React.FC = ({onChange, onChangeLanguage}) => {/* Панель выбора языка и загрузки файла */}
- { setLanguage(v) }} /> + { setLanguage(v) }} defaultState={{ value: "C++", text: "C++" }}/>