delete mission

This commit is contained in:
Виталий Лавшонок
2025-11-23 14:26:04 +03:00
parent ee0e44082a
commit d6ab1cba4d
8 changed files with 110 additions and 5 deletions

View File

@@ -2,6 +2,7 @@ import { FC } from 'react';
import { Modal } from './Modal'; import { Modal } from './Modal';
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 { cn } from '../../lib/cn';
interface ConfirmModalProps { interface ConfirmModalProps {
active: boolean; active: boolean;
@@ -11,6 +12,7 @@ interface ConfirmModalProps {
message?: string; message?: string;
confirmColor?: 'primary' | 'secondary' | 'error' | 'warning' | 'success'; confirmColor?: 'primary' | 'secondary' | 'error' | 'warning' | 'success';
confirmText?: string; confirmText?: string;
className?: string;
} }
const ConfirmModal: FC<ConfirmModalProps> = ({ const ConfirmModal: FC<ConfirmModalProps> = ({
@@ -21,10 +23,14 @@ const ConfirmModal: FC<ConfirmModalProps> = ({
message, message,
confirmColor = 'secondary', confirmColor = 'secondary',
confirmText = 'Ок', confirmText = 'Ок',
className,
}) => { }) => {
return ( return (
<Modal <Modal
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white fixed top-0 left-0" className={cn(
'bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white',
className,
)}
onOpenChange={setActive} onOpenChange={setActive}
open={active} open={active}
backdrop="blur" backdrop="blur"

View File

@@ -1,21 +1,25 @@
import { useParams, Navigate } from 'react-router-dom'; import { useParams, Navigate, useNavigate } from 'react-router-dom';
import CodeEditor from '../views/mission/codeeditor/CodeEditor'; import CodeEditor from '../views/mission/codeeditor/CodeEditor';
import Statement from '../views/mission/statement/Statement'; import Statement from '../views/mission/statement/Statement';
import { PrimaryButton } from '../components/button/PrimaryButton'; import { PrimaryButton } from '../components/button/PrimaryButton';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { useAppDispatch, useAppSelector } from '../redux/hooks';
import { fetchMySubmitsByMission, submitMission } from '../redux/slices/submit'; import { fetchMySubmitsByMission, submitMission } from '../redux/slices/submit';
import { fetchMissionById } from '../redux/slices/missions'; import { fetchMissionById, setMissionsStatus } from '../redux/slices/missions';
import Header from '../views/mission/statement/Header'; import Header from '../views/mission/statement/Header';
import MissionSubmissions from '../views/mission/statement/MissionSubmissions'; import MissionSubmissions from '../views/mission/statement/MissionSubmissions';
import { useQuery } from '../hooks/useQuery'; import { useQuery } from '../hooks/useQuery';
const Mission = () => { const Mission = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigate = useNavigate();
// Получаем параметры из URL // Получаем параметры из URL
const { missionId } = useParams<{ missionId: string }>(); const { missionId } = useParams<{ missionId: string }>();
const mission = useAppSelector((state) => state.missions.currentMission); const mission = useAppSelector((state) => state.missions.currentMission);
const missionStatus = useAppSelector(
(state) => state.missions.statuses.fetchById,
);
const missionIdNumber = Number(missionId); const missionIdNumber = Number(missionId);
const query = useQuery(); const query = useQuery();
@@ -74,6 +78,12 @@ const Mission = () => {
} }
}; };
}, []); }, []);
useEffect(() => {
if (missionStatus == 'failed') {
setMissionsStatus({ key: 'fetchById', status: 'idle' });
navigate(back ?? '/home/missions');
}
}, [missionStatus]);
useEffect(() => { useEffect(() => {
submissionsRef.current = submissions; submissionsRef.current = submissions;

View File

@@ -34,6 +34,7 @@ interface MissionsState {
fetchById: Status; fetchById: Status;
upload: Status; upload: Status;
fetchMy: Status; fetchMy: Status;
delete: Status;
}; };
error: string | null; error: string | null;
} }
@@ -49,6 +50,7 @@ const initialState: MissionsState = {
fetchById: 'idle', fetchById: 'idle',
upload: 'idle', upload: 'idle',
fetchMy: 'idle', fetchMy: 'idle',
delete: 'idle',
}, },
error: null, error: null,
}; };
@@ -141,6 +143,21 @@ export const uploadMission = createAsyncThunk(
}, },
); );
// DELETE /missions/{id}
export const deleteMission = createAsyncThunk(
'missions/deleteMission',
async (id: number, { rejectWithValue }) => {
try {
await axios.delete(`/missions/${id}`);
return id; // возвращаем id удалённой миссии
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Ошибка при удалении миссии',
);
}
},
);
// ─── Slice ──────────────────────────────────────────── // ─── Slice ────────────────────────────────────────────
const missionsSlice = createSlice({ const missionsSlice = createSlice({
@@ -245,6 +262,32 @@ const missionsSlice = createSlice({
state.error = action.payload; state.error = action.payload;
}, },
); );
// ─── DELETE MISSION ───
builder.addCase(deleteMission.pending, (state) => {
state.statuses.delete = 'loading';
state.error = null;
});
builder.addCase(
deleteMission.fulfilled,
(state, action: PayloadAction<number>) => {
state.statuses.delete = 'successful';
state.missions = state.missions.filter(
(m) => m.id !== action.payload,
);
if (state.currentMission?.id === action.payload) {
state.currentMission = null;
}
},
);
builder.addCase(
deleteMission.rejected,
(state, action: PayloadAction<any>) => {
state.statuses.delete = 'failed';
state.error = action.payload;
},
);
}, },
}); });

