diff --git a/package.json b/package.json
index 4c3cb17..5cdbb48 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
- "gen": "openapi-generator-cli generate -g typescript-axios -i ./proto/user/v1/openapi.yaml -o ./proto/user/v1/api"
+ "gen": "openapi-generator-cli generate -g typescript-axios -i ./proto/user/v1/openapi.yaml -o ./proto/user/v1/api && openapi-generator-cli generate -g typescript-axios -i ./proto/tester/v1/openapi.yaml -o ./proto/tester/v1/api"
},
"dependencies": {
"@mantine/core": "^7.17.0",
diff --git a/proto b/proto
index c1b7fd7..ea2a76c 160000
--- a/proto
+++ b/proto
@@ -1 +1 @@
-Subproject commit c1b7fd7a2d32678641ebd3acfe3d5b2eca5d0c72
+Subproject commit ea2a76c1f4001bba8405a5a447f085831dc5cf18
diff --git a/src/app/contests/[contest_id]/page.tsx b/src/app/contests/[contest_id]/page.tsx
index d16334e..e731252 100644
--- a/src/app/contests/[contest_id]/page.tsx
+++ b/src/app/contests/[contest_id]/page.tsx
@@ -1,27 +1 @@
-import React from 'react';
-import {Anchor, AppShell, AppShellHeader, AppShellMain} from "@mantine/core";
-import {Header} from "@/components/header";
-import Link from "next/link";
-
-type PageProps = {
- params: {
- contest_id: number
- }
-}
-
-const Page = ({params}: PageProps) => {
- return (
-
-
-
-
-
-
- Просмотр {params.contest_id}
-
-
-
- );
-};
-
-export default Page;
+export {ContestPage as default, generateMetadata} from "@/plain-pages/contest"
diff --git a/src/app/contests/[contest_id]/[task_id]/page.tsx b/src/app/contests/[contest_id]/tasks/[task_id]/page.tsx
similarity index 100%
rename from src/app/contests/[contest_id]/[task_id]/page.tsx
rename to src/app/contests/[contest_id]/tasks/[task_id]/page.tsx
diff --git a/src/app/contests/page.tsx b/src/app/contests/page.tsx
index a9253bd..f4cd945 100644
--- a/src/app/contests/page.tsx
+++ b/src/app/contests/page.tsx
@@ -1,108 +1 @@
-import React from 'react';
-import {
- AppShell,
- AppShellAside,
- AppShellHeader,
- AppShellMain,
- Button,
- Checkbox,
- Pagination,
- Stack,
- Table,
- TableTbody,
- TableTd,
- TableTh,
- TableThead,
- TableTr,
- TextInput,
- Title
-} from "@mantine/core";
-import {Header} from "@/components/header";
-import Link from "next/link";
-
-export const metadata = {
- title: 'Контесты',
- description: '',
-};
-
-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 = () => {
- const rows = contests.map((contest) => (
-
- {contest.title}
- {contest.startsAt}
- {contest.duration}
- {}
-
-
- ));
-
- return (
-
-
-
-
-
-
- Контесты
-
-
-
- Название
- Начало
- Длительность
-
-
-
- {rows}
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default Page;
+export {ContestsPage as default, generateMetadata} from "@/plain-pages/contests"
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index b1cbf7a..813a780 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -1,6 +1 @@
-export const metadata = {
- title: 'Вход в аккаунт',
- description: '',
-};
-
-export {LoginPage as default} from "@/plain-pages/login"
+export {LoginPage as default, generateMetadata} from "@/plain-pages/login"
diff --git a/src/app/workshop/[problem_id]/page.tsx b/src/app/problems/[problem_id]/page.tsx
similarity index 100%
rename from src/app/workshop/[problem_id]/page.tsx
rename to src/app/problems/[problem_id]/page.tsx
diff --git a/src/app/workshop/page.tsx b/src/app/problems/page.tsx
similarity index 100%
rename from src/app/workshop/page.tsx
rename to src/app/problems/page.tsx
diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx
index 206de3f..f77584b 100644
--- a/src/components/header/header.tsx
+++ b/src/components/header/header.tsx
@@ -99,7 +99,7 @@ const Header = () => {
visibleFrom="xs">
Пользователи
-
Мастерская
diff --git a/src/plain-pages/contest/index.ts b/src/plain-pages/contest/index.ts
new file mode 100644
index 0000000..4e27354
--- /dev/null
+++ b/src/plain-pages/contest/index.ts
@@ -0,0 +1 @@
+export * from "./page";
\ No newline at end of file
diff --git a/src/plain-pages/contest/page.tsx b/src/plain-pages/contest/page.tsx
new file mode 100644
index 0000000..ddc55f8
--- /dev/null
+++ b/src/plain-pages/contest/page.tsx
@@ -0,0 +1,33 @@
+"use server";
+
+import {ClientPage} from "./ui";
+import {Metadata} from "next";
+import {GetContest} from "@/shared/api";
+
+type Props = {
+ params: Promise<{ contest_id: number }>
+}
+
+const generateMetadata = async (props: Props): Promise => {
+ const contest_id = (await props.params).contest_id;
+
+ const response = await GetContest(contest_id);
+
+ return {
+ title: response.contest.title,
+ description: '',
+ };
+}
+
+
+const Page = async (props: Props) => {
+ const contest_id = (await props.params).contest_id;
+
+ const response = await GetContest(contest_id);
+
+ return (
+
+ )
+}
+
+export {Page as ContestPage, generateMetadata};
\ No newline at end of file
diff --git a/src/plain-pages/contest/ui.tsx b/src/plain-pages/contest/ui.tsx
new file mode 100644
index 0000000..c8324e3
--- /dev/null
+++ b/src/plain-pages/contest/ui.tsx
@@ -0,0 +1,123 @@
+"use client";
+
+import React from 'react';
+import {Header} from "@/components/header";
+import {Contest, GetContestResponseTasksInner} from "../../../proto/tester/v1/api";
+import {
+ ActionIcon,
+ Anchor,
+ Button,
+ Center,
+ Group,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Title
+} from "@mantine/core";
+import Link from "next/link";
+import {IconMail, IconTrash} from "@tabler/icons-react";
+import {DeleteTask} from "@/shared/api";
+import {useMutation} from "@tanstack/react-query";
+import {useRouter} from "next/navigation";
+
+type PageProps = {
+ contest: Contest,
+ tasks: Array
+}
+
+const Page = (props: PageProps) => {
+ const router = useRouter();
+
+ const mutation = useMutation({
+ mutationFn: DeleteTask,
+ onSuccess: async () => {
+ await router.refresh();
+ },
+ retry: false
+ });
+
+ const rows = props.tasks.map((task) => (
+
+
+
+ {`${task.task.position + 1}. ${task.task.title}`}
+
+
+ {task.best_solution.total_score}
+
+
+
+
+ mutation.mutate(task.task.id)}>
+
+
+
+
+ ))
+
+ return (
+
+
+
+
+ {props.contest.title}
+
+
+
+ Название
+ Баллы
+
+
+
+
+
+
+
+
+
+
+
+
+ {rows}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export {Page as ClientPage};
diff --git a/src/plain-pages/contests/index.ts b/src/plain-pages/contests/index.ts
new file mode 100644
index 0000000..4e27354
--- /dev/null
+++ b/src/plain-pages/contests/index.ts
@@ -0,0 +1 @@
+export * from "./page";
\ No newline at end of file
diff --git a/src/plain-pages/contests/page.tsx b/src/plain-pages/contests/page.tsx
new file mode 100644
index 0000000..9567966
--- /dev/null
+++ b/src/plain-pages/contests/page.tsx
@@ -0,0 +1,28 @@
+"use server";
+
+import {ClientPage} from "./ui";
+import {Metadata} from "next";
+import {ListContests} 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 response = await ListContests(page, 10);
+
+ return (
+
+ )
+}
+
+export {Page as ContestsPage, generateMetadata};
\ No newline at end of file
diff --git a/src/plain-pages/contests/ui.tsx b/src/plain-pages/contests/ui.tsx
new file mode 100644
index 0000000..b46ce5a
--- /dev/null
+++ b/src/plain-pages/contests/ui.tsx
@@ -0,0 +1,144 @@
+"use client";
+
+import React from 'react';
+import {
+ AppShell,
+ AppShellAside,
+ AppShellHeader,
+ AppShellMain,
+ Button,
+ Checkbox,
+ Pagination,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ 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";
+
+type Props = {
+ contests: ContestsListItem[],
+ page: number,
+ max_page: number
+}
+
+// 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 = (props: Props) => {
+ const rows = props.contests.map((contest) => (
+
+ {contest.title}
+
+
+
+ {/*{contest.startsAt}*/}
+ {/*{contest.duration}*/}
+ {/*{}*/}
+ {/**/}
+
+ ));
+
+ 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 {};
+ }}
+ />
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export {Page as ClientPage};
diff --git a/src/plain-pages/login/index.ts b/src/plain-pages/login/index.ts
index 38c6819..ec82b91 100644
--- a/src/plain-pages/login/index.ts
+++ b/src/plain-pages/login/index.ts
@@ -1 +1 @@
-export {Page as LoginPage} from "./page";
+export {LoginPage, generateMetadata} from "./page";
diff --git a/src/plain-pages/login/page.tsx b/src/plain-pages/login/page.tsx
index 85812f1..3c05740 100644
--- a/src/plain-pages/login/page.tsx
+++ b/src/plain-pages/login/page.tsx
@@ -1,110 +1,19 @@
-"use client";
+"use server";
-import {
- AppShell,
- AppShellHeader,
- AppShellMain,
- Button,
- Image,
- PasswordInput,
- Stack,
- TextInput,
- Title
-} from "@mantine/core";
-import {Header} from "@/components/header";
-import React from "react";
-import {useForm} from "@mantine/form";
-import {useRouter} from "next/navigation";
-import {useMutation} from "@tanstack/react-query";
-import axios from "axios";
-import NextImage from "next/image";
-import {Login} from "@/shared/api";
+import {ClientPage} from "./ui";
+import {Metadata} from "next";
-const Page = () => {
- const router = useRouter();
-
- const form = useForm({
- initialValues: {
- username: "",
- password: ""
- },
- });
-
- const mutation = useMutation({
- mutationFn: Login,
- onSuccess: async () => {
- await router.push("/")
- },
- onError: (error) => {
- form.clearErrors();
-
- if (axios.isAxiosError(error)) {
- if (error.response?.status === 404) {
- form.setFieldError("username", "Неверный юзернейм или пароль")
- return
- }
- }
-
- form.setFieldError("username", "Что-то пошло не так. Попробуйте позже.")
- },
- retry: false
- });
-
- const onSubmit = (event) => {
- event.preventDefault()
- mutation.mutate(form.getValues())
- }
+const generateMetadata = async (): Promise => {
+ return {
+ title: 'Вход в аккаунт',
+ description: '',
+ };
+}
+const Page = async () => {
return (
-
-
-
-
-
-
-
-
- );
-};
+
+ )
+}
-export {Page};
+export {Page as LoginPage, generateMetadata};
\ No newline at end of file
diff --git a/src/plain-pages/login/ui.tsx b/src/plain-pages/login/ui.tsx
new file mode 100644
index 0000000..84d9e0a
--- /dev/null
+++ b/src/plain-pages/login/ui.tsx
@@ -0,0 +1,110 @@
+"use client";
+
+import {
+ AppShell,
+ AppShellHeader,
+ AppShellMain,
+ Button,
+ Image,
+ PasswordInput,
+ Stack,
+ TextInput,
+ Title
+} from "@mantine/core";
+import {Header} from "@/components/header";
+import React from "react";
+import {useForm} from "@mantine/form";
+import {useRouter} from "next/navigation";
+import {useMutation} from "@tanstack/react-query";
+import axios from "axios";
+import NextImage from "next/image";
+import {Login} from "@/shared/api";
+
+const Page = () => {
+ const router = useRouter();
+
+ const form = useForm({
+ initialValues: {
+ username: "",
+ password: ""
+ },
+ });
+
+ const mutation = useMutation({
+ mutationFn: Login,
+ onSuccess: async () => {
+ await router.push("/")
+ },
+ onError: (error) => {
+ form.clearErrors();
+
+ if (axios.isAxiosError(error)) {
+ if (error.response?.status === 404) {
+ form.setFieldError("username", "Неверный юзернейм или пароль")
+ return
+ }
+ }
+
+ form.setFieldError("username", "Что-то пошло не так. Попробуйте позже.")
+ },
+ retry: false
+ });
+
+ const onSubmit = (event) => {
+ event.preventDefault()
+ mutation.mutate(form.getValues())
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export {Page as ClientPage};
diff --git a/src/shared/api/config.ts b/src/shared/api/config.ts
deleted file mode 100644
index 969f0ed..0000000
--- a/src/shared/api/config.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import {Configuration, DefaultApi} from "../../../proto/user/v1/api";
-
-const configuration = new Configuration({
- basePath: "http://localhost:60005",
-});
-
-const authApi = new DefaultApi(configuration);
-
-export {authApi};
\ No newline at end of file
diff --git a/src/shared/api/index.ts b/src/shared/api/index.ts
index 0e03d36..27393f3 100644
--- a/src/shared/api/index.ts
+++ b/src/shared/api/index.ts
@@ -1 +1,2 @@
-export * from "./ms-auth";
\ No newline at end of file
+export * from "./ms-auth";
+export * from "./ms-tester";
\ No newline at end of file
diff --git a/src/shared/api/ms-auth.ts b/src/shared/api/ms-auth.ts
index 7d1333f..b91cfab 100644
--- a/src/shared/api/ms-auth.ts
+++ b/src/shared/api/ms-auth.ts
@@ -3,7 +3,13 @@
import {AxiosRequestConfig} from "axios";
import {decode} from "jsonwebtoken";
import {cookies} from "next/headers";
-import {authApi} from "./config";
+import {Configuration, DefaultApi} from "../../../proto/user/v1/api";
+
+const configuration = new Configuration({
+ basePath: "http://localhost:60005",
+});
+
+const authApi = new DefaultApi(configuration);
export type Credentials = {
username: string,
diff --git a/src/shared/api/ms-tester.ts b/src/shared/api/ms-tester.ts
new file mode 100644
index 0000000..a75a5a9
--- /dev/null
+++ b/src/shared/api/ms-tester.ts
@@ -0,0 +1,72 @@
+"use server";
+
+import {Configuration, DefaultApi} from "../../../proto/tester/v1/api";
+import {cookies} from "next/headers";
+import {AxiosRequestConfig} from "axios";
+
+const configuration = new Configuration({
+ basePath: "http://localhost:60060",
+});
+
+const testerApi = new DefaultApi(configuration);
+
+const CookieName: any = "SESSIONID";
+
+export const ListContests = 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.listContests(page, pageSize, options);
+
+ return response.data;
+};
+
+export const GetContest = 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.getContest(id, options);
+
+ return response.data;
+};
+
+export const DeleteTask = async (taskId: 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.deleteTask(taskId, options);
+ return response.data;
+}
\ No newline at end of file