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: `

Для осуществления запросов программа должна использовать стандартный -вывод.

-

Ваша программа должна выводить запросы — целые числа xi (1 ≤ xi ≤ n) -по одному в строке (не забывайте выводить <<перевод -строки>> после каждого значения xi). После -вывода каждой строки программа должна выполнить операцию -flush.

-

Каждое из значений xi обозначает -очередной запрос к системе. Ответ на запрос программа сможет прочесть из -стандартного ввода. В случае, если ваша программа угадала число 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 ( - <> +
- + Gate logo @@ -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} - - + + ); }; 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} + + Ограничения + + + + + Условие +