View File

@@ -1,12 +1,14 @@
import { FC, useEffect } from 'react'; import { FC, useEffect, useState } from 'react';
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
import { setMenuActiveProfilePage } from '../../../../redux/slices/store'; import { setMenuActiveProfilePage } from '../../../../redux/slices/store';
import { cn } from '../../../../lib/cn'; import { cn } from '../../../../lib/cn';
import MissionsBlock from './MissionsBlock'; import MissionsBlock from './MissionsBlock';
import { import {
deleteMission,
fetchMyMissions, fetchMyMissions,
setMissionsStatus, setMissionsStatus,
} from '../../../../redux/slices/missions'; } from '../../../../redux/slices/missions';
import ConfirmModal from '../../../../components/modal/ConfirmModal';
interface ItemProps { interface ItemProps {
count: number; count: number;
@@ -43,6 +45,8 @@ const Missions = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const missions = useAppSelector((state) => state.missions.missions); const missions = useAppSelector((state) => state.missions.missions);
const status = useAppSelector((state) => state.missions.statuses.fetchMy); const status = useAppSelector((state) => state.missions.statuses.fetchMy);
const [modalDeleteTask, setModalDeleteTask] = useState<boolean>(false);
const [taskdeleteId, setTaskDeleteId] = useState<number>(0);
useEffect(() => { useEffect(() => {
dispatch(setMenuActiveProfilePage('missions')); dispatch(setMenuActiveProfilePage('missions'));
@@ -99,9 +103,23 @@ const Missions = () => {
<MissionsBlock <MissionsBlock
missions={missions ?? []} missions={missions ?? []}
title="Мои миссии" title="Мои миссии"
setTastDeleteId={setTaskDeleteId}
setDeleteModalActive={setModalDeleteTask}
/> />
</div> </div>
</div> </div>
<ConfirmModal
active={modalDeleteTask}
setActive={setModalDeleteTask}
title="Подтвердите действия"
message={`Вы действительно хотите удалить задачу #${taskdeleteId}?`}
confirmColor="error"
confirmText="Удалить"
onConfirmClick={() => {
dispatch(deleteMission(taskdeleteId));
}}
/>
</div> </div>
); );
}; };

View File

@@ -8,12 +8,17 @@ interface MissionsBlockProps {
missions: Mission[]; missions: Mission[];
title: string; title: string;
className?: string; className?: string;
setTastDeleteId: (v: number) => void;
setDeleteModalActive: (v: boolean) => void;
} }
const MissionsBlock: FC<MissionsBlockProps> = ({ const MissionsBlock: FC<MissionsBlockProps> = ({
missions, missions,
title, title,
className, className,
setTastDeleteId,
setDeleteModalActive,
}) => { }) => {
const [active, setActive] = useState<boolean>(true); const [active, setActive] = useState<boolean>(true);
@@ -59,6 +64,8 @@ const MissionsBlock: FC<MissionsBlockProps> = ({
memoryLimit={v.memoryLimit} memoryLimit={v.memoryLimit}
difficulty={v.difficulty} difficulty={v.difficulty}
type={i % 2 ? 'second' : 'first'} type={i % 2 ? 'second' : 'first'}
setTastDeleteId={setTastDeleteId}
setDeleteModalActive={setDeleteModalActive}
/> />
))} ))}
</div> </div>

