feat: add profile features

This commit is contained in:
Vyacheslav1557 2025-02-16 23:06:30 +05:00
parent 07026be380
commit 953ce64ef2
27 changed files with 2214 additions and 231 deletions

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "proto"]
path = proto
url = https://git.sch9.ru/new_gate/contracts

4
Makefile Normal file
View 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
View 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
View file

@ -0,0 +1,7 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.11.0"
}
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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
View 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};

View file

@ -0,0 +1 @@
export {LoginPage} from "./ui";

123
src/_pages/login/ui.tsx Normal file
View 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};

View 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>строка &lt;&lt;<code>&lt;</code>&gt;&gt; (без кавычек), если
загаданное число меньше числа из запроса;</p></li>
<li><p>строка &lt;&lt;<code>&gt;=</code>&gt;&gt; (без кавычек), если
загаданное число больше либо равно числу из запроса.</p></li>
</ul>
<p>В случае, если ваша программа наверняка угадала нужное число <span
class="math inline"><em>x</em></span>, выведите строку вида
&lt;&lt;<code>! x</code>&gt;&gt;, где <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>В следующих строках на вход вашей программе будут подаваться строки
вида &lt;&lt;<code>&lt;</code>&gt;&gt; и
&lt;&lt;<code>&gt;=</code>&gt;&gt;. <span
class="math inline"><em>i</em></span>-я из этих строк является ответом
системы на ваш <span class="math inline"><em>i</em></span>-й запрос.
После того, как ваша программа угадала число, выведите
&lt;&lt;<code>! x</code>&gt;&gt; (без кавычек), где <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>)
по одному в строке (не забывайте выводить &lt;&lt;<em>перевод
строки</em>&gt;&gt; после каждого значения <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>, выведите строку вида
&lt;&lt;<code>! x</code>&gt;&gt; (без кавычек), где <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}>Последние посылки&nbsp;
<Anchor
component={Link}
href="/"
fs="italic"
c="var(--mantine-color-bright)" fw={500}>
(посмотреть все)
</Anchor>:
</Text>
</Stack>
</aside>
</Group>
</>
);
};
export default Page;

View file

@ -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>
<Anchor component={Link} href="/contests/1/A">
Просмотр {params.contest_id}
</Anchor>
</AppShellMain>
</AppShell>
);

View file

@ -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>
);
};

View file

@ -1,3 +0,0 @@
.black_and_white {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white))
}

View file

@ -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>
<QueryClientProvider client={queryClient}>
<MantineProvider defaultColorScheme="light" withGlobalClasses forceColorScheme="light">
<AppShell>
{children}
</AppShell>
</MantineProvider>
</QueryClientProvider>
</body>
</html>
);

6
src/app/login/page.tsx Normal file
View file

@ -0,0 +1,6 @@
export const metadata = {
title: 'Вход в аккаунт',
description: '',
};
export {LoginPage as default} from "@/_pages/login"

View file

@ -11,7 +11,7 @@ export default function Page() {
return (
<AppShell header={{height: 70}}>
<AppShellHeader>
<Header state="logged-out"/>
<Header/>
</AppShellHeader>
<AppShellMain>

View file

@ -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>
);

View file

@ -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;

View file

@ -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>
);
};

View 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;

View file

@ -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>
);
};

View 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);
}
}

View 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};

View file

@ -0,0 +1 @@
export {Code} from "./code";

View 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};

View file

@ -20,33 +20,45 @@ 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">
<Button variant="default" visibleFrom="sm" onClick={() => mutation.mutate()}>
Выйти
</Button>
<Avatar component={Link} href="/users/1" color="gray" size="60">
@ -56,7 +68,16 @@ const Profile = ({state}: ProfileProps) => {
);
}
const Header = ({state}: ProfileProps) => {
return (
<Group justify="flex-end">
<Button ml="76" component={Link} href="/login" variant="default">
Войти
</Button>
</Group>
);
}
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>