upload mission modal
This commit is contained in:
19
src/App.tsx
19
src/App.tsx
@@ -1,14 +1,12 @@
|
|||||||
import { Route, Routes } from "react-router-dom";
|
import { Route, Routes } from 'react-router-dom';
|
||||||
// import { PrimaryButton } from "./components/button/PrimaryButton";
|
// import { PrimaryButton } from "./components/button/PrimaryButton";
|
||||||
// import { SecondaryButton } from "./components/button/SecondaryButton";
|
// import { SecondaryButton } from "./components/button/SecondaryButton";
|
||||||
// import { Checkbox } from "./components/checkbox/Checkbox";
|
// import { Checkbox } from "./components/checkbox/Checkbox";
|
||||||
// import { Input } from "./components/input/Input";
|
// import { Input } from "./components/input/Input";
|
||||||
// import { Switch } from "./components/switch/Switch";
|
// import { Switch } from "./components/switch/Switch";
|
||||||
import Home from "./pages/Home";
|
import Home from './pages/Home';
|
||||||
import Mission from "./pages/Mission";
|
import Mission from './pages/Mission';
|
||||||
import UploadMissionForm from "./views/mission/UploadMissionForm";
|
import ArticleEditor from './pages/ArticleEditor';
|
||||||
import MarkdownEditor from "./views/articleeditor/Editor";
|
|
||||||
import ArticleEditor from "./pages/ArticleEditor";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -17,11 +15,12 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/home/*" element={<Home />} />
|
<Route path="/home/*" element={<Home />} />
|
||||||
<Route path="/mission/:missionId" element={<Mission />} />
|
<Route path="/mission/:missionId" element={<Mission />} />
|
||||||
<Route path="/article/create/*" element={<ArticleEditor />} />
|
<Route
|
||||||
<Route path="/upload" element={<UploadMissionForm/>}/>
|
path="/article/create/*"
|
||||||
<Route path="*" element={<MarkdownEditor onChange={(value: string) => {console.log(value)}}/>} />
|
element={<ArticleEditor />}
|
||||||
|
/>
|
||||||
|
<Route path="*" element={<Home />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { eyeClosed, eyeOpen } from "../../assets/icons/input";
|
|||||||
|
|
||||||
interface inputProps {
|
interface inputProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
type: "text" | "email" | "password" | "first_name";
|
type: "text" | "email" | "password" | "first_name" | "number";
|
||||||
error?: string;
|
error?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import axios from "../../axios";
|
import axios from '../../axios';
|
||||||
|
|
||||||
|
// ─── Типы ────────────────────────────────────────────
|
||||||
|
|
||||||
|
type Status = 'idle' | 'loading' | 'successful' | 'failed';
|
||||||
|
|
||||||
// Типы данных
|
|
||||||
interface Statement {
|
interface Statement {
|
||||||
id: number;
|
id: number;
|
||||||
language: string;
|
language: string;
|
||||||
@@ -9,7 +12,7 @@ interface Statement {
|
|||||||
mediaFiles?: { id: number; fileName: string; mediaUrl: string }[];
|
mediaFiles?: { id: number; fileName: string; mediaUrl: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Mission {
|
export interface Mission {
|
||||||
id: number;
|
id: number;
|
||||||
authorId: number;
|
authorId: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -24,123 +27,189 @@ interface MissionsState {
|
|||||||
missions: Mission[];
|
missions: Mission[];
|
||||||
currentMission: Mission | null;
|
currentMission: Mission | null;
|
||||||
hasNextPage: boolean;
|
hasNextPage: boolean;
|
||||||
status: "idle" | "loading" | "successful" | "failed";
|
statuses: {
|
||||||
|
fetchList: Status;
|
||||||
|
fetchById: Status;
|
||||||
|
upload: Status;
|
||||||
|
};
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализация состояния
|
// ─── Инициализация состояния ──────────────────────────
|
||||||
|
|
||||||
const initialState: MissionsState = {
|
const initialState: MissionsState = {
|
||||||
missions: [],
|
missions: [],
|
||||||
currentMission: null,
|
currentMission: null,
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
status: "idle",
|
statuses: {
|
||||||
|
fetchList: 'idle',
|
||||||
|
fetchById: 'idle',
|
||||||
|
upload: 'idle',
|
||||||
|
},
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// AsyncThunk: Получение списка миссий
|
// ─── Async Thunks ─────────────────────────────────────
|
||||||
|
|
||||||
|
// GET /missions
|
||||||
export const fetchMissions = createAsyncThunk(
|
export const fetchMissions = createAsyncThunk(
|
||||||
"missions/fetchMissions",
|
'missions/fetchMissions',
|
||||||
async (
|
async (
|
||||||
{ page = 0, pageSize = 10, tags = [] }: { page?: number; pageSize?: number; tags?: string[] },
|
{
|
||||||
{ rejectWithValue }
|
page = 0,
|
||||||
|
pageSize = 10,
|
||||||
|
tags = [],
|
||||||
|
}: { page?: number; pageSize?: number; tags?: string[] },
|
||||||
|
{ rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const params: any = { page, pageSize };
|
const params: any = { page, pageSize };
|
||||||
if (tags) params.tags = tags;
|
if (tags.length) params.tags = tags;
|
||||||
const response = await axios.get("/missions", { params });
|
const response = await axios.get('/missions', { params });
|
||||||
return response.data; // { hasNextPage, missions }
|
return response.data; // { missions, hasNextPage }
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch missions");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Ошибка при получении миссий',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// AsyncThunk: Получение миссии по id
|
// GET /missions/{id}
|
||||||
export const fetchMissionById = createAsyncThunk(
|
export const fetchMissionById = createAsyncThunk(
|
||||||
"missions/fetchMissionById",
|
'missions/fetchMissionById',
|
||||||
async (id: number, { rejectWithValue }) => {
|
async (id: number, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/missions/${id}`);
|
const response = await axios.get(`/missions/${id}`);
|
||||||
return response.data; // Mission
|
return response.data; // Mission
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch mission");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Ошибка при получении миссии',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// AsyncThunk: Загрузка миссии
|
// POST /missions/upload
|
||||||
export const uploadMission = createAsyncThunk(
|
export const uploadMission = createAsyncThunk(
|
||||||
"missions/uploadMission",
|
'missions/uploadMission',
|
||||||
async (
|
async (
|
||||||
{ file, name, difficulty, tags }: { file: File; name: string; difficulty: number; tags: string[] },
|
{
|
||||||
{ rejectWithValue }
|
file,
|
||||||
|
name,
|
||||||
|
difficulty,
|
||||||
|
tags,
|
||||||
|
}: { file: File; name: string; difficulty: number; tags: string[] },
|
||||||
|
{ rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("MissionFile", file);
|
formData.append('MissionFile', file);
|
||||||
formData.append("Name", name);
|
formData.append('Name', name);
|
||||||
formData.append("Difficulty", difficulty.toString());
|
formData.append('Difficulty', difficulty.toString());
|
||||||
tags.forEach(tag => formData.append("Tags", tag));
|
tags.forEach((tag) => formData.append('Tags', tag));
|
||||||
|
|
||||||
const response = await axios.post("/missions/upload", formData, {
|
const response = await axios.post('/missions/upload', formData, {
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
});
|
});
|
||||||
return response.data; // Mission
|
return response.data; // Mission
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Failed to upload mission");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Ошибка при загрузке миссии',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Slice
|
// ─── Slice ────────────────────────────────────────────
|
||||||
|
|
||||||
const missionsSlice = createSlice({
|
const missionsSlice = createSlice({
|
||||||
name: "missions",
|
name: 'missions',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {},
|
reducers: {
|
||||||
|
clearCurrentMission: (state) => {
|
||||||
|
state.currentMission = null;
|
||||||
|
},
|
||||||
|
setMissionsStatus: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
key: keyof MissionsState['statuses'];
|
||||||
|
status: Status;
|
||||||
|
}>,
|
||||||
|
) => {
|
||||||
|
const { key, status } = action.payload;
|
||||||
|
state.statuses[key] = status;
|
||||||
|
},
|
||||||
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
// fetchMissions
|
// ─── FETCH MISSIONS ───
|
||||||
builder.addCase(fetchMissions.pending, (state) => {
|
builder.addCase(fetchMissions.pending, (state) => {
|
||||||
state.status = "loading";
|
state.statuses.fetchList = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(fetchMissions.fulfilled, (state, action: PayloadAction<{ missions: Mission[]; hasNextPage: boolean }>) => {
|
builder.addCase(
|
||||||
state.status = "successful";
|
fetchMissions.fulfilled,
|
||||||
|
(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
missions: Mission[];
|
||||||
|
hasNextPage: boolean;
|
||||||
|
}>,
|
||||||
|
) => {
|
||||||
|
state.statuses.fetchList = 'successful';
|
||||||
state.missions = action.payload.missions;
|
state.missions = action.payload.missions;
|
||||||
state.hasNextPage = action.payload.hasNextPage;
|
state.hasNextPage = action.payload.hasNextPage;
|
||||||
});
|
},
|
||||||
builder.addCase(fetchMissions.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.status = "failed";
|
builder.addCase(
|
||||||
|
fetchMissions.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.statuses.fetchList = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// fetchMissionById
|
// ─── FETCH MISSION BY ID ───
|
||||||
builder.addCase(fetchMissionById.pending, (state) => {
|
builder.addCase(fetchMissionById.pending, (state) => {
|
||||||
state.status = "loading";
|
state.statuses.fetchById = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(fetchMissionById.fulfilled, (state, action: PayloadAction<Mission>) => {
|
builder.addCase(
|
||||||
state.status = "successful";
|
fetchMissionById.fulfilled,
|
||||||
|
(state, action: PayloadAction<Mission>) => {
|
||||||
|
state.statuses.fetchById = 'successful';
|
||||||
state.currentMission = action.payload;
|
state.currentMission = action.payload;
|
||||||
});
|
},
|
||||||
builder.addCase(fetchMissionById.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.status = "failed";
|
builder.addCase(
|
||||||
|
fetchMissionById.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.statuses.fetchById = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// uploadMission
|
// ─── UPLOAD MISSION ───
|
||||||
builder.addCase(uploadMission.pending, (state) => {
|
builder.addCase(uploadMission.pending, (state) => {
|
||||||
state.status = "loading";
|
state.statuses.upload = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(uploadMission.fulfilled, (state, action: PayloadAction<Mission>) => {
|
builder.addCase(
|
||||||
state.status = "successful";
|
uploadMission.fulfilled,
|
||||||
state.missions.unshift(action.payload); // Добавляем новую миссию в начало списка
|
(state, action: PayloadAction<Mission>) => {
|
||||||
});
|
state.statuses.upload = 'successful';
|
||||||
builder.addCase(uploadMission.rejected, (state, action: PayloadAction<any>) => {
|
state.missions.unshift(action.payload);
|
||||||
state.status = "failed";
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(
|
||||||
|
uploadMission.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.statuses.upload = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const { clearCurrentMission, setMissionsStatus } = missionsSlice.actions;
|
||||||
export const missionsReducer = missionsSlice.reducer;
|
export const missionsReducer = missionsSlice.reducer;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import MissionItem from "./MissionItem";
|
import MissionItem from "./MissionItem";
|
||||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { fetchMissions } from "../../../redux/slices/missions";
|
import { fetchMissions } from "../../../redux/slices/missions";
|
||||||
|
import ModalCreate from "./ModalCreate";
|
||||||
|
|
||||||
|
|
||||||
export interface Mission {
|
export interface Mission {
|
||||||
@@ -22,7 +23,7 @@ export interface Mission {
|
|||||||
const Missions = () => {
|
const Missions = () => {
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const naivgate = useNavigate();
|
const [modalActive, setModalActive] = useState<boolean>(false);
|
||||||
|
|
||||||
const missions = useAppSelector((state) => state.missions.missions);
|
const missions = useAppSelector((state) => state.missions.missions);
|
||||||
|
|
||||||
@@ -41,8 +42,8 @@ const Missions = () => {
|
|||||||
Задачи
|
Задачи
|
||||||
</div>
|
</div>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
onClick={() => {naivgate("/upload")}}
|
onClick={() => {setModalActive(true)}}
|
||||||
text="Создать задачу"
|
text="Добавить задачу"
|
||||||
className="absolute right-0"
|
className="absolute right-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,6 +76,8 @@ const Missions = () => {
|
|||||||
pages
|
pages
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ModalCreate setActive={setModalActive} active={modalActive} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
160
src/views/home/missions/ModalCreate.tsx
Normal file
160
src/views/home/missions/ModalCreate.tsx
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import { FC, useEffect, useState } from 'react';
|
||||||
|
import { Modal } from '../../../components/modal/Modal';
|
||||||
|
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||||
|
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||||
|
import { Input } from '../../../components/input/Input';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
|
import {
|
||||||
|
setMissionsStatus,
|
||||||
|
uploadMission,
|
||||||
|
} from '../../../redux/slices/missions';
|
||||||
|
|
||||||
|
interface ModalCreateProps {
|
||||||
|
active: boolean;
|
||||||
|
setActive: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
||||||
|
const [name, setName] = useState<string>('');
|
||||||
|
const [difficulty, setDifficulty] = useState<number>(1);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [tagInput, setTagInput] = useState<string>('');
|
||||||
|
const [tags, setTags] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const status = useAppSelector((state) => state.missions.statuses.upload);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const addTag = () => {
|
||||||
|
const newTag = tagInput.trim();
|
||||||
|
if (newTag && !tags.includes(newTag)) {
|
||||||
|
setTags([...tags, newTag]);
|
||||||
|
setTagInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTag = (tagToRemove: string) => {
|
||||||
|
setTags(tags.filter((tag) => tag !== tagToRemove));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files && e.target.files[0]) {
|
||||||
|
setFile(e.target.files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!file) return alert('Выберите файл миссии!');
|
||||||
|
dispatch(uploadMission({ file, name, difficulty, tags }));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === 'successful') {
|
||||||
|
alert('Миссия успешно загружена!');
|
||||||
|
setName('');
|
||||||
|
setDifficulty(1);
|
||||||
|
setTags([]);
|
||||||
|
setFile(null);
|
||||||
|
dispatch(setMissionsStatus({ key: 'upload', status: 'idle' }));
|
||||||
|
setActive(false);
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
|
||||||
|
onOpenChange={setActive}
|
||||||
|
open={active}
|
||||||
|
backdrop="blur"
|
||||||
|
>
|
||||||
|
<div className="w-[500px]">
|
||||||
|
<div className="font-bold text-[30px]">Добавить задачу</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
name="name"
|
||||||
|
autocomplete="name"
|
||||||
|
className="mt-[10px]"
|
||||||
|
type="text"
|
||||||
|
label="Название"
|
||||||
|
defaultState={name}
|
||||||
|
onChange={setName}
|
||||||
|
placeholder="В яблочко"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
name="difficulty"
|
||||||
|
autocomplete="difficulty"
|
||||||
|
className="mt-[10px]"
|
||||||
|
type="number"
|
||||||
|
label="Сложность"
|
||||||
|
defaultState={'' + difficulty}
|
||||||
|
onChange={(v) => setDifficulty(Number(v))}
|
||||||
|
placeholder="1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<label className="block mb-2">Файл задачи</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept=".zip"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Теги */}
|
||||||
|
<div className="mb-[50px] max-w-[600px]">
|
||||||
|
<div className="grid grid-cols-[1fr,140px] items-end gap-2">
|
||||||
|
<Input
|
||||||
|
name="articleTag"
|
||||||
|
autocomplete="articleTag"
|
||||||
|
className="mt-[20px] max-w-[600px]"
|
||||||
|
type="text"
|
||||||
|
label="Теги"
|
||||||
|
onChange={(v) => setTagInput(v)}
|
||||||
|
defaultState={tagInput}
|
||||||
|
placeholder="arrays"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') addTag();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={addTag}
|
||||||
|
text="Добавить"
|
||||||
|
className="h-[40px] w-[140px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-[10px] mt-2">
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<div
|
||||||
|
key={tag}
|
||||||
|
className="flex items-center gap-1 bg-liquid-lighter px-3 py-1 rounded-full"
|
||||||
|
>
|
||||||
|
<span>{tag}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => removeTag(tag)}
|
||||||
|
className="text-liquid-red font-bold ml-[5px]"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={handleSubmit}
|
||||||
|
text={status === 'loading' ? 'Загрузка...' : 'Создать'}
|
||||||
|
disabled={status === 'loading'}
|
||||||
|
/>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={() => setActive(false)}
|
||||||
|
text="Отмена"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalCreate;
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
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;
|
|
||||||
Reference in New Issue
Block a user