upload mission
This commit is contained in:
97
src/App.tsx
97
src/App.tsx
@@ -5,9 +5,11 @@ 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/problem/codeeditor/CodeEditor";
|
||||
import Statement from "./views/problem/statement/Statement";
|
||||
import ProblemStatement from "./views/problem/statement/Proble";
|
||||
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";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -15,97 +17,14 @@ function App() {
|
||||
<div className="relative w-full max-w-[1600px] h-full ">
|
||||
<Routes>
|
||||
<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={<ProblemStatement />} />
|
||||
<Route path="*" element={<MissionStatement />} />
|
||||
</Routes>
|
||||
|
||||
</div>
|
||||
|
||||
{/* <Switch
|
||||
className=" fixed top-0 left-0 z-full"
|
||||
variant="theme"
|
||||
color="secondary"
|
||||
onChange={(state: boolean) => {
|
||||
document.documentElement.setAttribute(
|
||||
"data-theme",
|
||||
state ? "dark" : "light"
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="">
|
||||
<Input
|
||||
id="first_name"
|
||||
label="Фамилия"
|
||||
variant="bordered"
|
||||
//placeholder="test"
|
||||
radius="sm"
|
||||
className="m-2"
|
||||
required
|
||||
onChange={(state: string) => {
|
||||
console.log(state);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
variant="flat"
|
||||
id="given_name"
|
||||
label="Имя"
|
||||
//placeholder="test"
|
||||
radius="sm"
|
||||
className="m-2"
|
||||
required
|
||||
onChange={(state: string) => {
|
||||
console.log(state);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
variant="faded"
|
||||
type="email"
|
||||
label="Почта"
|
||||
radius="sm"
|
||||
className="m-2"
|
||||
required
|
||||
onChange={(state: string) => {
|
||||
console.log(state);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
labelType="in-fixed"
|
||||
type="password"
|
||||
label="Пароль"
|
||||
radius="sm"
|
||||
className="m-2"
|
||||
required
|
||||
onChange={(state: string) => {
|
||||
console.log(state);
|
||||
}}
|
||||
/>
|
||||
<Checkbox onChange={() => { }} label="test" color="default" defaultState={true}/>
|
||||
<Checkbox onChange={() => { }} label="test" color="primary" defaultState={true}/>
|
||||
<Checkbox onChange={() => { }} label="test" color="secondary" defaultState={true}/>
|
||||
<Checkbox onChange={() => { }} label="test" color="success" defaultState={true}/>
|
||||
<Checkbox onChange={() => { }} label="test" color="warning" defaultState={true}/>
|
||||
<Checkbox onChange={() => { }} label="test" color="danger" defaultState={true}/>
|
||||
<Switch onChange={() => { }} color="default" defaultState={true}/>
|
||||
<Switch onChange={() => { }} color="primary" defaultState={true}/>
|
||||
<Switch onChange={() => { }} color="secondary" defaultState={true}/>
|
||||
<Switch onChange={() => { }} color="success" defaultState={true}/>
|
||||
<Switch onChange={() => { }} color="warning" defaultState={true}/>
|
||||
<Switch onChange={() => { }} color="danger" defaultState={true}/>
|
||||
|
||||
|
||||
<div className="grid grid-rows-3 grid-cols-2 grid-flow-col">
|
||||
|
||||
<PrimaryButton onClick={() => { }} text="Button" className="m-5" />
|
||||
<PrimaryButton onClick={() => { }} text="Button" className="m-5" />
|
||||
<PrimaryButton onClick={() => { }} text="Button" disabled className="m-5" />
|
||||
<SecondaryButton onClick={() => { }} text="Button" className="m-5" />
|
||||
<SecondaryButton onClick={() => { }} text="Button" className="m-5" />
|
||||
<SecondaryButton onClick={() => { }} text="Button" disabled className="m-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full"></div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -6,7 +6,7 @@ import Menu from "../views/home/menu/Menu";
|
||||
import { useAppDispatch, useAppSelector } from "../redux/hooks";
|
||||
import { useEffect } from "react";
|
||||
import { fetchWhoAmI } from "../redux/slices/auth";
|
||||
import Problems from "../views/home/problems/Problems";
|
||||
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";
|
||||
@@ -31,7 +31,7 @@ const Home = () => {
|
||||
<Route path="login" element={<Login />} />
|
||||
<Route path="account" element={<Login />} />
|
||||
<Route path="register" element={<Register />} />
|
||||
<Route path="problems/*" element={<Problems/>} />
|
||||
<Route path="missions/*" element={<Missions/>} />
|
||||
<Route path="articles/*" element={<Articles/>} />
|
||||
<Route path="groups/*" element={<Groups/>} />
|
||||
<Route path="contests/*" element={<Contests/>} />
|
||||
|
||||
26
src/pages/Mission.tsx
Normal file
26
src/pages/Mission.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useParams, Navigate } from 'react-router-dom';
|
||||
|
||||
|
||||
|
||||
const Mission = () => {
|
||||
|
||||
// Получаем параметры из URL
|
||||
const { missionId } = useParams<{ missionId: string }>();
|
||||
|
||||
// Если missionId нет, редиректим на /home
|
||||
if (!missionId) {
|
||||
return <Navigate to="/home" replace />;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full bg-liquid-background grid grid-cols-[250px,1fr,250px] divide-x-[1px] divide-liquid-lighter">
|
||||
|
||||
<div className="bg-red-300 w-full h-full">
|
||||
{missionId}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Mission;
|
||||
@@ -6,7 +6,7 @@ interface AuthState {
|
||||
jwt: string | null;
|
||||
refreshToken: string | null;
|
||||
username: string | null;
|
||||
status: "idle" | "loading" | "succeeded" | "failed";
|
||||
status: "idle" | "loading" | "successful" | "failed";
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ const authSlice = createSlice({
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(registerUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => {
|
||||
state.status = "succeeded";
|
||||
state.status = "successful";
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
|
||||
state.jwt = action.payload.jwt;
|
||||
state.refreshToken = action.payload.refreshToken;
|
||||
@@ -113,7 +113,7 @@ const authSlice = createSlice({
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(loginUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => {
|
||||
state.status = "succeeded";
|
||||
state.status = "successful";
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
|
||||
state.jwt = action.payload.jwt;
|
||||
state.refreshToken = action.payload.refreshToken;
|
||||
@@ -129,7 +129,7 @@ const authSlice = createSlice({
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(refreshToken.fulfilled, (state, action: PayloadAction<{ username: string }>) => {
|
||||
state.status = "succeeded";
|
||||
state.status = "successful";
|
||||
state.username = action.payload.username;
|
||||
});
|
||||
builder.addCase(refreshToken.rejected, (state, action: PayloadAction<any>) => {
|
||||
@@ -143,7 +143,7 @@ const authSlice = createSlice({
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchWhoAmI.fulfilled, (state, action: PayloadAction<{ username: string }>) => {
|
||||
state.status = "succeeded";
|
||||
state.status = "successful";
|
||||
state.username = action.payload.username;
|
||||
});
|
||||
builder.addCase(fetchWhoAmI.rejected, (state, action: PayloadAction<any>) => {
|
||||
|
||||
146
src/redux/slices/missions.ts
Normal file
146
src/redux/slices/missions.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
||||
import axios from "../../axios";
|
||||
|
||||
// Типы данных
|
||||
interface Statement {
|
||||
id: number;
|
||||
language: string;
|
||||
statementTexts: Record<string, string>;
|
||||
mediaFiles?: { id: number; fileName: string; mediaUrl: string }[];
|
||||
}
|
||||
|
||||
interface Mission {
|
||||
id: number;
|
||||
authorId: number;
|
||||
name: string;
|
||||
difficulty: number;
|
||||
tags: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
statements: Statement[] | null;
|
||||
}
|
||||
|
||||
interface MissionsState {
|
||||
missions: Mission[];
|
||||
currentMission: Mission | null;
|
||||
hasNextPage: boolean;
|
||||
status: "idle" | "loading" | "successful" | "failed";
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// Инициализация состояния
|
||||
const initialState: MissionsState = {
|
||||
missions: [],
|
||||
currentMission: null,
|
||||
hasNextPage: false,
|
||||
status: "idle",
|
||||
error: null,
|
||||
};
|
||||
|
||||
// AsyncThunk: Получение списка миссий
|
||||
export const fetchMissions = createAsyncThunk(
|
||||
"missions/fetchMissions",
|
||||
async (
|
||||
{ page = 0, pageSize = 10, tags }: { page?: number; pageSize?: number; tags?: string[] },
|
||||
{ rejectWithValue }
|
||||
) => {
|
||||
try {
|
||||
const params: any = { page, pageSize };
|
||||
if (tags) params.tags = tags;
|
||||
const response = await axios.get("/missions", { params });
|
||||
return response.data; // { hasNextPage, missions }
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch missions");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// AsyncThunk: Получение миссии по id
|
||||
export const fetchMissionById = createAsyncThunk(
|
||||
"missions/fetchMissionById",
|
||||
async (id: number, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await axios.get(`/missions/${id}`);
|
||||
return response.data; // Mission
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch mission");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// AsyncThunk: Загрузка миссии
|
||||
export const uploadMission = createAsyncThunk(
|
||||
"missions/uploadMission",
|
||||
async (
|
||||
{ file, name, difficulty, tags }: { file: File; name: string; difficulty: number; tags: string[] },
|
||||
{ rejectWithValue }
|
||||
) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("MissionFile", file);
|
||||
formData.append("Name", name);
|
||||
formData.append("Difficulty", difficulty.toString());
|
||||
tags.forEach(tag => formData.append("Tags", tag));
|
||||
|
||||
const response = await axios.post("/missions/upload", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
return response.data; // Mission
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(err.response?.data?.message || "Failed to upload mission");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Slice
|
||||
const missionsSlice = createSlice({
|
||||
name: "missions",
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
// fetchMissions
|
||||
builder.addCase(fetchMissions.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchMissions.fulfilled, (state, action: PayloadAction<{ missions: Mission[]; hasNextPage: boolean }>) => {
|
||||
state.status = "successful";
|
||||
state.missions = action.payload.missions;
|
||||
state.hasNextPage = action.payload.hasNextPage;
|
||||
});
|
||||
builder.addCase(fetchMissions.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
// fetchMissionById
|
||||
builder.addCase(fetchMissionById.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchMissionById.fulfilled, (state, action: PayloadAction<Mission>) => {
|
||||
state.status = "successful";
|
||||
state.currentMission = action.payload;
|
||||
});
|
||||
builder.addCase(fetchMissionById.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
|
||||
// uploadMission
|
||||
builder.addCase(uploadMission.pending, (state) => {
|
||||
state.status = "loading";
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(uploadMission.fulfilled, (state, action: PayloadAction<Mission>) => {
|
||||
state.status = "successful";
|
||||
state.missions.unshift(action.payload); // Добавляем новую миссию в начало списка
|
||||
});
|
||||
builder.addCase(uploadMission.rejected, (state, action: PayloadAction<any>) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const missionsReducer = missionsSlice.reducer;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { authReducer } from "./slices/auth";
|
||||
import { storeReducer } from "./slices/store";
|
||||
import { missionsReducer } from "./slices/missions";
|
||||
|
||||
|
||||
// использование
|
||||
@@ -17,6 +18,7 @@ export const store = configureStore({
|
||||
//user: userReducer,
|
||||
auth: authReducer,
|
||||
store: storeReducer,
|
||||
missions: missionsReducer,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { cn } from "../../../lib/cn";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
import { Balloon } from "../../../assets/icons/auth";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
@@ -18,13 +18,14 @@ const Login = () => {
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
|
||||
|
||||
const { status, error, jwt } = useAppSelector((state) => state.auth);
|
||||
const { status, jwt } = useAppSelector((state) => state.auth);
|
||||
|
||||
|
||||
const [err, setErr] = useState<string>("");
|
||||
// const [err, setErr] = useState<string>("");
|
||||
|
||||
// После успешного логина
|
||||
useEffect(() => {
|
||||
console.log(submitClicked);
|
||||
dispatch(setMenuActivePage("account"))
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { cn } from "../../../lib/cn";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
import { Balloon } from "../../../assets/icons/auth";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -23,11 +23,12 @@ const Register = () => {
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>("");
|
||||
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
|
||||
|
||||
const { status, error, jwt } = useAppSelector((state) => state.auth);
|
||||
const { status, jwt } = useAppSelector((state) => state.auth);
|
||||
|
||||
// После успешной регистрации — переход в систему
|
||||
|
||||
useEffect(() => {
|
||||
console.log(submitClicked);
|
||||
dispatch(setMenuActivePage("account"))
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@ import { useAppSelector } from "../../../redux/hooks";
|
||||
const Menu = () => {
|
||||
const menuItems = [
|
||||
{text: "Главная", href: "/home", icon: Home, page: "home" },
|
||||
{text: "Задачи", href: "/home/problems", icon: Clipboard, page: "problems" },
|
||||
{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" },
|
||||
{text: "Загрузка", href: "/upload", icon: Account, page: "p" },
|
||||
];
|
||||
const activePage = useAppSelector((state) => state.store.menu.activePage);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { IconError, IconSuccess } from "../../../assets/icons/problems";
|
||||
import { IconError, IconSuccess } from "../../../assets/icons/missions";
|
||||
|
||||
export interface ProblemItemProps {
|
||||
export interface MissionItemProps {
|
||||
id: number;
|
||||
authorId: number;
|
||||
name: string;
|
||||
@@ -26,7 +26,7 @@ export function formatBytesToMB(bytes: number): string {
|
||||
return `${megabytes} МБ`;
|
||||
}
|
||||
|
||||
const ProblemItem: React.FC<ProblemItemProps> = ({
|
||||
const MissionItem: React.FC<MissionItemProps> = ({
|
||||
id, name, difficulty, timeLimit, memoryLimit, type, status
|
||||
}) => {
|
||||
console.log(id);
|
||||
@@ -66,4 +66,4 @@ const ProblemItem: React.FC<ProblemItemProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ProblemItem;
|
||||
export default MissionItem;
|
||||
@@ -1,11 +1,11 @@
|
||||
import ProblemItem from "./ProblemItem";
|
||||
import MissionItem from "./MissionItem";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { useAppDispatch } from "../../../redux/hooks";
|
||||
import { useEffect } from "react";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
|
||||
|
||||
export interface Problem {
|
||||
export interface Mission {
|
||||
id: number;
|
||||
authorId: number;
|
||||
name: string;
|
||||
@@ -18,11 +18,11 @@ export interface Problem {
|
||||
}
|
||||
|
||||
|
||||
const Problems = () => {
|
||||
const Missions = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const problems: Problem[] = [
|
||||
const missions: Mission[] = [
|
||||
{
|
||||
"id": 1,
|
||||
"authorId": 1,
|
||||
@@ -466,7 +466,7 @@ const Problems = () => {
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("problems"))
|
||||
dispatch(setMenuActivePage("missions"))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -490,8 +490,8 @@ const Problems = () => {
|
||||
|
||||
<div>
|
||||
|
||||
{problems.map((v, i) => (
|
||||
<ProblemItem key={i} {...v} 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} {...v} type={i % 2 == 0 ? "first" : "second"} status={i == 0 || i == 3 || i == 7 ? "success" : (i == 2 || i == 4 || i == 9 ? "error" : "empty")}/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -504,4 +504,4 @@ const Problems = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Problems;
|
||||
export default Missions;
|
||||
101
src/views/mission/UploadMissionForm.tsx
Normal file
101
src/views/mission/UploadMissionForm.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { useState } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../../redux/hooks";
|
||||
import { uploadMission } from "../../redux/slices/missions";
|
||||
|
||||
const UploadMissionForm: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { status, error } = useAppSelector(state => state.missions);
|
||||
|
||||
// Локальные состояния формы
|
||||
const [name, setName] = useState("");
|
||||
const [difficulty, setDifficulty] = useState(1);
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [tagsValue, setTagsValue] = useState<string>("");
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!file) return alert("Выберите файл миссии!");
|
||||
|
||||
try {
|
||||
dispatch(uploadMission({ file, name, difficulty, tags }));
|
||||
|
||||
alert("Миссия успешно загружена!");
|
||||
setName("");
|
||||
setDifficulty(1);
|
||||
setTags([]);
|
||||
setFile(null);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Ошибка при загрузке миссии: " + err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
setFile(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTagsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTagsValue(e.target.value);
|
||||
const value = e.target.value;
|
||||
const tagsArray = value.split(",").map(tag => tag.trim()).filter(tag => tag);
|
||||
setTags(tagsArray);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-4 border rounded">
|
||||
<div className="mb-4">
|
||||
<label className="block mb-1">Название миссии</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
className="w-full border px-2 py-1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block mb-1">Сложность</label>
|
||||
<input
|
||||
type="number"
|
||||
value={difficulty}
|
||||
min={1}
|
||||
max={5}
|
||||
onChange={e => setDifficulty(Number(e.target.value))}
|
||||
className="w-full border px-2 py-1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block mb-1">Теги (через запятую)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tagsValue}
|
||||
onChange={handleTagsChange}
|
||||
className="w-full border px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block mb-1">Файл миссии</label>
|
||||
<input type="file" onChange={handleFileChange} accept=".zip" required />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "loading"}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
>
|
||||
{status === "loading" ? "Загрузка..." : "Загрузить миссию"}
|
||||
</button>
|
||||
|
||||
{status === "failed" && error && <p className="text-red-500 mt-2">{error}</p>}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadMissionForm;
|
||||
@@ -10,7 +10,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export default function ProblemStatement() {
|
||||
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 = `
|
||||
Reference in New Issue
Block a user