View File

@@ -1,6 +1,8 @@
import { cn } from '../../../../lib/cn'; import { cn } from '../../../../lib/cn';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Edit } from '../../../../assets/icons/input'; import { Edit } from '../../../../assets/icons/input';
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
import { deleteMission } from '../../../../redux/slices/missions';
export interface MissionItemProps { export interface MissionItemProps {
id: number; id: number;
@@ -14,6 +16,8 @@ export interface MissionItemProps {
updatedAt?: string; updatedAt?: string;
type?: 'first' | 'second'; type?: 'first' | 'second';
status?: 'empty' | 'success' | 'error'; status?: 'empty' | 'success' | 'error';
setTastDeleteId: (v: number) => void;
setDeleteModalActive: (v: boolean) => void;
} }
export function formatMilliseconds(ms: number): string { export function formatMilliseconds(ms: number): string {
@@ -35,11 +39,17 @@ const MissionItem: React.FC<MissionItemProps> = ({
memoryLimit = 256 * 1024 * 1024, memoryLimit = 256 * 1024 * 1024,
type, type,
status, status,
setTastDeleteId,
setDeleteModalActive,
}) => { }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useAppDispatch();
const difficultyItems = ['Easy', 'Medium', 'Hard']; const difficultyItems = ['Easy', 'Medium', 'Hard'];
const difficultyString = const difficultyString =
difficultyItems[Math.min(Math.max(0, difficulty - 1), 2)]; difficultyItems[Math.min(Math.max(0, difficulty - 1), 2)];
const deleteStatus = useAppSelector(
(state) => state.missions.statuses.delete,
);
return ( return (
<div <div
@@ -76,9 +86,17 @@ const MissionItem: React.FC<MissionItemProps> = ({
<div className="h-[24px] w-[24px]"> <div className="h-[24px] w-[24px]">
<img <img
src={Edit} src={Edit}
className="hover:bg-liquid-light rounded-[8px] transition-all duration-300" className={cn(
'hover:bg-liquid-light rounded-[8px] transition-all duration-300',
deleteStatus == 'loading' &&
'cursor-default pointer-events-none hover:bg-transparent opacity-35',
)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (deleteStatus != 'loading') {
setTastDeleteId(id);
setDeleteModalActive(true);
}
}} }}
/> />
</div> </div>

View File

@@ -81,6 +81,7 @@ const ModalLeave: FC<ModalLeaveProps> = ({
</div> </div>
</div> </div>
<ConfirmModal <ConfirmModal
className=" fixed top-0 left-0"
active={modalConfirmActive} active={modalConfirmActive}
setActive={setModalConfirmActive} setActive={setModalConfirmActive}
title="Подтвердите действия" title="Подтвердите действия"

View File

@@ -157,6 +157,7 @@ const ModalUpdate: FC<ModalUpdateProps> = ({
</div> </div>
<ConfirmModal <ConfirmModal
className=" fixed top-0 left-0"
active={modalConfirmDeleteUser} active={modalConfirmDeleteUser}
setActive={setModalConfirmDeleteUser} setActive={setModalConfirmDeleteUser}
title="Подтвердите действия" title="Подтвердите действия"
@@ -176,6 +177,7 @@ const ModalUpdate: FC<ModalUpdateProps> = ({
/> />
<ConfirmModal <ConfirmModal
className=" fixed top-0 left-0"
active={modalConfirmRoleActive} active={modalConfirmRoleActive}
setActive={setModalConfirmRoleActive} setActive={setModalConfirmRoleActive}
title="Подтвердите действия" title="Подтвердите действия"