+
+
+ } />
+ } />
+ }/>
+ } />
+
- {/*
{
- document.documentElement.setAttribute(
- "data-theme",
- state ? "dark" : "light"
- );
- }}
- />
-
- */}
);
}
diff --git a/src/assets/icons/groups/book.png b/src/assets/icons/groups/book.png
new file mode 100644
index 0000000..260af3b
Binary files /dev/null and b/src/assets/icons/groups/book.png differ
diff --git a/src/assets/icons/groups/chevron-down.svg b/src/assets/icons/groups/chevron-down.svg
new file mode 100644
index 0000000..cda1cc2
--- /dev/null
+++ b/src/assets/icons/groups/chevron-down.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/groups/edit.svg b/src/assets/icons/groups/edit.svg
new file mode 100644
index 0000000..abafac2
--- /dev/null
+++ b/src/assets/icons/groups/edit.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/groups/eye-closed.svg b/src/assets/icons/groups/eye-closed.svg
new file mode 100644
index 0000000..a3a57eb
--- /dev/null
+++ b/src/assets/icons/groups/eye-closed.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/groups/eye-open.png b/src/assets/icons/groups/eye-open.png
new file mode 100644
index 0000000..eee5690
Binary files /dev/null and b/src/assets/icons/groups/eye-open.png differ
diff --git a/src/assets/icons/groups/index.ts b/src/assets/icons/groups/index.ts
new file mode 100644
index 0000000..86bef2e
--- /dev/null
+++ b/src/assets/icons/groups/index.ts
@@ -0,0 +1,8 @@
+import Book from "./book.png"
+import EyeClosed from "./eye-closed.svg";
+import EyeOpen from "./eye-open.png";
+import Edit from "./edit.svg";
+import UserAdd from "./user-profile-add.svg";
+import ChevroneDown from "./chevron-down.svg"
+
+export {Book, Edit, EyeClosed, EyeOpen, UserAdd, ChevroneDown}
\ No newline at end of file
diff --git a/src/assets/icons/groups/user-profile-add.svg b/src/assets/icons/groups/user-profile-add.svg
new file mode 100644
index 0000000..20bbea5
--- /dev/null
+++ b/src/assets/icons/groups/user-profile-add.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/header/arrow-left-sm.svg b/src/assets/icons/header/arrow-left-sm.svg
new file mode 100644
index 0000000..a78e6db
--- /dev/null
+++ b/src/assets/icons/header/arrow-left-sm.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/header/chevron-left.svg b/src/assets/icons/header/chevron-left.svg
new file mode 100644
index 0000000..77ecb49
--- /dev/null
+++ b/src/assets/icons/header/chevron-left.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/header/chevron-right.svg b/src/assets/icons/header/chevron-right.svg
new file mode 100644
index 0000000..5df23ae
--- /dev/null
+++ b/src/assets/icons/header/chevron-right.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/header/index.ts b/src/assets/icons/header/index.ts
new file mode 100644
index 0000000..5ea2a4b
--- /dev/null
+++ b/src/assets/icons/header/index.ts
@@ -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}
\ No newline at end of file
diff --git a/src/assets/icons/missions/icon-error.svg b/src/assets/icons/missions/icon-error.svg
new file mode 100644
index 0000000..4d7e75d
--- /dev/null
+++ b/src/assets/icons/missions/icon-error.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/missions/icon-success.svg b/src/assets/icons/missions/icon-success.svg
new file mode 100644
index 0000000..343b6c1
--- /dev/null
+++ b/src/assets/icons/missions/icon-success.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/missions/index.ts b/src/assets/icons/missions/index.ts
new file mode 100644
index 0000000..30948a7
--- /dev/null
+++ b/src/assets/icons/missions/index.ts
@@ -0,0 +1,4 @@
+import IconSuccess from "./icon-success.svg"
+import IconError from "./icon-error.svg"
+
+export {IconError, IconSuccess}
\ No newline at end of file
diff --git a/src/axios.ts b/src/axios.ts
index 1dc691d..3da69b2 100644
--- a/src/axios.ts
+++ b/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;
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index cb7eb27..4058f4a 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -5,7 +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);
@@ -18,8 +23,8 @@ const Home = () => {
return (
-
-
+
+
@@ -27,10 +32,18 @@ const Home = () => {
} />
} />
} />
-
+
} />
+ } />
+ } />
+ } />
+ {name} {dispatch(logout())}}>выйти>} />
-
+ {
+
+ } />
+
+ }
);
};
diff --git a/src/pages/Mission.tsx b/src/pages/Mission.tsx
new file mode 100644
index 0000000..01814da
--- /dev/null
+++ b/src/pages/Mission.tsx
@@ -0,0 +1,188 @@
+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, useRef, useState } from 'react';
+import { useAppDispatch, useAppSelector } from '../redux/hooks';
+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 = () => {
+
+ const dispatch = useAppDispatch();
+
+ // Получаем параметры из URL
+ const { missionId } = useParams<{ missionId: string }>();
+ const mission = useAppSelector((state) => state.missions.currentMission);
+ const missionIdNumber = Number(missionId);
+ if (!missionId || isNaN(missionIdNumber)) {
+ return
;
+ }
+
+ const [code, setCode] = useState
("");
+ const [language, setLanguage] = useState("");
+
+ const pollingRef = useRef(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;
+ }
+ };
+ }, []);
+
+
+ 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 Загрузка...
;
+ }
+
+
+
+ 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 {
+ // 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: missionIdNumber,
+ legend: statementTexts.legend,
+ timeLimit: statementTexts.timeLimit,
+ output: statementTexts.output,
+ input: statementTexts.input,
+ sampleTests: statementTexts.sampleTests,
+ name: statementTexts.name,
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ { setCode(value); }}
+ onChangeLanguage={((value: string) => { setLanguage(value); })}
+ />
+
+
+
{
+ await dispatch(submitMission({
+ missionId: missionIdNumber,
+ language: language,
+ languageVersion: "latest",
+ sourceCode: code,
+ contestId: null,
+
+ })).unwrap();
+ dispatch(fetchMySubmitsByMission(missionIdNumber));
+ }} />
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Mission;
diff --git a/src/redux/slices/auth.ts b/src/redux/slices/auth.ts
index ba9e681..41a31ce 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;
}
@@ -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) => {
@@ -97,10 +116,12 @@ const authSlice = createSlice({
state.error = null;
});
builder.addCase(registerUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => {
- state.status = "succeeded";
- axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
+ state.status = "successful";
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) => {
state.status = "failed";
@@ -113,10 +134,12 @@ const authSlice = createSlice({
state.error = null;
});
builder.addCase(loginUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => {
- state.status = "succeeded";
- axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
+ state.status = "successful";
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) => {
state.status = "failed";
@@ -129,7 +152,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,13 +166,22 @@ 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) => {
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}`;
+ }
+ });
},
});
diff --git a/src/redux/slices/missions.ts b/src/redux/slices/missions.ts
new file mode 100644
index 0000000..ee1bcb8
--- /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[];
+}
+
+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/slices/submit.ts b/src/redux/slices/submit.ts
new file mode 100644
index 0000000..fbe3265
--- /dev/null
+++ b/src/redux/slices/submit.ts
@@ -0,0 +1,184 @@
+import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
+import axios from "../../axios";
+
+// Типы данных
+export interface Submit {
+ id?: number;
+ missionId: number;
+ language: string;
+ languageVersion: string;
+ sourceCode: string;
+ contestId: number | null;
+}
+
+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; // ✅ добавлено
+ currentSubmit?: Submit;
+ status: "idle" | "loading" | "successful" | "failed";
+ error: string | null;
+}
+
+// Начальное состояние
+const initialState: SubmitState = {
+ submits: [],
+ submitsById: {}, // ✅ инициализация
+ currentSubmit: undefined,
+ status: "idle",
+ error: null,
+};
+
+// AsyncThunk: Отправка решения
+export const submitMission = createAsyncThunk(
+ "submit/submitMission",
+ async (submitData: Submit, { rejectWithValue }) => {
+ try {
+ const response = await axios.post("/submits", submitData);
+ return response.data;
+ } catch (err: any) {
+ return rejectWithValue(err.response?.data?.message || "Submit failed");
+ }
+ }
+);
+
+// AsyncThunk: Получить все свои отправки
+export const fetchMySubmits = createAsyncThunk(
+ "submit/fetchMySubmits",
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await axios.get("/submits/my");
+ return response.data as Submit[];
+ } catch (err: any) {
+ return rejectWithValue(err.response?.data?.message || "Failed to fetch submits");
+ }
+ }
+);
+
+// AsyncThunk: Получить конкретную отправку по ID
+export const fetchSubmitById = createAsyncThunk(
+ "submit/fetchSubmitById",
+ async (id: number, { rejectWithValue }) => {
+ try {
+ const response = await axios.get(`/submits/${id}`);
+ return response.data as Submit;
+ } catch (err: any) {
+ return rejectWithValue(err.response?.data?.message || "Failed to fetch submit");
+ }
+ }
+);
+
+// ✅ AsyncThunk: Получить отправки для конкретной миссии (новая структура)
+export const fetchMySubmitsByMission = createAsyncThunk(
+ "submit/fetchMySubmitsByMission",
+ async (missionId: number, { rejectWithValue }) => {
+ try {
+ const response = await axios.get(`/submits/my/mission/${missionId}`);
+ return { missionId, data: response.data as MissionSubmit[] };
+ } catch (err: any) {
+ return rejectWithValue(err.response?.data?.message || "Failed to fetch mission submits");
+ }
+ }
+);
+
+// Slice
+const submitSlice = createSlice({
+ name: "submit",
+ initialState,
+ reducers: {
+ clearCurrentSubmit: (state) => {
+ state.currentSubmit = undefined;
+ state.status = "idle";
+ state.error = null;
+ },
+ clearSubmitsByMission: (state, action: PayloadAction) => {
+ delete state.submitsById[action.payload];
+ },
+ },
+ extraReducers: (builder) => {
+ // Отправка решения
+ builder.addCase(submitMission.pending, (state) => {
+ state.status = "loading";
+ state.error = null;
+ });
+ builder.addCase(submitMission.fulfilled, (state, action: PayloadAction) => {
+ state.status = "successful";
+ state.submits.push(action.payload);
+ });
+ builder.addCase(submitMission.rejected, (state, action: PayloadAction) => {
+ state.status = "failed";
+ state.error = action.payload;
+ });
+
+ // Получить все свои отправки
+ builder.addCase(fetchMySubmits.pending, (state) => {
+ state.status = "loading";
+ state.error = null;
+ });
+ builder.addCase(fetchMySubmits.fulfilled, (state, action: PayloadAction) => {
+ state.status = "successful";
+ state.submits = action.payload;
+ });
+ builder.addCase(fetchMySubmits.rejected, (state, action: PayloadAction) => {
+ state.status = "failed";
+ state.error = action.payload;
+ });
+
+ // Получить отправку по ID
+ builder.addCase(fetchSubmitById.pending, (state) => {
+ state.status = "loading";
+ state.error = null;
+ });
+ builder.addCase(fetchSubmitById.fulfilled, (state, action: PayloadAction) => {
+ state.status = "successful";
+ state.currentSubmit = action.payload;
+ });
+ builder.addCase(fetchSubmitById.rejected, (state, action: PayloadAction) => {
+ state.status = "failed";
+ state.error = action.payload;
+ });
+
+ // ✅ Получить отправки по миссии
+ builder.addCase(fetchMySubmitsByMission.pending, (state) => {
+ state.status = "loading";
+ state.error = null;
+ });
+ 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) => {
+ state.status = "failed";
+ state.error = action.payload;
+ });
+ },
+});
+
+export const { clearCurrentSubmit, clearSubmitsByMission } = submitSlice.actions;
+export const submitReducer = submitSlice.reducer;
diff --git a/src/redux/store.ts b/src/redux/store.ts
index 509071b..6ea89a8 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -1,6 +1,8 @@
import { configureStore } from "@reduxjs/toolkit";
import { authReducer } from "./slices/auth";
import { storeReducer } from "./slices/store";
+import { missionsReducer } from "./slices/missions";
+import { submitReducer } from "./slices/submit";
// использование
@@ -17,6 +19,8 @@ export const store = configureStore({
//user: userReducer,
auth: authReducer,
store: storeReducer,
+ missions: missionsReducer,
+ submin: submitReducer,
},
});
diff --git a/src/styles/index.css b/src/styles/index.css
index 6356eee..2063dc2 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -2,11 +2,14 @@
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
+@import "./latex-container.css";
+
* {
-webkit-tap-highlight-color: transparent; /* Отключаем выделение синим при тапе на телефоне*/
/* outline: 1px solid green; */
}
+
:root {
color-scheme: light dark;
width: 100%;
@@ -19,12 +22,13 @@
/* font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; */
font-weight: 400;
line-height: 1.5;
+ background-color: var(--color-liquid-background);
color: rgba(255, 255, 255, 0.87);
}
#root {
width: 100%;
- height: 100%;
+ height: 100vh;
}
body {
@@ -37,7 +41,6 @@ body {
}
-
/* Общий контейнер полосы прокрутки */
.thin-scrollbar::-webkit-scrollbar {
width: 4px; /* ширина вертикального */
@@ -73,3 +76,42 @@ body {
cursor: pointer;
}
+
+
+/* Общий контейнер полосы прокрутки */
+.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;
+ padding-left: 8px;
+}
+html::-webkit-scrollbar {
+ width: 8px; /* ширина вертикального */
+}
+/* Трек (фон) */
+html::-webkit-scrollbar-track {
+ background: transparent;
+}
+/* Ползунок (thumb) */
+html::-webkit-scrollbar-thumb {
+ background-color: var(--color-liquid-lighter);
+ border-radius: 1000px;
+ cursor: pointer;
+}
\ No newline at end of file
diff --git a/src/styles/latex-container.css b/src/styles/latex-container.css
new file mode 100644
index 0000000..600b047
--- /dev/null
+++ b/src/styles/latex-container.css
@@ -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;
+}
+
diff --git a/src/views/home/articles/ArticleItem.tsx b/src/views/home/articles/ArticleItem.tsx
new file mode 100644
index 0000000..9676fab
--- /dev/null
+++ b/src/views/home/articles/ArticleItem.tsx
@@ -0,0 +1,41 @@
+import { cn } from "../../../lib/cn";
+
+export interface ArticleItemProps {
+ id: number;
+ name: string;
+ tags: string[];
+}
+
+const ArticleItem: React.FC = ({
+ id, name, tags
+}) => {
+ return (
+
+
+
+
+ #{id}
+
+
+ {name}
+
+
+
+ {tags.map((v, i) =>
+
+ {v}
+
+ )}
+
+
+
+ );
+};
+
+export default ArticleItem;
diff --git a/src/views/home/articles/Articles.tsx b/src/views/home/articles/Articles.tsx
new file mode 100644
index 0000000..de5f1d4
--- /dev/null
+++ b/src/views/home/articles/Articles.tsx
@@ -0,0 +1,171 @@
+import { useEffect } from "react";
+import { SecondaryButton } from "../../../components/button/SecondaryButton";
+import { useAppDispatch } from "../../../redux/hooks";
+import ArticleItem from "./ArticleItem";
+import { setMenuActivePage } from "../../../redux/slices/store";
+
+
+export interface Article {
+ id: number;
+ name: string;
+ tags: string[];
+}
+
+
+const Articles = () => {
+
+ const dispatch = useAppDispatch();
+
+ const articles: Article[] = [
+ {
+ "id": 1,
+ "name": "Todo List App",
+ "tags": ["Sertificated", "state", "list"],
+ },
+ {
+ "id": 2,
+ "name": "Search Filter Component",
+ "tags": ["filter", "props", "hooks"],
+ },
+ {
+ "id": 3,
+ "name": "User Card List",
+ "tags": ["components", "props", "array"],
+ },
+ {
+ "id": 4,
+ "name": "Theme Switcher",
+ "tags": ["Sertificated", "theme", "hooks"],
+ },
+ {
+ "id": 2,
+ "name": "Search Filter Component",
+ "tags": ["filter", "props", "hooks"],
+ },
+ {
+ "id": 3,
+ "name": "User Card List",
+ "tags": ["components", "props", "array"],
+ },
+ {
+ "id": 4,
+ "name": "Theme Switcher",
+ "tags": ["Sertificated", "theme", "hooks"],
+ },
+ {
+ "id": 2,
+ "name": "Search Filter Component",
+ "tags": ["filter", "props", "hooks"],
+ },
+ {
+ "id": 3,
+ "name": "User Card List",
+ "tags": ["components", "props", "array"],
+ },
+ {
+ "id": 4,
+ "name": "Theme Switcher",
+ "tags": ["Sertificated", "theme", "hooks"],
+ },
+ {
+ "id": 2,
+ "name": "Search Filter Component",
+ "tags": ["filter", "props", "hooks"],
+ },
+ {
+ "id": 3,
+ "name": "User Card List",
+ "tags": ["components", "props", "array"],
+ },
+ {
+ "id": 4,
+ "name": "Theme Switcher",
+ "tags": ["Sertificated", "theme", "hooks"],
+ },
+ {
+ "id": 2,
+ "name": "Search Filter Component",
+ "tags": ["filter", "props", "hooks"],
+ },
+ {
+ "id": 3,
+ "name": "User Card List",
+ "tags": ["components", "props", "array"],
+ },
+ {
+ "id": 4,
+ "name": "Theme Switcher",
+ "tags": ["Sertificated", "theme", "hooks"],
+ },
+ {
+ "id": 2,
+ "name": "Search Filter Component",
+ "tags": ["filter", "props", "hooks"],
+ },
+ {
+ "id": 3,
+ "name": "User Card List",
+ "tags": ["components", "props", "array"],
+ },
+ {
+ "id": 4,
+ "name": "Theme Switcher",
+ "tags": ["Sertificated", "theme", "hooks"],
+ },
+ {
+ "id": 2,
+ "name": "Search Filter Component",
+ "tags": ["filter", "props", "hooks"],
+ },
+ {
+ "id": 3,
+ "name": "User Card List",
+ "tags": ["components", "props", "array"],
+ },
+ {
+ "id": 4,
+ "name": "Theme Switcher",
+ "tags": ["Sertificated", "theme", "hooks"],
+ }
+ ];
+
+ useEffect(() => {
+ dispatch(setMenuActivePage("articles"))
+ }, []);
+
+ return (
+
+
+
+
+
+ Статьи
+
+
{ }}
+ text="Создать статью"
+ className="absolute right-0"
+ />
+
+
+
+
+
+
+
+
+ {articles.map((v, i) => (
+
+ ))}
+
+
+
+
+ pages
+
+
+
+ );
+};
+
+export default Articles;
diff --git a/src/views/home/auth/Login.tsx b/src/views/home/auth/Login.tsx
index 6013949..e1d5432 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,14 +18,17 @@ 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(() => {
dispatch(setMenuActivePage("account"))
+ }, []);
+
+ useEffect(() => {
if (jwt) {
navigate("/home/offices"); // или другая страница после входа
}
@@ -33,7 +36,6 @@ const Login = () => {
const handleLogin = () => {
// setErr(err == "" ? "Неверная почта и/или пароль" : "");
- // console.log(123);
setSubmitClicked(true);
if (!username || !password) return;
@@ -58,8 +60,8 @@ const Login = () => {
-
{setUsername(v)}} placeholder="login"/>
-
{setPassword(v)}} placeholder="abCD1234" />
+
{ setUsername(v) }} placeholder="login" />
+
{ setPassword(v) }} placeholder="abCD1234" />
{
/>
{}}
+ onClick={() => { }}
>

diff --git a/src/views/home/auth/Register.tsx b/src/views/home/auth/Register.tsx
index 7aae6c7..4f39ef7 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,15 @@ 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(() => {
+ dispatch(setMenuActivePage("account"))
+ }, []);
+
useEffect(() => {
- dispatch(setMenuActivePage("account"));
if (jwt) {
navigate("/home");
}
@@ -66,7 +70,7 @@ const Register = () => {
{ console.log(value) }}
+ onChange={(value: boolean) => { value; }}
className="p-0 w-fit m-[2.75px]"
size="md"
color="secondary"
diff --git a/src/views/home/contests/ContestItem.tsx b/src/views/home/contests/ContestItem.tsx
new file mode 100644
index 0000000..5451ae7
--- /dev/null
+++ b/src/views/home/contests/ContestItem.tsx
@@ -0,0 +1,72 @@
+import { cn } from "../../../lib/cn";
+
+export interface ContestItemProps {
+ id: number;
+ name: string;
+ authors: string[];
+ startAt: string;
+ registerAt: string;
+ duration: number;
+ members: number;
+ statusRegister: "reg" | "nonreg";
+ type: "first" | "second";
+}
+
+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 ContestItem: React.FC = ({
+ id, name, authors, startAt, registerAt, duration, members, statusRegister, type
+}) => {
+ const now = new Date();
+
+ const waitTime = new Date(startAt).getTime() - now.getTime();
+
+ return (
+
+
+ {name}
+
+
+ {authors.map((v, i) =>
{v}
)}
+
+
+ {formatDate(startAt)}
+
+
+ {duration}
+
+ {
+ waitTime > 0 &&
+
+ {waitTime}
+
+ }
+
+ {members}
+
+
+ {statusRegister}
+
+
+
+ );
+};
+
+export default ContestItem;
diff --git a/src/views/home/contests/Contests.tsx b/src/views/home/contests/Contests.tsx
new file mode 100644
index 0000000..2d68158
--- /dev/null
+++ b/src/views/home/contests/Contests.tsx
@@ -0,0 +1,131 @@
+import { useEffect } from "react";
+import { SecondaryButton } from "../../../components/button/SecondaryButton";
+import { cn } from "../../../lib/cn";
+import { useAppDispatch } from "../../../redux/hooks";
+import ContestsBlock from "./ContestsBlock";
+import { setMenuActivePage } from "../../../redux/slices/store";
+
+
+interface Contest {
+ id: number;
+ name: string;
+ authors: string[];
+ startAt: string;
+ registerAt: string;
+ duration: number;
+ members: number;
+ statusRegister: "reg" | "nonreg";
+}
+
+
+
+const Contests = () => {
+
+ const dispatch = useAppDispatch();
+ const now = new Date();
+ const contests: Contest[] = [
+ // === Прошедшие контесты ===
+ {
+ id: 1,
+ name: "Code Marathon 2025",
+ authors: ["tourist", "Petr", "Semen", "Rotar"],
+ startAt: "2025-09-15T10:00:00.000Z",
+ registerAt: "2025-09-10T10:00:00.000Z",
+ duration: 180,
+ members: 4821,
+ statusRegister: "reg",
+ },
+ {
+ id: 2,
+ name: "Autumn Cup 2025",
+ authors: ["awoo", "Benq"],
+ startAt: "2025-09-25T17:00:00.000Z",
+ registerAt: "2025-09-20T17:00:00.000Z",
+ duration: 150,
+ members: 3670,
+ statusRegister: "nonreg",
+ },
+
+ // === Контесты, которые сейчас идут ===
+ {
+ id: 3,
+ name: "Halloween Challenge",
+ authors: ["Errichto", "Radewoosh"],
+ startAt: "2025-10-29T10:00:00.000Z", // начался сегодня
+ registerAt: "2025-10-25T10:00:00.000Z",
+ duration: 240,
+ members: 5123,
+ statusRegister: "reg",
+ },
+ {
+ id: 4,
+ name: "October Blitz",
+ authors: ["neal", "Um_nik"],
+ startAt: "2025-10-29T12:00:00.000Z",
+ registerAt: "2025-10-24T12:00:00.000Z",
+ duration: 300,
+ members: 2890,
+ statusRegister: "nonreg",
+ },
+
+ // === Контесты, которые еще не начались ===
+ {
+ id: 5,
+ name: "Winter Warmup",
+ authors: ["tourist", "rng_58"],
+ startAt: "2025-11-05T18:00:00.000Z",
+ registerAt: "2025-11-01T18:00:00.000Z",
+ duration: 180,
+ members: 2100,
+ statusRegister: "reg",
+ },
+ {
+ id: 6,
+ name: "Global Coding Cup",
+ authors: ["maroonrk", "kostka"],
+ startAt: "2025-11-12T15:00:00.000Z",
+ registerAt: "2025-11-08T15:00:00.000Z",
+ duration: 240,
+ members: 1520,
+ statusRegister: "nonreg",
+ },
+ ];
+
+ useEffect(() => {
+ dispatch(setMenuActivePage("contests"))
+ }, []);
+
+ return (
+
+
+
+
+
+ Контесты
+
+
{ }}
+ text="Создать группу"
+ className="absolute right-0"
+ />
+
+
+
+
+
+
+
+
{
+ const endTime = new Date(contest.startAt).getTime() + contest.duration * 60 * 1000;
+ return endTime >= now.getTime();
+ })} />
+ {
+ const endTime = new Date(contest.startAt).getTime() + contest.duration * 60 * 1000;
+ return endTime < now.getTime();
+ })} />
+
+
+ );
+};
+
+export default Contests;
diff --git a/src/views/home/contests/ContestsBlock.tsx b/src/views/home/contests/ContestsBlock.tsx
new file mode 100644
index 0000000..ec14e55
--- /dev/null
+++ b/src/views/home/contests/ContestsBlock.tsx
@@ -0,0 +1,63 @@
+import { useState, FC } from "react";
+import { cn } from "../../../lib/cn";
+import { ChevroneDown } from "../../../assets/icons/groups";
+import ContestItem from "./ContestItem";
+
+
+interface Contest {
+ id: number;
+ name: string;
+ authors: string[];
+ startAt: string;
+ registerAt: string;
+ duration: number;
+ members: number;
+ statusRegister: "reg" | "nonreg";
+}
+
+interface GroupsBlockProps {
+ contests: Contest[];
+ title: string;
+ className?: string;
+}
+
+
+const GroupsBlock: FC = ({ contests, title, className }) => {
+
+
+ const [active, setActive] = useState(title != "Скрытые");
+
+
+ return (
+
+
+
{
+ setActive(!active)
+ }}>
+
{title}
+

+
+
+
+
+ {
+ contests.map((v, i) => )
+ }
+
+
+
+
+
+ );
+};
+
+export default GroupsBlock;
diff --git a/src/views/home/groups/GroupItem.tsx b/src/views/home/groups/GroupItem.tsx
new file mode 100644
index 0000000..d4821d4
--- /dev/null
+++ b/src/views/home/groups/GroupItem.tsx
@@ -0,0 +1,58 @@
+import { cn } from "../../../lib/cn";
+import { Book, UserAdd, Edit, EyeClosed, EyeOpen } from "../../../assets/icons/groups";
+
+export interface GroupItemProps {
+ id: number;
+ role: "menager" | "member" | "owner" | "viewer";
+ visible: boolean;
+ name: string;
+}
+
+
+interface IconComponentProps {
+ src: string;
+}
+
+const IconComponent: React.FC = ({
+ src
+}) => {
+
+ return
+}
+
+const GroupItem: React.FC = ({
+ id, name, visible, role
+}) => {
+ return (
+
+
+

+
+
+ {name}
+
+
+ {
+ (role == "menager" || role == "owner") &&
+ }
+ {
+ (role == "menager" || role == "owner") &&
+ }
+ {
+ visible == false &&
+ }
+ {
+ visible == true &&
+ }
+
+
+
+
+ );
+};
+
+export default GroupItem;
diff --git a/src/views/home/groups/Groups.tsx b/src/views/home/groups/Groups.tsx
new file mode 100644
index 0000000..34559f6
--- /dev/null
+++ b/src/views/home/groups/Groups.tsx
@@ -0,0 +1,71 @@
+import { useEffect } from "react";
+import { SecondaryButton } from "../../../components/button/SecondaryButton";
+import { cn } from "../../../lib/cn";
+import { useAppDispatch } from "../../../redux/hooks";
+import GroupsBlock from "./GroupsBlock";
+import { setMenuActivePage } from "../../../redux/slices/store";
+
+
+export interface Group {
+ id: number;
+ role: "menager" | "member" | "owner" | "viewer";
+ visible: boolean;
+ name: string;
+}
+
+
+const Groups = () => {
+
+ const dispatch = useAppDispatch();
+
+ const groups: Group[] = [
+ { id: 1, role: "owner", name: "Main Administration", visible: true },
+ { id: 2, role: "menager", name: "Project Managers", visible: true },
+ { id: 3, role: "member", name: "Developers", visible: true },
+ { id: 4, role: "viewer", name: "QA Viewers", visible: true },
+ { id: 5, role: "member", name: "Design Team", visible: true },
+ { id: 6, role: "owner", name: "Executive Board", visible: true },
+ { id: 7, role: "menager", name: "HR Managers", visible: true },
+ { id: 8, role: "viewer", name: "Marketing Reviewers", visible: false },
+ { id: 9, role: "member", name: "Content Creators", visible: false },
+ { id: 10, role: "menager", name: "Support Managers", visible: true },
+ { id: 11, role: "viewer", name: "External Auditors", visible: false },
+ { id: 12, role: "member", name: "Frontend Developers", visible: true },
+ { id: 13, role: "member", name: "Backend Developers", visible: true },
+ { id: 14, role: "viewer", name: "Guest Access", visible: false },
+ { id: 15, role: "menager", name: "Operations", visible: true },
+ ];
+
+ useEffect(() => {
+ dispatch(setMenuActivePage("groups"))
+ }, []);
+
+ return (
+
+
+
+
+
+ Группы
+
+
{ }}
+ text="Создать группу"
+ className="absolute right-0"
+ />
+
+
+
+
+
+
+
+
v.visible && (v.role == "owner" || v.role == "menager"))} />
+ v.visible && (v.role == "member" || v.role == "viewer"))} />
+ v.visible == false)} />
+
+
+ );
+};
+
+export default Groups;
diff --git a/src/views/home/groups/GroupsBlock.tsx b/src/views/home/groups/GroupsBlock.tsx
new file mode 100644
index 0000000..05e7e76
--- /dev/null
+++ b/src/views/home/groups/GroupsBlock.tsx
@@ -0,0 +1,59 @@
+import { useState, FC } from "react";
+import GroupItem from "./GroupItem";
+import { cn } from "../../../lib/cn";
+import { ChevroneDown } from "../../../assets/icons/groups";
+
+
+export interface Group {
+ id: number;
+ role: "menager" | "member" | "owner" | "viewer";
+ visible: boolean;
+ name: string;
+}
+
+interface GroupsBlockProps {
+ groups: Group[];
+ title: string;
+ className?: string;
+}
+
+
+const GroupsBlock: FC = ({ groups, title, className }) => {
+
+
+ const [active, setActive] = useState(title != "Скрытые");
+
+
+ return (
+
+
+
{
+ setActive(!active)
+ }}>
+
{title}
+

+
+
+
+
+
+ {
+ groups.map((v, i) => )
+ }
+
+
+
+
+ );
+};
+
+export default GroupsBlock;
diff --git a/src/views/home/menu/Menu.tsx b/src/views/home/menu/Menu.tsx
index 0e505a1..0e5cab3 100644
--- a/src/views/home/menu/Menu.tsx
+++ b/src/views/home/menu/Menu.tsx
@@ -6,17 +6,17 @@ import { useAppSelector } from "../../../redux/hooks";
const Menu = () => {
const menuItems = [
{text: "Главная", href: "/home", icon: Home, page: "home" },
- {text: "Задачи", href: "/home", icon: Clipboard, page: "clipboard" },
- {text: "Статьи", href: "/home", icon: Openbook, page: "openbool" },
- {text: "Группы", href: "/home", icon: Users, page: "users" },
- {text: "Контесты", href: "/home", icon: Cup, page: "cup" },
+ {text: "Задачи", href: "/home/missions", icon: Clipboard, page: "missions" },
+ {text: "Статьи", href: "/home/articles", icon: Openbook, page: "articles" },
+ {text: "Группы", href: "/home/groups", icon: Users, page: "groups" },
+ {text: "Контесты", href: "/home/contests", icon: Cup, page: "contests" },
{text: "Аккаунт", href: "/home/account", icon: Account, page: "account" },
];
const activePage = useAppSelector((state) => state.store.menu.activePage);
return (
-
-

+
+
{menuItems.map((v, i) => (