diff --git a/src/App.tsx b/src/App.tsx
index 55f9dea..474940a 100644
--- a/src/App.tsx
+++ b/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() {
} />
+ } />
+ }/>
} />
} />
- } />
+ } />
-
- {/* {
- document.documentElement.setAttribute(
- "data-theme",
- state ? "dark" : "light"
- );
- }}
- />
-
- */}
);
}
diff --git a/src/assets/icons/problems/icon-error.svg b/src/assets/icons/missions/icon-error.svg
similarity index 100%
rename from src/assets/icons/problems/icon-error.svg
rename to src/assets/icons/missions/icon-error.svg
diff --git a/src/assets/icons/problems/icon-success.svg b/src/assets/icons/missions/icon-success.svg
similarity index 100%
rename from src/assets/icons/problems/icon-success.svg
rename to src/assets/icons/missions/icon-success.svg
diff --git a/src/assets/icons/problems/index.ts b/src/assets/icons/missions/index.ts
similarity index 100%
rename from src/assets/icons/problems/index.ts
rename to src/assets/icons/missions/index.ts
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index 1cc8c5a..4f41306 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -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 = () => {
} />
} />
} />
- } />
+ } />
} />
} />
} />
diff --git a/src/pages/Mission.tsx b/src/pages/Mission.tsx
new file mode 100644
index 0000000..751f89c
--- /dev/null
+++ b/src/pages/Mission.tsx
@@ -0,0 +1,26 @@
+import { useParams, Navigate } from 'react-router-dom';
+
+
+
+const Mission = () => {
+
+ // Получаем параметры из URL
+ const { missionId } = useParams<{ missionId: string }>();
+
+ // Если missionId нет, редиректим на /home
+ if (!missionId) {
+ return ;
+ }
+
+
+ return (
+
+ );
+};
+
+export default Mission;
diff --git a/src/redux/slices/auth.ts b/src/redux/slices/auth.ts
index ba9e681..cd40719 100644
--- a/src/redux/slices/auth.ts
+++ b/src/redux/slices/auth.ts
@@ -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) => {
@@ -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) => {
diff --git a/src/redux/slices/missions.ts b/src/redux/slices/missions.ts
new file mode 100644
index 0000000..571942d
--- /dev/null
+++ b/src/redux/slices/missions.ts
@@ -0,0 +1,146 @@
+import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
+import axios from "../../axios";
+
+// Типы данных
+interface Statement {
+ id: number;
+ language: string;
+ statementTexts: Record;
+ 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) => {
+ 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) => {
+ state.status = "successful";
+ state.currentMission = action.payload;
+ });
+ builder.addCase(fetchMissionById.rejected, (state, action: PayloadAction) => {
+ 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) => {
+ state.status = "successful";
+ state.missions.unshift(action.payload); // Добавляем новую миссию в начало списка
+ });
+ builder.addCase(uploadMission.rejected, (state, action: PayloadAction) => {
+ state.status = "failed";
+ state.error = action.payload;
+ });
+ },
+});
+
+export const missionsReducer = missionsSlice.reducer;
diff --git a/src/redux/store.ts b/src/redux/store.ts
index 509071b..834d822 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -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,
},
});
diff --git a/src/views/home/auth/Login.tsx b/src/views/home/auth/Login.tsx
index fc06b64..7301b12 100644
--- a/src/views/home/auth/Login.tsx
+++ b/src/views/home/auth/Login.tsx
@@ -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("");
const [submitClicked, setSubmitClicked] = useState(false);
- const { status, error, jwt } = useAppSelector((state) => state.auth);
+ const { status, jwt } = useAppSelector((state) => state.auth);
- const [err, setErr] = useState("");
+ // const [err, setErr] = useState("");
// После успешного логина
useEffect(() => {
+ console.log(submitClicked);
dispatch(setMenuActivePage("account"))
}, []);
diff --git a/src/views/home/auth/Register.tsx b/src/views/home/auth/Register.tsx
index e2a6e8f..38f0e12 100644
--- a/src/views/home/auth/Register.tsx
+++ b/src/views/home/auth/Register.tsx
@@ -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("");
const [submitClicked, setSubmitClicked] = useState(false);
- const { status, error, jwt } = useAppSelector((state) => state.auth);
+ const { status, jwt } = useAppSelector((state) => state.auth);
// После успешной регистрации — переход в систему
useEffect(() => {
+ console.log(submitClicked);
dispatch(setMenuActivePage("account"))
}, []);
diff --git a/src/views/home/menu/Menu.tsx b/src/views/home/menu/Menu.tsx
index 63ae749..ddf329b 100644
--- a/src/views/home/menu/Menu.tsx
+++ b/src/views/home/menu/Menu.tsx
@@ -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);
diff --git a/src/views/home/problems/ProblemItem.tsx b/src/views/home/missions/MissionItem.tsx
similarity index 92%
rename from src/views/home/problems/ProblemItem.tsx
rename to src/views/home/missions/MissionItem.tsx
index 5410cd5..8c679b2 100644
--- a/src/views/home/problems/ProblemItem.tsx
+++ b/src/views/home/missions/MissionItem.tsx
@@ -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 = ({
+const MissionItem: React.FC = ({
id, name, difficulty, timeLimit, memoryLimit, type, status
}) => {
console.log(id);
@@ -66,4 +66,4 @@ const ProblemItem: React.FC = ({
);
};
-export default ProblemItem;
+export default MissionItem;
diff --git a/src/views/home/problems/Problems.tsx b/src/views/home/missions/Missions.tsx
similarity index 98%
rename from src/views/home/problems/Problems.tsx
rename to src/views/home/missions/Missions.tsx
index 5c1ae29..5969183 100644
--- a/src/views/home/problems/Problems.tsx
+++ b/src/views/home/missions/Missions.tsx
@@ -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 = () => {
- {problems.map((v, i) => (
-
+ {missions.map((v, i) => (
+
))}
@@ -504,4 +504,4 @@ const Problems = () => {
);
};
-export default Problems;
+export default Missions;
diff --git a/src/views/mission/UploadMissionForm.tsx b/src/views/mission/UploadMissionForm.tsx
new file mode 100644
index 0000000..f5ea22f
--- /dev/null
+++ b/src/views/mission/UploadMissionForm.tsx
@@ -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([]);
+ const [tagsValue, setTagsValue] = useState("");
+ const [file, setFile] = useState(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) => {
+ if (e.target.files && e.target.files[0]) {
+ setFile(e.target.files[0]);
+ }
+ };
+
+ const handleTagsChange = (e: React.ChangeEvent) => {
+ setTagsValue(e.target.value);
+ const value = e.target.value;
+ const tagsArray = value.split(",").map(tag => tag.trim()).filter(tag => tag);
+ setTags(tagsArray);
+ };
+
+ return (
+
+ );
+};
+
+export default UploadMissionForm;
diff --git a/src/views/problem/codeeditor/CodeEditor.tsx b/src/views/mission/codeeditor/CodeEditor.tsx
similarity index 100%
rename from src/views/problem/codeeditor/CodeEditor.tsx
rename to src/views/mission/codeeditor/CodeEditor.tsx
diff --git a/src/views/problem/statement/Proble.tsx b/src/views/mission/statement/Mission.tsx
similarity index 99%
rename from src/views/problem/statement/Proble.tsx
rename to src/views/mission/statement/Mission.tsx
index e1a3ce6..e597ad3 100644
--- a/src/views/problem/statement/Proble.tsx
+++ b/src/views/mission/statement/Mission.tsx
@@ -10,7 +10,7 @@ declare global {
}
}
-export default function ProblemStatement() {
+export default function MissionStatement() {
const containerRef = useRef(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 = `
diff --git a/src/views/problem/statement/Statement.tsx b/src/views/mission/statement/Statement.tsx
similarity index 100%
rename from src/views/problem/statement/Statement.tsx
rename to src/views/mission/statement/Statement.tsx