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"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^7.15.1",
|
"@mantine/core": "^7.16.3",
|
||||||
"@mantine/hooks": "^7.15.1",
|
"@mantine/dropzone": "^7.16.3",
|
||||||
"@tabler/icons-react": "^3.26.0",
|
"@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",
|
"next": "15.1.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"redux-query": "3.6.1-rc.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@openapitools/openapi-generator-cli": "^2.16.3",
|
||||||
"@types/react": "^19",
|
"@types/node": "^20.17.19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react": "^19.0.9",
|
||||||
"postcss": "^8.4.49",
|
"@types/react-dom": "^19.0.3",
|
||||||
|
"postcss": "^8.5.2",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"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-md': '62em',
|
||||||
'mantine-breakpoint-lg': '75em',
|
'mantine-breakpoint-lg': '75em',
|
||||||
'mantine-breakpoint-xl': '88em',
|
'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 React from 'react';
|
||||||
import {AppShell, AppShellHeader, AppShellMain} from "@mantine/core";
|
import {Anchor, AppShell, AppShellHeader, AppShellMain} from "@mantine/core";
|
||||||
import {Header} from "@/components/header";
|
import {Header} from "@/components/header";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
params: {
|
params: {
|
||||||
|
@ -12,10 +13,12 @@ const Page = ({params}: PageProps) => {
|
||||||
return (
|
return (
|
||||||
<AppShell header={{height: 70}}>
|
<AppShell header={{height: 70}}>
|
||||||
<AppShellHeader>
|
<AppShellHeader>
|
||||||
<Header state="logged-in"/>
|
<Header/>
|
||||||
</AppShellHeader>
|
</AppShellHeader>
|
||||||
<AppShellMain>
|
<AppShellMain>
|
||||||
Просмотр {params.contest_id}
|
<Anchor component={Link} href="/contests/1/A">
|
||||||
|
Просмотр {params.contest_id}
|
||||||
|
</Anchor>
|
||||||
</AppShellMain>
|
</AppShellMain>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,21 +1,106 @@
|
||||||
import React from 'react';
|
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 {Header} from "@/components/header";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Контесты',
|
title: 'Контесты',
|
||||||
description: '',
|
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 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 (
|
return (
|
||||||
<AppShell header={{height: 70}}>
|
<AppShell header={{height: 70}}>
|
||||||
<AppShellHeader>
|
<AppShellHeader>
|
||||||
<Header state="logged-in"/>
|
<Header/>
|
||||||
</AppShellHeader>
|
</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>
|
</AppShellMain>
|
||||||
|
<AppShellAside withBorder={false} px="16" >
|
||||||
|
<Stack pt="16">
|
||||||
|
<TextInput placeholder="Поиск"/>
|
||||||
|
<Checkbox label="Завершенные"/>
|
||||||
|
</Stack>
|
||||||
|
</AppShellAside>
|
||||||
</AppShell>
|
</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 '@mantine/core/styles.css';
|
||||||
import "./global.css"
|
import '@mantine/dropzone/styles.css';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {AppShell, ColorSchemeScript, mantineHtmlProps, MantineProvider} from '@mantine/core';
|
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 }) {
|
export default function RootLayout({children}: { children: any }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" {...mantineHtmlProps}>
|
<html lang="en" {...mantineHtmlProps}>
|
||||||
<head>
|
<head>
|
||||||
<ColorSchemeScript defaultColorScheme="dark"/>
|
<ColorSchemeScript defaultColorScheme="light"/>
|
||||||
<link rel="shortcut icon" href="/gate_logo.svg"/>
|
<link rel="shortcut icon" href="/gate_logo.svg"/>
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
|
@ -16,11 +22,13 @@ export default function RootLayout({children}: { children: any }) {
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<MantineProvider defaultColorScheme="dark" withGlobalClasses>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AppShell>
|
<MantineProvider defaultColorScheme="light" withGlobalClasses forceColorScheme="light">
|
||||||
{children}
|
<AppShell>
|
||||||
</AppShell>
|
{children}
|
||||||
</MantineProvider>
|
</AppShell>
|
||||||
|
</MantineProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 (
|
return (
|
||||||
<AppShell header={{height: 70}}>
|
<AppShell header={{height: 70}}>
|
||||||
<AppShellHeader>
|
<AppShellHeader>
|
||||||
<Header state="logged-out"/>
|
<Header/>
|
||||||
</AppShellHeader>
|
</AppShellHeader>
|
||||||
<AppShellMain>
|
<AppShellMain>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
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";
|
import {Header} from "@/components/header";
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
|
@ -8,14 +8,25 @@ type PageProps = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const roles = [
|
||||||
|
"Participant", "Moderator", "Admin"
|
||||||
|
]
|
||||||
|
|
||||||
const Page = ({params}: PageProps) => {
|
const Page = ({params}: PageProps) => {
|
||||||
return (
|
return (
|
||||||
<AppShell header={{height: 70}}>
|
<AppShell header={{height: 70}}>
|
||||||
<AppShellHeader>
|
<AppShellHeader>
|
||||||
<Header state="logged-in"/>
|
<Header/>
|
||||||
</AppShellHeader>
|
</AppShellHeader>
|
||||||
<AppShellMain>
|
<AppShellMain px="16">
|
||||||
Страница пользователя {params.user_id}
|
<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>
|
</AppShellMain>
|
||||||
</AppShell>
|
</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 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 {Header} from "@/components/header";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {IconPencil} from "@tabler/icons-react";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Пользователи',
|
title: 'Пользователи',
|
||||||
description: '',
|
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 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 (
|
return (
|
||||||
<AppShell header={{height: 70}}>
|
<AppShell header={{height: 70}}>
|
||||||
<AppShellHeader>
|
<AppShellHeader>
|
||||||
<Header state="logged-in"/>
|
<Header/>
|
||||||
</AppShellHeader>
|
</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>
|
</AppShellMain>
|
||||||
|
<AppShellAside withBorder={false} px="16">
|
||||||
|
<Stack pt="16">
|
||||||
|
<TextInput placeholder="Поиск"/>
|
||||||
|
</Stack>
|
||||||
|
</AppShellAside>
|
||||||
</AppShell>
|
</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 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 {Header} from "@/components/header";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {IconCheck, IconPencil, IconUser} from "@tabler/icons-react";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Мастерская',
|
title: 'Мастерская',
|
||||||
description: '',
|
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 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 (
|
return (
|
||||||
<AppShell header={{height: 70}}>
|
<AppShell header={{height: 70}}>
|
||||||
<AppShellHeader>
|
<AppShellHeader>
|
||||||
<Header state="logged-in"/>
|
<Header/>
|
||||||
</AppShellHeader>
|
</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>
|
</AppShellMain>
|
||||||
|
<AppShellAside withBorder={false} px="16">
|
||||||
|
<Stack pt="16">
|
||||||
|
<Stack gap="5">
|
||||||
|
<Button title="Создать контест">Создать контест</Button>
|
||||||
|
<Button title="Создать задачу">Создать задачу</Button>
|
||||||
|
</Stack>
|
||||||
|
<TextInput placeholder="Поиск"/>
|
||||||
|
</Stack>
|
||||||
|
</AppShellAside>
|
||||||
</AppShell>
|
</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 NextImage from "next/image"
|
||||||
import {IconUser} from "@tabler/icons-react";
|
import {IconUser} from "@tabler/icons-react";
|
||||||
import {useDisclosure} from "@mantine/hooks";
|
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 = {
|
const {isPending, isSuccess} = useQuery({
|
||||||
state: "logged-in" | "logged-out" | "pending"
|
queryKey: ['me'],
|
||||||
}
|
queryFn: GetMe,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
const Profile = ({state}: ProfileProps) => {
|
const mutation = useMutation({
|
||||||
if (state == "logged-out") {
|
mutationFn: Logout,
|
||||||
return (
|
|
||||||
<Button ml="76" component={Link} href="/users/login" variant="default">
|
|
||||||
Войти
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == "pending") {
|
onSuccess: async () => {
|
||||||
|
await router.push("/login")
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Не удалось выйти из аккаунта. Попробуйте позже.", error)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
return (
|
return (
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Skeleton height="36" width="77"/>
|
<Button component={Link} href="/login" variant="default">
|
||||||
|
Войти
|
||||||
|
</Button>
|
||||||
<Skeleton height="60" circle/>
|
<Skeleton height="60" circle/>
|
||||||
</Group>
|
</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 (
|
return (
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button variant="default" visibleFrom="sm">
|
<Button ml="76" component={Link} href="/login" variant="default">
|
||||||
Выйти
|
Войти
|
||||||
</Button>
|
</Button>
|
||||||
<Avatar component={Link} href="/users/1" color="gray" size="60">
|
|
||||||
<IconUser size="32"/>
|
|
||||||
</Avatar>
|
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header = ({state}: ProfileProps) => {
|
const Header = () => {
|
||||||
const [drawerOpened, {toggle: toggleDrawer, close: closeDrawer}] = useDisclosure(false);
|
const [drawerOpened, {toggle: toggleDrawer, close: closeDrawer}] = useDisclosure(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -77,11 +98,12 @@ const Header = ({state}: ProfileProps) => {
|
||||||
visibleFrom="xs">
|
visibleFrom="xs">
|
||||||
Пользователи
|
Пользователи
|
||||||
</Anchor>
|
</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>
|
</Anchor>
|
||||||
</Group>
|
</Group>
|
||||||
<Profile state={state}/>
|
<Profile/>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue