feat: add profile features
This commit is contained in:
parent
07026be380
commit
953ce64ef2
27 changed files with 2214 additions and 231 deletions
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "proto"]
|
||||
path = proto
|
||||
url = https://git.sch9.ru/new_gate/contracts
|
4
Makefile
Normal file
4
Makefile
Normal file
|
@ -0,0 +1,4 @@
|
|||
gen:
|
||||
@protoc --proto_path=proto --openapi_out=proto/user/v1 \
|
||||
proto/user/v1/user.proto
|
||||
@pnpm openapi-generator-cli generate -g typescript-axios -i ./proto/user/v1/openapi.yaml -o ./proto/user/v1/api
|
9
configutation.ts
Normal file
9
configutation.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import {Configuration, UserServiceApi} from "./proto/user/v1/api";
|
||||
|
||||
const configuration = new Configuration({
|
||||
basePath: "http://localhost:60001"
|
||||
})
|
||||
|
||||
const userApi = new UserServiceApi(configuration);
|
||||
|
||||
export {userApi};
|
7
openapitools.json
Normal file
7
openapitools.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||
"spaces": 2,
|
||||
"generator-cli": {
|
||||
"version": "7.11.0"
|
||||
}
|
||||
}
|
25
package.json
25
package.json
|
@ -9,20 +9,27 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/core": "^7.15.1",
|
||||
"@mantine/hooks": "^7.15.1",
|
||||
"@tabler/icons-react": "^3.26.0",
|
||||
"@mantine/core": "^7.16.3",
|
||||
"@mantine/dropzone": "^7.16.3",
|
||||
"@mantine/form": "^7.16.3",
|
||||
"@mantine/hooks": "^7.16.3",
|
||||
"@reduxjs/toolkit": "^2.5.1",
|
||||
"@tabler/icons-react": "^3.30.0",
|
||||
"@tanstack/react-query": "^5.66.3",
|
||||
"axios": "^1.7.9",
|
||||
"next": "15.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"redux-query": "3.6.1-rc.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"postcss": "^8.4.49",
|
||||
"@openapitools/openapi-generator-cli": "^2.16.3",
|
||||
"@types/node": "^20.17.19",
|
||||
"@types/react": "^19.0.9",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"postcss": "^8.5.2",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
|
1523
pnpm-lock.yaml
generated
1523
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -8,6 +8,7 @@ module.exports = {
|
|||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
'mantine-breakpoint-xxl': '100em',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
17
src/_pages/login/api.ts
Normal file
17
src/_pages/login/api.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
"use client";
|
||||
|
||||
import {AxiosRequestConfig, AxiosResponse} from "axios";
|
||||
import {userApi} from "../../../configutation";
|
||||
|
||||
const Login = async (username: string, password: string): Promise<AxiosResponse<void>> => {
|
||||
const options: AxiosRequestConfig = {
|
||||
withCredentials: true
|
||||
}
|
||||
|
||||
return await userApi.userServiceLogin({
|
||||
username: username,
|
||||
password: password
|
||||
}, options);
|
||||
}
|
||||
|
||||
export {Login};
|
1
src/_pages/login/index.ts
Normal file
1
src/_pages/login/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export {LoginPage} from "./ui";
|
123
src/_pages/login/ui.tsx
Normal file
123
src/_pages/login/ui.tsx
Normal file
|
@ -0,0 +1,123 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
AppShell,
|
||||
AppShellHeader,
|
||||
AppShellMain,
|
||||
Button,
|
||||
Image,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import {Header} from "@/components/header";
|
||||
import NextImage from "next/image";
|
||||
import {Login} from "./api";
|
||||
import {useForm} from '@mantine/form';
|
||||
import React from "react";
|
||||
import {useRouter} from 'next/navigation';
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
|
||||
|
||||
type Props = {
|
||||
params: {
|
||||
redirectTo: string
|
||||
}
|
||||
}
|
||||
|
||||
type Credentials = {
|
||||
username: string,
|
||||
password: string
|
||||
}
|
||||
|
||||
const Page = (props: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
username: "",
|
||||
password: ""
|
||||
},
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (credentials: Credentials) => {
|
||||
await Login(credentials.username, credentials.password)
|
||||
},
|
||||
|
||||
onSuccess: async () => {
|
||||
// FIXME: Possible vulnerability. Should check if the link is from the same resource.
|
||||
await router.push(props.params.redirectTo || "/")
|
||||
},
|
||||
onError: (error) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.status === 404) {
|
||||
form.setFieldError("username", "Неверный юзернейм или пароль")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
form.setFieldError("username", "Что-то пошло не так. Попробуйте позже.")
|
||||
},
|
||||
});
|
||||
|
||||
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="10%"
|
||||
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 LoginPage};
|
142
src/app/contests/[contest_id]/[task_id]/page.tsx
Normal file
142
src/app/contests/[contest_id]/[task_id]/page.tsx
Normal file
|
@ -0,0 +1,142 @@
|
|||
"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: `<p>Эта задача немного необычна — в ней вам предстоит реализовать
|
||||
интерактивное взаимодействие с тестирующей системой. Это означает, что
|
||||
вы можете делать запросы и получать ответы в online-режиме. Обратите
|
||||
внимание, что ввод/вывод в этой задаче — стандартный (то есть с экрана
|
||||
на экран). После вывода очередного запроса обязательно используйте
|
||||
функции очистки потока, чтобы часть вашего вывода не осталась в
|
||||
каком-нибудь буфере. Например, на С++ надо использовать функцию
|
||||
<code>fflush(stdout)</code>, на Java вызов
|
||||
<code>System.out.flush()</code>, на Pascal <code>flush(output)</code> и
|
||||
<code>stdout.flush()</code> для языка Python.</p>
|
||||
<p>В этой задаче вам предстоит в интерактивном режиме угадать число
|
||||
<span class="math inline"><em>x</em></span>, которое загадала
|
||||
тестирующая система. Про загаданное число <span
|
||||
class="math inline"><em>x</em></span> известно, что оно целое и лежит в
|
||||
границах от <span class="math inline">1</span> до <span
|
||||
class="math inline"><em>n</em></span> включительно (значение <span
|
||||
class="math inline"><em>n</em></span> известно заранее).</p>
|
||||
<p>Вы можете делать запросы к тестирующей системе, каждый запрос — это
|
||||
вывод одного целого числа от <span class="math inline">1</span> до <span
|
||||
class="math inline"><em>n</em></span>. Есть два варианта ответа
|
||||
тестирующей системы на запрос:</p>
|
||||
<ul>
|
||||
<li><p>строка <<<code><</code>>> (без кавычек), если
|
||||
загаданное число меньше числа из запроса;</p></li>
|
||||
<li><p>строка <<<code>>=</code>>> (без кавычек), если
|
||||
загаданное число больше либо равно числу из запроса.</p></li>
|
||||
</ul>
|
||||
<p>В случае, если ваша программа наверняка угадала нужное число <span
|
||||
class="math inline"><em>x</em></span>, выведите строку вида
|
||||
<<<code>! x</code>>>, где <span
|
||||
class="math inline"><em>x</em></span> — это ответ, и завершите работу
|
||||
своей программы.</p>
|
||||
<p>Вашей программе разрешается сделать не более <span
|
||||
class="math inline">25</span> запросов.</p>`,
|
||||
input: `<p>Для чтения ответов на запросы программа должна использовать
|
||||
стандартный ввод.</p>
|
||||
<p>В первой строке входных данных будет содержаться целое положительное
|
||||
число <span class="math inline"><em>n</em></span> (<span
|
||||
class="math inline">1 ≤ <em>n</em> ≤ 10<sup>6</sup></span>) —
|
||||
максимально возможное число, которое может быть загадано.</p>
|
||||
<p>В следующих строках на вход вашей программе будут подаваться строки
|
||||
вида <<<code><</code>>> и
|
||||
<<<code>>=</code>>>. <span
|
||||
class="math inline"><em>i</em></span>-я из этих строк является ответом
|
||||
системы на ваш <span class="math inline"><em>i</em></span>-й запрос.
|
||||
После того, как ваша программа угадала число, выведите
|
||||
<<<code>! x</code>>> (без кавычек), где <span
|
||||
class="math inline"><em>x</em></span> — это ответ, и завершите работу
|
||||
своей программы.</p>
|
||||
<p>Тестирующая система даст вашей программе прочитать ответ на запрос из
|
||||
входных данных только после того, как ваша программа вывела
|
||||
соответствующий запрос системе и выполнила операцию
|
||||
<code>flush</code>.</p>`,
|
||||
name: `<p>Отгадай число</p>`,
|
||||
output: `<p>Для осуществления запросов программа должна использовать стандартный
|
||||
вывод.</p>
|
||||
<p>Ваша программа должна выводить запросы — целые числа <span
|
||||
class="math inline"><em>x</em><sub><em>i</em></sub></span> (<span
|
||||
class="math inline">1 ≤ <em>x</em><sub><em>i</em></sub> ≤ <em>n</em></span>)
|
||||
по одному в строке (не забывайте выводить <<<em>перевод
|
||||
строки</em>>> после каждого значения <span
|
||||
class="math inline"><em>x</em><sub><em>i</em></sub></span>). После
|
||||
вывода каждой строки программа должна выполнить операцию
|
||||
<code>flush</code>.</p>
|
||||
<p>Каждое из значений <span
|
||||
class="math inline"><em>x</em><sub><em>i</em></sub></span> обозначает
|
||||
очередной запрос к системе. Ответ на запрос программа сможет прочесть из
|
||||
стандартного ввода. В случае, если ваша программа угадала число <span
|
||||
class="math inline"><em>x</em></span>, выведите строку вида
|
||||
<<<code>! x</code>>> (без кавычек), где <span
|
||||
class="math inline"><em>x</em></span> — ответ, и завершите работу
|
||||
программы.</p>`
|
||||
}
|
||||
|
||||
const Page = ({params}: PageProps) => {
|
||||
return (
|
||||
<>
|
||||
<header>
|
||||
<Header/>
|
||||
</header>
|
||||
<Group align="start" justify="center">
|
||||
<nav style={{padding: "var(--mantine-spacing-xs) var(--mantine-spacing-md)", width: "fit-content"}}>
|
||||
<Stack w={200}>
|
||||
<Anchor component={Link} href="/contests/1" c="var(--mantine-color-bright)">
|
||||
<Title c="black" order={4} ta="center">
|
||||
Простой контест с длинным названием из восьми слов
|
||||
</Title>
|
||||
</Anchor>
|
||||
<SegmentedControl
|
||||
orientation="vertical"
|
||||
withItemsBorders={false}
|
||||
data={problems}
|
||||
fullWidth
|
||||
defaultValue={problems[0]}/>
|
||||
</Stack>
|
||||
</nav>
|
||||
<main style={{flex: 5, maxWidth: "1080px"}}>
|
||||
<div dangerouslySetInnerHTML={{__html: problem.legend + problem.legend + problem.legend}}>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
<aside style={{padding: "var(--mantine-spacing-xs) var(--mantine-spacing-md)", width: "fit-content"}}>
|
||||
<Stack>
|
||||
<Code/>
|
||||
<Text fw={500}>Последние посылки
|
||||
<Anchor
|
||||
component={Link}
|
||||
href="/"
|
||||
fs="italic"
|
||||
c="var(--mantine-color-bright)" fw={500}>
|
||||
(посмотреть все)
|
||||
</Anchor>:
|
||||
</Text>
|
||||
</Stack>
|
||||
</aside>
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import {AppShell, AppShellHeader, AppShellMain} from "@mantine/core";
|
||||
import {Anchor, AppShell, AppShellHeader, AppShellMain} from "@mantine/core";
|
||||
import {Header} from "@/components/header";
|
||||
import Link from "next/link";
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
|
@ -12,10 +13,12 @@ const Page = ({params}: PageProps) => {
|
|||
return (
|
||||
<AppShell header={{height: 70}}>
|
||||
<AppShellHeader>
|
||||
<Header state="logged-in"/>
|
||||
<Header/>
|
||||
</AppShellHeader>
|
||||
<AppShellMain>
|
||||
Просмотр {params.contest_id}
|
||||
<Anchor component={Link} href="/contests/1/A">
|
||||
Просмотр {params.contest_id}
|
||||
</Anchor>
|
||||
</AppShellMain>
|
||||
</AppShell>
|
||||
);
|
||||
|
|
|
@ -1,21 +1,106 @@
|
|||
import React from 'react';
|
||||
import {AppShell, AppShellHeader, AppShellMain} from "@mantine/core";
|
||||
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 state="logged-in"/>
|
||||
<Header/>
|
||||
</AppShellHeader>
|
||||
<AppShellMain>
|
||||
Контесты
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.black_and_white {
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white))
|
||||
}
|
|
@ -1,14 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import '@mantine/core/styles.css';
|
||||
import "./global.css"
|
||||
import '@mantine/dropzone/styles.css';
|
||||
|
||||
import React from 'react';
|
||||
import {AppShell, ColorSchemeScript, mantineHtmlProps, MantineProvider} from '@mantine/core';
|
||||
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
||||
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export default function RootLayout({children}: { children: any }) {
|
||||
return (
|
||||
<html lang="en" {...mantineHtmlProps}>
|
||||
<head>
|
||||
<ColorSchemeScript defaultColorScheme="dark"/>
|
||||
<ColorSchemeScript defaultColorScheme="light"/>
|
||||
<link rel="shortcut icon" href="/gate_logo.svg"/>
|
||||
<meta
|
||||
name="viewport"
|
||||
|
@ -16,11 +22,13 @@ export default function RootLayout({children}: { children: any }) {
|
|||
/>
|
||||
</head>
|
||||
<body>
|
||||
<MantineProvider defaultColorScheme="dark" withGlobalClasses>
|
||||
<AppShell>
|
||||
{children}
|
||||
</AppShell>
|
||||
</MantineProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MantineProvider defaultColorScheme="light" withGlobalClasses forceColorScheme="light">
|
||||
<AppShell>
|
||||
{children}
|
||||
</AppShell>
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
6
src/app/login/page.tsx
Normal file
6
src/app/login/page.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const metadata = {
|
||||
title: 'Вход в аккаунт',
|
||||
description: '',
|
||||
};
|
||||
|
||||
export {LoginPage as default} from "@/_pages/login"
|
|
@ -11,7 +11,7 @@ export default function Page() {
|
|||
return (
|
||||
<AppShell header={{height: 70}}>
|
||||
<AppShellHeader>
|
||||
<Header state="logged-out"/>
|
||||
<Header/>
|
||||
</AppShellHeader>
|
||||
<AppShellMain>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import {AppShell, AppShellHeader, AppShellMain} from "@mantine/core";
|
||||
import {AppShell, AppShellHeader, AppShellMain, Button, Group, Select, Stack, TextInput} from "@mantine/core";
|
||||
import {Header} from "@/components/header";
|
||||
|
||||
type PageProps = {
|
||||
|
@ -8,14 +8,25 @@ type PageProps = {
|
|||
}
|
||||
}
|
||||
|
||||
const roles = [
|
||||
"Participant", "Moderator", "Admin"
|
||||
]
|
||||
|
||||
const Page = ({params}: PageProps) => {
|
||||
return (
|
||||
<AppShell header={{height: 70}}>
|
||||
<AppShellHeader>
|
||||
<Header state="logged-in"/>
|
||||
<Header/>
|
||||
</AppShellHeader>
|
||||
<AppShellMain>
|
||||
Страница пользователя {params.user_id}
|
||||
<AppShellMain px="16">
|
||||
<Stack align="center">
|
||||
<Group align="end" w="fit-content" m="auto" pt="16" gap="16">
|
||||
<TextInput label="Никнейм" placeholder="Никнейм" defaultValue="user228"/>
|
||||
<Select data={roles} label="Роль" defaultValue={roles[0]} allowDeselect={false}/>
|
||||
<Button label="Пароль">Сменить пароль</Button>
|
||||
</Group>
|
||||
<Button disabled w="fit-content">Сохранить</Button>
|
||||
</Stack>
|
||||
</AppShellMain>
|
||||
</AppShell>
|
||||
);
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
AppShell,
|
||||
AppShellHeader,
|
||||
AppShellMain,
|
||||
Button,
|
||||
Image,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import {Header} from "@/components/header";
|
||||
import NextImage from "next/image";
|
||||
|
||||
export const metadata = {
|
||||
title: 'Вход в аккаунт',
|
||||
description: '',
|
||||
};
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<AppShell header={{height: 70}}>
|
||||
<AppShellHeader>
|
||||
<Header state="pending"/>
|
||||
</AppShellHeader>
|
||||
<AppShellMain>
|
||||
<Stack align="center" justify="center" w="fit-content" m="auto" mt="10%" p="md"
|
||||
className="black_and_white">
|
||||
<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" w="250"/>
|
||||
<PasswordInput label="Пароль" placeholder="Пароль" w="250"/>
|
||||
</Stack>
|
||||
<Button>Войти</Button>
|
||||
</Stack>
|
||||
</AppShellMain>
|
||||
</AppShell>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
|
@ -1,21 +1,97 @@
|
|||
import React from 'react';
|
||||
import {AppShell, AppShellHeader, AppShellMain} from "@mantine/core";
|
||||
import {
|
||||
ActionIcon,
|
||||
AppShell,
|
||||
AppShellAside,
|
||||
AppShellHeader,
|
||||
AppShellMain,
|
||||
Pagination,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
TextInput,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import {Header} from "@/components/header";
|
||||
import Link from "next/link";
|
||||
import {IconPencil} from "@tabler/icons-react";
|
||||
|
||||
export const metadata = {
|
||||
title: 'Пользователи',
|
||||
description: '',
|
||||
};
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: 1,
|
||||
username: "user228",
|
||||
email: "use***@gmail.com",
|
||||
role: "Admin",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: "user229",
|
||||
email: "use***@mail.ru",
|
||||
role: "Participant",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: "user365",
|
||||
email: "use***@yandex.ru",
|
||||
role: "Moderator",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
username: "user777",
|
||||
email: "use***@yahoo.com",
|
||||
role: "Moderator",
|
||||
},
|
||||
]
|
||||
|
||||
const Page = () => {
|
||||
const rows = users.map((user) => (
|
||||
<TableTr key={user.id}>
|
||||
<TableTd>{user.username}</TableTd>
|
||||
<TableTd>{user.email}</TableTd>
|
||||
<TableTd>{user.role}</TableTd>
|
||||
<TableTd>{<ActionIcon size="xs" component={Link} href={`/users/${user.id}`}>
|
||||
<IconPencil/>
|
||||
</ActionIcon>}
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
));
|
||||
|
||||
return (
|
||||
<AppShell header={{height: 70}}>
|
||||
<AppShellHeader>
|
||||
<Header state="logged-in"/>
|
||||
<Header/>
|
||||
</AppShellHeader>
|
||||
<AppShellMain>
|
||||
Пользователи
|
||||
<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="Поиск"/>
|
||||
</Stack>
|
||||
</AppShellAside>
|
||||
</AppShell>
|
||||
);
|
||||
};
|
||||
|
|
24
src/app/workshop/[problem_id]/page.tsx
Normal file
24
src/app/workshop/[problem_id]/page.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
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 (
|
||||
<AppShell header={{height: 70}}>
|
||||
<AppShellHeader>
|
||||
<Header/>
|
||||
</AppShellHeader>
|
||||
<AppShellMain>
|
||||
|
||||
</AppShellMain>
|
||||
</AppShell>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
|
@ -1,21 +1,106 @@
|
|||
import React from 'react';
|
||||
import {AppShell, AppShellHeader, AppShellMain} from "@mantine/core";
|
||||
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) => (
|
||||
<TableTr key={problem.id}>
|
||||
<TableTd>
|
||||
<Text td="underline">{problem.title}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Group align="end" gap="2" justify="center">
|
||||
<IconUser/>
|
||||
<Text td="underline">{problem.accepted}</Text>
|
||||
</Group>
|
||||
</TableTd>
|
||||
<TableTd>{<ActionIcon size="xs" component={Link} href={`/workshop/${problem.id}`}>
|
||||
<IconPencil/>
|
||||
</ActionIcon>}
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
));
|
||||
|
||||
return (
|
||||
<AppShell header={{height: 70}}>
|
||||
<AppShellHeader>
|
||||
<Header state="logged-in"/>
|
||||
<Header/>
|
||||
</AppShellHeader>
|
||||
<AppShellMain>
|
||||
Мастерская
|
||||
<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><IconCheck/></TableTh>
|
||||
<TableTh><IconPencil/></TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>{rows}</TableTbody>
|
||||
</Table>
|
||||
<Pagination total={10}/>
|
||||
</Stack>
|
||||
</AppShellMain>
|
||||
<AppShellAside withBorder={false} px="16">
|
||||
<Stack pt="16">
|
||||
<Stack gap="5">
|
||||
<Button title="Создать контест">Создать контест</Button>
|
||||
<Button title="Создать задачу">Создать задачу</Button>
|
||||
</Stack>
|
||||
<TextInput placeholder="Поиск"/>
|
||||
</Stack>
|
||||
</AppShellAside>
|
||||
</AppShell>
|
||||
);
|
||||
};
|
||||
|
|
35
src/components/code/code.module.css
Normal file
35
src/components/code/code.module.css
Normal file
|
@ -0,0 +1,35 @@
|
|||
.input {
|
||||
min-height: 253px;
|
||||
padding: var(--mantine-spacing-sm);
|
||||
|
||||
@media (max-width: $mantine-breakpoint-xxl) {
|
||||
min-height: 130px;
|
||||
padding: var(--mantine-spacing-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.code {
|
||||
width: 565px;
|
||||
height: fit-content;
|
||||
background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-8));
|
||||
padding: var(--mantine-spacing-md);
|
||||
border-radius: var(--mantine-radius-default);
|
||||
|
||||
@media (max-width: $mantine-breakpoint-xxl) {
|
||||
width: 365px;
|
||||
padding: var(--mantine-spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
.dropzone__content {
|
||||
padding: var(--mantine-spacing-sm);
|
||||
gap: var(--mantine-spacing-sm);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
|
||||
@media (max-width: $mantine-breakpoint-xxl) {
|
||||
min-height: 130px;
|
||||
padding: var(--mantine-spacing-xs);
|
||||
}
|
||||
}
|
67
src/components/code/code.tsx
Normal file
67
src/components/code/code.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
"use client";
|
||||
|
||||
import {Button, Group, rem, SegmentedControl, Select, Stack, Text, Textarea} from "@mantine/core";
|
||||
import {useState} from "react";
|
||||
import {Dropzone, DropzoneAccept, DropzoneIdle, DropzoneReject, IMAGE_MIME_TYPE} from "@mantine/dropzone";
|
||||
import {IconCloudUpload, IconUpload, IconX} from "@tabler/icons-react";
|
||||
import classes from "./code.module.css";
|
||||
|
||||
const languages = ["python", "cpp", "golang", "ruby"]
|
||||
|
||||
const Code = () => {
|
||||
const [value, setValue] = useState("Код");
|
||||
|
||||
return (
|
||||
<Stack className={classes.code}>
|
||||
<Select data={languages} allowDeselect={false} defaultValue={languages[0]} label="Компилятор"/>
|
||||
<Group>
|
||||
<Text fw={500}>Решение</Text>
|
||||
<SegmentedControl data={['Код', 'Файл']} value={value} onChange={setValue}/>
|
||||
</Group>
|
||||
{
|
||||
value === "Код" ? <Textarea classNames={{input: classes.input}}/> : <Dropzone
|
||||
onDrop={(files) => console.log('accepted files', files)}
|
||||
onReject={(files) => console.log('rejected files', files)}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={IMAGE_MIME_TYPE}
|
||||
gap={0}
|
||||
>
|
||||
<DropzoneAccept>
|
||||
<IconUpload
|
||||
style={{width: rem(52), height: rem(52), color: 'var(--mantine-color-blue-6)'}}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</DropzoneAccept>
|
||||
<DropzoneReject>
|
||||
<IconX
|
||||
style={{width: rem(52), height: rem(52), color: 'var(--mantine-color-red-6)'}}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</DropzoneReject>
|
||||
<DropzoneIdle>
|
||||
<Stack className={classes.dropzone__content}>
|
||||
<IconCloudUpload
|
||||
style={{
|
||||
width: rem(52),
|
||||
height: rem(52),
|
||||
color: 'var(--mantine-color-bright)',
|
||||
marginBottom: rem(20)
|
||||
}}
|
||||
stroke={1.5}
|
||||
/>
|
||||
<Text size="xl" inline fw={700}>
|
||||
Загрузить решение
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline ta="center">
|
||||
Перетащите сюда файл который вы хотите отправить или выберите его нажав сюда
|
||||
</Text>
|
||||
</Stack>
|
||||
</DropzoneIdle>
|
||||
</Dropzone>
|
||||
}
|
||||
<Button>Отправить решение</Button>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export {Code};
|
1
src/components/code/index.ts
Normal file
1
src/components/code/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export {Code} from "./code";
|
24
src/components/header/api.ts
Normal file
24
src/components/header/api.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
"use client";
|
||||
|
||||
import {AxiosRequestConfig, AxiosResponse} from "axios";
|
||||
import {userApi} from "../../../configutation";
|
||||
|
||||
|
||||
const GetMe = async () => {
|
||||
const options: AxiosRequestConfig = {
|
||||
withCredentials: true,
|
||||
timeout: 50,
|
||||
}
|
||||
|
||||
return await userApi.userServiceGetUser(0, true, options);
|
||||
}
|
||||
|
||||
const Logout = async (): Promise<AxiosResponse<void>> => {
|
||||
const options: AxiosRequestConfig = {
|
||||
withCredentials: true
|
||||
}
|
||||
|
||||
return await userApi.userServiceLogout(options);
|
||||
}
|
||||
|
||||
export {GetMe, Logout};
|
|
@ -20,43 +20,64 @@ import Link from "next/link";
|
|||
import NextImage from "next/image"
|
||||
import {IconUser} from "@tabler/icons-react";
|
||||
import {useDisclosure} from "@mantine/hooks";
|
||||
import {useMutation, useQuery} from "@tanstack/react-query";
|
||||
import {GetMe, Logout} from "./api";
|
||||
import {useRouter} from "next/navigation";
|
||||
|
||||
const Profile = () => {
|
||||
const router = useRouter();
|
||||
|
||||
type ProfileProps = {
|
||||
state: "logged-in" | "logged-out" | "pending"
|
||||
}
|
||||
const {isPending, isSuccess} = useQuery({
|
||||
queryKey: ['me'],
|
||||
queryFn: GetMe,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const Profile = ({state}: ProfileProps) => {
|
||||
if (state == "logged-out") {
|
||||
return (
|
||||
<Button ml="76" component={Link} href="/users/login" variant="default">
|
||||
Войти
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
const mutation = useMutation({
|
||||
mutationFn: Logout,
|
||||
|
||||
if (state == "pending") {
|
||||
onSuccess: async () => {
|
||||
await router.push("/login")
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Не удалось выйти из аккаунта. Попробуйте позже.", error)
|
||||
},
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<Group justify="flex-end">
|
||||
<Skeleton height="36" width="77"/>
|
||||
<Button component={Link} href="/login" variant="default">
|
||||
Войти
|
||||
</Button>
|
||||
<Skeleton height="60" circle/>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
if (isSuccess) {
|
||||
return (
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" visibleFrom="sm" onClick={() => mutation.mutate()}>
|
||||
Выйти
|
||||
</Button>
|
||||
<Avatar component={Link} href="/users/1" color="gray" size="60">
|
||||
<IconUser size="32"/>
|
||||
</Avatar>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" visibleFrom="sm">
|
||||
Выйти
|
||||
<Button ml="76" component={Link} href="/login" variant="default">
|
||||
Войти
|
||||
</Button>
|
||||
<Avatar component={Link} href="/users/1" color="gray" size="60">
|
||||
<IconUser size="32"/>
|
||||
</Avatar>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
const Header = ({state}: ProfileProps) => {
|
||||
const Header = () => {
|
||||
const [drawerOpened, {toggle: toggleDrawer, close: closeDrawer}] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
|
@ -77,11 +98,12 @@ const Header = ({state}: ProfileProps) => {
|
|||
visibleFrom="xs">
|
||||
Пользователи
|
||||
</Anchor>
|
||||
<Anchor component={Link} href="/workshop" className={classes.link} underline="never" visibleFrom="xs">
|
||||
<Anchor component={Link} href="/workshop" className={classes.link} underline="never"
|
||||
visibleFrom="xs">
|
||||
Мастерская
|
||||
</Anchor>
|
||||
</Group>
|
||||
<Profile state={state}/>
|
||||
<Profile/>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue