formatting
This commit is contained in:
@@ -1,17 +1,21 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import axios from "../../axios";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
|
||||
import MarkdownPreview from "./MarckDownPreview";
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import axios from '../../axios';
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
|
||||
import MarkdownPreview from './MarckDownPreview';
|
||||
|
||||
interface MarkdownEditorProps {
|
||||
defaultValue?: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const MarkdownEditor: FC<MarkdownEditorProps> = ({ defaultValue, onChange }) => {
|
||||
const [markdown, setMarkdown] = useState<string>(defaultValue || `# 🌙 Добро пожаловать в Markdown-редактор
|
||||
const MarkdownEditor: FC<MarkdownEditorProps> = ({
|
||||
defaultValue,
|
||||
onChange,
|
||||
}) => {
|
||||
const [markdown, setMarkdown] = useState<string>(
|
||||
defaultValue ||
|
||||
`# 🌙 Добро пожаловать в Markdown-редактор
|
||||
|
||||
Добро пожаловать в **Markdown-редактор**!
|
||||
Здесь ты можешь писать в формате Markdown и видеть результат **в реальном времени** 👇
|
||||
@@ -205,34 +209,42 @@ print(greet("Мир"))
|
||||
|
||||
**🖤 Конец демонстрации. Спасибо, что используешь Markdown-редактор!**
|
||||
|
||||
`);
|
||||
`,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(markdown);
|
||||
}, [markdown]);
|
||||
|
||||
// Обработчик вставки
|
||||
const handlePaste = async (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
const handlePaste = async (
|
||||
e: React.ClipboardEvent<HTMLTextAreaElement>,
|
||||
) => {
|
||||
const items = e.clipboardData.items;
|
||||
|
||||
for (const item of items) {
|
||||
if (item.type.startsWith("image/")) {
|
||||
if (item.type.startsWith('image/')) {
|
||||
e.preventDefault(); // предотвращаем вставку картинки как текста
|
||||
|
||||
const file = item.getAsFile();
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await axios.post("/media/upload", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
const response = await axios.post(
|
||||
'/media/upload',
|
||||
formData,
|
||||
{
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
},
|
||||
);
|
||||
|
||||
const imageUrl = response.data.url;
|
||||
// Вставляем ссылку на картинку в текст
|
||||
const cursorPos = (e.target as HTMLTextAreaElement).selectionStart;
|
||||
const cursorPos = (e.target as HTMLTextAreaElement)
|
||||
.selectionStart;
|
||||
const newText =
|
||||
markdown.slice(0, cursorPos) +
|
||||
`<img src=\"${imageUrl}\" alt=\"img\"/>` +
|
||||
@@ -240,7 +252,7 @@ print(greet("Мир"))
|
||||
|
||||
setMarkdown(newText);
|
||||
} catch (err) {
|
||||
console.error("Ошибка загрузки изображения:", err);
|
||||
console.error('Ошибка загрузки изображения:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,15 +263,22 @@ print(greet("Мир"))
|
||||
{/* Предпросмотр */}
|
||||
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
||||
<div className="p-4 border-r border-gray-700 flex flex-col h-full">
|
||||
<h2 className="text-lg font-semibold mb-3 text-gray-100">👀 Предпросмотр</h2>
|
||||
<MarkdownPreview content={markdown} className="h-[calc(100%-40px)]"/>
|
||||
<h2 className="text-lg font-semibold mb-3 text-gray-100">
|
||||
👀 Предпросмотр
|
||||
</h2>
|
||||
<MarkdownPreview
|
||||
content={markdown}
|
||||
className="h-[calc(100%-40px)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Редактор */}
|
||||
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
||||
<div className="p-4 border-r border-gray-700 flex flex-col h-full">
|
||||
<h2 className="text-lg font-semibold mb-3 text-gray-100">📝 Редактор</h2>
|
||||
<h2 className="text-lg font-semibold mb-3 text-gray-100">
|
||||
📝 Редактор
|
||||
</h2>
|
||||
<textarea
|
||||
value={markdown}
|
||||
onChange={(e) => setMarkdown(e.target.value)}
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
import React from "react";
|
||||
import { arrowLeft } from "../../assets/icons/header";
|
||||
import { Logo } from "../../assets/logos";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import React from 'react';
|
||||
import { arrowLeft } from '../../assets/icons/header';
|
||||
import { Logo } from '../../assets/logos';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface HeaderProps {
|
||||
backUrl?: string;
|
||||
}
|
||||
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({
|
||||
backUrl="/home/articles",
|
||||
}) => {
|
||||
const Header: React.FC<HeaderProps> = ({ backUrl = '/home/articles' }) => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<header className="w-full h-[60px] flex items-center px-4 gap-[20px]">
|
||||
<img src={Logo} alt="Logo" className="h-[28px] w-auto cursor-pointer" onClick={() => { navigate("/home") }} />
|
||||
<img
|
||||
src={Logo}
|
||||
alt="Logo"
|
||||
className="h-[28px] w-auto cursor-pointer"
|
||||
onClick={() => {
|
||||
navigate('/home');
|
||||
}}
|
||||
/>
|
||||
|
||||
<img src={arrowLeft} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate(backUrl) }} />
|
||||
<img
|
||||
src={arrowLeft}
|
||||
alt="back"
|
||||
className="h-[24px] w-[24px] cursor-pointer"
|
||||
onClick={() => {
|
||||
navigate(backUrl);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <div className="flex gap-[10px]">
|
||||
<img src={chevroneLeft} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate(`/mission/${missionId - 1}`) }} />
|
||||
<span>{missionId}</span>
|
||||
<img src={chevroneRight} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate(`/mission/${missionId + 1}`) }} />
|
||||
</div> */}
|
||||
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { FC } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import rehypeSanitize from "rehype-sanitize";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
import { FC } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import rehypeSanitize from 'rehype-sanitize';
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
|
||||
import { defaultSchema } from "hast-util-sanitize";
|
||||
import { cn } from "../../lib/cn";
|
||||
import { defaultSchema } from 'hast-util-sanitize';
|
||||
import { cn } from '../../lib/cn';
|
||||
|
||||
const schema = {
|
||||
...defaultSchema,
|
||||
@@ -15,9 +15,9 @@ const schema = {
|
||||
...defaultSchema.attributes,
|
||||
div: [
|
||||
...(defaultSchema.attributes?.div || []),
|
||||
["style"] // разрешаем атрибут style на div
|
||||
]
|
||||
}
|
||||
['style'], // разрешаем атрибут style на div
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
interface MarkdownPreviewProps {
|
||||
@@ -25,13 +25,25 @@ interface MarkdownPreviewProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const MarkdownPreview: FC<MarkdownPreviewProps> = ({ content, className="" }) => {
|
||||
const MarkdownPreview: FC<MarkdownPreviewProps> = ({
|
||||
content,
|
||||
className = '',
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn("flex-1 bg-[#161b22] rounded-lg shadow-lg p-6", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 bg-[#161b22] rounded-lg shadow-lg p-6',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="prose prose-invert max-w-none h-full overflow-auto pr-4 medium-scrollbar">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw, [rehypeSanitize, schema], rehypeHighlight]}
|
||||
rehypePlugins={[
|
||||
rehypeRaw,
|
||||
[rehypeSanitize, schema],
|
||||
rehypeHighlight,
|
||||
]}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { cn } from '../../../lib/cn';
|
||||
|
||||
export interface ArticleItemProps {
|
||||
id: number;
|
||||
@@ -6,17 +6,17 @@ export interface ArticleItemProps {
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const ArticleItem: React.FC<ArticleItemProps> = ({
|
||||
id, name, tags
|
||||
}) => {
|
||||
const ArticleItem: React.FC<ArticleItemProps> = ({ id, name, tags }) => {
|
||||
return (
|
||||
<div className={cn("w-full relative rounded-[10px] text-liquid-white mb-[20px]",
|
||||
// type == "first" ? "bg-liquid-lighter" : "bg-liquid-background",
|
||||
"gap-[20px] px-[20px] py-[10px] box-border ",
|
||||
"border-b-[1px] border-b-liquid-lighter",
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
'w-full relative rounded-[10px] text-liquid-white mb-[20px]',
|
||||
// type == "first" ? "bg-liquid-lighter" : "bg-liquid-background",
|
||||
'gap-[20px] px-[20px] py-[10px] box-border ',
|
||||
'border-b-[1px] border-b-liquid-lighter',
|
||||
)}
|
||||
>
|
||||
<div className="h-[23px] flex ">
|
||||
|
||||
<div className="text-[18px] font-bold w-[60px] mr-[20px] flex items-center">
|
||||
#{id}
|
||||
</div>
|
||||
@@ -25,15 +25,18 @@ const ArticleItem: React.FC<ArticleItemProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[14px] flex text-liquid-light gap-[10px] mt-[10px]">
|
||||
{tags.map((v, i) =>
|
||||
<div key={i} className={cn(
|
||||
"rounded-full px-[16px] py-[8px] bg-liquid-lighter",
|
||||
v == "Sertificated" && "text-liquid-green")}>
|
||||
{tags.map((v, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
'rounded-full px-[16px] py-[8px] bg-liquid-lighter',
|
||||
v == 'Sertificated' && 'text-liquid-green',
|
||||
)}
|
||||
>
|
||||
{v}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useEffect } from "react";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { useAppDispatch } from "../../../redux/hooks";
|
||||
import ArticleItem from "./ArticleItem";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||
import { useAppDispatch } from '../../../redux/hooks';
|
||||
import ArticleItem from './ArticleItem';
|
||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export interface Article {
|
||||
id: number;
|
||||
@@ -12,159 +11,152 @@ export interface Article {
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
|
||||
const Articles = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const articles: Article[] = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Todo List App",
|
||||
"tags": ["Sertificated", "state", "list"],
|
||||
id: 1,
|
||||
name: 'Todo List App',
|
||||
tags: ['Sertificated', 'state', 'list'],
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Search Filter Component",
|
||||
"tags": ["filter", "props", "hooks"],
|
||||
id: 2,
|
||||
name: 'Search Filter Component',
|
||||
tags: ['filter', 'props', 'hooks'],
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "User Card List",
|
||||
"tags": ["components", "props", "array"],
|
||||
id: 3,
|
||||
name: 'User Card List',
|
||||
tags: ['components', 'props', 'array'],
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Theme Switcher",
|
||||
"tags": ["Sertificated", "theme", "hooks"],
|
||||
id: 4,
|
||||
name: 'Theme Switcher',
|
||||
tags: ['Sertificated', 'theme', 'hooks'],
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Search Filter Component",
|
||||
"tags": ["filter", "props", "hooks"],
|
||||
id: 2,
|
||||
name: 'Search Filter Component',
|
||||
tags: ['filter', 'props', 'hooks'],
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "User Card List",
|
||||
"tags": ["components", "props", "array"],
|
||||
id: 3,
|
||||
name: 'User Card List',
|
||||
tags: ['components', 'props', 'array'],
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Theme Switcher",
|
||||
"tags": ["Sertificated", "theme", "hooks"],
|
||||
id: 4,
|
||||
name: 'Theme Switcher',
|
||||
tags: ['Sertificated', 'theme', 'hooks'],
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Search Filter Component",
|
||||
"tags": ["filter", "props", "hooks"],
|
||||
id: 2,
|
||||
name: 'Search Filter Component',
|
||||
tags: ['filter', 'props', 'hooks'],
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "User Card List",
|
||||
"tags": ["components", "props", "array"],
|
||||
id: 3,
|
||||
name: 'User Card List',
|
||||
tags: ['components', 'props', 'array'],
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Theme Switcher",
|
||||
"tags": ["Sertificated", "theme", "hooks"],
|
||||
id: 4,
|
||||
name: 'Theme Switcher',
|
||||
tags: ['Sertificated', 'theme', 'hooks'],
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Search Filter Component",
|
||||
"tags": ["filter", "props", "hooks"],
|
||||
id: 2,
|
||||
name: 'Search Filter Component',
|
||||
tags: ['filter', 'props', 'hooks'],
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "User Card List",
|
||||
"tags": ["components", "props", "array"],
|
||||
id: 3,
|
||||
name: 'User Card List',
|
||||
tags: ['components', 'props', 'array'],
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Theme Switcher",
|
||||
"tags": ["Sertificated", "theme", "hooks"],
|
||||
id: 4,
|
||||
name: 'Theme Switcher',
|
||||
tags: ['Sertificated', 'theme', 'hooks'],
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Search Filter Component",
|
||||
"tags": ["filter", "props", "hooks"],
|
||||
id: 2,
|
||||
name: 'Search Filter Component',
|
||||
tags: ['filter', 'props', 'hooks'],
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "User Card List",
|
||||
"tags": ["components", "props", "array"],
|
||||
id: 3,
|
||||
name: 'User Card List',
|
||||
tags: ['components', 'props', 'array'],
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Theme Switcher",
|
||||
"tags": ["Sertificated", "theme", "hooks"],
|
||||
id: 4,
|
||||
name: 'Theme Switcher',
|
||||
tags: ['Sertificated', 'theme', 'hooks'],
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Search Filter Component",
|
||||
"tags": ["filter", "props", "hooks"],
|
||||
id: 2,
|
||||
name: 'Search Filter Component',
|
||||
tags: ['filter', 'props', 'hooks'],
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "User Card List",
|
||||
"tags": ["components", "props", "array"],
|
||||
id: 3,
|
||||
name: 'User Card List',
|
||||
tags: ['components', 'props', 'array'],
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Theme Switcher",
|
||||
"tags": ["Sertificated", "theme", "hooks"],
|
||||
id: 4,
|
||||
name: 'Theme Switcher',
|
||||
tags: ['Sertificated', 'theme', 'hooks'],
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Search Filter Component",
|
||||
"tags": ["filter", "props", "hooks"],
|
||||
id: 2,
|
||||
name: 'Search Filter Component',
|
||||
tags: ['filter', 'props', 'hooks'],
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "User Card List",
|
||||
"tags": ["components", "props", "array"],
|
||||
id: 3,
|
||||
name: 'User Card List',
|
||||
tags: ['components', 'props', 'array'],
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Theme Switcher",
|
||||
"tags": ["Sertificated", "theme", "hooks"],
|
||||
}
|
||||
id: 4,
|
||||
name: 'Theme Switcher',
|
||||
tags: ['Sertificated', 'theme', 'hooks'],
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("articles"))
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage('articles'));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
|
||||
<div className="h-full box-border">
|
||||
|
||||
<div className="relative flex items-center mb-[20px]">
|
||||
<div className="h-[50px] text-[40px] font-bold text-liquid-white flex items-center">
|
||||
Статьи
|
||||
</div>
|
||||
<SecondaryButton
|
||||
onClick={() => {navigate("/article/create")}}
|
||||
onClick={() => {
|
||||
navigate('/article/create');
|
||||
}}
|
||||
text="Создать статью"
|
||||
className="absolute right-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-liquid-lighter h-[50px] mb-[20px]">
|
||||
|
||||
</div>
|
||||
<div className="bg-liquid-lighter h-[50px] mb-[20px]"></div>
|
||||
|
||||
<div>
|
||||
|
||||
{articles.map((v, i) => (
|
||||
<ArticleItem key={i} {...v} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
pages
|
||||
</div>
|
||||
<div>pages</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,113 +1,133 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { PrimaryButton } from "../../../components/button/PrimaryButton";
|
||||
import { Input } from "../../../components/input/Input";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { loginUser } from "../../../redux/slices/auth";
|
||||
import { useState, useEffect } from 'react';
|
||||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||
import { Input } from '../../../components/input/Input';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { loginUser } from '../../../redux/slices/auth';
|
||||
// import { cn } from "../../../lib/cn";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
import { Balloon } from "../../../assets/icons/auth";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { googleLogo } from "../../../assets/icons/input";
|
||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||
import { Balloon } from '../../../assets/icons/auth';
|
||||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||
import { googleLogo } from '../../../assets/icons/input';
|
||||
|
||||
const Login = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [username, setUsername] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
|
||||
const [username, setUsername] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
|
||||
|
||||
const { status, jwt } = useAppSelector((state) => state.auth);
|
||||
const { status, jwt } = useAppSelector((state) => state.auth);
|
||||
|
||||
// const [err, setErr] = useState<string>("");
|
||||
|
||||
// const [err, setErr] = useState<string>("");
|
||||
// После успешного логина
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage('account'));
|
||||
console.log(submitClicked);
|
||||
}, []);
|
||||
|
||||
// После успешного логина
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("account"))
|
||||
console.log(submitClicked);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (jwt) {
|
||||
navigate('/home/offices'); // или другая страница после входа
|
||||
}
|
||||
}, [jwt]);
|
||||
|
||||
useEffect(() => {
|
||||
if (jwt) {
|
||||
navigate("/home/offices"); // или другая страница после входа
|
||||
}
|
||||
}, [jwt]);
|
||||
const handleLogin = () => {
|
||||
// setErr(err == "" ? "Неверная почта и/или пароль" : "");
|
||||
setSubmitClicked(true);
|
||||
|
||||
const handleLogin = () => {
|
||||
// setErr(err == "" ? "Неверная почта и/или пароль" : "");
|
||||
setSubmitClicked(true);
|
||||
if (!username || !password) return;
|
||||
|
||||
if (!username || !password) return;
|
||||
dispatch(loginUser({ username, password }));
|
||||
};
|
||||
|
||||
dispatch(loginUser({ username, password }));
|
||||
};
|
||||
return (
|
||||
<div className="h-svh w-svw fixed pointer-events-none top-0 left-0 flex items-center justify-center">
|
||||
<div className="grid gap-[80px] grid-cols-[400px,384px] box-border relative ">
|
||||
<div className="flex items-center justify-center ">
|
||||
<img src={Balloon} />
|
||||
</div>
|
||||
<div className=" relative pointer-events-auto">
|
||||
<div>
|
||||
<div className="text-[40px] text-liquid-white font-bold h-[50px]">
|
||||
С возвращением
|
||||
</div>
|
||||
<div className="text-[18px] text-liquid-light font-bold h-[23px]">
|
||||
Вход в аккаунт
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className="h-svh w-svw fixed pointer-events-none top-0 left-0 flex items-center justify-center">
|
||||
<div className="grid gap-[80px] grid-cols-[400px,384px] box-border relative ">
|
||||
<div className="flex items-center justify-center ">
|
||||
<img src={Balloon} />
|
||||
</div>
|
||||
<div className=" relative pointer-events-auto">
|
||||
<div>
|
||||
<div className="text-[40px] text-liquid-white font-bold h-[50px]">
|
||||
С возвращением
|
||||
<Input
|
||||
name="login"
|
||||
autocomplete="login"
|
||||
className="mt-[10px]"
|
||||
type="text"
|
||||
label="Логин"
|
||||
onChange={(v) => {
|
||||
setUsername(v);
|
||||
}}
|
||||
placeholder="login"
|
||||
/>
|
||||
<Input
|
||||
name="password"
|
||||
autocomplete="password"
|
||||
className="mt-[10px]"
|
||||
type="password"
|
||||
label="Пароль"
|
||||
onChange={(v) => {
|
||||
setPassword(v);
|
||||
}}
|
||||
placeholder="abCD1234"
|
||||
/>
|
||||
|
||||
<div className="flex justify-end mt-[10px]">
|
||||
<Link
|
||||
to={''}
|
||||
className={
|
||||
'text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline '
|
||||
}
|
||||
>
|
||||
Забыли пароль?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-[10px]">
|
||||
<PrimaryButton
|
||||
className="w-full mb-[8px]"
|
||||
onClick={handleLogin}
|
||||
text={status === 'loading' ? 'Вход...' : 'Вход'}
|
||||
disabled={status === 'loading'}
|
||||
/>
|
||||
<SecondaryButton className="w-full" onClick={() => {}}>
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={googleLogo}
|
||||
className="h-[24px] w-[24px] mr-[15px]"
|
||||
/>
|
||||
Вход с Google
|
||||
</div>
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-[10px]">
|
||||
<span>
|
||||
Нет аккаунта?{' '}
|
||||
<Link
|
||||
to={'/home/register'}
|
||||
className={
|
||||
'text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline '
|
||||
}
|
||||
>
|
||||
Регистрация
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[18px] text-liquid-light font-bold h-[23px]">
|
||||
Вход в аккаунт
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<Input name="login" autocomplete="login" className="mt-[10px]" type="text" label="Логин" onChange={(v) => { setUsername(v) }} placeholder="login" />
|
||||
<Input name="password" autocomplete="password" className="mt-[10px]" type="password" label="Пароль" onChange={(v) => { setPassword(v) }} placeholder="abCD1234" />
|
||||
|
||||
<div className="flex justify-end mt-[10px]">
|
||||
<Link
|
||||
to={""}
|
||||
className={"text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline "}>
|
||||
Забыли пароль?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-[10px]">
|
||||
<PrimaryButton
|
||||
className="w-full mb-[8px]"
|
||||
onClick={handleLogin}
|
||||
text={status === "loading" ? "Вход..." : "Вход"}
|
||||
disabled={status === "loading"}
|
||||
/>
|
||||
<SecondaryButton
|
||||
className="w-full"
|
||||
onClick={() => { }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<img src={googleLogo} className="h-[24px] w-[24px] mr-[15px]" />
|
||||
Вход с Google
|
||||
</div>
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="flex justify-center mt-[10px]">
|
||||
<span>
|
||||
Нет аккаунта? <Link
|
||||
to={"/home/register"}
|
||||
className={"text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline "}>
|
||||
Регистрация
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
|
||||
@@ -1,126 +1,169 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { PrimaryButton } from "../../../components/button/PrimaryButton";
|
||||
import { Input } from "../../../components/input/Input";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { registerUser } from "../../../redux/slices/auth";
|
||||
import { useState, useEffect } from 'react';
|
||||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||
import { Input } from '../../../components/input/Input';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { registerUser } from '../../../redux/slices/auth';
|
||||
// import { cn } from "../../../lib/cn";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
import { Balloon } from "../../../assets/icons/auth";
|
||||
import { Link } from "react-router-dom";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { Checkbox } from "../../../components/checkbox/Checkbox";
|
||||
import { googleLogo } from "../../../assets/icons/input";
|
||||
|
||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||
import { Balloon } from '../../../assets/icons/auth';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||
import { Checkbox } from '../../../components/checkbox/Checkbox';
|
||||
import { googleLogo } from '../../../assets/icons/input';
|
||||
|
||||
const Register = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [username, setUsername] = useState<string>("");
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>("");
|
||||
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
|
||||
const [username, setUsername] = useState<string>('');
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>('');
|
||||
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
|
||||
|
||||
const { status, jwt } = useAppSelector((state) => state.auth);
|
||||
const { status, jwt } = useAppSelector((state) => state.auth);
|
||||
|
||||
// После успешной регистрации — переход в систему
|
||||
// После успешной регистрации — переход в систему
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("account"))
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage('account'));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (jwt) {
|
||||
navigate("/home");
|
||||
}
|
||||
console.log(submitClicked);
|
||||
}, [jwt]);
|
||||
useEffect(() => {
|
||||
if (jwt) {
|
||||
navigate('/home');
|
||||
}
|
||||
console.log(submitClicked);
|
||||
}, [jwt]);
|
||||
|
||||
const handleRegister = () => {
|
||||
setSubmitClicked(true);
|
||||
const handleRegister = () => {
|
||||
setSubmitClicked(true);
|
||||
|
||||
if (!username || !email || !password || !confirmPassword) return;
|
||||
if (password !== confirmPassword) return;
|
||||
if (!username || !email || !password || !confirmPassword) return;
|
||||
if (password !== confirmPassword) return;
|
||||
|
||||
dispatch(registerUser({ username, email, password }));
|
||||
};
|
||||
dispatch(registerUser({ username, email, password }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-svh w-svw fixed pointer-events-none top-0 left-0 flex items-center justify-center">
|
||||
<div className="grid gap-[80px] grid-cols-[400px,384px] box-border relative ">
|
||||
<div className="flex items-center justify-center ">
|
||||
<img src={Balloon} />
|
||||
</div>
|
||||
<div className=" relative pointer-events-auto">
|
||||
<div>
|
||||
<div className="text-[40px] text-liquid-white font-bold h-[50px]">
|
||||
Добро пожаловать
|
||||
return (
|
||||
<div className="h-svh w-svw fixed pointer-events-none top-0 left-0 flex items-center justify-center">
|
||||
<div className="grid gap-[80px] grid-cols-[400px,384px] box-border relative ">
|
||||
<div className="flex items-center justify-center ">
|
||||
<img src={Balloon} />
|
||||
</div>
|
||||
<div className=" relative pointer-events-auto">
|
||||
<div>
|
||||
<div className="text-[40px] text-liquid-white font-bold h-[50px]">
|
||||
Добро пожаловать
|
||||
</div>
|
||||
<div className="text-[18px] text-liquid-light font-bold h-[23px]">
|
||||
Регистрация
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
name="email"
|
||||
autocomplete="email"
|
||||
className="mt-[10px]"
|
||||
type="email"
|
||||
label="Почта"
|
||||
onChange={(v) => {
|
||||
setEmail(v);
|
||||
}}
|
||||
placeholder="example@gmail.com"
|
||||
/>
|
||||
<Input
|
||||
name="login"
|
||||
autocomplete="login"
|
||||
className="mt-[10px]"
|
||||
type="text"
|
||||
label="Логин пользователя"
|
||||
onChange={(v) => {
|
||||
setUsername(v);
|
||||
}}
|
||||
placeholder="login"
|
||||
/>
|
||||
<Input
|
||||
name="password"
|
||||
autocomplete="password"
|
||||
className="mt-[10px]"
|
||||
type="password"
|
||||
label="Пароль"
|
||||
onChange={(v) => {
|
||||
setPassword(v);
|
||||
}}
|
||||
placeholder="abCD1234"
|
||||
/>
|
||||
<Input
|
||||
name="confirm-password"
|
||||
autocomplete="confirm-password"
|
||||
className="mt-[10px]"
|
||||
type="password"
|
||||
label="Повторите пароль"
|
||||
onChange={(v) => {
|
||||
setConfirmPassword(v);
|
||||
}}
|
||||
placeholder="abCD1234"
|
||||
/>
|
||||
|
||||
<div className=" flex items-center mt-[10px] h-[24px]">
|
||||
<Checkbox
|
||||
onChange={(value: boolean) => {
|
||||
value;
|
||||
}}
|
||||
className="p-0 w-fit m-[2.75px]"
|
||||
size="md"
|
||||
color="secondary"
|
||||
variant="default"
|
||||
/>
|
||||
<span className="text-[14px] font-medium text-liquid-light h-[18px] ml-[10px]">
|
||||
Я принимаю{' '}
|
||||
<Link to={'/home'} className={' underline'}>
|
||||
политику конфиденциальности
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-[10px]">
|
||||
<PrimaryButton
|
||||
className="w-full mb-[8px]"
|
||||
onClick={() => handleRegister()}
|
||||
text={
|
||||
status === 'loading'
|
||||
? 'Регистрация...'
|
||||
: 'Регистрация'
|
||||
}
|
||||
disabled={status === 'loading'}
|
||||
/>
|
||||
<SecondaryButton className="w-full" onClick={() => {}}>
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={googleLogo}
|
||||
className="h-[24px] w-[24px] mr-[15px]"
|
||||
/>
|
||||
Регистрация с Google
|
||||
</div>
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-[10px]">
|
||||
<span>
|
||||
Уже есть аккаунт?{' '}
|
||||
<Link
|
||||
to={'/home/login'}
|
||||
className={
|
||||
'text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline '
|
||||
}
|
||||
>
|
||||
Авторизация
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[18px] text-liquid-light font-bold h-[23px]">
|
||||
Регистрация
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<Input name="email" autocomplete="email" className="mt-[10px]" type="email" label="Почта" onChange={(v) => {setEmail(v)}} placeholder="example@gmail.com" />
|
||||
<Input name="login" autocomplete="login" className="mt-[10px]" type="text" label="Логин пользователя" onChange={(v) => {setUsername(v)}} placeholder="login" />
|
||||
<Input name="password" autocomplete="password" className="mt-[10px]" type="password" label="Пароль" onChange={(v) => {setPassword(v)}} placeholder="abCD1234" />
|
||||
<Input name="confirm-password" autocomplete="confirm-password" className="mt-[10px]" type="password" label="Повторите пароль" onChange={(v) => {setConfirmPassword(v)}} placeholder="abCD1234" />
|
||||
|
||||
<div className=" flex items-center mt-[10px] h-[24px]">
|
||||
<Checkbox
|
||||
onChange={(value: boolean) => { value; }}
|
||||
className="p-0 w-fit m-[2.75px]"
|
||||
size="md"
|
||||
color="secondary"
|
||||
variant="default" />
|
||||
<span className="text-[14px] font-medium text-liquid-light h-[18px] ml-[10px]">
|
||||
Я принимаю <Link
|
||||
to={"/home"}
|
||||
className={" underline"}
|
||||
>
|
||||
политику конфиденциальности
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="mt-[10px]">
|
||||
<PrimaryButton
|
||||
className="w-full mb-[8px]"
|
||||
onClick={() => handleRegister()}
|
||||
text={status === "loading" ? "Регистрация..." : "Регистрация"}
|
||||
disabled={status === "loading"}
|
||||
/>
|
||||
<SecondaryButton
|
||||
className="w-full"
|
||||
onClick={() => { }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<img src={googleLogo} className="h-[24px] w-[24px] mr-[15px]" />
|
||||
Регистрация с Google
|
||||
</div>
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex justify-center mt-[10px]">
|
||||
<span>
|
||||
Уже есть аккаунт? <Link
|
||||
to={"/home/login"}
|
||||
className={"text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline "}>
|
||||
Авторизация
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
|
||||
@@ -1,65 +1,74 @@
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { Account } from "../../../assets/icons/auth";
|
||||
import { PrimaryButton } from "../../../components/button/PrimaryButton";
|
||||
import { ReverseButton } from "../../../components/button/ReverseButton";
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { Account } from '../../../assets/icons/auth';
|
||||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||
import { ReverseButton } from '../../../components/button/ReverseButton';
|
||||
|
||||
export interface ContestItemProps {
|
||||
name: string;
|
||||
startAt: string;
|
||||
duration: number;
|
||||
members: number;
|
||||
statusRegister: "reg" | "nonreg";
|
||||
type: "first" | "second";
|
||||
statusRegister: 'reg' | 'nonreg';
|
||||
type: 'first' | 'second';
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const date = new Date(dateString);
|
||||
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const year = date.getFullYear();
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
|
||||
const hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
return `${day}/${month}/${year}\n${hours}:${minutes}`;
|
||||
return `${day}/${month}/${year}\n${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
function formatWaitTime(ms: number): string {
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) {
|
||||
const remainder = days % 10;
|
||||
let suffix = "дней";
|
||||
if (remainder === 1 && days !== 11) suffix = "день";
|
||||
else if (remainder >= 2 && remainder <= 4 && (days < 10 || days > 20)) suffix = "дня";
|
||||
return `${days} ${suffix}`;
|
||||
} else if (hours > 0) {
|
||||
const mins = minutes % 60;
|
||||
return mins > 0 ? `${hours} ч ${mins} мин` : `${hours} ч`;
|
||||
} else {
|
||||
return `${minutes} мин`;
|
||||
}
|
||||
if (days > 0) {
|
||||
const remainder = days % 10;
|
||||
let suffix = 'дней';
|
||||
if (remainder === 1 && days !== 11) suffix = 'день';
|
||||
else if (remainder >= 2 && remainder <= 4 && (days < 10 || days > 20))
|
||||
suffix = 'дня';
|
||||
return `${days} ${suffix}`;
|
||||
} else if (hours > 0) {
|
||||
const mins = minutes % 60;
|
||||
return mins > 0 ? `${hours} ч ${mins} мин` : `${hours} ч`;
|
||||
} else {
|
||||
return `${minutes} мин`;
|
||||
}
|
||||
}
|
||||
|
||||
const ContestItem: React.FC<ContestItemProps> = ({
|
||||
name, startAt, duration, members, statusRegister, type
|
||||
name,
|
||||
startAt,
|
||||
duration,
|
||||
members,
|
||||
statusRegister,
|
||||
type,
|
||||
}) => {
|
||||
const now = new Date();
|
||||
|
||||
const waitTime = new Date(startAt).getTime() - now.getTime();
|
||||
|
||||
return (
|
||||
<div className={cn("w-full box-border relative rounded-[10px] px-[20px] py-[10px] text-liquid-white text-[16px] leading-[20px]",
|
||||
waitTime <= 0 ? "grid grid-cols-6" : "grid grid-cols-7",
|
||||
"items-center font-bold text-liquid-white",
|
||||
type == "first" ? " bg-liquid-lighter" : " bg-liquid-background"
|
||||
)}>
|
||||
<div className="text-left font-bold text-[18px]">
|
||||
{name}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'w-full box-border relative rounded-[10px] px-[20px] py-[10px] text-liquid-white text-[16px] leading-[20px]',
|
||||
waitTime <= 0 ? 'grid grid-cols-6' : 'grid grid-cols-7',
|
||||
'items-center font-bold text-liquid-white',
|
||||
type == 'first'
|
||||
? ' bg-liquid-lighter'
|
||||
: ' bg-liquid-background',
|
||||
)}
|
||||
>
|
||||
<div className="text-left font-bold text-[18px]">{name}</div>
|
||||
<div className="text-center text-liquid-brightmain font-normal ">
|
||||
{/* {authors.map((v, i) => <p key={i}>{v}</p>)} */}
|
||||
valavshonok
|
||||
@@ -67,29 +76,29 @@ const ContestItem: React.FC<ContestItemProps> = ({
|
||||
<div className="text-center text-nowrap whitespace-pre-line">
|
||||
{formatDate(startAt)}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
{formatWaitTime(duration)}
|
||||
</div>
|
||||
{
|
||||
waitTime > 0 &&
|
||||
<div className="text-center">{formatWaitTime(duration)}</div>
|
||||
{waitTime > 0 && (
|
||||
<div className="text-center whitespace-pre-line ">
|
||||
|
||||
{"До начала\n" + formatWaitTime(waitTime)}
|
||||
{'До начала\n' + formatWaitTime(waitTime)}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
<div className="items-center justify-center flex gap-[10px] flex-row w-full">
|
||||
<div>{members}</div>
|
||||
<img src={Account} className="h-[24px] w-[24px]"/>
|
||||
<img src={Account} className="h-[24px] w-[24px]" />
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
{
|
||||
statusRegister == "reg" ?
|
||||
<> <PrimaryButton onClick={() => {}} text="Регистрация"/></>
|
||||
:
|
||||
<> <ReverseButton onClick={() => {}} text="Вы записаны"/></>
|
||||
}
|
||||
{statusRegister == 'reg' ? (
|
||||
<>
|
||||
{' '}
|
||||
<PrimaryButton onClick={() => {}} text="Регистрация" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{' '}
|
||||
<ReverseButton onClick={() => {}} text="Вы записаны" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect } from "react";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
||||
import ContestsBlock from "./ContestsBlock";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
import { fetchContests } from "../../../redux/slices/contests";
|
||||
import { useEffect } from 'react';
|
||||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import ContestsBlock from './ContestsBlock';
|
||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||
import { fetchContests } from '../../../redux/slices/contests';
|
||||
|
||||
const Contests = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -17,12 +17,14 @@ const Contests = () => {
|
||||
|
||||
// При загрузке страницы — выставляем активную вкладку и подгружаем контесты
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("contests"));
|
||||
dispatch(setMenuActivePage('contests'));
|
||||
dispatch(fetchContests({}));
|
||||
}, []);
|
||||
|
||||
if (loading == "loading") {
|
||||
return <div className="text-liquid-white p-4">Загрузка контестов...</div>;
|
||||
if (loading == 'loading') {
|
||||
return (
|
||||
<div className="text-liquid-white p-4">Загрузка контестов...</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
@@ -33,7 +35,11 @@ const Contests = () => {
|
||||
<div className="h-full w-[calc(100%+250px)] box-border p-[20px] pt-[20p]">
|
||||
<div className="h-full box-border">
|
||||
<div className="relative flex items-center mb-[20px]">
|
||||
<div className={cn("h-[50px] text-[40px] font-bold text-liquid-white flex items-center")}>
|
||||
<div
|
||||
className={cn(
|
||||
'h-[50px] text-[40px] font-bold text-liquid-white flex items-center',
|
||||
)}
|
||||
>
|
||||
Контесты
|
||||
</div>
|
||||
<SecondaryButton
|
||||
@@ -49,8 +55,7 @@ const Contests = () => {
|
||||
className="mb-[20px]"
|
||||
title="Текущие"
|
||||
contests={contests.filter((contest) => {
|
||||
const endTime =
|
||||
new Date(contest.endsAt).getTime()
|
||||
const endTime = new Date(contest.endsAt).getTime();
|
||||
return endTime >= now.getTime();
|
||||
})}
|
||||
/>
|
||||
@@ -59,8 +64,7 @@ const Contests = () => {
|
||||
className="mb-[20px]"
|
||||
title="Прошедшие"
|
||||
contests={contests.filter((contest) => {
|
||||
const endTime =
|
||||
new Date(contest.endsAt).getTime()
|
||||
const endTime = new Date(contest.endsAt).getTime();
|
||||
return endTime < now.getTime();
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { useState, FC } from "react";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { ChevroneDown } from "../../../assets/icons/groups";
|
||||
import ContestItem from "./ContestItem";
|
||||
import { Contest } from "../../../redux/slices/contests";
|
||||
|
||||
|
||||
|
||||
import { useState, FC } from 'react';
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { ChevroneDown } from '../../../assets/icons/groups';
|
||||
import ContestItem from './ContestItem';
|
||||
import { Contest } from '../../../redux/slices/contests';
|
||||
|
||||
interface ContestsBlockProps {
|
||||
contests: Contest[];
|
||||
@@ -13,46 +10,61 @@ interface ContestsBlockProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
||||
const ContestsBlock: FC<ContestsBlockProps> = ({ contests, title, className }) => {
|
||||
|
||||
|
||||
const [active, setActive] = useState<boolean>(title != "Скрытые");
|
||||
|
||||
const ContestsBlock: FC<ContestsBlockProps> = ({
|
||||
contests,
|
||||
title,
|
||||
className,
|
||||
}) => {
|
||||
const [active, setActive] = useState<boolean>(title != 'Скрытые');
|
||||
|
||||
return (
|
||||
|
||||
<div className={cn(" border-b-[1px] border-b-liquid-lighter rounded-[10px]",
|
||||
className
|
||||
)}>
|
||||
<div className={cn(" h-[40px] text-[24px] font-bold flex gap-[10px] items-center cursor-pointer border-b-[1px] border-b-transparent transition-all duration-300",
|
||||
active && "border-b-liquid-lighter"
|
||||
<div
|
||||
className={cn(
|
||||
' border-b-[1px] border-b-liquid-lighter rounded-[10px]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
' h-[40px] text-[24px] font-bold flex gap-[10px] items-center cursor-pointer border-b-[1px] border-b-transparent transition-all duration-300',
|
||||
active && 'border-b-liquid-lighter',
|
||||
)}
|
||||
onClick={() => {
|
||||
setActive(!active)
|
||||
}}>
|
||||
setActive(!active);
|
||||
}}
|
||||
>
|
||||
<span>{title}</span>
|
||||
<img src={ChevroneDown} className={cn("transition-all duration-300",
|
||||
active && "rotate-180"
|
||||
)} />
|
||||
<img
|
||||
src={ChevroneDown}
|
||||
className={cn(
|
||||
'transition-all duration-300',
|
||||
active && 'rotate-180',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(" grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300",
|
||||
active && "grid-rows-[1fr] opacity-100"
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300',
|
||||
active && 'grid-rows-[1fr] opacity-100',
|
||||
)}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="pb-[10px] pt-[20px]">
|
||||
{
|
||||
contests.map((v, i) => <ContestItem
|
||||
key={i}
|
||||
name={v.name}
|
||||
startAt={v.startsAt}
|
||||
statusRegister={"reg"}
|
||||
duration={new Date(v.endsAt).getTime() - new Date(v.startsAt).getTime()}
|
||||
members={v.members.length}
|
||||
type={i % 2 ? "second" : "first"} />)
|
||||
}
|
||||
{contests.map((v, i) => (
|
||||
<ContestItem
|
||||
key={i}
|
||||
name={v.name}
|
||||
startAt={v.startsAt}
|
||||
statusRegister={'reg'}
|
||||
duration={
|
||||
new Date(v.endsAt).getTime() -
|
||||
new Date(v.startsAt).getTime()
|
||||
}
|
||||
members={v.members.length}
|
||||
type={i % 2 ? 'second' : 'first'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { FC } from "react";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { useParams, Navigate } from "react-router-dom";
|
||||
import { FC } from 'react';
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { useParams, Navigate } from 'react-router-dom';
|
||||
|
||||
interface GroupsBlockProps {}
|
||||
|
||||
const Group: FC<GroupsBlockProps> = () => {
|
||||
const { groupId } = useParams<{ groupId: string }>();
|
||||
const groupIdNumber = Number(groupId);
|
||||
const { groupId } = useParams<{ groupId: string }>();
|
||||
const groupIdNumber = Number(groupId);
|
||||
|
||||
if (!groupId || isNaN(groupIdNumber) || !groupIdNumber) {
|
||||
return <Navigate to="/home/groups" replace />;
|
||||
}
|
||||
if (!groupId || isNaN(groupIdNumber) || !groupIdNumber) {
|
||||
return <Navigate to="/home/groups" replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-b-[1px] border-b-liquid-lighter rounded-[10px]"
|
||||
)}
|
||||
>
|
||||
{groupIdNumber}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-b-[1px] border-b-liquid-lighter rounded-[10px]',
|
||||
)}
|
||||
>
|
||||
{groupIdNumber}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Group;
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { Book, UserAdd, Edit, EyeClosed, EyeOpen } from "../../../assets/icons/groups";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { GroupUpdate } from "./Groups";
|
||||
import { cn } from '../../../lib/cn';
|
||||
import {
|
||||
Book,
|
||||
UserAdd,
|
||||
Edit,
|
||||
EyeClosed,
|
||||
EyeOpen,
|
||||
} from '../../../assets/icons/groups';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { GroupUpdate } from './Groups';
|
||||
|
||||
export interface GroupItemProps {
|
||||
id: number;
|
||||
role: "menager" | "member" | "owner" | "viewer";
|
||||
role: 'menager' | 'member' | 'owner' | 'viewer';
|
||||
visible: boolean;
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -13,61 +19,64 @@ export interface GroupItemProps {
|
||||
setUpdateGroup: (value: GroupUpdate) => void;
|
||||
}
|
||||
|
||||
|
||||
interface IconComponentProps {
|
||||
src: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const IconComponent: React.FC<IconComponentProps> = ({
|
||||
src,
|
||||
onClick
|
||||
}) => {
|
||||
|
||||
return <img
|
||||
src={src}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onClick)
|
||||
onClick();
|
||||
}}
|
||||
className="hover:bg-liquid-light rounded-[5px] cursor-pointer transition-all duration-300"
|
||||
/>
|
||||
}
|
||||
const IconComponent: React.FC<IconComponentProps> = ({ src, onClick }) => {
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onClick) onClick();
|
||||
}}
|
||||
className="hover:bg-liquid-light rounded-[5px] cursor-pointer transition-all duration-300"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const GroupItem: React.FC<GroupItemProps> = ({
|
||||
id, name, visible, role, description, setUpdateGroup, setUpdateActive
|
||||
id,
|
||||
name,
|
||||
visible,
|
||||
role,
|
||||
description,
|
||||
setUpdateGroup,
|
||||
setUpdateActive,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className={cn("w-full h-[120px] box-border relative rounded-[10px] p-[10px] text-liquid-white bg-liquid-lighter cursor-pointer",
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'w-full h-[120px] box-border relative rounded-[10px] p-[10px] text-liquid-white bg-liquid-lighter cursor-pointer',
|
||||
)}
|
||||
onClick={() => navigate(`/group/${id}`)}
|
||||
>
|
||||
<div className="grid grid-cols-[100px,1fr] gap-[20px]">
|
||||
<img src={Book} className="bg-liquid-brightmain rounded-[10px]"/>
|
||||
<img
|
||||
src={Book}
|
||||
className="bg-liquid-brightmain rounded-[10px]"
|
||||
/>
|
||||
<div className="grid grid-flow-row grid-rows-[1fr,24px]">
|
||||
<div className="text-[18px] font-bold">
|
||||
{name}
|
||||
</div>
|
||||
<div className="text-[18px] font-bold">{name}</div>
|
||||
<div className=" flex gap-[10px]">
|
||||
{
|
||||
(role == "menager" || role == "owner") && <IconComponent src={UserAdd}/>
|
||||
}
|
||||
{
|
||||
(role == "menager" || role == "owner") && <IconComponent src={Edit} onClick={() => {
|
||||
|
||||
setUpdateGroup({id, name, description });
|
||||
setUpdateActive(true);
|
||||
}} />
|
||||
}
|
||||
{
|
||||
visible == false && <IconComponent src={EyeOpen} />
|
||||
}
|
||||
{
|
||||
visible == true && <IconComponent src={EyeClosed} />
|
||||
}
|
||||
{(role == 'menager' || role == 'owner') && (
|
||||
<IconComponent src={UserAdd} />
|
||||
)}
|
||||
{(role == 'menager' || role == 'owner') && (
|
||||
<IconComponent
|
||||
src={Edit}
|
||||
onClick={() => {
|
||||
setUpdateGroup({ id, name, description });
|
||||
setUpdateActive(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{visible == false && <IconComponent src={EyeOpen} />}
|
||||
{visible == true && <IconComponent src={EyeClosed} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
||||
import GroupsBlock from "./GroupsBlock";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
import { fetchMyGroups } from "../../../redux/slices/groups";
|
||||
import ModalCreate from "./ModalCreate";
|
||||
import ModalUpdate from "./ModalUpdate";
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import GroupsBlock from './GroupsBlock';
|
||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||
import { fetchMyGroups } from '../../../redux/slices/groups';
|
||||
import ModalCreate from './ModalCreate';
|
||||
import ModalUpdate from './ModalUpdate';
|
||||
|
||||
export interface GroupUpdate {
|
||||
id: number;
|
||||
@@ -17,11 +17,14 @@ export interface GroupUpdate {
|
||||
const Groups = () => {
|
||||
const [modalActive, setModalActive] = useState<boolean>(false);
|
||||
const [modelUpdateActive, setModalUpdateActive] = useState<boolean>(false);
|
||||
const [updateGroup, setUpdateGroup] = useState<GroupUpdate>({ id: 0, name: "", description: "" });
|
||||
const [updateGroup, setUpdateGroup] = useState<GroupUpdate>({
|
||||
id: 0,
|
||||
name: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
||||
// Берём группы из стора
|
||||
const groups = useAppSelector((store) => store.groups.groups);
|
||||
|
||||
@@ -29,8 +32,8 @@ const Groups = () => {
|
||||
const currentUserName = useAppSelector((store) => store.auth.username);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("groups"));
|
||||
dispatch(fetchMyGroups())
|
||||
dispatch(setMenuActivePage('groups'));
|
||||
dispatch(fetchMyGroups());
|
||||
}, [dispatch]);
|
||||
|
||||
// Разделяем группы
|
||||
@@ -44,17 +47,23 @@ const Groups = () => {
|
||||
const hidden: typeof groups = []; // пока пустые, без логики
|
||||
|
||||
groups.forEach((group) => {
|
||||
const me = group.members.find((m) => m.username === currentUserName);
|
||||
const me = group.members.find(
|
||||
(m) => m.username === currentUserName,
|
||||
);
|
||||
if (!me) return;
|
||||
|
||||
if (me.role === "Administrator") {
|
||||
if (me.role === 'Administrator') {
|
||||
managed.push(group);
|
||||
} else {
|
||||
current.push(group);
|
||||
}
|
||||
});
|
||||
|
||||
return { managedGroups: managed, currentGroups: current, hiddenGroups: hidden };
|
||||
return {
|
||||
managedGroups: managed,
|
||||
currentGroups: current,
|
||||
hiddenGroups: hidden,
|
||||
};
|
||||
}, [groups, currentUserName]);
|
||||
|
||||
return (
|
||||
@@ -63,13 +72,15 @@ const Groups = () => {
|
||||
<div className="relative flex items-center mb-[20px]">
|
||||
<div
|
||||
className={cn(
|
||||
"h-[50px] text-[40px] font-bold text-liquid-white flex items-center"
|
||||
'h-[50px] text-[40px] font-bold text-liquid-white flex items-center',
|
||||
)}
|
||||
>
|
||||
Группы
|
||||
</div>
|
||||
<SecondaryButton
|
||||
onClick={() => { setModalActive(true); }}
|
||||
onClick={() => {
|
||||
setModalActive(true);
|
||||
}}
|
||||
text="Создать группу"
|
||||
className="absolute right-0"
|
||||
/>
|
||||
@@ -83,7 +94,6 @@ const Groups = () => {
|
||||
groups={managedGroups}
|
||||
setUpdateActive={setModalUpdateActive}
|
||||
setUpdateGroup={setUpdateGroup}
|
||||
|
||||
/>
|
||||
<GroupsBlock
|
||||
className="mb-[20px]"
|
||||
@@ -101,7 +111,6 @@ const Groups = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<ModalCreate setActive={setModalActive} active={modalActive} />
|
||||
<ModalUpdate
|
||||
setActive={setModalUpdateActive}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState, FC } from "react";
|
||||
import GroupItem from "./GroupItem";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { ChevroneDown } from "../../../assets/icons/groups";
|
||||
import { Group } from "../../../redux/slices/groups";
|
||||
import { GroupUpdate } from "./Groups";
|
||||
import { useState, FC } from 'react';
|
||||
import GroupItem from './GroupItem';
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { ChevroneDown } from '../../../assets/icons/groups';
|
||||
import { Group } from '../../../redux/slices/groups';
|
||||
import { GroupUpdate } from './Groups';
|
||||
|
||||
interface GroupsBlockProps {
|
||||
groups: Group[];
|
||||
@@ -13,46 +13,60 @@ interface GroupsBlockProps {
|
||||
setUpdateGroup: (value: GroupUpdate) => void;
|
||||
}
|
||||
|
||||
|
||||
const GroupsBlock: FC<GroupsBlockProps> = ({ groups, title, className, setUpdateActive, setUpdateGroup }) => {
|
||||
|
||||
|
||||
const [active, setActive] = useState<boolean>(title != "Скрытые");
|
||||
|
||||
const GroupsBlock: FC<GroupsBlockProps> = ({
|
||||
groups,
|
||||
title,
|
||||
className,
|
||||
setUpdateActive,
|
||||
setUpdateGroup,
|
||||
}) => {
|
||||
const [active, setActive] = useState<boolean>(title != 'Скрытые');
|
||||
|
||||
return (
|
||||
|
||||
<div className={cn(" border-b-[1px] border-b-liquid-lighter rounded-[10px]",
|
||||
className
|
||||
)}>
|
||||
<div className={cn(" h-[40px] text-[24px] font-bold flex gap-[10px] border-b-[1px] border-b-transparent items-center cursor-pointer transition-all duration-300",
|
||||
active && " border-b-liquid-lighter"
|
||||
<div
|
||||
className={cn(
|
||||
' border-b-[1px] border-b-liquid-lighter rounded-[10px]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
' h-[40px] text-[24px] font-bold flex gap-[10px] border-b-[1px] border-b-transparent items-center cursor-pointer transition-all duration-300',
|
||||
active && ' border-b-liquid-lighter',
|
||||
)}
|
||||
onClick={() => {
|
||||
setActive(!active)
|
||||
}}>
|
||||
setActive(!active);
|
||||
}}
|
||||
>
|
||||
<span>{title}</span>
|
||||
<img src={ChevroneDown} className={cn("transition-all duration-300",
|
||||
active && "rotate-180"
|
||||
)}/>
|
||||
<img
|
||||
src={ChevroneDown}
|
||||
className={cn(
|
||||
'transition-all duration-300',
|
||||
active && 'rotate-180',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(" grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300",
|
||||
active && "grid-rows-[1fr] opacity-100"
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300',
|
||||
active && 'grid-rows-[1fr] opacity-100',
|
||||
)}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
|
||||
<div className="grid grid-cols-3 gap-[20px] pt-[20px] pb-[20px] box-border">
|
||||
{
|
||||
groups.map((v, i) => <GroupItem
|
||||
key={i}
|
||||
id={v.id}
|
||||
visible={true}
|
||||
description={v.description}
|
||||
setUpdateActive={setUpdateActive}
|
||||
setUpdateGroup={setUpdateGroup}
|
||||
role={"owner"}
|
||||
name={v.name}/>)
|
||||
}
|
||||
{groups.map((v, i) => (
|
||||
<GroupItem
|
||||
key={i}
|
||||
id={v.id}
|
||||
visible={true}
|
||||
description={v.description}
|
||||
setUpdateActive={setUpdateActive}
|
||||
setUpdateGroup={setUpdateGroup}
|
||||
role={'owner'}
|
||||
name={v.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { Modal } from "../../../components/modal/Modal";
|
||||
import { PrimaryButton } from "../../../components/button/PrimaryButton";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { Input } from "../../../components/input/Input";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
||||
import { createGroup } from "../../../redux/slices/groups";
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { Modal } from '../../../components/modal/Modal';
|
||||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||
import { Input } from '../../../components/input/Input';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import { createGroup } from '../../../redux/slices/groups';
|
||||
|
||||
interface ModalCreateProps {
|
||||
active: boolean;
|
||||
@@ -12,27 +12,63 @@ interface ModalCreateProps {
|
||||
}
|
||||
|
||||
const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
||||
const [name, setName] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [name, setName] = useState<string>('');
|
||||
const [description, setDescription] = useState<string>('');
|
||||
const status = useAppSelector((state) => state.groups.statuses.create);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (status == "successful") {
|
||||
if (status == 'successful') {
|
||||
setActive(false);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<Modal className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white" onOpenChange={setActive} open={active} backdrop="blur" >
|
||||
<Modal
|
||||
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
|
||||
onOpenChange={setActive}
|
||||
open={active}
|
||||
backdrop="blur"
|
||||
>
|
||||
<div className="w-[500px]">
|
||||
<div className="font-bold text-[30px]">Создать группу</div>
|
||||
<Input name="name" autocomplete="name" className="mt-[10px]" type="text" label="Название" onChange={(v) => { setName(v) }} placeholder="login" />
|
||||
<Input name="description" autocomplete="description" className="mt-[10px]" type="text" label="Описание" onChange={(v) => { setDescription(v) }} placeholder="login" />
|
||||
<Input
|
||||
name="name"
|
||||
autocomplete="name"
|
||||
className="mt-[10px]"
|
||||
type="text"
|
||||
label="Название"
|
||||
onChange={(v) => {
|
||||
setName(v);
|
||||
}}
|
||||
placeholder="login"
|
||||
/>
|
||||
<Input
|
||||
name="description"
|
||||
autocomplete="description"
|
||||
className="mt-[10px]"
|
||||
type="text"
|
||||
label="Описание"
|
||||
onChange={(v) => {
|
||||
setDescription(v);
|
||||
}}
|
||||
placeholder="login"
|
||||
/>
|
||||
|
||||
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
|
||||
<PrimaryButton onClick={() => { dispatch(createGroup({ name, description })) }} text="Создать" disabled={status == "loading"} />
|
||||
<SecondaryButton onClick={() => { setActive(false); }} text="Отмена" />
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
dispatch(createGroup({ name, description }));
|
||||
}}
|
||||
text="Создать"
|
||||
disabled={status == 'loading'}
|
||||
/>
|
||||
<SecondaryButton
|
||||
onClick={() => {
|
||||
setActive(false);
|
||||
}}
|
||||
text="Отмена"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -40,4 +76,3 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
||||
};
|
||||
|
||||
export default ModalCreate;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { Modal } from "../../../components/modal/Modal";
|
||||
import { PrimaryButton } from "../../../components/button/PrimaryButton";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { Input } from "../../../components/input/Input";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
||||
import { deleteGroup, updateGroup } from "../../../redux/slices/groups";
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { Modal } from '../../../components/modal/Modal';
|
||||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||
import { Input } from '../../../components/input/Input';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import { deleteGroup, updateGroup } from '../../../redux/slices/groups';
|
||||
|
||||
interface ModalUpdateProps {
|
||||
active: boolean;
|
||||
@@ -14,36 +14,95 @@ interface ModalUpdateProps {
|
||||
groupDescription: string;
|
||||
}
|
||||
|
||||
const ModalUpdate: FC<ModalUpdateProps> = ({ active, setActive, groupName, groupId, groupDescription }) => {
|
||||
const [name, setName] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const statusUpdate = useAppSelector((state) => state.groups.statuses.update);
|
||||
const statusDelete = useAppSelector((state) => state.groups.statuses.delete);
|
||||
const ModalUpdate: FC<ModalUpdateProps> = ({
|
||||
active,
|
||||
setActive,
|
||||
groupName,
|
||||
groupId,
|
||||
groupDescription,
|
||||
}) => {
|
||||
const [name, setName] = useState<string>('');
|
||||
const [description, setDescription] = useState<string>('');
|
||||
const statusUpdate = useAppSelector(
|
||||
(state) => state.groups.statuses.update,
|
||||
);
|
||||
const statusDelete = useAppSelector(
|
||||
(state) => state.groups.statuses.delete,
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (statusUpdate == "successful"){
|
||||
if (statusUpdate == 'successful') {
|
||||
setActive(false);
|
||||
}
|
||||
}, [statusUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (statusDelete == "successful"){
|
||||
if (statusDelete == 'successful') {
|
||||
setActive(false);
|
||||
}
|
||||
}, [statusDelete]);
|
||||
|
||||
return (
|
||||
<Modal className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white" onOpenChange={setActive} open={active} backdrop="blur" >
|
||||
<Modal
|
||||
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
|
||||
onOpenChange={setActive}
|
||||
open={active}
|
||||
backdrop="blur"
|
||||
>
|
||||
<div className="w-[500px]">
|
||||
<div className="font-bold text-[30px]">Изменить группу {groupName} #{groupId}</div>
|
||||
<Input name="name" autocomplete="name" className="mt-[10px]" type="text" label="Новое название" defaultState={groupName} onChange={(v) => { setName(v)}} placeholder="login"/>
|
||||
<Input name="description" autocomplete="description" className="mt-[10px]" type="text" label="Описание" onChange={(v) => { setDescription(v)}} placeholder="login" defaultState={groupDescription}/>
|
||||
<div className="font-bold text-[30px]">
|
||||
Изменить группу {groupName} #{groupId}
|
||||
</div>
|
||||
<Input
|
||||
name="name"
|
||||
autocomplete="name"
|
||||
className="mt-[10px]"
|
||||
type="text"
|
||||
label="Новое название"
|
||||
defaultState={groupName}
|
||||
onChange={(v) => {
|
||||
setName(v);
|
||||
}}
|
||||
placeholder="login"
|
||||
/>
|
||||
<Input
|
||||
name="description"
|
||||
autocomplete="description"
|
||||
className="mt-[10px]"
|
||||
type="text"
|
||||
label="Описание"
|
||||
onChange={(v) => {
|
||||
setDescription(v);
|
||||
}}
|
||||
placeholder="login"
|
||||
defaultState={groupDescription}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
|
||||
<PrimaryButton onClick={() => {dispatch(deleteGroup(groupId))}} text="Удалить" disabled={statusDelete=="loading"} color="error"/>
|
||||
<PrimaryButton onClick={() => {dispatch(updateGroup({name, description, groupId}))}} text="Обновить" disabled={statusUpdate=="loading"}/>
|
||||
<SecondaryButton onClick={() => {setActive(false);}} text="Отмена" />
|
||||
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
dispatch(deleteGroup(groupId));
|
||||
}}
|
||||
text="Удалить"
|
||||
disabled={statusDelete == 'loading'}
|
||||
color="error"
|
||||
/>
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
updateGroup({ name, description, groupId }),
|
||||
);
|
||||
}}
|
||||
text="Обновить"
|
||||
disabled={statusUpdate == 'loading'}
|
||||
/>
|
||||
<SecondaryButton
|
||||
onClick={() => {
|
||||
setActive(false);
|
||||
}}
|
||||
text="Отмена"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -51,4 +110,3 @@ const ModalUpdate: FC<ModalUpdateProps> = ({ active, setActive, groupName, group
|
||||
};
|
||||
|
||||
export default ModalUpdate;
|
||||
|
||||
|
||||
@@ -1,29 +1,63 @@
|
||||
import { Logo } from "../../../assets/logos";
|
||||
import {Account, Clipboard, Cup, Home, Openbook, Users} from "../../../assets/icons/menu";
|
||||
import MenuItem from "./MenuItem";
|
||||
import { useAppSelector } from "../../../redux/hooks";
|
||||
import { Logo } from '../../../assets/logos';
|
||||
import {
|
||||
Account,
|
||||
Clipboard,
|
||||
Cup,
|
||||
Home,
|
||||
Openbook,
|
||||
Users,
|
||||
} from '../../../assets/icons/menu';
|
||||
import MenuItem from './MenuItem';
|
||||
import { useAppSelector } from '../../../redux/hooks';
|
||||
|
||||
const Menu = () => {
|
||||
const menuItems = [
|
||||
{text: "Главная", href: "/home", icon: Home, page: "home" },
|
||||
{text: "Задачи", href: "/home/missions", icon: Clipboard, page: "missions" },
|
||||
{text: "Статьи", href: "/home/articles", icon: Openbook, page: "articles" },
|
||||
{text: "Группы", href: "/home/groups", icon: Users, page: "groups" },
|
||||
{text: "Контесты", href: "/home/contests", icon: Cup, page: "contests" },
|
||||
{text: "Аккаунт", href: "/home/account", icon: Account, page: "account" },
|
||||
];
|
||||
const activePage = useAppSelector((state) => state.store.menu.activePage);
|
||||
const menuItems = [
|
||||
{ text: 'Главная', href: '/home', icon: Home, page: 'home' },
|
||||
{
|
||||
text: 'Задачи',
|
||||
href: '/home/missions',
|
||||
icon: Clipboard,
|
||||
page: 'missions',
|
||||
},
|
||||
{
|
||||
text: 'Статьи',
|
||||
href: '/home/articles',
|
||||
icon: Openbook,
|
||||
page: 'articles',
|
||||
},
|
||||
{ text: 'Группы', href: '/home/groups', icon: Users, page: 'groups' },
|
||||
{
|
||||
text: 'Контесты',
|
||||
href: '/home/contests',
|
||||
icon: Cup,
|
||||
page: 'contests',
|
||||
},
|
||||
{
|
||||
text: 'Аккаунт',
|
||||
href: '/home/account',
|
||||
icon: Account,
|
||||
page: 'account',
|
||||
},
|
||||
];
|
||||
const activePage = useAppSelector((state) => state.store.menu.activePage);
|
||||
|
||||
return (
|
||||
<div className="w-[250px] fixed top-0 items-center box-border p-[20px] pt-[35px]">
|
||||
<img src={Logo} className="w-[173px]" />
|
||||
<div className="">
|
||||
{menuItems.map((v, i) => (
|
||||
<MenuItem key={i} icon={v.icon} text={v.text} href={v.href} active={v.page == activePage} page={v.page}/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="w-[250px] fixed top-0 items-center box-border p-[20px] pt-[35px]">
|
||||
<img src={Logo} className="w-[173px]" />
|
||||
<div className="">
|
||||
{menuItems.map((v, i) => (
|
||||
<MenuItem
|
||||
key={i}
|
||||
icon={v.icon}
|
||||
text={v.text}
|
||||
href={v.href}
|
||||
active={v.page == activePage}
|
||||
page={v.page}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
|
||||
@@ -1,37 +1,44 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAppDispatch } from "../../../redux/hooks";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAppDispatch } from '../../../redux/hooks';
|
||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||
|
||||
interface MenuItemProps {
|
||||
icon: string; // SVG или любой JSX
|
||||
text: string;
|
||||
href: string;
|
||||
page: string;
|
||||
active?: boolean; // необязательный, по умолчанию false
|
||||
icon: string; // SVG или любой JSX
|
||||
text: string;
|
||||
href: string;
|
||||
page: string;
|
||||
active?: boolean; // необязательный, по умолчанию false
|
||||
}
|
||||
|
||||
const MenuItem: React.FC<MenuItemProps> = ({ icon, text = "", href = "", active = false, page = "" }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const MenuItem: React.FC<MenuItemProps> = ({
|
||||
icon,
|
||||
text = '',
|
||||
href = '',
|
||||
active = false,
|
||||
page = '',
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={href}
|
||||
className={`
|
||||
return (
|
||||
<Link
|
||||
to={href}
|
||||
className={`
|
||||
flex items-center gap-3 p-[16px] rounded-[10px\] h-[40px] text-[18px] font-bold
|
||||
transition-all duration-300 text-liquid-white mt-[20px]
|
||||
active:scale-95
|
||||
${active ? "bg-liquid-darkmain hover:bg-liquid-lighter hover:ring-[1px] hover:ring-liquid-darkmain hover:ring-inset"
|
||||
: " hover:bg-liquid-lighter"}
|
||||
${
|
||||
active
|
||||
? 'bg-liquid-darkmain hover:bg-liquid-lighter hover:ring-[1px] hover:ring-liquid-darkmain hover:ring-inset'
|
||||
: ' hover:bg-liquid-lighter'
|
||||
}
|
||||
`}
|
||||
onClick={
|
||||
() => dispatch(setMenuActivePage(page))
|
||||
}
|
||||
>
|
||||
<img src={icon} />
|
||||
<span>{text}</span>
|
||||
</Link>
|
||||
);
|
||||
onClick={() => dispatch(setMenuActivePage(page))}
|
||||
>
|
||||
<img src={icon} />
|
||||
<span>{text}</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuItem;
|
||||
|
||||
@@ -1,71 +1,78 @@
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { IconError, IconSuccess } from "../../../assets/icons/missions";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { IconError, IconSuccess } from '../../../assets/icons/missions';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export interface MissionItemProps {
|
||||
id: number;
|
||||
authorId: number;
|
||||
name: string;
|
||||
difficulty: "Easy" | "Medium" | "Hard";
|
||||
difficulty: 'Easy' | 'Medium' | 'Hard';
|
||||
tags: string[];
|
||||
timeLimit: number;
|
||||
memoryLimit: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
type: "first" | "second";
|
||||
status: "empty" | "success" | "error";
|
||||
type: 'first' | 'second';
|
||||
status: 'empty' | 'success' | 'error';
|
||||
}
|
||||
|
||||
export function formatMilliseconds(ms: number): string {
|
||||
const rounded = Math.round(ms) / 1000;
|
||||
const formatted = rounded.toString().replace(/\.?0+$/, '');
|
||||
return `${formatted} c`;
|
||||
const rounded = Math.round(ms) / 1000;
|
||||
const formatted = rounded.toString().replace(/\.?0+$/, '');
|
||||
return `${formatted} c`;
|
||||
}
|
||||
|
||||
export function formatBytesToMB(bytes: number): string {
|
||||
const megabytes = Math.floor(bytes / (1024 * 1024));
|
||||
return `${megabytes} МБ`;
|
||||
const megabytes = Math.floor(bytes / (1024 * 1024));
|
||||
return `${megabytes} МБ`;
|
||||
}
|
||||
|
||||
const MissionItem: React.FC<MissionItemProps> = ({
|
||||
id, name, difficulty, timeLimit, memoryLimit, type, status
|
||||
id,
|
||||
name,
|
||||
difficulty,
|
||||
timeLimit,
|
||||
memoryLimit,
|
||||
type,
|
||||
status,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className={cn("h-[44px] w-full relative rounded-[10px] text-liquid-white",
|
||||
type == "first" ? "bg-liquid-lighter" : "bg-liquid-background",
|
||||
"grid grid-cols-[80px,1fr,1fr,60px,24px] grid-flow-col gap-[20px] px-[20px] box-border items-center",
|
||||
status == "error" && "border-l-[11px] border-l-liquid-red pl-[9px]",
|
||||
status == "success" && "border-l-[11px] border-l-liquid-green pl-[9px]",
|
||||
"cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300",
|
||||
)}
|
||||
onClick={() => {navigate(`/mission/${id}`)}}
|
||||
<div
|
||||
className={cn(
|
||||
'h-[44px] w-full relative rounded-[10px] text-liquid-white',
|
||||
type == 'first' ? 'bg-liquid-lighter' : 'bg-liquid-background',
|
||||
'grid grid-cols-[80px,1fr,1fr,60px,24px] grid-flow-col gap-[20px] px-[20px] box-border items-center',
|
||||
status == 'error' &&
|
||||
'border-l-[11px] border-l-liquid-red pl-[9px]',
|
||||
status == 'success' &&
|
||||
'border-l-[11px] border-l-liquid-green pl-[9px]',
|
||||
'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300',
|
||||
)}
|
||||
onClick={() => {
|
||||
navigate(`/mission/${id}`);
|
||||
}}
|
||||
>
|
||||
<div className="text-[18px] font-bold">
|
||||
#{id}
|
||||
</div>
|
||||
<div className="text-[18px] font-bold">
|
||||
{name}
|
||||
</div>
|
||||
<div className="text-[18px] font-bold">#{id}</div>
|
||||
<div className="text-[18px] font-bold">{name}</div>
|
||||
<div className="text-[12px] text-right">
|
||||
стандартный ввод/вывод {formatMilliseconds(timeLimit)}, {formatBytesToMB(memoryLimit)}
|
||||
стандартный ввод/вывод {formatMilliseconds(timeLimit)},{' '}
|
||||
{formatBytesToMB(memoryLimit)}
|
||||
</div>
|
||||
<div className={cn(
|
||||
"text-center text-[18px]",
|
||||
difficulty == "Hard" && "text-liquid-red",
|
||||
difficulty == "Medium" && "text-liquid-orange",
|
||||
difficulty == "Easy" && "text-liquid-green",
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
'text-center text-[18px]',
|
||||
difficulty == 'Hard' && 'text-liquid-red',
|
||||
difficulty == 'Medium' && 'text-liquid-orange',
|
||||
difficulty == 'Easy' && 'text-liquid-green',
|
||||
)}
|
||||
>
|
||||
{difficulty}
|
||||
</div>
|
||||
<div className="h-[24px] w-[24px]">
|
||||
{
|
||||
status == "error" && <img src={IconError}/>
|
||||
}
|
||||
{
|
||||
status == "success" && <img src={IconSuccess}/>
|
||||
}
|
||||
{status == 'error' && <img src={IconError} />}
|
||||
{status == 'success' && <img src={IconSuccess} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import MissionItem from "./MissionItem";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
||||
import { useEffect, useState } from "react";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { fetchMissions } from "../../../redux/slices/missions";
|
||||
import ModalCreate from "./ModalCreate";
|
||||
|
||||
import MissionItem from './MissionItem';
|
||||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { fetchMissions } from '../../../redux/slices/missions';
|
||||
import ModalCreate from './ModalCreate';
|
||||
|
||||
export interface Mission {
|
||||
id: number;
|
||||
authorId: number;
|
||||
name: string;
|
||||
difficulty: "Easy" | "Medium" | "Hard";
|
||||
difficulty: 'Easy' | 'Medium' | 'Hard';
|
||||
tags: string[];
|
||||
timeLimit: number;
|
||||
memoryLimit: number;
|
||||
@@ -21,60 +20,60 @@ export interface Mission {
|
||||
}
|
||||
|
||||
const Missions = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const [modalActive, setModalActive] = useState<boolean>(false);
|
||||
|
||||
const missions = useAppSelector((state) => state.missions.missions);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("missions"))
|
||||
dispatch(fetchMissions({}))
|
||||
dispatch(setMenuActivePage('missions'));
|
||||
dispatch(fetchMissions({}));
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
|
||||
<div className="h-full box-border">
|
||||
|
||||
<div className="relative flex items-center mb-[20px]">
|
||||
<div className="h-[50px] text-[40px] font-bold text-liquid-white flex items-center">
|
||||
Задачи
|
||||
</div>
|
||||
<SecondaryButton
|
||||
onClick={() => {setModalActive(true)}}
|
||||
<SecondaryButton
|
||||
onClick={() => {
|
||||
setModalActive(true);
|
||||
}}
|
||||
text="Добавить задачу"
|
||||
className="absolute right-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-liquid-lighter h-[50px] mb-[20px]">
|
||||
|
||||
</div>
|
||||
<div className="bg-liquid-lighter h-[50px] mb-[20px]"></div>
|
||||
|
||||
<div>
|
||||
|
||||
{missions.map((v, i) => (
|
||||
<MissionItem
|
||||
key={i}
|
||||
id={v.id}
|
||||
authorId={v.authorId}
|
||||
name={v.name}
|
||||
difficulty={"Easy"}
|
||||
tags={v.tags}
|
||||
timeLimit={1000}
|
||||
memoryLimit={256 * 1024 * 1024}
|
||||
createdAt={v.createdAt}
|
||||
updatedAt={v.updatedAt}
|
||||
type={i % 2 == 0 ? "first" : "second"}
|
||||
status={i == 0 || i == 3 || i == 7 ? "success" : (i == 2 || i == 4 || i == 9 ? "error" : "empty")}/>
|
||||
))}
|
||||
{missions.map((v, i) => (
|
||||
<MissionItem
|
||||
key={i}
|
||||
id={v.id}
|
||||
authorId={v.authorId}
|
||||
name={v.name}
|
||||
difficulty={'Easy'}
|
||||
tags={v.tags}
|
||||
timeLimit={1000}
|
||||
memoryLimit={256 * 1024 * 1024}
|
||||
createdAt={v.createdAt}
|
||||
updatedAt={v.updatedAt}
|
||||
type={i % 2 == 0 ? 'first' : 'second'}
|
||||
status={
|
||||
i == 0 || i == 3 || i == 7
|
||||
? 'success'
|
||||
: i == 2 || i == 4 || i == 9
|
||||
? 'error'
|
||||
: 'empty'
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
pages
|
||||
</div>
|
||||
<div>pages</div>
|
||||
</div>
|
||||
|
||||
<ModalCreate setActive={setModalActive} active={modalActive} />
|
||||
|
||||
@@ -59,6 +59,10 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMissionsStatus({ key: 'upload', status: 'idle' }));
|
||||
}, [active]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
|
||||
@@ -152,6 +156,8 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
||||
text="Отмена"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status == 'failed' && <div>error</div>}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,141 +1,153 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { upload } from "../../../assets/icons/input";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { DropDownList } from "../../../components/drop-down-list/DropDownList";
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { upload } from '../../../assets/icons/input';
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { DropDownList } from '../../../components/drop-down-list/DropDownList';
|
||||
|
||||
const languageMap: Record<string, string> = {
|
||||
c: "cpp",
|
||||
"C++": "cpp",
|
||||
java: "java",
|
||||
python: "python",
|
||||
pascal: "pascal",
|
||||
kotlin: "kotlin",
|
||||
csharp: "csharp"
|
||||
c: 'cpp',
|
||||
'C++': 'cpp',
|
||||
java: 'java',
|
||||
python: 'python',
|
||||
pascal: 'pascal',
|
||||
kotlin: 'kotlin',
|
||||
csharp: 'csharp',
|
||||
};
|
||||
|
||||
export interface CodeEditorProps {
|
||||
export interface CodeEditorProps {
|
||||
onChange: (value: string) => void;
|
||||
onChangeLanguage: (value: string) => void;
|
||||
}
|
||||
|
||||
const CodeEditor: React.FC<CodeEditorProps> = ({onChange, onChangeLanguage}) => {
|
||||
const [language, setLanguage] = useState<string>("C++");
|
||||
const [code, setCode] = useState<string>("");
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||
onChange,
|
||||
onChangeLanguage,
|
||||
}) => {
|
||||
const [language, setLanguage] = useState<string>('C++');
|
||||
const [code, setCode] = useState<string>('');
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
|
||||
const items = [
|
||||
{ value: 'c', text: 'C' },
|
||||
{ value: 'C++', text: 'C++' },
|
||||
{ value: 'java', text: 'Java' },
|
||||
{ value: 'python', text: 'Python' },
|
||||
{ value: 'pascal', text: 'Pascal' },
|
||||
{ value: 'kotlin', text: 'Kotlin' },
|
||||
{ value: 'csharp', text: 'C#' },
|
||||
];
|
||||
|
||||
const items = [
|
||||
{ value: "c", text: "C" },
|
||||
{ value: "C++", text: "C++" },
|
||||
{ value: "java", text: "Java" },
|
||||
{ value: "python", text: "Python" },
|
||||
{ value: "pascal", text: "Pascal" },
|
||||
{ value: "kotlin", text: "Kotlin" },
|
||||
{ value: "csharp", text: "C#" },
|
||||
];
|
||||
useEffect(() => {
|
||||
onChange(code);
|
||||
}, [code]);
|
||||
useEffect(() => {
|
||||
onChangeLanguage(language);
|
||||
}, [language]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(code);
|
||||
}, [code])
|
||||
useEffect(() => {
|
||||
onChangeLanguage(language);
|
||||
}, [language])
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const text = event.target?.result;
|
||||
if (typeof text === "string") setCode(text);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const text = event.target?.result;
|
||||
if (typeof text === 'string') setCode(text);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
e.target.value = '';
|
||||
};
|
||||
reader.readAsText(file);
|
||||
e.target.value = "";
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const droppedFile = e.dataTransfer.files[0];
|
||||
if (!droppedFile) return;
|
||||
const handleDrop = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const droppedFile = e.dataTransfer.files[0];
|
||||
if (!droppedFile) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const text = event.target?.result;
|
||||
if (typeof text === "string") setCode(text);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const text = event.target?.result;
|
||||
if (typeof text === 'string') setCode(text);
|
||||
};
|
||||
reader.readAsText(droppedFile);
|
||||
};
|
||||
reader.readAsText(droppedFile);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault(); // обязательно
|
||||
};
|
||||
const handleDragOver = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault(); // обязательно
|
||||
};
|
||||
|
||||
const handleDragEnter = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
const handleDragEnter = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
};
|
||||
const handleDragLeave = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full">
|
||||
{/* Панель выбора языка и загрузки файла */}
|
||||
<div className="flex items-center justify-between py-3 ">
|
||||
<div className="flex items-center gap-[20px]">
|
||||
<DropDownList items={items} onChange={(v) => { setLanguage(v) }} defaultState={{ value: "C++", text: "C++" }}/>
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full">
|
||||
{/* Панель выбора языка и загрузки файла */}
|
||||
<div className="flex items-center justify-between py-3 ">
|
||||
<div className="flex items-center gap-[20px]">
|
||||
<DropDownList
|
||||
items={items}
|
||||
onChange={(v) => {
|
||||
setLanguage(v);
|
||||
}}
|
||||
defaultState={{ value: 'C++', text: 'C++' }}
|
||||
/>
|
||||
|
||||
<label
|
||||
className={cn("h-[40px] w-[250px] rounded-[10px] px-[16px] relative flex items-center cursor-pointer transition-all bg-liquid-lighter outline-dashed outline-[2px] outline-transparent active:scale-[95%]",
|
||||
isDragging && "outline-blue-500 "
|
||||
)}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<span className="text-[18px] text-liquid-white font-bold pointer-events-none">
|
||||
{"Загрузить решение"}
|
||||
</span>
|
||||
<img src={upload} className="absolute right-[16px] pointer-events-none" />
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => handleFileUpload(e)}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
className={cn(
|
||||
'h-[40px] w-[250px] rounded-[10px] px-[16px] relative flex items-center cursor-pointer transition-all bg-liquid-lighter outline-dashed outline-[2px] outline-transparent active:scale-[95%]',
|
||||
isDragging && 'outline-blue-500 ',
|
||||
)}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<span className="text-[18px] text-liquid-white font-bold pointer-events-none">
|
||||
{'Загрузить решение'}
|
||||
</span>
|
||||
<img
|
||||
src={upload}
|
||||
className="absolute right-[16px] pointer-events-none"
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => handleFileUpload(e)}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Monaco Editor */}
|
||||
<div className="bg-[#1E1E1E] py-[10px] h-full rounded-[10px]">
|
||||
<Editor
|
||||
width="100%"
|
||||
height="100%"
|
||||
language={languageMap[language]}
|
||||
value={code}
|
||||
onChange={(value) => setCode(value ?? '')}
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
fontSize: 14,
|
||||
minimap: { enabled: false },
|
||||
automaticLayout: true,
|
||||
quickSuggestions: true,
|
||||
suggestOnTriggerCharacters: true,
|
||||
tabSize: 4,
|
||||
insertSpaces: true,
|
||||
detectIndentation: false,
|
||||
autoIndent: 'full',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Monaco Editor */}
|
||||
<div className="bg-[#1E1E1E] py-[10px] h-full rounded-[10px]">
|
||||
<Editor
|
||||
width="100%"
|
||||
height="100%"
|
||||
language={languageMap[language]}
|
||||
value={code}
|
||||
onChange={(value) => setCode(value ?? "")}
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
fontSize: 14,
|
||||
minimap: { enabled: false },
|
||||
automaticLayout: true,
|
||||
quickSuggestions: true,
|
||||
suggestOnTriggerCharacters: true,
|
||||
tabSize: 4,
|
||||
insertSpaces: true,
|
||||
detectIndentation: false,
|
||||
autoIndent: "full",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeEditor;
|
||||
|
||||
@@ -1,28 +1,57 @@
|
||||
import React from "react";
|
||||
import { chevroneLeft, chevroneRight, arrowLeft } from "../../../assets/icons/header";
|
||||
import { Logo } from "../../../assets/logos";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import React from 'react';
|
||||
import {
|
||||
chevroneLeft,
|
||||
chevroneRight,
|
||||
arrowLeft,
|
||||
} from '../../../assets/icons/header';
|
||||
import { Logo } from '../../../assets/logos';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface HeaderProps {
|
||||
missionId: number;
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({
|
||||
missionId
|
||||
}) => {
|
||||
const Header: React.FC<HeaderProps> = ({ missionId }) => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<header className="w-full h-[60px] flex items-center px-4 gap-[20px]">
|
||||
<img src={Logo} alt="Logo" className="h-[28px] w-auto cursor-pointer" onClick={() => { navigate("/home") }} />
|
||||
<img
|
||||
src={Logo}
|
||||
alt="Logo"
|
||||
className="h-[28px] w-auto cursor-pointer"
|
||||
onClick={() => {
|
||||
navigate('/home');
|
||||
}}
|
||||
/>
|
||||
|
||||
<img src={arrowLeft} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate("/home/missions") }} />
|
||||
<img
|
||||
src={arrowLeft}
|
||||
alt="back"
|
||||
className="h-[24px] w-[24px] cursor-pointer"
|
||||
onClick={() => {
|
||||
navigate('/home/missions');
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex gap-[10px]">
|
||||
<img src={chevroneLeft} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate(`/mission/${missionId - 1}`) }} />
|
||||
<img
|
||||
src={chevroneLeft}
|
||||
alt="back"
|
||||
className="h-[24px] w-[24px] cursor-pointer"
|
||||
onClick={() => {
|
||||
navigate(`/mission/${missionId - 1}`);
|
||||
}}
|
||||
/>
|
||||
<span>{missionId}</span>
|
||||
<img src={chevroneRight} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate(`/mission/${missionId + 1}`) }} />
|
||||
<img
|
||||
src={chevroneRight}
|
||||
alt="back"
|
||||
className="h-[24px] w-[24px] cursor-pointer"
|
||||
onClick={() => {
|
||||
navigate(`/mission/${missionId + 1}`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,113 +1,131 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
MathJax?: {
|
||||
startup?: { promise?: Promise<void> };
|
||||
typesetPromise?: (elements?: Element[]) => Promise<void>;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
interface Window {
|
||||
MathJax?: {
|
||||
startup?: { promise?: Promise<void> };
|
||||
typesetPromise?: (elements?: Element[]) => Promise<void>;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface MediaFile {
|
||||
id: number;
|
||||
fileName: string;
|
||||
mediaUrl: string;
|
||||
id: number;
|
||||
fileName: string;
|
||||
mediaUrl: string;
|
||||
}
|
||||
|
||||
interface LaTextContainerProps {
|
||||
html: string;
|
||||
latex: string;
|
||||
mediaFiles?: MediaFile[];
|
||||
html: string;
|
||||
latex: string;
|
||||
mediaFiles?: MediaFile[];
|
||||
}
|
||||
|
||||
let mathJaxPromise: Promise<void> | null = null;
|
||||
|
||||
const loadMathJax = () => {
|
||||
if (mathJaxPromise) return mathJaxPromise;
|
||||
if (mathJaxPromise) return mathJaxPromise;
|
||||
|
||||
mathJaxPromise = new Promise<void>((resolve, reject) => {
|
||||
if (window.MathJax?.typesetPromise) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
mathJaxPromise = new Promise<void>((resolve, reject) => {
|
||||
if (window.MathJax?.typesetPromise) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
(window as any).MathJax = {
|
||||
tex: {
|
||||
inlineMath: [["$$$", "$$$"]],
|
||||
displayMath: [["$$$$$$", "$$$$$$"]],
|
||||
processEscapes: true,
|
||||
},
|
||||
options: {
|
||||
skipHtmlTags: ["script", "noscript", "style", "textarea", "pre", "code"],
|
||||
},
|
||||
startup: { typeset: false },
|
||||
};
|
||||
(window as any).MathJax = {
|
||||
tex: {
|
||||
inlineMath: [['$$$', '$$$']],
|
||||
displayMath: [['$$$$$$', '$$$$$$']],
|
||||
processEscapes: true,
|
||||
},
|
||||
options: {
|
||||
skipHtmlTags: [
|
||||
'script',
|
||||
'noscript',
|
||||
'style',
|
||||
'textarea',
|
||||
'pre',
|
||||
'code',
|
||||
],
|
||||
},
|
||||
startup: { typeset: false },
|
||||
};
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.id = "mathjax-script";
|
||||
script.src = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js";
|
||||
script.async = true;
|
||||
const script = document.createElement('script');
|
||||
script.id = 'mathjax-script';
|
||||
script.src =
|
||||
'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
|
||||
script.async = true;
|
||||
|
||||
script.onload = () => {
|
||||
window.MathJax?.startup?.promise?.then(resolve).catch(reject);
|
||||
};
|
||||
script.onload = () => {
|
||||
window.MathJax?.startup?.promise?.then(resolve).catch(reject);
|
||||
};
|
||||
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
return mathJaxPromise;
|
||||
return mathJaxPromise;
|
||||
};
|
||||
|
||||
const replaceImages = (html: string, latex: string, mediaFiles?: MediaFile[]) => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
const replaceImages = (
|
||||
html: string,
|
||||
latex: string,
|
||||
mediaFiles?: MediaFile[],
|
||||
) => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
const latexImageNames = Array.from(latex.matchAll(/\\includegraphics\{(.+?)\}/g)).map(
|
||||
(match) => match[1]
|
||||
);
|
||||
const latexImageNames = Array.from(
|
||||
latex.matchAll(/\\includegraphics\{(.+?)\}/g),
|
||||
).map((match) => match[1]);
|
||||
|
||||
const imgs = doc.querySelectorAll<HTMLImageElement>("img.tex-graphics");
|
||||
const imgs = doc.querySelectorAll<HTMLImageElement>('img.tex-graphics');
|
||||
|
||||
imgs.forEach((img, idx) => {
|
||||
const imageName = latexImageNames[idx];
|
||||
if (!imageName || !mediaFiles) return;
|
||||
const mediaFile = mediaFiles.find((f) => f.fileName === imageName);
|
||||
if (mediaFile) img.src = mediaFile.mediaUrl;
|
||||
});
|
||||
imgs.forEach((img, idx) => {
|
||||
const imageName = latexImageNames[idx];
|
||||
if (!imageName || !mediaFiles) return;
|
||||
const mediaFile = mediaFiles.find((f) => f.fileName === imageName);
|
||||
if (mediaFile) img.src = mediaFile.mediaUrl;
|
||||
});
|
||||
|
||||
return doc.body.innerHTML;
|
||||
return doc.body.innerHTML;
|
||||
};
|
||||
|
||||
const LaTextContainer: React.FC<LaTextContainerProps> = ({ html, latex, mediaFiles }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [processedHtml, setProcessedHtml] = useState<string>(html);
|
||||
const LaTextContainer: React.FC<LaTextContainerProps> = ({
|
||||
html,
|
||||
latex,
|
||||
mediaFiles,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [processedHtml, setProcessedHtml] = useState<string>(html);
|
||||
|
||||
// 1️⃣ Обновляем HTML при изменении входных данных
|
||||
useEffect(() => {
|
||||
setProcessedHtml(replaceImages(html, latex, mediaFiles));
|
||||
}, [html, latex, mediaFiles]);
|
||||
// 1️⃣ Обновляем HTML при изменении входных данных
|
||||
useEffect(() => {
|
||||
setProcessedHtml(replaceImages(html, latex, mediaFiles));
|
||||
}, [html, latex, mediaFiles]);
|
||||
|
||||
// 2️⃣ После рендера обновленного HTML применяем MathJax
|
||||
useEffect(() => {
|
||||
const renderMath = () => {
|
||||
if (containerRef.current && window.MathJax?.typesetPromise) {
|
||||
window.MathJax.typesetPromise([containerRef.current]).catch(console.error);
|
||||
}
|
||||
};
|
||||
// 2️⃣ После рендера обновленного HTML применяем MathJax
|
||||
useEffect(() => {
|
||||
const renderMath = () => {
|
||||
if (containerRef.current && window.MathJax?.typesetPromise) {
|
||||
window.MathJax.typesetPromise([containerRef.current]).catch(
|
||||
console.error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
loadMathJax().then(renderMath).catch(console.error);
|
||||
}, [processedHtml]); // 👈 ключевой момент — триггерим именно по processedHtml
|
||||
loadMathJax().then(renderMath).catch(console.error);
|
||||
}, [processedHtml]); // 👈 ключевой момент — триггерим именно по processedHtml
|
||||
|
||||
return (
|
||||
<div
|
||||
className="latex-container"
|
||||
ref={containerRef}
|
||||
dangerouslySetInnerHTML={{ __html: processedHtml }}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="latex-container"
|
||||
ref={containerRef}
|
||||
dangerouslySetInnerHTML={{ __html: processedHtml }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LaTextContainer;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import SubmissionItem from "./SubmissionItem";
|
||||
import { useAppSelector } from "../../../redux/hooks";
|
||||
import { FC, useEffect } from "react";
|
||||
|
||||
|
||||
import SubmissionItem from './SubmissionItem';
|
||||
import { useAppSelector } from '../../../redux/hooks';
|
||||
import { FC, useEffect } from 'react';
|
||||
|
||||
export interface Mission {
|
||||
id: number;
|
||||
authorId: number;
|
||||
name: string;
|
||||
difficulty: "Easy" | "Medium" | "Hard";
|
||||
difficulty: 'Easy' | 'Medium' | 'Hard';
|
||||
tags: string[];
|
||||
timeLimit: number;
|
||||
memoryLimit: number;
|
||||
@@ -16,39 +14,45 @@ export interface Mission {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface MissionSubmissionsProps{
|
||||
interface MissionSubmissionsProps {
|
||||
missionId: number;
|
||||
}
|
||||
|
||||
const MissionSubmissions: FC<MissionSubmissionsProps> = ({missionId}) => {
|
||||
const submissions = useAppSelector((state) => state.submin.submitsById[missionId]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}, []);
|
||||
const MissionSubmissions: FC<MissionSubmissionsProps> = ({ missionId }) => {
|
||||
const submissions = useAppSelector(
|
||||
(state) => state.submin.submitsById[missionId],
|
||||
);
|
||||
|
||||
useEffect(() => {}, []);
|
||||
|
||||
const checkStatus = (status: string) => {
|
||||
if (status == "IncorrectAnswer")
|
||||
return "wronganswer";
|
||||
if (status == "TimeLimitError")
|
||||
return "timelimit";
|
||||
if (status == 'IncorrectAnswer') return 'wronganswer';
|
||||
if (status == 'TimeLimitError') return 'timelimit';
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full box-border overflow-y-scroll overflow-x-hidden thin-scrollbar pr-[10px]">
|
||||
|
||||
|
||||
{submissions && submissions.map((v, i) => (
|
||||
<SubmissionItem
|
||||
key={i}
|
||||
id={v.id}
|
||||
language={v.solution.language}
|
||||
time={v.solution.time}
|
||||
verdict={v.solution.testerMessage?.includes("Compilation failed") ? "Compilation failed" : v.solution.testerMessage}
|
||||
type={i % 2 ? "second" : "first"}
|
||||
status={v.solution.testerMessage == "All tests passed" ? "success" : checkStatus(v.solution.testerErrorCode)}
|
||||
{submissions &&
|
||||
submissions.map((v, i) => (
|
||||
<SubmissionItem
|
||||
key={i}
|
||||
id={v.id}
|
||||
language={v.solution.language}
|
||||
time={v.solution.time}
|
||||
verdict={
|
||||
v.solution.testerMessage?.includes(
|
||||
'Compilation failed',
|
||||
)
|
||||
? 'Compilation failed'
|
||||
: v.solution.testerMessage
|
||||
}
|
||||
type={i % 2 ? 'second' : 'first'}
|
||||
status={
|
||||
v.solution.testerMessage == 'All tests passed'
|
||||
? 'success'
|
||||
: checkStatus(v.solution.testerErrorCode)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,53 +1,47 @@
|
||||
import React, { FC } from "react";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import LaTextContainer from "./LaTextContainer";
|
||||
import { CopyIcon } from "../../../assets/icons/missions";
|
||||
import React, { FC } from 'react';
|
||||
import { cn } from '../../../lib/cn';
|
||||
import LaTextContainer from './LaTextContainer';
|
||||
import { CopyIcon } from '../../../assets/icons/missions';
|
||||
// import FullLatexRenderer from "./FullLatexRenderer";
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface CopyableDivPropd{
|
||||
interface CopyableDivPropd {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const CopyableDiv: FC<CopyableDivPropd> = ({ content }) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
alert("Скопировано!");
|
||||
} catch (err) {
|
||||
console.error("Ошибка копирования:", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative p-[10px] bg-liquid-lighter rounded-[10px] whitespace-pre-line"
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
{content}
|
||||
|
||||
|
||||
<img
|
||||
src={CopyIcon}
|
||||
alt="copy"
|
||||
className={cn("absolute top-2 right-2 w-6 h-6 cursor-pointer opacity-0 transition-all duration-300 hover:h-7 hover:w-7 hover:top-[6px] hover:right-[6px]",
|
||||
hovered && " opacity-100"
|
||||
)}
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
alert('Скопировано!');
|
||||
} catch (err) {
|
||||
console.error('Ошибка копирования:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative p-[10px] bg-liquid-lighter rounded-[10px] whitespace-pre-line"
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
{content}
|
||||
|
||||
<img
|
||||
src={CopyIcon}
|
||||
alt="copy"
|
||||
className={cn(
|
||||
'absolute top-2 right-2 w-6 h-6 cursor-pointer opacity-0 transition-all duration-300 hover:h-7 hover:w-7 hover:top-[6px] hover:right-[6px]',
|
||||
hovered && ' opacity-100',
|
||||
)}
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface StatementData {
|
||||
id?: number;
|
||||
@@ -65,10 +59,10 @@ export interface StatementData {
|
||||
}
|
||||
|
||||
function extractDivByClass(html: string, className: string): string {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
const div = doc.querySelector(`div.${className}`);
|
||||
return div ? div.outerHTML : "";
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const div = doc.querySelector(`div.${className}`);
|
||||
return div ? div.outerHTML : '';
|
||||
}
|
||||
|
||||
const Statement: React.FC<StatementData> = ({
|
||||
@@ -77,63 +71,110 @@ const Statement: React.FC<StatementData> = ({
|
||||
tags,
|
||||
timeLimit = 1000,
|
||||
memoryLimit = 256 * 1024 * 1024,
|
||||
legend = "",
|
||||
input = "",
|
||||
output = "",
|
||||
legend = '',
|
||||
input = '',
|
||||
output = '',
|
||||
sampleTests = [],
|
||||
notes = "",
|
||||
html = "",
|
||||
notes = '',
|
||||
html = '',
|
||||
mediaFiles,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full medium-scrollbar pl-[20px] pr-[12px] gap-[20px] text-liquid-white overflow-y-scroll thin-dark-scrollbar [scrollbar-gutter:stable]">
|
||||
<div>
|
||||
<p className="h-[50px] text-[40px] font-bold text-liquid-white">{name}</p>
|
||||
<p className="h-[23px] text-[18px] font-bold text-liquid-light">Задача #{id}</p>
|
||||
<p className="h-[50px] text-[40px] font-bold text-liquid-white">
|
||||
{name}
|
||||
</p>
|
||||
<p className="h-[23px] text-[18px] font-bold text-liquid-light">
|
||||
Задача #{id}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-[10px] w-full flex-wrap">
|
||||
{tags && tags.map((v, i) => <div key={i} className="px-[16px] py-[8px] rounded-full bg-liquid-lighter ">{v}</div>)}
|
||||
{tags &&
|
||||
tags.map((v, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="px-[16px] py-[8px] rounded-full bg-liquid-lighter "
|
||||
>
|
||||
{v}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<p className="text-liquid-white h-[20px] text-[18px] font-bold"><span className="text-liquid-light">ограничение по времени на тест:</span> {timeLimit / 1000} секунда</p>
|
||||
<p className="text-liquid-white h-[20px] text-[18px] font-bold"><span className="text-liquid-light">ограничение по памяти на тест:</span> {memoryLimit / 1024 / 1024} мегабайт</p>
|
||||
<p className="text-liquid-white h-[20px] text-[18px] font-bold"><span className="text-liquid-light">ввод:</span> стандартный ввод</p>
|
||||
<p className="text-liquid-white h-[20px] text-[18px] font-bold"><span className="text-liquid-light">вывод:</span> стандартный вывод</p>
|
||||
<p className="text-liquid-white h-[20px] text-[18px] font-bold">
|
||||
<span className="text-liquid-light">
|
||||
ограничение по времени на тест:
|
||||
</span>{' '}
|
||||
{timeLimit / 1000} секунда
|
||||
</p>
|
||||
<p className="text-liquid-white h-[20px] text-[18px] font-bold">
|
||||
<span className="text-liquid-light">
|
||||
ограничение по памяти на тест:
|
||||
</span>{' '}
|
||||
{memoryLimit / 1024 / 1024} мегабайт
|
||||
</p>
|
||||
<p className="text-liquid-white h-[20px] text-[18px] font-bold">
|
||||
<span className="text-liquid-light">ввод:</span> стандартный
|
||||
ввод
|
||||
</p>
|
||||
<p className="text-liquid-white h-[20px] text-[18px] font-bold">
|
||||
<span className="text-liquid-light">вывод:</span>{' '}
|
||||
стандартный вывод
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-[10px] mt-[20px]">
|
||||
<LaTextContainer html={extractDivByClass(html, "legend")} latex={legend} mediaFiles={mediaFiles}/>
|
||||
<LaTextContainer
|
||||
html={extractDivByClass(html, 'legend')}
|
||||
latex={legend}
|
||||
mediaFiles={mediaFiles}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<LaTextContainer html={extractDivByClass(html, "input-specification")} latex={input} mediaFiles={mediaFiles}/>
|
||||
<LaTextContainer
|
||||
html={extractDivByClass(html, 'input-specification')}
|
||||
latex={input}
|
||||
mediaFiles={mediaFiles}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<LaTextContainer html={extractDivByClass(html, "output-specification")} latex={output} mediaFiles={mediaFiles}/>
|
||||
<LaTextContainer
|
||||
html={extractDivByClass(html, 'output-specification')}
|
||||
latex={output}
|
||||
mediaFiles={mediaFiles}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<div className="text-[18px] font-bold">{sampleTests.length == 1 ? "Пример" : "Примеры"}</div>
|
||||
|
||||
<div className="text-[18px] font-bold">
|
||||
{sampleTests.length == 1 ? 'Пример' : 'Примеры'}
|
||||
</div>
|
||||
|
||||
{sampleTests.map((v, i) =>
|
||||
{sampleTests.map((v, i) => (
|
||||
<div key={i} className="flex flex-col gap-[10px]">
|
||||
<div className="text-[14px] font-bold">Входные данные</div>
|
||||
<CopyableDiv content={v.input}/>
|
||||
<div className="text-[14px] font-bold">Выходные данные</div>
|
||||
<CopyableDiv content={v.output}/>
|
||||
<div className="text-[14px] font-bold">
|
||||
Входные данные
|
||||
</div>
|
||||
<CopyableDiv content={v.input} />
|
||||
<div className="text-[14px] font-bold">
|
||||
Выходные данные
|
||||
</div>
|
||||
<CopyableDiv content={v.output} />
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<LaTextContainer html={extractDivByClass(html, "note")} latex={notes} mediaFiles={mediaFiles}/>
|
||||
<LaTextContainer
|
||||
html={extractDivByClass(html, 'note')}
|
||||
latex={notes}
|
||||
mediaFiles={mediaFiles}
|
||||
/>
|
||||
<div>Автор: Jacks</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default Statement;
|
||||
export default Statement;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { cn } from '../../../lib/cn';
|
||||
// import { IconError, IconSuccess } from "../../../assets/icons/missions";
|
||||
// import { useNavigate } from "react-router-dom";
|
||||
|
||||
@@ -7,8 +7,8 @@ export interface SubmissionItemProps {
|
||||
language: string;
|
||||
time: string;
|
||||
verdict: string;
|
||||
type: "first" | "second";
|
||||
status?: "success" | "wronganswer" | "timelimit";
|
||||
type: 'first' | 'second';
|
||||
status?: 'success' | 'wronganswer' | 'timelimit';
|
||||
}
|
||||
|
||||
export function formatMilliseconds(ms: number): string {
|
||||
@@ -23,16 +23,16 @@ export function formatBytesToMB(bytes: number): string {
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const date = new Date(dateString);
|
||||
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const year = date.getFullYear();
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
|
||||
const hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
return `${day}/${month}/${year}\n${hours}:${minutes}`;
|
||||
return `${day}/${month}/${year}\n${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
const SubmissionItem: React.FC<SubmissionItemProps> = ({
|
||||
@@ -46,30 +46,34 @@ const SubmissionItem: React.FC<SubmissionItemProps> = ({
|
||||
// const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className={cn(" w-full relative rounded-[10px] text-liquid-white",
|
||||
type == "first" ? "bg-liquid-lighter" : "bg-liquid-background",
|
||||
"grid grid-cols-[80px,1fr,1fr,2fr] grid-flow-col gap-[20px] px-[20px] box-border items-center",
|
||||
status == "wronganswer" && "border-l-[11px] border-l-liquid-red pl-[9px]",
|
||||
status == "timelimit" && "border-l-[11px] border-l-liquid-orange pl-[9px]",
|
||||
status == "success" && "border-l-[11px] border-l-liquid-green pl-[9px]",
|
||||
"cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300",
|
||||
)}
|
||||
onClick={() => { }}
|
||||
<div
|
||||
className={cn(
|
||||
' w-full relative rounded-[10px] text-liquid-white',
|
||||
type == 'first' ? 'bg-liquid-lighter' : 'bg-liquid-background',
|
||||
'grid grid-cols-[80px,1fr,1fr,2fr] grid-flow-col gap-[20px] px-[20px] box-border items-center',
|
||||
status == 'wronganswer' &&
|
||||
'border-l-[11px] border-l-liquid-red pl-[9px]',
|
||||
status == 'timelimit' &&
|
||||
'border-l-[11px] border-l-liquid-orange pl-[9px]',
|
||||
status == 'success' &&
|
||||
'border-l-[11px] border-l-liquid-green pl-[9px]',
|
||||
'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300',
|
||||
)}
|
||||
onClick={() => {}}
|
||||
>
|
||||
<div className="text-[18px] font-bold">
|
||||
#{id}
|
||||
</div>
|
||||
<div className="text-[18px] font-bold">#{id}</div>
|
||||
<div className="text-[18px] font-bold text-center">
|
||||
{formatDate(time)}
|
||||
</div>
|
||||
<div className="text-[18px] font-bold text-center">
|
||||
{language}
|
||||
</div>
|
||||
<div className={cn("text-[18px] font-bold text-center",
|
||||
status == "wronganswer" && "text-liquid-red",
|
||||
status == "timelimit" && "text-liquid-orange",
|
||||
status == "success" && "text-liquid-green",
|
||||
)} >
|
||||
<div className="text-[18px] font-bold text-center">{language}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'text-[18px] font-bold text-center',
|
||||
status == 'wronganswer' && 'text-liquid-red',
|
||||
status == 'timelimit' && 'text-liquid-orange',
|
||||
status == 'success' && 'text-liquid-green',
|
||||
)}
|
||||
>
|
||||
{verdict}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user