formatting
This commit is contained in:
@@ -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