diff --git a/package.json b/package.json
index 5cdbb48..94150b3 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"@tanstack/react-query": "^5.66.7",
"axios": "^1.7.9",
"jsonwebtoken": "^9.0.2",
+ "katex": "^0.16.21",
"next": "^15.1.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 536ab0b..e8ab973 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -29,6 +29,9 @@ dependencies:
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
+ katex:
+ specifier: ^0.16.21
+ version: 0.16.21
next:
specifier: ^15.1.7
version: 15.1.7(react-dom@19.0.0)(react@19.0.0)
@@ -867,7 +870,6 @@ packages:
/commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
- dev: true
/compare-versions@4.1.4:
resolution: {integrity: sha512-FemMreK9xNyL8gQevsdRMrvO4lFCkQP7qbuktn1q8ndcNk1+0mz7lgE7b/sNvbhVgY4w6tMN1FDp6aADjqw2rw==}
@@ -1413,6 +1415,13 @@ packages:
safe-buffer: 5.2.1
dev: false
+ /katex@0.16.21:
+ resolution: {integrity: sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==}
+ hasBin: true
+ dependencies:
+ commander: 8.3.0
+ dev: false
+
/klona@2.0.6:
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
engines: {node: '>= 8'}
diff --git a/proto b/proto
index ea2a76c..16781a4 160000
--- a/proto
+++ b/proto
@@ -1 +1 @@
-Subproject commit ea2a76c1f4001bba8405a5a447f085831dc5cf18
+Subproject commit 16781a46412eea455f27372045c216126c39d628
diff --git a/src/app/contests/[contest_id]/tasks/[task_id]/page.tsx b/src/app/contests/[contest_id]/tasks/[task_id]/page.tsx
index ed05216..80b6f43 100644
--- a/src/app/contests/[contest_id]/tasks/[task_id]/page.tsx
+++ b/src/app/contests/[contest_id]/tasks/[task_id]/page.tsx
@@ -1,142 +1 @@
-"use client";
-
-import React from 'react';
-import {Anchor, Group, SegmentedControl, Stack, Text, Title} from "@mantine/core";
-import {Header} from "@/components/header";
-import {Code} from "@/components/code";
-import Link from "next/link";
-
-type PageProps = {
- params: {
- task_id: string
- }
-}
-
-const problems = [
- "A. Сумма двух чисел",
- "B. Разность двух чисел",
- "C. Театральная площадь"
-]
-
-const problem = {
- legend: `
Эта задача немного необычна — в ней вам предстоит реализовать
-интерактивное взаимодействие с тестирующей системой. Это означает, что
-вы можете делать запросы и получать ответы в online-режиме. Обратите
-внимание, что ввод/вывод в этой задаче — стандартный (то есть с экрана
-на экран). После вывода очередного запроса обязательно используйте
-функции очистки потока, чтобы часть вашего вывода не осталась в
-каком-нибудь буфере. Например, на С++ надо использовать функцию
-fflush(stdout)
, на Java вызов
-System.out.flush()
, на Pascal flush(output)
и
-stdout.flush()
для языка Python.
-В этой задаче вам предстоит в интерактивном режиме угадать число
-x , которое загадала
-тестирующая система. Про загаданное число x известно, что оно целое и лежит в
-границах от 1 до n включительно (значение n известно заранее).
-Вы можете делать запросы к тестирующей системе, каждый запрос — это
-вывод одного целого числа от 1 до n . Есть два варианта ответа
-тестирующей системы на запрос:
-
-строка <<<
>> (без кавычек), если
-загаданное число меньше числа из запроса;
-строка <<>=
>> (без кавычек), если
-загаданное число больше либо равно числу из запроса.
-
-В случае, если ваша программа наверняка угадала нужное число x , выведите строку вида
-<<! x
>>, где x — это ответ, и завершите работу
-своей программы.
-Вашей программе разрешается сделать не более 25 запросов.
`,
- input: `Для чтения ответов на запросы программа должна использовать
-стандартный ввод.
-В первой строке входных данных будет содержаться целое положительное
-число n (1 ≤ n ≤ 106 ) —
-максимально возможное число, которое может быть загадано.
-В следующих строках на вход вашей программе будут подаваться строки
-вида <<<
>> и
-<<>=
>>. i -я из этих строк является ответом
-системы на ваш i -й запрос.
-После того, как ваша программа угадала число, выведите
-<<! x
>> (без кавычек), где x — это ответ, и завершите работу
-своей программы.
-Тестирующая система даст вашей программе прочитать ответ на запрос из
-входных данных только после того, как ваша программа вывела
-соответствующий запрос системе и выполнила операцию
-flush
.
`,
- name: `Отгадай число
`,
- output: `Для осуществления запросов программа должна использовать стандартный
-вывод.
-Ваша программа должна выводить запросы — целые числа x i (1 ≤ x i ≤ n )
-по одному в строке (не забывайте выводить <<перевод
-строки >> после каждого значения x i ). После
-вывода каждой строки программа должна выполнить операцию
-flush
.
-Каждое из значений x i обозначает
-очередной запрос к системе. Ответ на запрос программа сможет прочесть из
-стандартного ввода. В случае, если ваша программа угадала число x , выведите строку вида
-<<! x
>> (без кавычек), где x — ответ, и завершите работу
-программы.
`
-}
-
-const Page = ({params}: PageProps) => {
- return (
- <>
-
-
-
-
-
-
- Простой контест с длинным названием из восьми слов
-
-
-
-
-
-
-
-
-
-
-
-
-
- Последние посылки
-
- (посмотреть все)
- :
-
-
-
-
- >
- );
-};
-
-export default Page;
+export {TaskPage as default, generateMetadata} from "@/plain-pages/task";
\ No newline at end of file
diff --git a/src/app/problems/[problem_id]/edit/page.tsx b/src/app/problems/[problem_id]/edit/page.tsx
new file mode 100644
index 0000000..2e9e5f4
--- /dev/null
+++ b/src/app/problems/[problem_id]/edit/page.tsx
@@ -0,0 +1 @@
+export {ProblemEditPage as default, generateMetadata} from "@/plain-pages/problem-edit";
\ No newline at end of file
diff --git a/src/app/problems/[problem_id]/page.tsx b/src/app/problems/[problem_id]/page.tsx
index aac54c9..f2df577 100644
--- a/src/app/problems/[problem_id]/page.tsx
+++ b/src/app/problems/[problem_id]/page.tsx
@@ -1,24 +1 @@
-import React from 'react';
-import {AppShell, AppShellHeader, AppShellMain} from "@mantine/core";
-import {Header} from "@/components/header";
-
-type PageProps = {
- params: {
- problem_id: number
- }
-}
-
-const Page = ({params}: PageProps) => {
- return (
-
-
-
-
-
-
-
-
- );
-};
-
-export default Page;
+export {ProblemPage as default, generateMetadata} from "@/plain-pages/problem";
\ No newline at end of file
diff --git a/src/app/problems/page.tsx b/src/app/problems/page.tsx
index 2be7474..08faaa7 100644
--- a/src/app/problems/page.tsx
+++ b/src/app/problems/page.tsx
@@ -1,108 +1 @@
-import React from 'react';
-import {
- ActionIcon,
- AppShell,
- AppShellAside,
- AppShellHeader,
- AppShellMain,
- Button,
- Group,
- Pagination,
- Stack,
- Table,
- TableTbody,
- TableTd,
- TableTh,
- TableThead,
- TableTr,
- Text,
- TextInput,
- Title
-} from "@mantine/core";
-import {Header} from "@/components/header";
-import Link from "next/link";
-import {IconCheck, IconPencil, IconUser} from "@tabler/icons-react";
-
-export const metadata = {
- title: 'Мастерская',
- description: '',
-};
-
-const problems = [
- {
- id: 1,
- title: "Линейка",
- accepted: 102,
- },
- {
- id: 2,
- title: "Секретное сообщение",
- accepted: 152,
- },
- {
- id: 3,
- title: "Арзуб",
- accepted: 342,
- },
- {
- id: 4,
- title: "Укладка доминошками",
- accepted: 89,
- },
-]
-
-
-const Page = () => {
- const rows = problems.map((problem) => (
-
-
- {problem.title}
-
-
-
-
- {problem.accepted}
-
-
- {
-
- }
-
-
- ));
-
- return (
-
-
-
-
-
-
- Архив задач
-
-
-
- Название
-
-
-
-
- {rows}
-
-
-
-
-
-
-
- Создать контест
- Создать задачу
-
-
-
-
-
- );
-};
-
-export default Page;
+export {ProblemsPage as default, generateMetadata} from "@/plain-pages/problems";
\ No newline at end of file
diff --git a/src/app/solutions/[solution_id]/page.tsx b/src/app/solutions/[solution_id]/page.tsx
new file mode 100644
index 0000000..f2df577
--- /dev/null
+++ b/src/app/solutions/[solution_id]/page.tsx
@@ -0,0 +1 @@
+export {ProblemPage as default, generateMetadata} from "@/plain-pages/problem";
\ No newline at end of file
diff --git a/src/app/solutions/page.tsx b/src/app/solutions/page.tsx
new file mode 100644
index 0000000..b1d612c
--- /dev/null
+++ b/src/app/solutions/page.tsx
@@ -0,0 +1 @@
+export {SolutionsPage as default, generateMetadata} from "@/plain-pages/solutions";
\ No newline at end of file
diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx
index f77584b..e734e1a 100644
--- a/src/components/header/header.tsx
+++ b/src/components/header/header.tsx
@@ -82,9 +82,9 @@ const Header = () => {
const [drawerOpened, {toggle: toggleDrawer, close: closeDrawer}] = useDisclosure(false);
return (
- <>
+
-
+
@@ -134,7 +134,7 @@ const Header = () => {
- >
+
);
}
diff --git a/src/components/problem/index.ts b/src/components/problem/index.ts
new file mode 100644
index 0000000..88113a4
--- /dev/null
+++ b/src/components/problem/index.ts
@@ -0,0 +1 @@
+export * from "./problem";
\ No newline at end of file
diff --git a/src/components/problem/problem.css b/src/components/problem/problem.css
new file mode 100644
index 0000000..5828564
--- /dev/null
+++ b/src/components/problem/problem.css
@@ -0,0 +1,60 @@
+.container {
+ max-width: 60em;
+ padding: 0 10px 10px 10px;
+ hyphens: auto;
+ overflow-wrap: break-word;
+ text-rendering: optimizeLegibility;
+ font-kerning: normal;
+ color: #1a1a1a;
+ background-color: #fdfdfd;
+}
+
+.content {
+ & table {
+ margin: 1em 0;
+ overflow-x: auto;
+ font-variant-numeric: lining-nums tabular-nums;
+ }
+
+ & table & caption {
+ margin-bottom: 0.75em;
+ }
+
+ & table, & th, & td {
+ border: 1px solid black;
+ border-collapse: collapse;
+ }
+
+ & th {
+ padding: 0.25em 0.5em 0.25em 0.5em;
+ }
+
+ & td {
+ padding: 0.125em 0.5em 0.25em 0.5em;
+ }
+
+ .center {
+ display: flex;
+ justify-content: center;
+ }
+
+ & ul, & ol {
+ margin: 0;
+ }
+
+ & p {
+ margin: 0.75em;
+ }
+
+ & p {
+ line-height: 1.2;
+ }
+
+ & a {
+ color: #1a1a1a;
+ }
+
+ & a:visited {
+ color: #1a1a1a;
+ }
+}
\ No newline at end of file
diff --git a/src/components/problem/problem.tsx b/src/components/problem/problem.tsx
new file mode 100644
index 0000000..7300c07
--- /dev/null
+++ b/src/components/problem/problem.tsx
@@ -0,0 +1,116 @@
+'use client';
+
+import {Stack, Text, Title} from "@mantine/core";
+import {useEffect, useRef} from 'react';
+import katex from 'katex';
+import 'katex/dist/katex.min.css';
+import './problem.css';
+
+type Props = {
+ problem: {
+ id: number,
+ title: string,
+ time_limit: number,
+ memory_limit: number,
+ legend_html: string,
+ input_format_html: string,
+ output_format_html: string,
+ notes_html: string,
+ scoring_html: string,
+ created_at: string,
+ updated_at: string
+ }
+
+ letter?: string
+}
+
+const prettifyTimeLimit = (time_limit: number) => {
+ if (time_limit % 1000 === 0) {
+ return `${time_limit / 1000} сек`
+ }
+
+ return `${time_limit} мс`
+}
+
+const prettifyMemoryLimit = (memory_limit: number) => {
+ if (memory_limit % 1000 === 0) {
+ return `${memory_limit / 1000} ГБ`
+ }
+
+ return `${memory_limit} МБ`
+}
+
+const Problem = ({problem, letter}: Props) => {
+ letter = letter || 'A';
+
+ const ref = useRef(null);
+
+ useEffect(() => {
+ if (ref.current) {
+ const mathElements = ref.current.querySelectorAll('.math');
+ mathElements.forEach((element) => {
+ if (!element.hasAttribute('data-rendered')) {
+ katex.render(element.textContent || '', element, {
+ throwOnError: false,
+ displayMode: element.classList.contains('display'),
+ });
+ // Помечаем элемент как обработанный
+ element.setAttribute('data-rendered', 'true');
+ }
+ });
+ }
+
+ return () => {
+ if (ref.current) {
+ ref.current.querySelectorAll('.math').forEach((element) => {
+ element.removeAttribute('data-rendered');
+ });
+ }
+ };
+ }, [problem, letter]);
+
+ return (
+
+
+ {letter}. {problem.title}
+
+
+ ограничение по времени: {prettifyTimeLimit(problem.time_limit)}
+
+
+ ограничение по памяти: {prettifyMemoryLimit(problem.memory_limit)}
+
+
+
+ {problem.legend_html && (
+
+ )}
+ {problem.input_format_html && (
+ <>
+ Входные данные
+
+ >
+ )}
+ {problem.output_format_html && (
+ <>
+ Выходные данные
+
+ >
+ )}
+ {problem.scoring_html && (
+ <>
+ Система оценки
+
+ >
+ )}
+ {problem.notes_html && (
+ <>
+ Примечание
+
+ >
+ )}
+
+ );
+}
+
+export {Problem};
\ No newline at end of file
diff --git a/src/plain-pages/contest/ui.tsx b/src/plain-pages/contest/ui.tsx
index c8324e3..7d3c7a0 100644
--- a/src/plain-pages/contest/ui.tsx
+++ b/src/plain-pages/contest/ui.tsx
@@ -23,6 +23,7 @@ import {IconMail, IconTrash} from "@tabler/icons-react";
import {DeleteTask} from "@/shared/api";
import {useMutation} from "@tanstack/react-query";
import {useRouter} from "next/navigation";
+import {numberToLetters} from "@/shared/lib";
type PageProps = {
contest: Contest,
@@ -48,10 +49,10 @@ const Page = (props: PageProps) => {
c="var(--mantine-link-color)"
underline="always"
>
- {`${task.task.position + 1}. ${task.task.title}`}
+ {`${numberToLetters(task.task.position)}. ${task.task.title}`}
- {task.best_solution.total_score}
+ {task.solution.score}
Мои посылки
@@ -66,12 +67,10 @@ const Page = (props: PageProps) => {
))
return (
-
-
+ <>
+
-
+
{props.contest.title}
@@ -95,20 +94,20 @@ const Page = (props: PageProps) => {
-
+
Все посылки
-
+
Редактировать контест
-
+
Добавить пользователя
-
+
Добавить задачу
@@ -116,7 +115,7 @@ const Page = (props: PageProps) => {
-
+ >
);
};
diff --git a/src/plain-pages/contests/page.tsx b/src/plain-pages/contests/page.tsx
index 9567966..f3f6a44 100644
--- a/src/plain-pages/contests/page.tsx
+++ b/src/plain-pages/contests/page.tsx
@@ -18,10 +18,10 @@ type Props = {
const Page = async (props: Props) => {
const page = (await props.searchParams).page || 1;
- const response = await ListContests(page, 10);
+ const contestsList = await ListContests(page, 10);
return (
-
+
)
}
diff --git a/src/plain-pages/contests/ui.tsx b/src/plain-pages/contests/ui.tsx
index b46ce5a..de5be00 100644
--- a/src/plain-pages/contests/ui.tsx
+++ b/src/plain-pages/contests/ui.tsx
@@ -2,12 +2,8 @@
import React from 'react';
import {
- AppShell,
- AppShellAside,
- AppShellHeader,
- AppShellMain,
Button,
- Checkbox,
+ Group,
Pagination,
Stack,
Table,
@@ -16,52 +12,44 @@ import {
TableTh,
TableThead,
TableTr,
- TextInput,
Title
} from "@mantine/core";
import {Header} from "@/components/header";
import Link from "next/link";
-import {ContestsListItem} from "../../../proto/tester/v1/api";
+import * as testerv1 from "../../../proto/tester/v1/api";
type Props = {
- contests: ContestsListItem[],
- page: number,
- max_page: number
+ contests: testerv1.ContestsListItem[],
+ pagination: testerv1.Pagination,
}
-// const contests = [
-// {
-// id: 1,
-// title: "Соревнование 1",
-// startsAt: (new Date()).toISOString(),
-// duration: "02:00",
-// started: true
-// },
-// {
-// id: 2,
-// title: "Соревнование 2",
-// startsAt: (new Date()).toISOString(),
-// duration: "02:00",
-// started: false
-// },
-// {
-// id: 3,
-// title: "Соревнование 3",
-// startsAt: (new Date()).toISOString(),
-// duration: "02:00",
-// started: false
-// },
-// {
-// id: 4,
-// title: "Соревнование 4",
-// startsAt: (new Date()).toISOString(),
-// duration: "02:00",
-// started: false
-// }
-// ]
+const Page = ({contests, pagination}: Props) => {
+ const getItemProps = (page) => ({
+ component: Link,
+ href: `/contests?page=${page}`,
+ });
-const Page = (props: Props) => {
- const rows = props.contests.map((contest) => (
+ const getControlProps = (control) => {
+ if (control === 'next') {
+ if (pagination.page === pagination.total) {
+ return {component: Link, href: `/contests?page=${pagination.page}`};
+ }
+
+ return {component: Link, href: `/contests?page=${+pagination.page + 1}`};
+ }
+
+ if (control === 'previous') {
+ if (pagination.page === 1) {
+ return {component: Link, href: `/contests?page=${pagination.page}`};
+ }
+ return {component: Link, href: `/contests?page=${+pagination.page - 1}`};
+ }
+
+ return {};
+ };
+
+
+ const rows = contests.map((contest) => (
{contest.title}
@@ -86,58 +74,38 @@ const Page = (props: Props) => {
));
return (
-
-
-
-
-
-
- Контесты
-
-
-
- Название
- {/*Начало */}
- {/*Длительность */}
-
-
-
- {rows}
-
- ({
- component: Link,
- href: `/contests?page=${page}`,
- })}
- getControlProps={(control) => {
- if (control === 'next') {
- if (props.page === props.max_page) {
- return {component: Link, href: `/contests?page=${props.page}`};
- }
-
- return {component: Link, href: `/contests?page=${+props.page + 1}`};
- }
-
- if (control === 'previous') {
- if (props.page === 1) {
- return {component: Link, href: `/contests?page=${props.page}`};
- }
- return {component: Link, href: `/contests?page=${+props.page - 1}`};
- }
-
- return {};
- }}
- />
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
+ Контесты
+
+
+
+ Название
+ {/*Начало */}
+ {/*Длительность */}
+
+
+
+ {rows}
+
+
+
+
+
+ {/**/}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ >
);
};
diff --git a/src/plain-pages/login/ui.tsx b/src/plain-pages/login/ui.tsx
index 84d9e0a..8b32c82 100644
--- a/src/plain-pages/login/ui.tsx
+++ b/src/plain-pages/login/ui.tsx
@@ -1,16 +1,6 @@
"use client";
-import {
- AppShell,
- AppShellHeader,
- AppShellMain,
- Button,
- Image,
- PasswordInput,
- Stack,
- TextInput,
- Title
-} from "@mantine/core";
+import {Button, Image, PasswordInput, Stack, TextInput, Title} from "@mantine/core";
import {Header} from "@/components/header";
import React from "react";
import {useForm} from "@mantine/form";
@@ -56,11 +46,9 @@ const Page = () => {
}
return (
-
-
-
-
-
+ <>
+
+
-
-
+
+ >
);
};
diff --git a/src/plain-pages/new-user/ui.tsx b/src/plain-pages/new-user/ui.tsx
index 1c278e6..a7f1c0d 100644
--- a/src/plain-pages/new-user/ui.tsx
+++ b/src/plain-pages/new-user/ui.tsx
@@ -1,6 +1,6 @@
"use client";
-import {AppShell, AppShellHeader, AppShellMain, Button, PasswordInput, Stack, TextInput, Title} from "@mantine/core";
+import {Button, PasswordInput, Stack, TextInput, Title} from "@mantine/core";
import {Header} from "@/components/header";
import React from "react";
import {useForm} from "@mantine/form";
@@ -40,14 +40,10 @@ const Page = () => {
}
return (
-
-
-
-
-
-
-
+
+ >
);
};
diff --git a/src/plain-pages/problem-edit/index.ts b/src/plain-pages/problem-edit/index.ts
new file mode 100644
index 0000000..cc2206f
--- /dev/null
+++ b/src/plain-pages/problem-edit/index.ts
@@ -0,0 +1 @@
+export * from "./page"
\ No newline at end of file
diff --git a/src/plain-pages/problem-edit/page.tsx b/src/plain-pages/problem-edit/page.tsx
new file mode 100644
index 0000000..d94bc7a
--- /dev/null
+++ b/src/plain-pages/problem-edit/page.tsx
@@ -0,0 +1,33 @@
+'use server';
+
+import React from 'react';
+import {Metadata} from "next";
+import {ClientPage} from "./ui";
+import {GetProblem} from "@/shared/api";
+
+type Props = {
+ params: Promise<{ problem_id: number }>
+}
+
+const generateMetadata = async (props: Props): Promise => {
+ const problem_id = (await props.params).problem_id;
+
+ const problem = await GetProblem(problem_id);
+
+ return {
+ title: problem.problem.title,
+ description: '',
+ };
+}
+
+const Page = async (props: Props) => {
+ const problem_id = (await props.params).problem_id;
+
+ const problem = await GetProblem(problem_id);
+
+ return (
+
+ )
+};
+
+export {Page as ProblemEditPage, generateMetadata};
diff --git a/src/plain-pages/problem-edit/ui.tsx b/src/plain-pages/problem-edit/ui.tsx
new file mode 100644
index 0000000..e200fac
--- /dev/null
+++ b/src/plain-pages/problem-edit/ui.tsx
@@ -0,0 +1,136 @@
+'use client';
+
+import React from 'react';
+import {Button, Group, NumberInput, Stack, Textarea, TextInput, Title} from "@mantine/core";
+import {Header} from "@/components/header";
+import * as testerv1 from "../../../proto/tester/v1/api";
+import {useForm} from "@mantine/form";
+import {useMutation} from "@tanstack/react-query";
+import {UpdateProblem} from "@/shared/api";
+import {useRouter} from "next/navigation";
+
+type Props = {
+ problem: testerv1.Problem
+}
+
+const ClientPage = ({problem}: Props) => {
+ const router = useRouter();
+
+ const form = useForm({
+ initialValues: {
+ title: problem.title,
+ time_limit: problem.time_limit,
+ memory_limit: problem.memory_limit,
+ legend: problem.legend,
+ input_format: problem.input_format,
+ output_format: problem.output_format,
+ notes: problem.notes,
+ scoring: problem.scoring
+ },
+ });
+
+ const mutation = useMutation({
+ mutationFn: async (data: any) => {
+ return await UpdateProblem(problem.id, data);
+ },
+ onSuccess: async () => {
+ form.resetDirty();
+ await router.refresh();
+ },
+ onError: (error) => {
+ form.clearErrors();
+ },
+ retry: false
+ });
+
+ const onSubmit = (event) => {
+ event.preventDefault();
+ mutation.mutate(form.getValues())
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ Сохранить
+
+
+
+ {problem.title}
+
+ Ограничения
+
+
+
+
+ Условие
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export {ClientPage};
\ No newline at end of file
diff --git a/src/plain-pages/problem/index.ts b/src/plain-pages/problem/index.ts
new file mode 100644
index 0000000..cc2206f
--- /dev/null
+++ b/src/plain-pages/problem/index.ts
@@ -0,0 +1 @@
+export * from "./page"
\ No newline at end of file
diff --git a/src/plain-pages/problem/page.tsx b/src/plain-pages/problem/page.tsx
new file mode 100644
index 0000000..be941a8
--- /dev/null
+++ b/src/plain-pages/problem/page.tsx
@@ -0,0 +1,33 @@
+'use server';
+
+import React from 'react';
+import {Metadata} from "next";
+import {ClientPage} from "./ui";
+import {GetProblem} from "@/shared/api";
+
+type Props = {
+ params: Promise<{ problem_id: number }>
+}
+
+const generateMetadata = async (props: Props): Promise => {
+ const problem_id = (await props.params).problem_id;
+
+ const problem = await GetProblem(problem_id);
+
+ return {
+ title: `A. ${problem.problem.title}`,
+ description: '',
+ };
+}
+
+const Page = async (props: Props) => {
+ const problem_id = (await props.params).problem_id;
+
+ const problem = await GetProblem(problem_id);
+
+ return (
+
+ )
+};
+
+export {Page as ProblemPage, generateMetadata};
diff --git a/src/plain-pages/problem/ui.tsx b/src/plain-pages/problem/ui.tsx
new file mode 100644
index 0000000..58a6353
--- /dev/null
+++ b/src/plain-pages/problem/ui.tsx
@@ -0,0 +1,34 @@
+'use client';
+
+import React from 'react';
+import {Button, Group, Stack} from "@mantine/core";
+import {Header} from "@/components/header";
+import * as testerv1 from "../../../proto/tester/v1/api";
+import {Problem} from "@/components/problem";
+import Link from "next/link";
+
+type Props = {
+ problem: testerv1.Problem
+}
+
+const ClientPage = ({problem}: Props) => {
+ return (
+ <>
+
+
+
+
+
+ Редактировать
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export {ClientPage};
\ No newline at end of file
diff --git a/src/plain-pages/problems/index.ts b/src/plain-pages/problems/index.ts
new file mode 100644
index 0000000..4e27354
--- /dev/null
+++ b/src/plain-pages/problems/index.ts
@@ -0,0 +1 @@
+export * from "./page";
\ No newline at end of file
diff --git a/src/plain-pages/problems/page.tsx b/src/plain-pages/problems/page.tsx
new file mode 100644
index 0000000..840ee2d
--- /dev/null
+++ b/src/plain-pages/problems/page.tsx
@@ -0,0 +1,30 @@
+'use server';
+
+import React from 'react';
+import {Metadata} from "next";
+import {ClientPage} from "./ui";
+import {ListProblems} from "@/shared/api";
+
+const generateMetadata = async (): Promise => {
+ return {
+ title: 'Мастерская',
+ description: '',
+ };
+}
+
+type Props = {
+ searchParams: Promise<{ page: number }>
+}
+
+
+const Page = async (props: Props) => {
+ const page = (await props.searchParams).page || 1;
+
+ const problemsList = await ListProblems(page, 10);
+
+ return (
+
+ )
+};
+
+export {Page as ProblemsPage, generateMetadata};
diff --git a/src/plain-pages/problems/ui.tsx b/src/plain-pages/problems/ui.tsx
new file mode 100644
index 0000000..0517da3
--- /dev/null
+++ b/src/plain-pages/problems/ui.tsx
@@ -0,0 +1,119 @@
+'use client';
+
+import React from 'react';
+import {
+ ActionIcon,
+ AppShellAside,
+ Button,
+ Group,
+ Pagination,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ TextInput,
+ Title
+} from "@mantine/core";
+import {IconCheck, IconPencil, IconUser} from "@tabler/icons-react";
+import Link from "next/link";
+import {Header} from "@/components/header";
+import * as testerv1 from "../../../proto/tester/v1/api";
+
+type Props = {
+ problems: testerv1.ProblemsListItem[],
+ pagination: testerv1.Pagination,
+}
+
+const ClientPage = ({problems, pagination}: Props) => {
+ const getItemProps = (page) => ({
+ component: Link,
+ href: `/problems?page=${page}`,
+ });
+
+ const getControlProps = (control) => {
+ if (control === 'next') {
+ if (pagination.page === pagination.total) {
+ return {component: Link, href: `/problems?page=${pagination.page}`};
+ }
+
+ return {component: Link, href: `/problems?page=${+pagination.page + 1}`};
+ }
+
+ if (control === 'previous') {
+ if (pagination.page === 1) {
+ return {component: Link, href: `/problems?page=${pagination.page}`};
+ }
+ return {component: Link, href: `/problems?page=${+pagination.page - 1}`};
+ }
+
+ return {};
+ };
+
+ const rows = problems.map((problem) => (
+
+
+ {problem.title}
+
+
+
+
+ {123}
+
+
+
+
+
+
+
+
+ ));
+
+ return (
+ <>
+
+
+
+
+ Архив задач
+
+
+
+ Название
+
+
+
+
+ {rows}
+
+
+
+
+
+ {/**/}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* Создать контест */}
+ {/* Создать задачу */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* */}
+ >
+ );
+};
+
+export {ClientPage};
\ No newline at end of file
diff --git a/src/plain-pages/solution/index.ts b/src/plain-pages/solution/index.ts
new file mode 100644
index 0000000..cc2206f
--- /dev/null
+++ b/src/plain-pages/solution/index.ts
@@ -0,0 +1 @@
+export * from "./page"
\ No newline at end of file
diff --git a/src/plain-pages/solution/page.tsx b/src/plain-pages/solution/page.tsx
new file mode 100644
index 0000000..c4dd412
--- /dev/null
+++ b/src/plain-pages/solution/page.tsx
@@ -0,0 +1,35 @@
+'use server';
+
+import React from 'react';
+import {Metadata} from "next";
+import {ClientPage} from "./ui";
+import {GetSolution} from "@/shared/api";
+
+type Props = {
+ params: Promise<{ solution_id: number }>
+}
+
+const generateMetadata = async (props: Props): Promise => {
+ const solutionId = (await props.params).solution_id;
+
+ const solution = await GetSolution(solutionId);
+
+ return {
+ title: `Посылка #${solution.solution.id}`,
+ description: '',
+ };
+}
+
+const Page = async (props: Props) => {
+ const solutionId = (await props.params).solution_id;
+
+ console.log(solutionId);
+
+ const solution = await GetSolution(solutionId);
+
+ return (
+
+ )
+};
+
+export {Page as SolutionPage, generateMetadata};
diff --git a/src/plain-pages/solution/ui.tsx b/src/plain-pages/solution/ui.tsx
new file mode 100644
index 0000000..5542bc7
--- /dev/null
+++ b/src/plain-pages/solution/ui.tsx
@@ -0,0 +1,72 @@
+'use client';
+
+import React from 'react';
+import {Code, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title} from "@mantine/core";
+import {Header} from "@/components/header";
+import * as testerv1 from "../../../proto/tester/v1/api";
+
+type Props = {
+ solution: testerv1.Solution
+}
+
+const ClientPage = ({solution}: Props) => {
+ const rows = [solution].map((solution) => (
+
+
+ {solution.created_at}
+
+
+ user123
+
+
+ C. Арбуз
+
+
+ PyPy 3.12
+
+
+ AC
+
+
+ 91 мс
+
+
+ 4600 КБ
+
+
+ ));
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Когда
+ Кто
+ Задача
+ Язык
+ Вердикт
+ Время
+ Память
+
+
+ {rows}
+
+
+ Код решения
+
+ {solution.solution}
+
+
+
+
+
+ >
+ );
+};
+
+export {ClientPage};
\ No newline at end of file
diff --git a/src/plain-pages/solutions/index.ts b/src/plain-pages/solutions/index.ts
new file mode 100644
index 0000000..cc2206f
--- /dev/null
+++ b/src/plain-pages/solutions/index.ts
@@ -0,0 +1 @@
+export * from "./page"
\ No newline at end of file
diff --git a/src/plain-pages/solutions/page.tsx b/src/plain-pages/solutions/page.tsx
new file mode 100644
index 0000000..0d7d969
--- /dev/null
+++ b/src/plain-pages/solutions/page.tsx
@@ -0,0 +1,30 @@
+'use server';
+
+import React from 'react';
+import {Metadata} from "next";
+import {ClientPage} from "./ui";
+import {ListSolutions} from "@/shared/api";
+
+const generateMetadata = async (): Promise => {
+ return {
+ title: 'Посылки',
+ description: '',
+ };
+}
+
+type Props = {
+ searchParams: Promise<{ page: number }>
+}
+
+
+const Page = async (props: Props) => {
+ const page = (await props.searchParams).page || 1;
+
+ const problemsList = await ListSolutions(page, 10);
+
+ return (
+
+ )
+};
+
+export {Page as SolutionsPage, generateMetadata};
diff --git a/src/plain-pages/solutions/ui.tsx b/src/plain-pages/solutions/ui.tsx
new file mode 100644
index 0000000..89ae9c3
--- /dev/null
+++ b/src/plain-pages/solutions/ui.tsx
@@ -0,0 +1,99 @@
+'use client';
+
+import React from 'react';
+import {Pagination, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title} from "@mantine/core";
+import Link from "next/link";
+import {Header} from "@/components/header";
+import * as testerv1 from "../../../proto/tester/v1/api";
+
+type Props = {
+ solutions: testerv1.SolutionsListItem[],
+ pagination: testerv1.Pagination,
+}
+
+const ClientPage = ({solutions, pagination}: Props) => {
+ const getItemProps = (page) => ({
+ component: Link,
+ href: `/solutions?page=${page}`,
+ });
+
+ const getControlProps = (control) => {
+ if (control === 'next') {
+ if (pagination.page === pagination.total) {
+ return {component: Link, href: `/solutions?page=${pagination.page}`};
+ }
+
+ return {component: Link, href: `/solutions?page=${+pagination.page + 1}`};
+ }
+
+ if (control === 'previous') {
+ if (pagination.page === 1) {
+ return {component: Link, href: `/solutions?page=${pagination.page}`};
+ }
+ return {component: Link, href: `/solutions?page=${+pagination.page - 1}`};
+ }
+
+ return {};
+ };
+
+ const rows = solutions.map((solution) => (
+
+
+ {solution.created_at}
+
+
+ user123
+
+
+ C. Арбуз
+
+
+ PyPy 3.12
+
+
+ AC
+
+
+ 91 мс
+
+
+ 4600 КБ
+
+
+ ));
+
+ return (
+ <>
+
+
+
+
+ Посылки
+
+
+
+ Когда
+ Кто
+ Задача
+ Язык
+ Вердикт
+ Время
+ Память
+
+
+ {rows}
+
+
+
+
+
+ >
+ );
+};
+
+export {ClientPage};
\ No newline at end of file
diff --git a/src/plain-pages/task/index.ts b/src/plain-pages/task/index.ts
new file mode 100644
index 0000000..cc2206f
--- /dev/null
+++ b/src/plain-pages/task/index.ts
@@ -0,0 +1 @@
+export * from "./page"
\ No newline at end of file
diff --git a/src/plain-pages/task/page.tsx b/src/plain-pages/task/page.tsx
new file mode 100644
index 0000000..f7a9c8d
--- /dev/null
+++ b/src/plain-pages/task/page.tsx
@@ -0,0 +1,34 @@
+'use server';
+
+import React from 'react';
+import {Metadata} from "next";
+import {ClientPage} from "./ui";
+import {GetTask} from "@/shared/api";
+import {numberToLetters} from "@/shared/lib";
+
+type Props = {
+ params: Promise<{ task_id: number }>
+}
+
+const generateMetadata = async (props: Props): Promise => {
+ const task_id = (await props.params).task_id;
+
+ const task = await GetTask(task_id);
+
+ return {
+ title: `${numberToLetters(task.task.position)}. ${task.task.title}`,
+ description: '',
+ };
+}
+
+const Page = async (props: Props) => {
+ const task_id = (await props.params).task_id;
+
+ const task = await GetTask(task_id);
+
+ return (
+
+ )
+};
+
+export {Page as TaskPage, generateMetadata};
diff --git a/src/plain-pages/task/ui.tsx b/src/plain-pages/task/ui.tsx
new file mode 100644
index 0000000..2229d81
--- /dev/null
+++ b/src/plain-pages/task/ui.tsx
@@ -0,0 +1,65 @@
+"use client";
+
+import React from 'react';
+import {Anchor, Group, SegmentedControl, Stack, Text, Title} from "@mantine/core";
+import {Header} from "@/components/header";
+import {Code} from "@/components/code";
+import Link from "next/link";
+import * as testerv1 from "../../../proto/tester/v1/api";
+import {Problem} from "@/components/problem";
+import {numberToLetters} from "@/shared/lib";
+
+type PageProps = {
+ tasks: testerv1.TasksListItem[]
+ contest: testerv1.Contest,
+ task: testerv1.Task
+}
+
+
+const Page = ({tasks, contest, task}: PageProps) => {
+ const getTaskData = item => ({
+ label: `${numberToLetters(item.position)}. ${item.title}`,
+ value: item
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+ {contest.title}
+
+
+ item.id === task.id))}
+ />
+
+
+
+
+
+
+ Последние посылки
+
+ (посмотреть все)
+ :
+
+
+
+
+ >
+ );
+};
+
+export {Page as ClientPage};
diff --git a/src/plain-pages/user/ui.tsx b/src/plain-pages/user/ui.tsx
index 37a0970..4b121e2 100644
--- a/src/plain-pages/user/ui.tsx
+++ b/src/plain-pages/user/ui.tsx
@@ -1,7 +1,7 @@
"use client";
import React, {useState} from 'react';
-import {AppShell, AppShellHeader, AppShellMain, Button, Group, Select, Stack, TextInput, Title} from "@mantine/core";
+import {Button, Group, Select, Stack, TextInput, Title} from "@mantine/core";
import {Header} from "@/components/header";
import {User} from "../../../proto/user/v1/api";
import {UpdateUser} from "@/shared/api";
@@ -24,42 +24,42 @@ const ClientPage = (props: Props) => {
const [user, setUser] = useState(props.user);
return (
-
-
-
-
-
-
-
- Профиль пользователя {props.user.username}
-
-
- setUser({...user, username: event.target.value})}
- disabled={!props.canEdit}
- />
- setUser({...user, role: roles.indexOf(event)})}
- disabled={!props.canEdit}
- />
-
- UpdateUser(user.id!, user.role, user.username)}
- >
- Сохранить
-
-
-
-
+ <>
+
+
+
+
+
+ Профиль пользователя {props.user.username}
+
+
+ setUser({...user, username: event.target.value})}
+ disabled={!props.canEdit}
+ />
+ setUser({...user, role: roles.indexOf(event)})}
+ disabled={!props.canEdit}
+ />
+
+ UpdateUser(user.id!, user.role, user.username)}
+ >
+ Сохранить
+
+
+
+
+ >
);
};
diff --git a/src/plain-pages/users/page.tsx b/src/plain-pages/users/page.tsx
index 6d3c192..d0b3bdb 100644
--- a/src/plain-pages/users/page.tsx
+++ b/src/plain-pages/users/page.tsx
@@ -19,10 +19,10 @@ type Props = {
const Page = async (props: Props) => {
const page = (await props.searchParams).page || 1;
- const users = await GetUsers(page, 10);
+ const usersList = await GetUsers(page, 10);
return (
-
+
)
}
diff --git a/src/plain-pages/users/ui.tsx b/src/plain-pages/users/ui.tsx
index 7da7981..e730ba6 100644
--- a/src/plain-pages/users/ui.tsx
+++ b/src/plain-pages/users/ui.tsx
@@ -2,11 +2,6 @@
import {
ActionIcon,
- AppShell,
- AppShellAside,
- AppShellHeader,
- AppShellMain,
- Button,
Pagination,
Stack,
Table,
@@ -15,20 +10,18 @@ import {
TableTh,
TableThead,
TableTr,
- TextInput,
Title
} from "@mantine/core";
import Link from "next/link";
import {IconPencil} from "@tabler/icons-react";
import {Header} from "@/components/header";
import React from "react";
-import {User} from "../../../proto/user/v1/api";
+import * as userv1 from "../../../proto/user/v1/api";
type Props = {
- users: User[],
- page: number
- max_page: number
+ users: userv1.User[],
+ pagination: userv1.Pagination
};
const roles = [
@@ -38,8 +31,32 @@ const roles = [
];
-const ClientPage = (props: Props) => {
- const rows = props.users.map((user) => (
+const ClientPage = ({users, pagination}: Props) => {
+ const getControlProps = (control) => {
+ if (control === 'next') {
+ if (pagination.page === pagination.total) {
+ return {component: Link, href: `/users?page=${pagination.page}`};
+ }
+
+ return {component: Link, href: `/users?page=${+pagination.page + 1}`};
+ }
+
+ if (control === 'previous') {
+ if (pagination.page === 1) {
+ return {component: Link, href: `/users?page=${pagination.page}`};
+ }
+ return {component: Link, href: `/users?page=${+pagination.page - 1}`};
+ }
+
+ return {};
+ }
+
+ const getItemProps = (page) => ({
+ component: Link,
+ href: `/users?page=${page}`,
+ });
+
+ const rows = users.map((user) => (
{user.username}
{/*{user.email} */}
@@ -52,60 +69,41 @@ const ClientPage = (props: Props) => {
));
return (
-
-
-
-
-
-
- Пользователи
-
-
-
- Никнейм
- {/*Почта */}
- Роль
-
-
-
- {rows}
-
- ({
- component: Link,
- href: `/users?page=${page}`,
- })}
- getControlProps={(control) => {
- if (control === 'next') {
- if (props.page === props.max_page) {
- return {component: Link, href: `/users?page=${props.page}`};
- }
-
- return {component: Link, href: `/users?page=${+props.page + 1}`};
- }
-
- if (control === 'previous') {
- if (props.page === 1) {
- return {component: Link, href: `/users?page=${props.page}`};
- }
- return {component: Link, href: `/users?page=${+props.page - 1}`};
- }
-
- return {};
- }}
- />
+ <>
+
+
+
+
+ Пользователи
+
+
+
+ Никнейм
+ {/*Почта */}
+ Роль
+
+
+
+ {rows}
+
+
+
-
-
-
-
-
- Добавить пользователя
-
-
-
-
+
+
+ {/**/}
+ {/* */}
+ {/* */}
+ {/* */}
+ {/* Добавить пользователя*/}
+ {/* */}
+ {/* */}
+ {/* */}
+ >
);
};
diff --git a/src/shared/api/ms-tester.ts b/src/shared/api/ms-tester.ts
index a75a5a9..cbb70c9 100644
--- a/src/shared/api/ms-tester.ts
+++ b/src/shared/api/ms-tester.ts
@@ -1,6 +1,6 @@
"use server";
-import {Configuration, DefaultApi} from "../../../proto/tester/v1/api";
+import {Configuration, DefaultApi, UpdateProblemRequest} from "../../../proto/tester/v1/api";
import {cookies} from "next/headers";
import {AxiosRequestConfig} from "axios";
@@ -68,5 +68,135 @@ export const DeleteTask = async (taskId: number) => {
};
const response = await testerApi.deleteTask(taskId, options);
+ return response.data;
+}
+
+export const ListProblems = async (page: number, pageSize: number) => {
+ const cookieStore = await cookies();
+
+ const session = cookieStore.get(CookieName);
+
+ if (session === undefined) {
+ throw new Error("Session id not found");
+ }
+
+ const options: AxiosRequestConfig = {
+ headers: {
+ 'Authorization': "Bearer " + session.value
+ }
+ };
+
+ const response = await testerApi.listProblems(page, pageSize, options);
+
+ return response.data;
+}
+
+export const ListSolutions = async (page: number, pageSize: number) => {
+ const cookieStore = await cookies();
+
+ const session = cookieStore.get(CookieName);
+
+ if (session === undefined) {
+ throw new Error("Session id not found");
+ }
+
+ const options: AxiosRequestConfig = {
+ headers: {
+ 'Authorization': "Bearer " + session.value
+ }
+ };
+
+ const response = await testerApi.listSolutions(
+ page,
+ pageSize,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ options,
+ );
+
+ return response.data;
+}
+
+export const GetProblem = async (id: number) => {
+ const cookieStore = await cookies();
+
+ const session = cookieStore.get(CookieName);
+
+ if (session === undefined) {
+ throw new Error("Session id not found");
+ }
+
+ const options: AxiosRequestConfig = {
+ headers: {
+ 'Authorization': "Bearer " + session.value
+ }
+ };
+
+ const response = await testerApi.getProblem(id, options);
+
+ return response.data;
+}
+
+export const GetSolution = async (id: number) => {
+ const cookieStore = await cookies();
+
+ const session = cookieStore.get(CookieName);
+
+ if (session === undefined) {
+ throw new Error("Session id not found");
+ }
+
+ const options: AxiosRequestConfig = {
+ headers: {
+ 'Authorization': "Bearer " + session.value
+ }
+ };
+
+ const response = await testerApi.getSolution(id, options);
+
+ return response.data;
+}
+
+export const GetTask = async (id: number) => {
+ const cookieStore = await cookies();
+
+ const session = cookieStore.get(CookieName);
+
+ if (session === undefined) {
+ throw new Error("Session id not found");
+ }
+
+ const options: AxiosRequestConfig = {
+ headers: {
+ 'Authorization': "Bearer " + session.value
+ }
+ };
+
+ const response = await testerApi.getTask(id, options);
+
+ return response.data;
+}
+
+export const UpdateProblem = async (id: number, data: UpdateProblemRequest) => {
+ const cookieStore = await cookies();
+
+ const session = cookieStore.get(CookieName);
+
+ if (session === undefined) {
+ throw new Error("Session id not found");
+ }
+
+ const options: AxiosRequestConfig = {
+ headers: {
+ 'Authorization': "Bearer " + session.value
+ }
+ };
+
+ const response = await testerApi.updateProblem(id, data, options);
+
return response.data;
}
\ No newline at end of file
diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts
new file mode 100644
index 0000000..dacb51b
--- /dev/null
+++ b/src/shared/lib/index.ts
@@ -0,0 +1,17 @@
+function numberToLetters(num: number): string {
+ if (num <= 0) {
+ return '';
+ }
+
+ let result = '';
+ while (num > 0) {
+ const remainder = (num - 1) % 26;
+ const charCode = remainder + 65;
+ result = String.fromCharCode(charCode) + result;
+ num = Math.floor((num - 1) / 26);
+ }
+
+ return result;
+}
+
+export {numberToLetters};
\ No newline at end of file