submissions
This commit is contained in:
@@ -5,9 +5,6 @@ import { Route, Routes } from "react-router-dom";
|
||||
// import { Input } from "./components/input/Input";
|
||||
// import { Switch } from "./components/switch/Switch";
|
||||
import Home from "./pages/Home";
|
||||
// import CodeEditor from "./views/mission/codeeditor/CodeEditor";
|
||||
import Statement from "./views/mission/statement/Statement";
|
||||
import MissionStatement from "./views/mission/statement/Mission";
|
||||
import Mission from "./pages/Mission";
|
||||
import UploadMissionForm from "./views/mission/UploadMissionForm";
|
||||
|
||||
@@ -19,9 +16,7 @@ function App() {
|
||||
<Route path="/home/*" element={<Home />} />
|
||||
<Route path="/mission/:missionId" element={<Mission />} />
|
||||
<Route path="/upload" element={<UploadMissionForm/>}/>
|
||||
{/* <Route path="/editor" element={<div className="box-border p-[50px] w-full h-[800px] relative bg-red-8001"><CodeEditor /></div>} /> */}
|
||||
<Route path="/statement" element={<div className="box-border p-[50px] w-full h-[800px] relative bg-red-8001"><Statement /></div>} />
|
||||
<Route path="*" element={<MissionStatement />} />
|
||||
<Route path="*" element={<Home />} />
|
||||
</Routes>
|
||||
|
||||
</div>
|
||||
|
||||
3
src/assets/icons/header/arrow-left-sm.svg
Normal file
3
src/assets/icons/header/arrow-left-sm.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.1667 16.375L7 12M7 12L11.1667 7.625M7 12H17" stroke="#EDF6F7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 244 B |
3
src/assets/icons/header/chevron-left.svg
Normal file
3
src/assets/icons/header/chevron-left.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 17L10 12L15 7" stroke="#EDF6F7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 214 B |
3
src/assets/icons/header/chevron-right.svg
Normal file
3
src/assets/icons/header/chevron-right.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 7L15 12L10 17" stroke="#EDF6F7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 214 B |
5
src/assets/icons/header/index.ts
Normal file
5
src/assets/icons/header/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import arrowLeft from "./arrow-left-sm.svg";
|
||||
import chevroneLeft from "./chevron-left.svg"
|
||||
import chevroneRight from "./chevron-right.svg"
|
||||
|
||||
export {arrowLeft, chevroneLeft, chevroneRight}
|
||||
14
src/axios.ts
14
src/axios.ts
@@ -7,4 +7,18 @@ const instance = axios.create({
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor: автоматически подставляет JWT, если есть
|
||||
instance.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem("jwt"); // или можно брать из Redux через store.getState()
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default instance;
|
||||
|
||||
@@ -5,11 +5,12 @@ import Register from "../views/home/auth/Register";
|
||||
import Menu from "../views/home/menu/Menu";
|
||||
import { useAppDispatch, useAppSelector } from "../redux/hooks";
|
||||
import { useEffect } from "react";
|
||||
import { fetchWhoAmI } from "../redux/slices/auth";
|
||||
import { fetchWhoAmI, logout } from "../redux/slices/auth";
|
||||
import Missions from "../views/home/missions/Missions";
|
||||
import Articles from "../views/home/articles/Articles";
|
||||
import Groups from "../views/home/groups/Groups";
|
||||
import Contests from "../views/home/contests/Contests";
|
||||
import { PrimaryButton } from "../components/button/PrimaryButton";
|
||||
|
||||
const Home = () => {
|
||||
const name = useAppSelector((state) => state.auth.username);
|
||||
@@ -35,7 +36,7 @@ const Home = () => {
|
||||
<Route path="articles/*" element={<Articles/>} />
|
||||
<Route path="groups/*" element={<Groups/>} />
|
||||
<Route path="contests/*" element={<Contests/>} />
|
||||
<Route path="*" element={name} />
|
||||
<Route path="*" element={<>{name}<PrimaryButton onClick={() => {dispatch(logout())}}>выйти</PrimaryButton></>} />
|
||||
</Routes>
|
||||
</div>
|
||||
{
|
||||
|
||||
@@ -2,10 +2,12 @@ import { useParams, Navigate } from 'react-router-dom';
|
||||
import CodeEditor from '../views/mission/codeeditor/CodeEditor';
|
||||
import Statement, { StatementData } from '../views/mission/statement/Statement';
|
||||
import { PrimaryButton } from '../components/button/PrimaryButton';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
||||
import { submitMission } from '../redux/slices/submit';
|
||||
import { fetchMySubmitsByMission, submitMission } from '../redux/slices/submit';
|
||||
import { fetchMissionById } from '../redux/slices/missions';
|
||||
import Header from '../views/mission/statement/Header';
|
||||
import MissionSubmissions from '../views/mission/statement/MissionSubmissions';
|
||||
|
||||
const Mission = () => {
|
||||
|
||||
@@ -15,35 +17,86 @@ const Mission = () => {
|
||||
const { missionId } = useParams<{ missionId: string }>();
|
||||
const mission = useAppSelector((state) => state.missions.currentMission);
|
||||
const missionIdNumber = Number(missionId);
|
||||
|
||||
const [code, setCode] = useState<string>("");
|
||||
const [language, setLanguage] = useState<string>("");
|
||||
|
||||
// Если missionId нет, редиректим на /home
|
||||
|
||||
// Если missionId нет или не число — редиректим
|
||||
if (!missionId || isNaN(missionIdNumber)) {
|
||||
return <Navigate to="/home" replace />;
|
||||
}
|
||||
|
||||
const [code, setCode] = useState<string>("");
|
||||
const [language, setLanguage] = useState<string>("");
|
||||
|
||||
const pollingRef = useRef<number | null>(null);
|
||||
const submissions = useAppSelector((state) => state.submin.submitsById[missionIdNumber] || []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchMissionById(missionIdNumber));
|
||||
dispatch(fetchMySubmitsByMission(missionIdNumber));
|
||||
}, [missionIdNumber]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (pollingRef.current) {
|
||||
clearInterval(pollingRef.current);
|
||||
pollingRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!mission || !mission.statements || mission.statements.length === 0) {
|
||||
return <div>Загрузка или миссия не найдена...</div>;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (submissions.length === 0) return;
|
||||
|
||||
const hasWaiting = submissions.some(
|
||||
s => s.solution.status === "Waiting" || s.solution.testerState === "Waiting"
|
||||
);
|
||||
|
||||
if (hasWaiting) {
|
||||
startPolling();
|
||||
}
|
||||
}, [submissions]);
|
||||
|
||||
|
||||
if (!mission || !mission.statements || mission.statements.length === 0) {
|
||||
return <div>Загрузка...</div>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const statementRaw = mission.statements[0];
|
||||
interface StatementData {
|
||||
id: number;
|
||||
legend?: string;
|
||||
timeLimit?: number;
|
||||
output?: string;
|
||||
input?: string;
|
||||
sampleTests?: any[];
|
||||
name?: string;
|
||||
memoryLimit?: number;
|
||||
tags?: string[];
|
||||
notes?: string;
|
||||
html?: string;
|
||||
mediaFiles?: any[];
|
||||
}
|
||||
|
||||
let statementData: StatementData = { id: mission.id };
|
||||
|
||||
try {
|
||||
const statementTexts = JSON.parse(statementRaw.statementTexts["problem-properties.json"]);
|
||||
// console.log(mission);
|
||||
// 1. Берём первый statement с форматом Latex и языком russian
|
||||
const latexStatement = mission.statements.find(
|
||||
(stmt: any) => stmt && stmt.language === "russian" && stmt.format === "Latex"
|
||||
);
|
||||
|
||||
// 2. Берём первый statement с форматом Html и языком russian
|
||||
const htmlStatement = mission.statements.find(
|
||||
(stmt: any) => stmt && stmt.language === "russian" && stmt.format === "Html"
|
||||
);
|
||||
|
||||
if (!latexStatement) throw new Error("Не найден блок Latex на русском");
|
||||
if (!htmlStatement) throw new Error("Не найден блок Html на русском");
|
||||
|
||||
// 3. Парсим данные из problem-properties.json
|
||||
const statementTexts = JSON.parse(latexStatement.statementTexts["problem-properties.json"]);
|
||||
|
||||
statementData = {
|
||||
id: statementRaw.id,
|
||||
id: missionIdNumber,
|
||||
legend: statementTexts.legend,
|
||||
timeLimit: statementTexts.timeLimit,
|
||||
output: statementTexts.output,
|
||||
@@ -53,41 +106,81 @@ const statementRaw = mission.statements[0];
|
||||
memoryLimit: statementTexts.memoryLimit,
|
||||
tags: mission.tags,
|
||||
notes: statementTexts.notes,
|
||||
html: htmlStatement.statementTexts["problem.html"],
|
||||
mediaFiles: latexStatement.mediaFiles
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Ошибка парсинга statementTexts:", err);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const startPolling = () => {
|
||||
if (pollingRef.current)
|
||||
return;
|
||||
|
||||
pollingRef.current = setInterval(async () => {
|
||||
dispatch(fetchMySubmitsByMission(missionIdNumber));
|
||||
|
||||
const hasWaiting = submissions.some(
|
||||
(s: any) => s.solution.status == "Waiting" || s.solution.testerState === "Waiting"
|
||||
);
|
||||
if (!hasWaiting) {
|
||||
// Всё проверено — стоп
|
||||
if (pollingRef.current) {
|
||||
clearInterval(pollingRef.current);
|
||||
pollingRef.current = null;
|
||||
}
|
||||
}
|
||||
}, 5000); // 10 секунд
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full bg-liquid-background grid grid-cols-[minmax(0,1fr),minmax(0,1fr)] h-full gap-[20px] relative">
|
||||
<div><Statement
|
||||
id={missionIdNumber}
|
||||
{...statementData}
|
||||
|
||||
/>
|
||||
<div className="h-screen grid grid-rows-[60px,1fr]">
|
||||
<div className="">
|
||||
<Header missionId={missionIdNumber} />
|
||||
</div>
|
||||
<div className=' grid grid-rows-[1fr,200px] grid-flow-row h-full w-full gap-[20px]'>
|
||||
<div className='w-full relative'>
|
||||
<CodeEditor
|
||||
onChange={(value: string) => { setCode(value); }}
|
||||
onChangeLanguage={((value: string) => { setLanguage(value); })}
|
||||
|
||||
<div className="grid grid-cols-2 h-full min-h-0 gap-[20px]">
|
||||
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
||||
<Statement
|
||||
{...statementData}
|
||||
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<PrimaryButton text='Отправить' onClick={() => {
|
||||
dispatch(submitMission({
|
||||
missionId: missionIdNumber,
|
||||
language: language,
|
||||
languageVersion: "latest",
|
||||
sourceCode: code,
|
||||
contestId: null,
|
||||
|
||||
}))
|
||||
}} />
|
||||
<div className="overflow-y-auto min-h-0 overflow-hidden pb-[20px]">
|
||||
<div className=' grid grid-rows-[1fr,45px,230px] grid-flow-row h-full w-full gap-[20px] '>
|
||||
<div className='w-full relative '>
|
||||
<CodeEditor
|
||||
onChange={(value: string) => { setCode(value); }}
|
||||
onChangeLanguage={((value: string) => { setLanguage(value); })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<PrimaryButton text='Отправить' onClick={async () => {
|
||||
await dispatch(submitMission({
|
||||
missionId: missionIdNumber,
|
||||
language: language,
|
||||
languageVersion: "latest",
|
||||
sourceCode: code,
|
||||
contestId: null,
|
||||
|
||||
})).unwrap();
|
||||
dispatch(fetchMySubmitsByMission(missionIdNumber));
|
||||
}} />
|
||||
</div>
|
||||
|
||||
<div className='h-full w-full '>
|
||||
<MissionSubmissions missionId={missionIdNumber} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-0 bottom-0 left-1/2 w-[1px] bg-liquid-lighter transform -translate-x-1/2"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -77,6 +77,22 @@ export const fetchWhoAmI = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
// AsyncThunk: Загрузка токенов из localStorage
|
||||
export const loadTokensFromLocalStorage = createAsyncThunk(
|
||||
"auth/loadTokens",
|
||||
async (_, { dispatch }) => {
|
||||
const jwt = localStorage.getItem("jwt");
|
||||
const refreshToken = localStorage.getItem("refreshToken");
|
||||
|
||||
if (jwt && refreshToken) {
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${jwt}`;
|
||||
return { jwt, refreshToken };
|
||||
} else {
|
||||
return { jwt: null, refreshToken: null };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Slice
|
||||
const authSlice = createSlice({
|
||||
name: "auth",
|
||||
@@ -88,6 +104,9 @@ const authSlice = createSlice({
|
||||
state.username = null;
|
||||
state.status = "idle";
|
||||
state.error = null;
|
||||
localStorage.removeItem("jwt");
|
||||
localStorage.removeItem("refreshToken");
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
@@ -98,9 +117,11 @@ const authSlice = createSlice({
|
||||
});
|
||||
builder.addCase(registerUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => {
|
||||
state.status = "successful";
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
|
||||
state.jwt = action.payload.jwt;
|
||||
state.refreshToken = action.payload.refreshToken;
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
|
||||
localStorage.setItem("jwt", action.payload.jwt);
|
||||
localStorage.setItem("refreshToken", action.payload.refreshToken);
|
||||
});
|
||||
builder.addCase(registerUser.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
@@ -114,9 +135,11 @@ const authSlice = createSlice({
|
||||
});
|
||||
builder.addCase(loginUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => {
|
||||
state.status = "successful";
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
|
||||
state.jwt = action.payload.jwt;
|
||||
state.refreshToken = action.payload.refreshToken;
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
|
||||
localStorage.setItem("jwt", action.payload.jwt);
|
||||
localStorage.setItem("refreshToken", action.payload.refreshToken);
|
||||
});
|
||||
builder.addCase(loginUser.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
@@ -150,6 +173,15 @@ const authSlice = createSlice({
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
// Загрузка токенов из localStorage
|
||||
builder.addCase(loadTokensFromLocalStorage.fulfilled, (state, action: PayloadAction<{ jwt: string | null; refreshToken: string | null }>) => {
|
||||
state.jwt = action.payload.jwt;
|
||||
state.refreshToken = action.payload.refreshToken;
|
||||
if (action.payload.jwt) {
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ interface Mission {
|
||||
tags: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
statements: Statement[] | null;
|
||||
statements?: Statement[];
|
||||
}
|
||||
|
||||
interface MissionsState {
|
||||
@@ -120,7 +120,6 @@ const missionsSlice = createSlice({
|
||||
});
|
||||
builder.addCase(fetchMissionById.fulfilled, (state, action: PayloadAction<Mission>) => {
|
||||
state.status = "successful";
|
||||
console.log(action.payload);
|
||||
state.currentMission = action.payload;
|
||||
});
|
||||
builder.addCase(fetchMissionById.rejected, (state, action: PayloadAction<any>) => {
|
||||
|
||||
@@ -11,17 +11,33 @@ export interface Submit {
|
||||
contestId: number | null;
|
||||
}
|
||||
|
||||
export interface SubmitStatus {
|
||||
SubmitId: number;
|
||||
State: string;
|
||||
ErrorCode: string;
|
||||
Message: string;
|
||||
CurrentTest: number;
|
||||
AmountOfTests: number;
|
||||
export interface Solution {
|
||||
id: number;
|
||||
missionId: number;
|
||||
language: string;
|
||||
languageVersion: string;
|
||||
sourceCode: string;
|
||||
status: string;
|
||||
time: string;
|
||||
testerState: string;
|
||||
testerErrorCode: string;
|
||||
testerMessage: string;
|
||||
currentTest: number;
|
||||
amountOfTests: number;
|
||||
}
|
||||
|
||||
export interface MissionSubmit {
|
||||
id: number;
|
||||
userId: number;
|
||||
solution: Solution;
|
||||
contestId: number | null;
|
||||
contestName: string | null;
|
||||
sourceType: string;
|
||||
}
|
||||
|
||||
interface SubmitState {
|
||||
submits: Submit[];
|
||||
submitsById: Record<number, MissionSubmit[]>; // ✅ добавлено
|
||||
currentSubmit?: Submit;
|
||||
status: "idle" | "loading" | "successful" | "failed";
|
||||
error: string | null;
|
||||
@@ -30,6 +46,7 @@ interface SubmitState {
|
||||
// Начальное состояние
|
||||
const initialState: SubmitState = {
|
||||
submits: [],
|
||||
submitsById: {}, // ✅ инициализация
|
||||
currentSubmit: undefined,
|
||||
status: "idle",
|
||||
error: null,
|
||||
@@ -74,13 +91,13 @@ export const fetchSubmitById = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
// AsyncThunk: Получить свои отправки для конкретной миссии
|
||||
// ✅ AsyncThunk: Получить отправки для конкретной миссии (новая структура)
|
||||
export const fetchMySubmitsByMission = createAsyncThunk(
|
||||
"submit/fetchMySubmitsByMission",
|
||||
async (missionId: number, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get(`/submits/my/mission/${missionId}`);
|
||||
return response.data as Submit[];
|
||||
return { missionId, data: response.data as MissionSubmit[] };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch mission submits");
|
||||
}
|
||||
@@ -97,6 +114,9 @@ const submitSlice = createSlice({
|
||||
state.status = "idle";
|
||||
state.error = null;
|
||||
},
|
||||
clearSubmitsByMission: (state, action: PayloadAction<number>) => {
|
||||
delete state.submitsById[action.payload];
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Отправка решения
|
||||
@@ -141,15 +161,18 @@ const submitSlice = createSlice({
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
// Получить отправки по миссии
|
||||
// ✅ Получить отправки по миссии
|
||||
builder.addCase(fetchMySubmitsByMission.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchMySubmitsByMission.fulfilled, (state, action: PayloadAction<Submit[]>) => {
|
||||
state.status = "successful";
|
||||
state.submits = action.payload;
|
||||
});
|
||||
builder.addCase(
|
||||
fetchMySubmitsByMission.fulfilled,
|
||||
(state, action: PayloadAction<{ missionId: number; data: MissionSubmit[] }>) => {
|
||||
state.status = "successful";
|
||||
state.submitsById[action.payload.missionId] = action.payload.data;
|
||||
}
|
||||
);
|
||||
builder.addCase(fetchMySubmitsByMission.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
@@ -157,5 +180,5 @@ const submitSlice = createSlice({
|
||||
},
|
||||
});
|
||||
|
||||
export const { clearCurrentSubmit } = submitSlice.actions;
|
||||
export const { clearCurrentSubmit, clearSubmitsByMission } = submitSlice.actions;
|
||||
export const submitReducer = submitSlice.reducer;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
@import "./latex-container.css";
|
||||
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent; /* Отключаем выделение синим при тапе на телефоне*/
|
||||
/* outline: 1px solid green; */
|
||||
@@ -26,7 +28,7 @@
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -39,7 +41,6 @@ body {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Общий контейнер полосы прокрутки */
|
||||
.thin-scrollbar::-webkit-scrollbar {
|
||||
width: 4px; /* ширина вертикального */
|
||||
@@ -77,6 +78,25 @@ body {
|
||||
|
||||
|
||||
|
||||
/* Общий контейнер полосы прокрутки */
|
||||
.thin-dark-scrollbar::-webkit-scrollbar {
|
||||
width: 4px; /* ширина вертикального */
|
||||
}
|
||||
|
||||
/* Трек (фон) */
|
||||
.thin-dark-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Ползунок (thumb) */
|
||||
.thin-dark-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: var(--color-liquid-lighter);
|
||||
border-radius: 1000px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
|
||||
26
src/styles/latex-container.css
Normal file
26
src/styles/latex-container.css
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
.latex-container p {
|
||||
text-align: justify; /* выравнивание по ширине */
|
||||
text-justify: inter-word;
|
||||
margin-bottom: 0.8em; /* небольшой отступ между абзацами */
|
||||
line-height: 1.2;
|
||||
/* text-indent: 1em; */
|
||||
}
|
||||
|
||||
.latex-container ol {
|
||||
padding-left: 1.5em; /* отступ для нумерации */
|
||||
margin: 0.5em 0; /* небольшой отступ сверху и снизу */
|
||||
line-height: 1.5; /* удобный межстрочный интервал */
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.latex-container ol li {
|
||||
margin-bottom: 0.4em; /* расстояние между пунктами */
|
||||
}
|
||||
|
||||
.latex-container .section-title{
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ export interface ArticleItemProps {
|
||||
const ArticleItem: React.FC<ArticleItemProps> = ({
|
||||
id, name, tags
|
||||
}) => {
|
||||
console.log(id);
|
||||
return (
|
||||
<div className={cn("w-full relative rounded-[10px] text-liquid-white mb-[20px]",
|
||||
// type == "first" ? "bg-liquid-lighter" : "bg-liquid-background",
|
||||
|
||||
@@ -25,7 +25,6 @@ const Login = () => {
|
||||
|
||||
// После успешного логина
|
||||
useEffect(() => {
|
||||
console.log(submitClicked);
|
||||
dispatch(setMenuActivePage("account"))
|
||||
}, []);
|
||||
|
||||
@@ -37,7 +36,6 @@ const Login = () => {
|
||||
|
||||
const handleLogin = () => {
|
||||
// setErr(err == "" ? "Неверная почта и/или пароль" : "");
|
||||
// console.log(123);
|
||||
setSubmitClicked(true);
|
||||
|
||||
if (!username || !password) return;
|
||||
|
||||
@@ -28,7 +28,6 @@ const Register = () => {
|
||||
// После успешной регистрации — переход в систему
|
||||
|
||||
useEffect(() => {
|
||||
console.log(submitClicked);
|
||||
dispatch(setMenuActivePage("account"))
|
||||
}, []);
|
||||
|
||||
@@ -71,7 +70,7 @@ const Register = () => {
|
||||
|
||||
<div className=" flex items-center mt-[10px] h-[24px]">
|
||||
<Checkbox
|
||||
onChange={(value: boolean) => { console.log(value) }}
|
||||
onChange={(value: boolean) => { value; }}
|
||||
className="p-0 w-fit m-[2.75px]"
|
||||
size="md"
|
||||
color="secondary"
|
||||
|
||||
@@ -37,7 +37,6 @@ const GroupsBlock: FC<GroupsBlockProps> = ({ contests, title, className }) => {
|
||||
active && "border-b-liquid-lighter"
|
||||
)}
|
||||
onClick={() => {
|
||||
console.log(active);
|
||||
setActive(!active)
|
||||
}}>
|
||||
<span>{title}</span>
|
||||
|
||||
@@ -26,7 +26,6 @@ const IconComponent: React.FC<IconComponentProps> = ({
|
||||
const GroupItem: React.FC<GroupItemProps> = ({
|
||||
id, name, visible, role
|
||||
}) => {
|
||||
console.log(id);
|
||||
return (
|
||||
<div className={cn("w-full h-[120px] box-border relative rounded-[10px] p-[10px] text-liquid-white bg-liquid-lighter",
|
||||
)}>
|
||||
|
||||
@@ -33,7 +33,6 @@ const GroupsBlock: FC<GroupsBlockProps> = ({ groups, title, className }) => {
|
||||
active && " border-b-liquid-lighter"
|
||||
)}
|
||||
onClick={() => {
|
||||
console.log(active);
|
||||
setActive(!active)
|
||||
}}>
|
||||
<span>{title}</span>
|
||||
|
||||
@@ -20,14 +20,14 @@ export interface CodeEditorProps {
|
||||
}
|
||||
|
||||
const CodeEditor: React.FC<CodeEditorProps> = ({onChange, onChangeLanguage}) => {
|
||||
const [language, setLanguage] = useState<string>("cpp");
|
||||
const [language, setLanguage] = useState<string>("C++");
|
||||
const [code, setCode] = useState<string>("");
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
|
||||
|
||||
const items = [
|
||||
{ value: "c", text: "C" },
|
||||
{ value: "cpp", text: "C++" },
|
||||
{ value: "C++", text: "C++" },
|
||||
{ value: "java", text: "Java" },
|
||||
{ value: "python", text: "Python" },
|
||||
{ value: "pascal", text: "Pascal" },
|
||||
@@ -88,7 +88,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({onChange, onChangeLanguage}) =>
|
||||
{/* Панель выбора языка и загрузки файла */}
|
||||
<div className="flex items-center justify-between py-3 ">
|
||||
<div className="flex items-center gap-[20px]">
|
||||
<DropDownList items={items} onChange={(v) => { setLanguage(v) }} />
|
||||
<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%]",
|
||||
|
||||
30
src/views/mission/statement/Header.tsx
Normal file
30
src/views/mission/statement/Header.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
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 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={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}`) }} />
|
||||
<span>{missionId}</span>
|
||||
<img src={chevroneRight} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate(`/mission/${missionId + 1}`) }} />
|
||||
</div>
|
||||
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -1,14 +1,113 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface LaTextContainerProps {
|
||||
content: string;
|
||||
declare global {
|
||||
interface Window {
|
||||
MathJax?: {
|
||||
startup?: { promise?: Promise<void> };
|
||||
typesetPromise?: (elements?: Element[]) => Promise<void>;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const LaTextContainer: React.FC<LaTextContainerProps> = ({ content }) => {
|
||||
interface MediaFile {
|
||||
id: number;
|
||||
fileName: string;
|
||||
mediaUrl: string;
|
||||
}
|
||||
|
||||
return <div>
|
||||
{content}
|
||||
</div>;
|
||||
interface LaTextContainerProps {
|
||||
html: string;
|
||||
latex: string;
|
||||
mediaFiles?: MediaFile[];
|
||||
}
|
||||
|
||||
let mathJaxPromise: Promise<void> | null = null;
|
||||
|
||||
const loadMathJax = () => {
|
||||
if (mathJaxPromise) return mathJaxPromise;
|
||||
|
||||
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 },
|
||||
};
|
||||
|
||||
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.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
return mathJaxPromise;
|
||||
};
|
||||
|
||||
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 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;
|
||||
});
|
||||
|
||||
return doc.body.innerHTML;
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
// 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
|
||||
|
||||
return (
|
||||
<div
|
||||
className="latex-container"
|
||||
ref={containerRef}
|
||||
dangerouslySetInnerHTML={{ __html: processedHtml }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LaTextContainer;
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
MathJax?: {
|
||||
startup?: { promise?: Promise<void> };
|
||||
typesetPromise?: (elements?: Element[]) => Promise<void>;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function MissionStatement() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const legend = "В честь юбилея ректорат ЮФУ решил запустить акцию <<Сто и десять кексов>>. \r\n\r\n $x$, $a_i^2 + b_i^2 \le a_{i+1}^2$ В каждом корпусе университета открылась лавка с кексами, в которой каждый студент может получить бесплатные кексы.\r\n\r\nНе прошло и пары минут после открытия, как к лавкам набежали студенты и образовалось много очередей. Но самая большая очередь образовалась в главном корпусе ЮФУ. Изначально в этой очереди стояло $n$ студентов, но потом в течение следующих $m$ минут какие-то студенты приходили и вставали в очередь, а какие-то уходили.\r\n\r\nЗа каждым студентом закреплен номер его зачетной книжки, будем называть это число номером студента. У каждого студента будет уникальный номер, по которому можно однозначно его идентифицировать. Будем считать, что каждую минуту происходило одно из следующих событий:\r\n\r\n\\begin{enumerate}\r\n \\item Студент с номером $x$ пришел и встал перед студентом с номером $y$;\r\n \\item Студент с номером $x$ пришел и встал в конец очереди;\r\n \\item Студент с номером $x$ ушел из очереди; возможно, он потом вернется.\r\n\\end{enumerate}\r\n\r\nАналитикам стало интересно, а какой будет очередь после $m$ минут? \r\n\r\nПомогите им и сообщите конечное состояние очереди.\r\n\r\n";
|
||||
const htmlContent = `
|
||||
<DIV class="problem-statement">
|
||||
<DIV class="header">
|
||||
<DIV class="title">Очередь за кексами</DIV>
|
||||
<DIV class="time-limit">
|
||||
<DIV class="property-title">ограничение по времени на тест</DIV>1 секунда
|
||||
</DIV>
|
||||
<DIV class="memory-limit">
|
||||
<DIV class="property-title">ограничение по памяти на тест</DIV>256 мегабайт
|
||||
</DIV>
|
||||
<DIV class="input-file input-standard">
|
||||
<DIV class="property-title">ввод</DIV>стандартный ввод
|
||||
</DIV>
|
||||
<DIV class="output-file output-standard">
|
||||
<DIV class="property-title">вывод</DIV>стандартный вывод
|
||||
</DIV>
|
||||
</DIV>
|
||||
<DIV class="legend">
|
||||
<P>
|
||||
\\bf{test}
|
||||
$$$x$$$, $$$a_i^2 + b_i^2 \\le a_{i+1}^2$$$
|
||||
Some complex formula: $$$P(|S - E[S]| \\ge t) \\le 2 \\exp \\left( -\\frac{2 t^2 n^2}{\\sum_{i = 1}^n (b_i - a_i)^2} \\right).$$$
|
||||
В честь юбилея ректорат ЮФУ решил запустить акцию «Сто и десять кексов». В каждом корпусе
|
||||
университета открылась лавка с кексами, в которой каждый студент может получить бесплатные кексы.</P>
|
||||
<P>Не прошло и пары минут после открытия, как к лавкам набежали студенты и образовалось много очередей. Но
|
||||
самая большая очередь образовалась в главном корпусе ЮФУ. Изначально в этой очереди стояло $$$n$$$
|
||||
студентов, но потом в течение следующих $$$m$$$ минут какие-то студенты приходили и вставали в очередь,
|
||||
а какие-то уходили.</P>
|
||||
<P>За каждым студентом закреплен номер его зачетной книжки, будем называть это число номером студента. У
|
||||
каждого студента будет уникальный номер, по которому можно однозначно его идентифицировать. Будем
|
||||
считать, что каждую минуту происходило одно из следующих событий:</P>
|
||||
<P></P>
|
||||
<OL>
|
||||
<LI> Студент с номером $$$x$$$ пришел и встал перед студентом с номером $$$y$$$; </LI>
|
||||
<LI> Студент с номером $$$x$$$ пришел и встал в конец очереди; </LI>
|
||||
<LI> Студент с номером $$$x$$$ ушел из очереди; возможно, он потом вернется. </LI>
|
||||
</OL>
|
||||
<P></P>
|
||||
<P>Аналитикам стало интересно, а какой будет очередь после $$$m$$$ минут? </P>
|
||||
<P>Помогите им и сообщите конечное состояние очереди.</P>
|
||||
</DIV>
|
||||
<P></P>
|
||||
<P></P>
|
||||
<DIV class="input-specification">
|
||||
<DIV class="section-title">Входные данные</DIV>
|
||||
<P></P>
|
||||
<P>В первой строке заданы два целых числа $$$n$$$ и $$$m$$$ $$$(1 \\le n, m \\le 10^5)$$$ — текущее
|
||||
число студентов в очереди и количество изменений.</P>
|
||||
<P>В следующей строке задается $$$n$$$ целых <SPAN class="tex-font-style-bf">различных</SPAN> чисел $$$a_1,
|
||||
a_2, \\cdots , a_n$$$ $$$(1 \\le a_i \\le 10^9)$$$, где $$$a_i$$$ — номер студента, который
|
||||
стоит на $$$i$$$-й позиции в очереди.</P>
|
||||
<P>В следующих $$$m$$$ строках идет описание запросов изменения очереди.</P>
|
||||
<P>В каждой строке в зависимости от типа запроса задается два или три числа. Первое число $$$t_j$$$ $$$(1
|
||||
\\le t_j \\le 3)$$$ — тип события, которое произошло в $$$j$$$-ю минуту.</P>
|
||||
<P>Если $$$t_j = \\textbf{1}$$$, то в строке задается еще 2 числа $$$x$$$ $$$(1 \\le x_j \\le 10^9)$$$ и
|
||||
$$$y$$$ $$$(1 \\le y_j \\le 10^9)$$$ — номер студента, который пришел, и номер студента, перед
|
||||
которым он встанет в очереди. Гарантируется, что студент с номером $$$x$$$ ещё не занял очередь, а
|
||||
студент с номером $$$y$$$ уже стоит в ней. </P>
|
||||
<P>Если $$$t_j = \\textbf{2}$$$, то в строке задается еще 1 число $$$x$$$ $$$(1 \\le x_j \\le
|
||||
10^9)$$$ — номер студента, который пришел и встал в конец очереди. Гарантируется, что студент
|
||||
с номером $$$x$$$ ещё не занял очередь.</P>
|
||||
<P>Если $$$t_j = \\textbf{3}$$$, то в строке задается еще 1 число $$$x$$$ $$$(1 \\le x_j \\le
|
||||
10^9)$$$ — номер студента, который ушел из очереди. Гарантируется, что студент с номером
|
||||
$$$x$$$ стоит в очереди.</P>
|
||||
</DIV>
|
||||
<P></P>
|
||||
<P></P>
|
||||
<DIV class="output-specification">
|
||||
<DIV class="section-title">Выходные данные</DIV>
|
||||
<P></P>
|
||||
<P>В первой строке выведите одно число $$$|a|$$$ — длину очереди после выполнения всех запросов
|
||||
изменения.</P>
|
||||
<P>В следующей строке выведите $$$|a|$$$ чисел $$$a_1, a_2, \\cdots , a_{|a|}$$$, где $$$a_i$$$ —
|
||||
номер студента, который стоит на $$$i$$$-й позиции в очереди.</P>
|
||||
</DIV>
|
||||
<P></P>
|
||||
<P></P>
|
||||
<DIV class="sample-tests">
|
||||
<DIV class="section-title">Пример</DIV>
|
||||
<P></P>
|
||||
<P></P>
|
||||
<DIV class="sample-test">
|
||||
<DIV class="input">
|
||||
<DIV class="title">Входные данные</DIV>
|
||||
<PRE class="content" data-content="7 6
|
||||
1 2 3 4 5 6 7
|
||||
1 8 3
|
||||
2 9
|
||||
3 3
|
||||
1 3 9
|
||||
2 10
|
||||
3 1
|
||||
">
|
||||
<DIV class="test-example-line test-example-line-even test-example-line-0">7 6</DIV><DIV class="test-example-line test-example-line-even test-example-line-0">1 2 3 4 5 6 7</DIV><DIV class="test-example-line test-example-line-even test-example-line-0">1 8 3</DIV><DIV class="test-example-line test-example-line-even test-example-line-0">2 9</DIV><DIV class="test-example-line test-example-line-even test-example-line-0">3 3</DIV><DIV class="test-example-line test-example-line-even test-example-line-0">1 3 9</DIV><DIV class="test-example-line test-example-line-even test-example-line-0">2 10</DIV><DIV class="test-example-line test-example-line-even test-example-line-0">3 1</DIV></PRE>
|
||||
</DIV>
|
||||
<DIV class="output">
|
||||
<DIV class="title">Выходные данные</DIV>
|
||||
<PRE class="content">
|
||||
9
|
||||
2 8 4 5 6 7 3 9 10
|
||||
</PRE>
|
||||
</DIV>
|
||||
</DIV>
|
||||
</DIV>
|
||||
<P></P>
|
||||
<P></P>
|
||||
<DIV class="note">
|
||||
<DIV class="section-title">Примечание</DIV>
|
||||
<P></P>
|
||||
<P>Изначально очередь выглядит следующим образом:</P>
|
||||
<P><IMG class="tex-graphics" src="e29b6eb80ce5dcdb59a696421149e86cf24fff83.png"></P>
|
||||
<P>В первую минуту приходит студент с номером 8 и встает перед студентом с номером 3.</P>
|
||||
<P><IMG class="tex-graphics" src="cb6dba7c484189e88d8e1bc0e15767bbb44c0c69.png"></P>
|
||||
<P>Потом студент с номером 9 встает в конец очереди.</P>
|
||||
<P><IMG class="tex-graphics" src="b65f6e3c0bcc7e96380e577e3a79156116f6947e.png"></P>
|
||||
<P>Студент с номером 3 уходит из очереди.</P>
|
||||
<P><IMG class="tex-graphics" src="514e52cdf82b640ce29f9178bbd9be3379ab43dd.png"></P>
|
||||
<P>Потом он возвращается и становится перед студентом с номером 9.</P>
|
||||
<P><IMG class="tex-graphics" src="6f77902a9e98428961fb5c1bde374d946a82cdd2.png"></P>
|
||||
<P>После в конец очереди становится студент с номером 10.</P>
|
||||
<P><IMG class="tex-graphics" src="7b0ffd2ae443ff754e3131a6ddc77af4cb17a043.png"></P>
|
||||
<P>И студент с номером 1 уходит из очереди.</P>
|
||||
<P><IMG class="tex-graphics" src="15c12c02bcb2f87450906d26075f1336c6f8bb79.png"></P>
|
||||
<P>После $$$m$$$ событий очередь имеет следующий вид:</P>
|
||||
<P><IMG class="tex-graphics" src="ef8c0f8c7ba7108bef3d50979ae09eb267158ee6.png"></P>
|
||||
</DIV>
|
||||
</DIV>
|
||||
<P></P>
|
||||
<P> </P>
|
||||
|
||||
`;
|
||||
|
||||
useEffect(() => {
|
||||
// 1️⃣ Конфигурация MathJax
|
||||
(window as any).MathJax = {
|
||||
tex: {
|
||||
inlineMath: [["$$$", "$$$"]], // наш формат
|
||||
displayMath: [["$$$$$$", "$$$$$$"]],
|
||||
processEscapes: true,
|
||||
},
|
||||
options: {
|
||||
skipHtmlTags: ["script", "noscript", "style", "textarea", "pre", "code"],
|
||||
},
|
||||
startup: {
|
||||
typeset: false,
|
||||
},
|
||||
};
|
||||
|
||||
// 2️⃣ Подключаем MathJax
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js";
|
||||
script.async = true;
|
||||
|
||||
script.onload = () => {
|
||||
if (window.MathJax?.startup?.promise) {
|
||||
window.MathJax.startup.promise.then(() => renderMath());
|
||||
} else {
|
||||
renderMath();
|
||||
}
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
// 3️⃣ Отрисовка формул
|
||||
const renderMath = () => {
|
||||
if (containerRef.current && window.MathJax?.typesetPromise) {
|
||||
window.MathJax.typesetPromise([containerRef.current]).catch(console.error);
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
script.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div ref={containerRef} dangerouslySetInnerHTML={{ __html: htmlContent }} />;
|
||||
}
|
||||
61
src/views/mission/statement/MissionSubmissions.tsx
Normal file
61
src/views/mission/statement/MissionSubmissions.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import SubmissionItem from "./SubmissionItem";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
||||
import { FC, useEffect } from "react";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { fetchMissions } from "../../../redux/slices/missions";
|
||||
|
||||
|
||||
export interface Mission {
|
||||
id: number;
|
||||
authorId: number;
|
||||
name: string;
|
||||
difficulty: "Easy" | "Medium" | "Hard";
|
||||
tags: string[];
|
||||
timeLimit: number;
|
||||
memoryLimit: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface MissionSubmissionsProps{
|
||||
missionId: number;
|
||||
}
|
||||
|
||||
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";
|
||||
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)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MissionSubmissions;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import React from "react";
|
||||
// import { cn } from "../../../lib/cn";
|
||||
import LaTextContainer from "./LaTextContainer";
|
||||
// import FullLatexRenderer from "./FullLatexRenderer";
|
||||
|
||||
@@ -14,9 +14,16 @@ export interface StatementData {
|
||||
output?: string;
|
||||
sampleTests?: { input: string; output: string }[];
|
||||
notes?: string;
|
||||
html?: string;
|
||||
mediaFiles?: { id: number; fileName: string; mediaUrl: string }[];
|
||||
}
|
||||
|
||||
|
||||
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 Statement: React.FC<StatementData> = ({
|
||||
id,
|
||||
@@ -29,11 +36,12 @@ const Statement: React.FC<StatementData> = ({
|
||||
output = "",
|
||||
sampleTests = [],
|
||||
notes = "",
|
||||
html = "",
|
||||
mediaFiles,
|
||||
}) => {
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full bg-red-3001 overflow-y-scroll medium-scrollbar pl-[20px] pr-[12px] gap-[20px]">
|
||||
<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>
|
||||
@@ -50,35 +58,31 @@ const Statement: React.FC<StatementData> = ({
|
||||
<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]">
|
||||
<LaTextContainer content={legend} />
|
||||
<div className="flex flex-col gap-[10px] mt-[20px]">
|
||||
<LaTextContainer html={extractDivByClass(html, "legend")} latex={legend} mediaFiles={mediaFiles}/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<div>Входные данные</div>
|
||||
<LaTextContainer content={input} />
|
||||
<LaTextContainer html={extractDivByClass(html, "input-specification")} latex={input} mediaFiles={mediaFiles}/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<div>Выходные данные</div>
|
||||
<LaTextContainer content={output} />
|
||||
<LaTextContainer html={extractDivByClass(html, "output-specification")} latex={output} mediaFiles={mediaFiles}/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>{sampleTests.length == 1 ? "Пример" : "Примеры"}</div>
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<div className="text-[18px] font-bold">{sampleTests.length == 1 ? "Пример" : "Примеры"}</div>
|
||||
|
||||
|
||||
{sampleTests.map((v, i) =>
|
||||
<div key={i} className="flex flex-col gap-[10px]">
|
||||
<div>Входные данные</div>
|
||||
<div>{v.input}</div>
|
||||
<div>Выходные данные</div>
|
||||
<div>{v.output}</div>
|
||||
<div className="text-[14px] font-bold">Входные данные</div>
|
||||
<div className="p-[10px] bg-liquid-lighter rounded-[10px] whitespace-pre-line">{v.input}</div>
|
||||
<div className="text-[14px] font-bold">Выходные данные</div>
|
||||
<div className="p-[10px] bg-liquid-lighter rounded-[10px] whitespace-pre-line">{v.output}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<div>Примечание</div>
|
||||
<LaTextContainer content={notes} />
|
||||
<LaTextContainer html={extractDivByClass(html, "note")} latex={notes} mediaFiles={mediaFiles}/>
|
||||
<div>Автор: Jacks</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
79
src/views/mission/statement/SubmissionItem.tsx
Normal file
79
src/views/mission/statement/SubmissionItem.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { IconError, IconSuccess } from "../../../assets/icons/missions";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export interface SubmissionItemProps {
|
||||
id: number;
|
||||
language: string;
|
||||
time: string;
|
||||
verdict: string;
|
||||
type: "first" | "second";
|
||||
status?: "success" | "wronganswer" | "timelimit";
|
||||
}
|
||||
|
||||
export function formatMilliseconds(ms: number): string {
|
||||
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} МБ`;
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
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 hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
|
||||
return `${day}/${month}/${year}\n${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
const SubmissionItem: React.FC<SubmissionItemProps> = ({
|
||||
id,
|
||||
language,
|
||||
time,
|
||||
verdict,
|
||||
type,
|
||||
status,
|
||||
}) => {
|
||||
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-[10px,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 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",
|
||||
)} >
|
||||
{verdict}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubmissionItem;
|
||||
Reference in New Issue
Block a user