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 ( - - -
- - -
- - Gate logo - Войти в Gate - - - - - - -
-
- - ); -}; + + ) +} -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 ( + + +
+ + +
+ + Gate logo + Войти в Gate + + + + + + +
+
+ + ); +}; + +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