feat(tester): add Contest&Contests pages
This commit is contained in:
parent
b515ae3e67
commit
78e61899cd
22 changed files with 542 additions and 261 deletions
|
@ -7,7 +7,7 @@
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"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": {
|
"dependencies": {
|
||||||
"@mantine/core": "^7.17.0",
|
"@mantine/core": "^7.17.0",
|
||||||
|
|
2
proto
2
proto
|
@ -1 +1 @@
|
||||||
Subproject commit c1b7fd7a2d32678641ebd3acfe3d5b2eca5d0c72
|
Subproject commit ea2a76c1f4001bba8405a5a447f085831dc5cf18
|
|
@ -1,27 +1 @@
|
||||||
import React from 'react';
|
export {ContestPage as default, generateMetadata} from "@/plain-pages/contest"
|
||||||
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 (
|
|
||||||
<AppShell header={{height: 70}}>
|
|
||||||
<AppShellHeader>
|
|
||||||
<Header/>
|
|
||||||
</AppShellHeader>
|
|
||||||
<AppShellMain>
|
|
||||||
<Anchor component={Link} href="/contests/1/A">
|
|
||||||
Просмотр {params.contest_id}
|
|
||||||
</Anchor>
|
|
||||||
</AppShellMain>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
|
|
|
@ -1,108 +1 @@
|
||||||
import React from 'react';
|
export {ContestsPage as default, generateMetadata} from "@/plain-pages/contests"
|
||||||
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) => (
|
|
||||||
<TableTr key={contest.id}>
|
|
||||||
<TableTd>{contest.title}</TableTd>
|
|
||||||
<TableTd>{contest.startsAt}</TableTd>
|
|
||||||
<TableTd>{contest.duration}</TableTd>
|
|
||||||
<TableTd>{<Button size="xs"
|
|
||||||
disabled={!contest.started}
|
|
||||||
component={Link}
|
|
||||||
href={`/contests/${contest.id}`}
|
|
||||||
>
|
|
||||||
{contest.started ? "Войти в контест" : "Не началось"}
|
|
||||||
</Button>}
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShell header={{height: 70}}>
|
|
||||||
<AppShellHeader>
|
|
||||||
<Header/>
|
|
||||||
</AppShellHeader>
|
|
||||||
<AppShellMain px="16">
|
|
||||||
<Stack align="center" w="fit-content" m="auto" pt="16" gap="16">
|
|
||||||
<Title>Контесты</Title>
|
|
||||||
<Table horizontalSpacing="xl">
|
|
||||||
<TableThead>
|
|
||||||
<TableTr>
|
|
||||||
<TableTh>Название</TableTh>
|
|
||||||
<TableTh>Начало</TableTh>
|
|
||||||
<TableTh>Длительность</TableTh>
|
|
||||||
<TableTh></TableTh>
|
|
||||||
</TableTr>
|
|
||||||
</TableThead>
|
|
||||||
<TableTbody>{rows}</TableTbody>
|
|
||||||
</Table>
|
|
||||||
<Pagination total={10} />
|
|
||||||
</Stack>
|
|
||||||
</AppShellMain>
|
|
||||||
<AppShellAside withBorder={false} px="16" >
|
|
||||||
<Stack pt="16">
|
|
||||||
<TextInput placeholder="Поиск"/>
|
|
||||||
<Checkbox label="Завершенные"/>
|
|
||||||
</Stack>
|
|
||||||
</AppShellAside>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
|
|
|
@ -1,6 +1 @@
|
||||||
export const metadata = {
|
export {LoginPage as default, generateMetadata} from "@/plain-pages/login"
|
||||||
title: 'Вход в аккаунт',
|
|
||||||
description: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
export {LoginPage as default} from "@/plain-pages/login"
|
|
||||||
|
|
|
@ -99,7 +99,7 @@ const Header = () => {
|
||||||
visibleFrom="xs">
|
visibleFrom="xs">
|
||||||
Пользователи
|
Пользователи
|
||||||
</Anchor>
|
</Anchor>
|
||||||
<Anchor component={Link} href="/workshop" className={classes.link} underline="never"
|
<Anchor component={Link} href="/problems" className={classes.link} underline="never"
|
||||||
visibleFrom="xs">
|
visibleFrom="xs">
|
||||||
Мастерская
|
Мастерская
|
||||||
</Anchor>
|
</Anchor>
|
||||||
|
|
1
src/plain-pages/contest/index.ts
Normal file
1
src/plain-pages/contest/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./page";
|
33
src/plain-pages/contest/page.tsx
Normal file
33
src/plain-pages/contest/page.tsx
Normal file
|
@ -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<Metadata> => {
|
||||||
|
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 (
|
||||||
|
<ClientPage contest={response.contest} tasks={response.tasks}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {Page as ContestPage, generateMetadata};
|
123
src/plain-pages/contest/ui.tsx
Normal file
123
src/plain-pages/contest/ui.tsx
Normal file
|
@ -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<GetContestResponseTasksInner>
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => (
|
||||||
|
<TableTr key={task.task.id}>
|
||||||
|
<TableTd>
|
||||||
|
<Anchor component={Link}
|
||||||
|
href={`/contests/${props.contest.id}/tasks/${task.task.id}`}
|
||||||
|
c="var(--mantine-link-color)"
|
||||||
|
underline="always"
|
||||||
|
>
|
||||||
|
{`${task.task.position + 1}. ${task.task.title}`}
|
||||||
|
</Anchor>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>{task.best_solution.total_score}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button component={Link} href={`/`} size="xs">
|
||||||
|
Мои посылки
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<ActionIcon onClick={() => mutation.mutate(task.task.id)}>
|
||||||
|
<IconTrash/>
|
||||||
|
</ActionIcon>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="xl">
|
||||||
|
<header>
|
||||||
|
<Header/>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<Stack gap="xl" align="center">
|
||||||
|
<Title order={1}>{props.contest.title}</Title>
|
||||||
|
<Table horizontalSpacing="xl" align="center" w="fit-content">
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Название</TableTh>
|
||||||
|
<TableTh>Баллы</TableTh>
|
||||||
|
<TableTh>
|
||||||
|
<Center>
|
||||||
|
<IconMail/>
|
||||||
|
</Center>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh>
|
||||||
|
<Center>
|
||||||
|
<IconTrash/>
|
||||||
|
</Center>
|
||||||
|
</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>{rows}</TableTbody>
|
||||||
|
</Table>
|
||||||
|
<Stack>
|
||||||
|
<Center>
|
||||||
|
<Group>
|
||||||
|
<Button size="md">
|
||||||
|
Все посылки
|
||||||
|
</Button>
|
||||||
|
<Button size="md">
|
||||||
|
Редактировать контест
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Center>
|
||||||
|
<Center>
|
||||||
|
<Group>
|
||||||
|
<Button size="md">
|
||||||
|
Добавить пользователя
|
||||||
|
</Button>
|
||||||
|
<Button size="md">
|
||||||
|
Добавить задачу
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Center>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</main>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {Page as ClientPage};
|
1
src/plain-pages/contests/index.ts
Normal file
1
src/plain-pages/contests/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./page";
|
28
src/plain-pages/contests/page.tsx
Normal file
28
src/plain-pages/contests/page.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import {ClientPage} from "./ui";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {ListContests} from "@/shared/api";
|
||||||
|
|
||||||
|
const generateMetadata = async (): Promise<Metadata> => {
|
||||||
|
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 (
|
||||||
|
<ClientPage contests={response.contests} max_page={response.max_page} page={response.page}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {Page as ContestsPage, generateMetadata};
|
144
src/plain-pages/contests/ui.tsx
Normal file
144
src/plain-pages/contests/ui.tsx
Normal file
|
@ -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) => (
|
||||||
|
<TableTr key={contest.id}>
|
||||||
|
<TableTd>{contest.title}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button size="xs"
|
||||||
|
component={Link}
|
||||||
|
href={`/contests/${contest.id}`}
|
||||||
|
>
|
||||||
|
{"Войти в контест"}
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
{/*<TableTd>{contest.startsAt}</TableTd>*/}
|
||||||
|
{/*<TableTd>{contest.duration}</TableTd>*/}
|
||||||
|
{/*<TableTd>{<Button size="xs"*/}
|
||||||
|
{/* disabled={!contest.started}*/}
|
||||||
|
{/* component={Link}*/}
|
||||||
|
{/* href={`/contests/${contest.id}`}*/}
|
||||||
|
{/*>*/}
|
||||||
|
{/* {contest.started ? "Войти в контест" : "Не началось"}*/}
|
||||||
|
{/*</Button>}*/}
|
||||||
|
{/*</TableTd>*/}
|
||||||
|
</TableTr>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell header={{height: 70}}>
|
||||||
|
<AppShellHeader>
|
||||||
|
<Header/>
|
||||||
|
</AppShellHeader>
|
||||||
|
<AppShellMain px="16">
|
||||||
|
<Stack align="center" w="fit-content" m="auto" pt="16" gap="16">
|
||||||
|
<Title>Контесты</Title>
|
||||||
|
<Table horizontalSpacing="xl">
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Название</TableTh>
|
||||||
|
{/*<TableTh>Начало</TableTh>*/}
|
||||||
|
{/*<TableTh>Длительность</TableTh>*/}
|
||||||
|
<TableTh></TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>{rows}</TableTbody>
|
||||||
|
</Table>
|
||||||
|
<Pagination total={props.max_page}
|
||||||
|
value={props.page}
|
||||||
|
getItemProps={(page) => ({
|
||||||
|
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 {};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</AppShellMain>
|
||||||
|
<AppShellAside withBorder={false} px="16">
|
||||||
|
<Stack pt="16">
|
||||||
|
<TextInput placeholder="Поиск"/>
|
||||||
|
<Checkbox label="Завершенные"/>
|
||||||
|
</Stack>
|
||||||
|
</AppShellAside>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {Page as ClientPage};
|
|
@ -1 +1 @@
|
||||||
export {Page as LoginPage} from "./page";
|
export {LoginPage, generateMetadata} from "./page";
|
||||||
|
|
|
@ -1,110 +1,19 @@
|
||||||
"use client";
|
"use server";
|
||||||
|
|
||||||
import {
|
import {ClientPage} from "./ui";
|
||||||
AppShell,
|
import {Metadata} from "next";
|
||||||
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 generateMetadata = async (): Promise<Metadata> => {
|
||||||
const router = useRouter();
|
return {
|
||||||
|
title: 'Вход в аккаунт',
|
||||||
const form = useForm({
|
description: '',
|
||||||
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 Page = async () => {
|
||||||
return (
|
return (
|
||||||
<AppShell header={{height: 70}}>
|
<ClientPage/>
|
||||||
<AppShellHeader>
|
)
|
||||||
<Header/>
|
}
|
||||||
</AppShellHeader>
|
|
||||||
<AppShellMain>
|
|
||||||
<form
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
w="fit-content"
|
|
||||||
m="auto"
|
|
||||||
mt="5%"
|
|
||||||
p="md"
|
|
||||||
style={{color: "var(--mantine-color-bright)"}}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
component={NextImage}
|
|
||||||
src="/gate_logo.svg"
|
|
||||||
alt="Gate logo"
|
|
||||||
width="40"
|
|
||||||
height="40"
|
|
||||||
maw="40"
|
|
||||||
mah="40"
|
|
||||||
/>
|
|
||||||
<Title>Войти в Gate</Title>
|
|
||||||
<Stack w="100%" gap="0">
|
|
||||||
<TextInput
|
|
||||||
label="Username"
|
|
||||||
placeholder="Username"
|
|
||||||
key={form.key('username')}
|
|
||||||
w="250"
|
|
||||||
{...form.getInputProps('username')}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
label="Пароль"
|
|
||||||
placeholder="Пароль"
|
|
||||||
w="250"
|
|
||||||
key={form.key('password')}
|
|
||||||
{...form.getInputProps('password')}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Button type="submit" loading={mutation.isPending}>Войти</Button>
|
|
||||||
</Stack>
|
|
||||||
</form>
|
|
||||||
</AppShellMain>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export {Page};
|
export {Page as LoginPage, generateMetadata};
|
110
src/plain-pages/login/ui.tsx
Normal file
110
src/plain-pages/login/ui.tsx
Normal file
|
@ -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 (
|
||||||
|
<AppShell header={{height: 70}}>
|
||||||
|
<AppShellHeader>
|
||||||
|
<Header/>
|
||||||
|
</AppShellHeader>
|
||||||
|
<AppShellMain>
|
||||||
|
<form
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
w="fit-content"
|
||||||
|
m="auto"
|
||||||
|
mt="5%"
|
||||||
|
p="md"
|
||||||
|
style={{color: "var(--mantine-color-bright)"}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
component={NextImage}
|
||||||
|
src="/gate_logo.svg"
|
||||||
|
alt="Gate logo"
|
||||||
|
width="40"
|
||||||
|
height="40"
|
||||||
|
maw="40"
|
||||||
|
mah="40"
|
||||||
|
/>
|
||||||
|
<Title>Войти в Gate</Title>
|
||||||
|
<Stack w="100%" gap="0">
|
||||||
|
<TextInput
|
||||||
|
label="Username"
|
||||||
|
placeholder="Username"
|
||||||
|
key={form.key('username')}
|
||||||
|
w="250"
|
||||||
|
{...form.getInputProps('username')}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="Пароль"
|
||||||
|
placeholder="Пароль"
|
||||||
|
w="250"
|
||||||
|
key={form.key('password')}
|
||||||
|
{...form.getInputProps('password')}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Button type="submit" loading={mutation.isPending}>Войти</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</AppShellMain>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {Page as ClientPage};
|
|
@ -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};
|
|
|
@ -1 +1,2 @@
|
||||||
export * from "./ms-auth";
|
export * from "./ms-auth";
|
||||||
|
export * from "./ms-tester";
|
|
@ -3,7 +3,13 @@
|
||||||
import {AxiosRequestConfig} from "axios";
|
import {AxiosRequestConfig} from "axios";
|
||||||
import {decode} from "jsonwebtoken";
|
import {decode} from "jsonwebtoken";
|
||||||
import {cookies} from "next/headers";
|
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 = {
|
export type Credentials = {
|
||||||
username: string,
|
username: string,
|
||||||
|
|
72
src/shared/api/ms-tester.ts
Normal file
72
src/shared/api/ms-tester.ts
Normal file
|
@ -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;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue