add error toasts

This commit is contained in:
Виталий Лавшонок
2025-12-10 01:33:16 +03:00
parent 02de330034
commit d1a46435c4
17 changed files with 508 additions and 278 deletions

View File

@@ -58,7 +58,7 @@ export const Input: React.FC<inputProps> = ({
)} )}
value={value} value={value}
name={name} name={name}
autoComplete={autocomplete} autoComplete={autocomplete || undefined}
type={ type={
type == 'password' type == 'password'
? visible ? visible

View File

@@ -15,6 +15,7 @@ import {
} from '../redux/slices/articles'; } from '../redux/slices/articles';
import { useQuery } from '../hooks/useQuery'; import { useQuery } from '../hooks/useQuery';
import { ReverseButton } from '../components/button/ReverseButton'; import { ReverseButton } from '../components/button/ReverseButton';
import { cn } from '../lib/cn';
const ArticleEditor = () => { const ArticleEditor = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -24,6 +25,7 @@ const ArticleEditor = () => {
const back = query.get('back') ?? undefined; const back = query.get('back') ?? undefined;
const articleId = Number(query.get('articleId') ?? undefined); const articleId = Number(query.get('articleId') ?? undefined);
const refactor = articleId && !isNaN(articleId); const refactor = articleId && !isNaN(articleId);
const [clickSubmit, setClickSubmit] = useState<boolean>(false);
// Достаём данные из redux // Достаём данные из redux
const article = useAppSelector( const article = useAppSelector(
@@ -61,7 +63,6 @@ const ArticleEditor = () => {
const removeTag = (tagToRemove: string) => { const removeTag = (tagToRemove: string) => {
setTags(tags.filter((tag) => tag !== tagToRemove)); setTags(tags.filter((tag) => tag !== tagToRemove));
}; };
// ========================== // ==========================
// Эффекты по статусам // Эффекты по статусам
// ========================== // ==========================
@@ -96,6 +97,7 @@ const ArticleEditor = () => {
// Получение статьи // Получение статьи
// ========================== // ==========================
useEffect(() => { useEffect(() => {
setClickSubmit(false);
if (articleId) { if (articleId) {
dispatch(fetchArticleById(articleId)); dispatch(fetchArticleById(articleId));
} }
@@ -110,6 +112,18 @@ const ArticleEditor = () => {
} }
}, [article]); }, [article]);
const getNameErrorMessage = (): string => {
if (!clickSubmit) return '';
if (name == '') return 'Поле не может быть пустым';
return '';
};
const getContentErrorMessage = (): string => {
if (!clickSubmit) return '';
if (code == '') return 'Поле не может быть пустым';
return '';
};
// ========================== // ==========================
// Рендер // Рендер
// ========================== // ==========================
@@ -137,6 +151,7 @@ const ArticleEditor = () => {
<div className="flex gap-[20px]"> <div className="flex gap-[20px]">
<PrimaryButton <PrimaryButton
onClick={() => { onClick={() => {
setClickSubmit(true);
dispatch( dispatch(
updateArticle({ updateArticle({
articleId, articleId,
@@ -163,6 +178,7 @@ const ArticleEditor = () => {
) : ( ) : (
<PrimaryButton <PrimaryButton
onClick={() => { onClick={() => {
setClickSubmit(true);
dispatch( dispatch(
createArticle({ createArticle({
name, name,
@@ -188,6 +204,7 @@ const ArticleEditor = () => {
label="Название" label="Название"
onChange={setName} onChange={setName}
placeholder="Новая статья" placeholder="Новая статья"
error={getNameErrorMessage()}
/> />
{/* Теги */} {/* Теги */}
@@ -236,6 +253,14 @@ const ArticleEditor = () => {
text="Редактировать текст" text="Редактировать текст"
className="mt-[20px]" className="mt-[20px]"
/> />
<div
className={cn(
'text-liquid-red text-[14px] h-auto mt-[5px] whitespace-pre-line ',
getContentErrorMessage() == '' && 'h-0 mt-0',
)}
>
{getContentErrorMessage()}
</div>
<MarkdownPreview <MarkdownPreview
content={code} content={code}
className="bg-transparent border-liquid-lighter border-[3px] rounded-[20px] mt-[20px]" className="bg-transparent border-liquid-lighter border-[3px] rounded-[20px] mt-[20px]"

View File

@@ -1,16 +1,15 @@
// src/pages/Home.tsx // src/pages/Home.tsx
import { Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import Login from '../views/home/auth/Login'; import Login from '../views/home/auth/Login';
import Register from '../views/home/auth/Register'; import Register from '../views/home/auth/Register';
import Menu from '../views/home/menu/Menu'; import Menu from '../views/home/menu/Menu';
import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { useAppDispatch, useAppSelector } from '../redux/hooks';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { fetchWhoAmI, logout } from '../redux/slices/auth'; import { fetchWhoAmI } from '../redux/slices/auth';
import Missions from '../views/home/missions/Missions'; import Missions from '../views/home/missions/Missions';
import Articles from '../views/home/articles/Articles'; import Articles from '../views/home/articles/Articles';
import Groups from '../views/home/groups/Groups'; import Groups from '../views/home/groups/Groups';
import Contests from '../views/home/contests/Contests'; import Contests from '../views/home/contests/Contests';
import { PrimaryButton } from '../components/button/PrimaryButton';
import Group from '../views/home/group/Group'; import Group from '../views/home/group/Group';
import Contest from '../views/home/contest/Contest'; import Contest from '../views/home/contest/Contest';
import Account from '../views/home/account/Account'; import Account from '../views/home/account/Account';
@@ -19,14 +18,8 @@ import { MissionsRightPanel } from '../views/home/rightpanel/Missions';
import { ArticlesRightPanel } from '../views/home/rightpanel/Articles'; import { ArticlesRightPanel } from '../views/home/rightpanel/Articles';
import { GroupRightPanel } from '../views/home/rightpanel/group/Group'; import { GroupRightPanel } from '../views/home/rightpanel/group/Group';
import GroupInvite from '../views/home/groupinviter/GroupInvite'; import GroupInvite from '../views/home/groupinviter/GroupInvite';
import {
toastError,
toastSuccess,
toastWarning,
} from '../lib/toastNotification';
const Home = () => { const Home = () => {
const name = useAppSelector((state) => state.auth.username);
const jwt = useAppSelector((state) => state.auth.jwt); const jwt = useAppSelector((state) => state.auth.jwt);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@@ -59,53 +52,7 @@ const Home = () => {
<Route path="contest/:contestId/*" element={<Contest />} /> <Route path="contest/:contestId/*" element={<Contest />} />
<Route <Route
path="*" path="*"
element={ element={<Navigate to="/home/account" replace />}
<>
<p>{jwt}</p>
<PrimaryButton
onClick={() => {
if (jwt) {
navigator.clipboard.writeText(jwt);
alert(jwt);
}
}}
text="скопировать токен"
className="pt-[20px]"
/>
<p className="py-[20px]">{name}</p>
<PrimaryButton
onClick={() => {
dispatch(logout());
}}
>
выйти
</PrimaryButton>
<div className="flex mt-[20px] gap-[20px]">
<PrimaryButton
color="success"
text="Toast"
onClick={() => {
toastSuccess('Success');
}}
/>
<PrimaryButton
color="warning"
text="Toast"
onClick={() => {
toastWarning('Warning');
}}
/>
<PrimaryButton
color="error"
text="Toast"
onClick={() => {
toastError('Error');
}}
/>
</div>
</>
}
/> />
</Routes> </Routes>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from '../../axios'; import axios from '../../axios';
import { toastError } from '../../lib/toastNotification';
// ===================== // =====================
// Типы // Типы
@@ -120,9 +121,7 @@ export const fetchArticles = createAsyncThunk(
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при получении статей',
);
} }
}, },
); );
@@ -135,10 +134,7 @@ export const fetchMyArticles = createAsyncThunk(
const response = await axios.get<Article[]>('/articles/my'); const response = await axios.get<Article[]>('/articles/my');
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message ||
'Ошибка при получении моих статей',
);
} }
}, },
); );
@@ -151,9 +147,7 @@ export const fetchArticleById = createAsyncThunk(
const response = await axios.get<Article>(`/articles/${articleId}`); const response = await axios.get<Article>(`/articles/${articleId}`);
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при получении статьи',
);
} }
}, },
); );
@@ -177,9 +171,7 @@ export const createArticle = createAsyncThunk(
}); });
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при создании статьи',
);
} }
}, },
); );
@@ -207,9 +199,7 @@ export const updateArticle = createAsyncThunk(
); );
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при обновлении статьи',
);
} }
}, },
); );
@@ -222,9 +212,7 @@ export const deleteArticle = createAsyncThunk(
await axios.delete(`/articles/${articleId}`); await axios.delete(`/articles/${articleId}`);
return articleId; return articleId;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при удалении статьи',
);
} }
}, },
); );
@@ -263,7 +251,12 @@ const articlesSlice = createSlice({
); );
builder.addCase(fetchArticles.rejected, (state, action: any) => { builder.addCase(fetchArticles.rejected, (state, action: any) => {
state.fetchArticles.status = 'failed'; state.fetchArticles.status = 'failed';
state.fetchArticles.error = action.payload; const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// fetchMyArticles // fetchMyArticles
@@ -280,7 +273,12 @@ const articlesSlice = createSlice({
); );
builder.addCase(fetchMyArticles.rejected, (state, action: any) => { builder.addCase(fetchMyArticles.rejected, (state, action: any) => {
state.fetchMyArticles.status = 'failed'; state.fetchMyArticles.status = 'failed';
state.fetchMyArticles.error = action.payload; const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// fetchArticleById // fetchArticleById
@@ -297,7 +295,12 @@ const articlesSlice = createSlice({
); );
builder.addCase(fetchArticleById.rejected, (state, action: any) => { builder.addCase(fetchArticleById.rejected, (state, action: any) => {
state.fetchArticleById.status = 'failed'; state.fetchArticleById.status = 'failed';
state.fetchArticleById.error = action.payload; const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// createArticle // createArticle
@@ -314,7 +317,14 @@ const articlesSlice = createSlice({
); );
builder.addCase(createArticle.rejected, (state, action: any) => { builder.addCase(createArticle.rejected, (state, action: any) => {
state.createArticle.status = 'failed'; state.createArticle.status = 'failed';
state.createArticle.error = action.payload; state.createArticle.error = action.payload.title;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// updateArticle // updateArticle
@@ -331,7 +341,14 @@ const articlesSlice = createSlice({
); );
builder.addCase(updateArticle.rejected, (state, action: any) => { builder.addCase(updateArticle.rejected, (state, action: any) => {
state.updateArticle.status = 'failed'; state.updateArticle.status = 'failed';
state.updateArticle.error = action.payload; state.createArticle.error = action.payload.title;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// deleteArticle // deleteArticle
@@ -355,7 +372,12 @@ const articlesSlice = createSlice({
); );
builder.addCase(deleteArticle.rejected, (state, action: any) => { builder.addCase(deleteArticle.rejected, (state, action: any) => {
state.deleteArticle.status = 'failed'; state.deleteArticle.status = 'failed';
state.deleteArticle.error = action.payload; const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
}, },
}); });

View File

@@ -1,5 +1,6 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from '../../axios'; import axios from '../../axios';
import { toastError } from '../../lib/toastNotification';
// ===================== // =====================
// Типы // Типы
@@ -280,10 +281,7 @@ export const fetchParticipatingContests = createAsyncThunk(
); );
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message ||
'Failed to fetch participating contests',
);
} }
}, },
); );
@@ -297,9 +295,7 @@ export const fetchMySubmissions = createAsyncThunk(
); );
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Failed to fetch my submissions',
);
} }
}, },
); );
@@ -321,9 +317,7 @@ export const fetchContests = createAsyncThunk(
}); });
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Failed to fetch contests',
);
} }
}, },
); );
@@ -335,9 +329,7 @@ export const fetchContestById = createAsyncThunk(
const response = await axios.get<Contest>(`/contests/${id}`); const response = await axios.get<Contest>(`/contests/${id}`);
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Failed to fetch contest',
);
} }
}, },
); );
@@ -352,9 +344,7 @@ export const createContest = createAsyncThunk(
); );
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Failed to create contest',
);
} }
}, },
); );
@@ -375,9 +365,7 @@ export const updateContest = createAsyncThunk(
); );
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Failed to update contest',
);
} }
}, },
); );
@@ -389,9 +377,7 @@ export const deleteContest = createAsyncThunk(
await axios.delete(`/contests/${contestId}`); await axios.delete(`/contests/${contestId}`);
return contestId; return contestId;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Failed to delete contest',
);
} }
}, },
); );
@@ -403,9 +389,7 @@ export const fetchMyContests = createAsyncThunk(
const response = await axios.get<Contest[]>('/contests/my'); const response = await axios.get<Contest[]>('/contests/my');
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Failed to fetch my contests',
);
} }
}, },
); );
@@ -424,10 +408,7 @@ export const fetchRegisteredContests = createAsyncThunk(
); );
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message ||
'Failed to fetch registered contests',
);
} }
}, },
); );
@@ -451,10 +432,7 @@ export const addOrUpdateContestMember = createAsyncThunk(
); );
return { contestId, members: response.data }; return { contestId, members: response.data };
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message ||
'Failed to add or update contest member',
);
} }
}, },
); );
@@ -470,10 +448,7 @@ export const deleteContestMember = createAsyncThunk(
await axios.delete(`/contests/${contestId}/members/${memberId}`); await axios.delete(`/contests/${contestId}/members/${memberId}`);
return { contestId, memberId }; return { contestId, memberId };
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message ||
'Failed to delete contest member',
);
} }
}, },
); );
@@ -488,10 +463,7 @@ export const startContestAttempt = createAsyncThunk(
); );
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message ||
'Failed to start contest attempt',
);
} }
}, },
); );
@@ -506,9 +478,7 @@ export const fetchMyAttemptsInContest = createAsyncThunk(
); );
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Failed to fetch my attempts',
);
} }
}, },
); );
@@ -531,10 +501,7 @@ export const fetchContestMembers = createAsyncThunk(
); );
return { contestId, ...response.data }; return { contestId, ...response.data };
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message ||
'Failed to fetch contest members',
);
} }
}, },
); );
@@ -549,9 +516,7 @@ export const checkContestRegistration = createAsyncThunk(
); );
return { contestId, registered: response.data.registered }; return { contestId, registered: response.data.registered };
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Failed to check registration',
);
} }
}, },
); );
@@ -566,10 +531,7 @@ export const fetchUpcomingEligibleContests = createAsyncThunk(
); );
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message ||
'Failed to fetch upcoming eligible contests',
);
} }
}, },
); );
@@ -584,9 +546,7 @@ export const fetchMyAllAttempts = createAsyncThunk(
); );
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Failed to fetch my attempts',
);
} }
}, },
); );
@@ -601,9 +561,7 @@ export const fetchMyActiveAttempt = createAsyncThunk(
); );
return { contestId, attempt: response.data }; return { contestId, attempt: response.data };
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Failed to fetch active attempt',
);
} }
}, },
); );
@@ -642,7 +600,13 @@ const contestsSlice = createSlice({
); );
builder.addCase(fetchMySubmissions.rejected, (state, action: any) => { builder.addCase(fetchMySubmissions.rejected, (state, action: any) => {
state.fetchMySubmissions.status = 'failed'; state.fetchMySubmissions.status = 'failed';
state.fetchMySubmissions.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
builder.addCase(fetchContests.pending, (state) => { builder.addCase(fetchContests.pending, (state) => {
@@ -658,7 +622,13 @@ const contestsSlice = createSlice({
); );
builder.addCase(fetchContests.rejected, (state, action: any) => { builder.addCase(fetchContests.rejected, (state, action: any) => {
state.fetchContests.status = 'failed'; state.fetchContests.status = 'failed';
state.fetchContests.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
builder.addCase(fetchContestById.pending, (state) => { builder.addCase(fetchContestById.pending, (state) => {
@@ -673,7 +643,13 @@ const contestsSlice = createSlice({
); );
builder.addCase(fetchContestById.rejected, (state, action: any) => { builder.addCase(fetchContestById.rejected, (state, action: any) => {
state.fetchContestById.status = 'failed'; state.fetchContestById.status = 'failed';
state.fetchContestById.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
builder.addCase(createContest.pending, (state) => { builder.addCase(createContest.pending, (state) => {
@@ -688,7 +664,13 @@ const contestsSlice = createSlice({
); );
builder.addCase(createContest.rejected, (state, action: any) => { builder.addCase(createContest.rejected, (state, action: any) => {
state.createContest.status = 'failed'; state.createContest.status = 'failed';
state.createContest.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
builder.addCase(updateContest.pending, (state) => { builder.addCase(updateContest.pending, (state) => {
@@ -703,7 +685,13 @@ const contestsSlice = createSlice({
); );
builder.addCase(updateContest.rejected, (state, action: any) => { builder.addCase(updateContest.rejected, (state, action: any) => {
state.updateContest.status = 'failed'; state.updateContest.status = 'failed';
state.updateContest.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
builder.addCase(deleteContest.pending, (state) => { builder.addCase(deleteContest.pending, (state) => {
@@ -725,7 +713,13 @@ const contestsSlice = createSlice({
); );
builder.addCase(deleteContest.rejected, (state, action: any) => { builder.addCase(deleteContest.rejected, (state, action: any) => {
state.deleteContest.status = 'failed'; state.deleteContest.status = 'failed';
state.deleteContest.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
builder.addCase(fetchMyContests.pending, (state) => { builder.addCase(fetchMyContests.pending, (state) => {
@@ -740,7 +734,13 @@ const contestsSlice = createSlice({
); );
builder.addCase(fetchMyContests.rejected, (state, action: any) => { builder.addCase(fetchMyContests.rejected, (state, action: any) => {
state.fetchMyContests.status = 'failed'; state.fetchMyContests.status = 'failed';
state.fetchMyContests.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
builder.addCase(fetchRegisteredContests.pending, (state) => { builder.addCase(fetchRegisteredContests.pending, (state) => {
@@ -760,7 +760,15 @@ const contestsSlice = createSlice({
fetchRegisteredContests.rejected, fetchRegisteredContests.rejected,
(state, action: any) => { (state, action: any) => {
state.fetchRegisteredContests.status = 'failed'; state.fetchRegisteredContests.status = 'failed';
state.fetchRegisteredContests.error = action.payload; const errors = action.payload.errors as Record<
string,
string[]
>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}, },
); );
@@ -787,7 +795,13 @@ const contestsSlice = createSlice({
); );
builder.addCase(fetchContestMembers.rejected, (state, action: any) => { builder.addCase(fetchContestMembers.rejected, (state, action: any) => {
state.fetchContestMembers.status = 'failed'; state.fetchContestMembers.status = 'failed';
state.fetchContestMembers.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
builder.addCase(addOrUpdateContestMember.pending, (state) => { builder.addCase(addOrUpdateContestMember.pending, (state) => {
@@ -800,7 +814,15 @@ const contestsSlice = createSlice({
addOrUpdateContestMember.rejected, addOrUpdateContestMember.rejected,
(state, action: any) => { (state, action: any) => {
state.addOrUpdateMember.status = 'failed'; state.addOrUpdateMember.status = 'failed';
state.addOrUpdateMember.error = action.payload; const errors = action.payload.errors as Record<
string,
string[]
>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}, },
); );
@@ -812,7 +834,13 @@ const contestsSlice = createSlice({
}); });
builder.addCase(deleteContestMember.rejected, (state, action: any) => { builder.addCase(deleteContestMember.rejected, (state, action: any) => {
state.deleteContestMember.status = 'failed'; state.deleteContestMember.status = 'failed';
state.deleteContestMember.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
builder.addCase(startContestAttempt.pending, (state) => { builder.addCase(startContestAttempt.pending, (state) => {
@@ -827,7 +855,13 @@ const contestsSlice = createSlice({
); );
builder.addCase(startContestAttempt.rejected, (state, action: any) => { builder.addCase(startContestAttempt.rejected, (state, action: any) => {
state.startAttempt.status = 'failed'; state.startAttempt.status = 'failed';
state.startAttempt.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
builder.addCase(fetchMyAttemptsInContest.pending, (state) => { builder.addCase(fetchMyAttemptsInContest.pending, (state) => {
@@ -844,7 +878,15 @@ const contestsSlice = createSlice({
fetchMyAttemptsInContest.rejected, fetchMyAttemptsInContest.rejected,
(state, action: any) => { (state, action: any) => {
state.fetchMyAttemptsInContest.status = 'failed'; state.fetchMyAttemptsInContest.status = 'failed';
state.fetchMyAttemptsInContest.error = action.payload; const errors = action.payload.errors as Record<
string,
string[]
>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}, },
); );
@@ -860,7 +902,13 @@ const contestsSlice = createSlice({
); );
builder.addCase(fetchMyAllAttempts.rejected, (state, action: any) => { builder.addCase(fetchMyAllAttempts.rejected, (state, action: any) => {
state.fetchMyAllAttempts.status = 'failed'; state.fetchMyAllAttempts.status = 'failed';
state.fetchMyAllAttempts.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
builder.addCase(fetchMyActiveAttempt.pending, (state) => { builder.addCase(fetchMyActiveAttempt.pending, (state) => {
@@ -881,7 +929,13 @@ const contestsSlice = createSlice({
); );
builder.addCase(fetchMyActiveAttempt.rejected, (state, action: any) => { builder.addCase(fetchMyActiveAttempt.rejected, (state, action: any) => {
state.fetchMyActiveAttempt.status = 'failed'; state.fetchMyActiveAttempt.status = 'failed';
state.fetchMyActiveAttempt.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
builder.addCase(checkContestRegistration.pending, (state) => { builder.addCase(checkContestRegistration.pending, (state) => {
@@ -904,7 +958,15 @@ const contestsSlice = createSlice({
checkContestRegistration.rejected, checkContestRegistration.rejected,
(state, action: any) => { (state, action: any) => {
state.checkRegistration.status = 'failed'; state.checkRegistration.status = 'failed';
state.checkRegistration.error = action.payload; const errors = action.payload.errors as Record<
string,
string[]
>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}, },
); );
@@ -922,7 +984,15 @@ const contestsSlice = createSlice({
fetchUpcomingEligibleContests.rejected, fetchUpcomingEligibleContests.rejected,
(state, action: any) => { (state, action: any) => {
state.fetchUpcomingEligible.status = 'failed'; state.fetchUpcomingEligible.status = 'failed';
state.fetchUpcomingEligible.error = action.payload; const errors = action.payload.errors as Record<
string,
string[]
>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}, },
); );
@@ -944,7 +1014,15 @@ const contestsSlice = createSlice({
fetchParticipatingContests.rejected, fetchParticipatingContests.rejected,
(state, action: any) => { (state, action: any) => {
state.fetchParticipating.status = 'failed'; state.fetchParticipating.status = 'failed';
state.fetchParticipating.error = action.payload; const errors = action.payload.errors as Record<
string,
string[]
>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}, },
); );
}, },

View File

@@ -1,5 +1,6 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from '../../axios'; import axios from '../../axios';
import { toastError } from '../../lib/toastNotification';
// ========================================= // =========================================
// Типы // Типы
@@ -81,10 +82,7 @@ export const fetchGroupMessages = createAsyncThunk(
messages: response.data as ChatMessage[], messages: response.data as ChatMessage[],
}; };
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message ||
'Ошибка при получении сообщений группы',
);
} }
}, },
); );
@@ -99,9 +97,7 @@ export const sendGroupMessage = createAsyncThunk(
}); });
return response.data as ChatMessage; return response.data as ChatMessage;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при отправке сообщения',
);
} }
}, },
); );
@@ -167,7 +163,12 @@ const groupChatSlice = createSlice({
builder.addCase(fetchGroupMessages.rejected, (state, action: any) => { builder.addCase(fetchGroupMessages.rejected, (state, action: any) => {
state.fetchMessages.status = 'failed'; state.fetchMessages.status = 'failed';
state.fetchMessages.error = action.payload; const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// send message // send message
@@ -188,7 +189,12 @@ const groupChatSlice = createSlice({
builder.addCase(sendGroupMessage.rejected, (state, action: any) => { builder.addCase(sendGroupMessage.rejected, (state, action: any) => {
state.sendMessage.status = 'failed'; state.sendMessage.status = 'failed';
state.sendMessage.error = action.payload; const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
}, },
}); });

View File

@@ -1,5 +1,6 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from '../../axios'; import axios from '../../axios';
import { toastError } from '../../lib/toastNotification';
// ===================== // =====================
// Типы // Типы
@@ -104,9 +105,7 @@ export const fetchGroupPosts = createAsyncThunk(
); );
return { page, data: response.data as PostsPage }; return { page, data: response.data as PostsPage };
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка загрузки постов',
);
} }
}, },
); );
@@ -124,9 +123,7 @@ export const fetchPostById = createAsyncThunk(
); );
return response.data as Post; return response.data as Post;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка загрузки поста',
);
} }
}, },
); );
@@ -149,9 +146,7 @@ export const createPost = createAsyncThunk(
}); });
return response.data as Post; return response.data as Post;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка создания поста',
);
} }
}, },
); );
@@ -183,9 +178,7 @@ export const updatePost = createAsyncThunk(
); );
return response.data as Post; return response.data as Post;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка обновления поста',
);
} }
}, },
); );
@@ -201,9 +194,7 @@ export const deletePost = createAsyncThunk(
await axios.delete(`/groups/${groupId}/feed/${postId}`); await axios.delete(`/groups/${groupId}/feed/${postId}`);
return postId; return postId;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка удаления поста',
);
} }
}, },
); );
@@ -244,7 +235,13 @@ const postsSlice = createSlice({
); );
builder.addCase(fetchGroupPosts.rejected, (state, action: any) => { builder.addCase(fetchGroupPosts.rejected, (state, action: any) => {
state.fetchPosts.status = 'failed'; state.fetchPosts.status = 'failed';
state.fetchPosts.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// fetchPostById // fetchPostById
@@ -260,7 +257,13 @@ const postsSlice = createSlice({
); );
builder.addCase(fetchPostById.rejected, (state, action: any) => { builder.addCase(fetchPostById.rejected, (state, action: any) => {
state.fetchPostById.status = 'failed'; state.fetchPostById.status = 'failed';
state.fetchPostById.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// createPost // createPost
@@ -281,7 +284,13 @@ const postsSlice = createSlice({
); );
builder.addCase(createPost.rejected, (state, action: any) => { builder.addCase(createPost.rejected, (state, action: any) => {
state.createPost.status = 'failed'; state.createPost.status = 'failed';
state.createPost.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// updatePost // updatePost
@@ -310,7 +319,13 @@ const postsSlice = createSlice({
); );
builder.addCase(updatePost.rejected, (state, action: any) => { builder.addCase(updatePost.rejected, (state, action: any) => {
state.updatePost.status = 'failed'; state.updatePost.status = 'failed';
state.updatePost.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// deletePost // deletePost
@@ -338,7 +353,13 @@ const postsSlice = createSlice({
); );
builder.addCase(deletePost.rejected, (state, action: any) => { builder.addCase(deletePost.rejected, (state, action: any) => {
state.deletePost.status = 'failed'; state.deletePost.status = 'failed';
state.deletePost.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
}, },
}); });

View File

@@ -1,5 +1,6 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from '../../axios'; import axios from '../../axios';
import { toastError } from '../../lib/toastNotification';
// ===================== // =====================
// Типы // Типы
@@ -131,9 +132,7 @@ export const createGroup = createAsyncThunk(
const response = await axios.post('/groups', { name, description }); const response = await axios.post('/groups', { name, description });
return response.data as Group; return response.data as Group;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при создании группы',
);
} }
}, },
); );
@@ -155,9 +154,7 @@ export const updateGroup = createAsyncThunk(
}); });
return response.data as Group; return response.data as Group;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при обновлении группы',
);
} }
}, },
); );
@@ -169,9 +166,7 @@ export const deleteGroup = createAsyncThunk(
await axios.delete(`/groups/${groupId}`); await axios.delete(`/groups/${groupId}`);
return groupId; return groupId;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при удалении группы',
);
} }
}, },
); );
@@ -183,9 +178,7 @@ export const fetchMyGroups = createAsyncThunk(
const response = await axios.get('/groups/my'); const response = await axios.get('/groups/my');
return response.data.groups as Group[]; return response.data.groups as Group[];
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при получении групп',
);
} }
}, },
); );
@@ -197,9 +190,7 @@ export const fetchGroupById = createAsyncThunk(
const response = await axios.get(`/groups/${groupId}`); const response = await axios.get(`/groups/${groupId}`);
return response.data as Group; return response.data as Group;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при получении группы',
);
} }
}, },
); );
@@ -221,10 +212,7 @@ export const addGroupMember = createAsyncThunk(
}); });
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message ||
'Ошибка при добавлении участника',
);
} }
}, },
); );
@@ -239,9 +227,7 @@ export const removeGroupMember = createAsyncThunk(
await axios.delete(`/groups/${groupId}/members/${memberId}`); await axios.delete(`/groups/${groupId}/members/${memberId}`);
return { groupId, memberId }; return { groupId, memberId };
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при удалении участника',
);
} }
}, },
); );
@@ -258,10 +244,7 @@ export const fetchGroupJoinLink = createAsyncThunk(
const response = await axios.get(`/groups/${groupId}/join-link`); const response = await axios.get(`/groups/${groupId}/join-link`);
return response.data as { token: string; expiresAt: string }; return response.data as { token: string; expiresAt: string };
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message ||
'Ошибка при получении ссылки для присоединения',
);
} }
}, },
); );
@@ -274,10 +257,7 @@ export const joinGroupByToken = createAsyncThunk(
const response = await axios.post(`/groups/join/${token}`); const response = await axios.post(`/groups/join/${token}`);
return response.data as Group; return response.data as Group;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message ||
'Ошибка при присоединении к группе по ссылке',
);
} }
}, },
); );
@@ -314,7 +294,13 @@ const groupsSlice = createSlice({
); );
builder.addCase(fetchMyGroups.rejected, (state, action: any) => { builder.addCase(fetchMyGroups.rejected, (state, action: any) => {
state.fetchMyGroups.status = 'failed'; state.fetchMyGroups.status = 'failed';
state.fetchMyGroups.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// fetchGroupById // fetchGroupById
@@ -330,7 +316,13 @@ const groupsSlice = createSlice({
); );
builder.addCase(fetchGroupById.rejected, (state, action: any) => { builder.addCase(fetchGroupById.rejected, (state, action: any) => {
state.fetchGroupById.status = 'failed'; state.fetchGroupById.status = 'failed';
state.fetchGroupById.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// createGroup // createGroup
@@ -347,7 +339,13 @@ const groupsSlice = createSlice({
); );
builder.addCase(createGroup.rejected, (state, action: any) => { builder.addCase(createGroup.rejected, (state, action: any) => {
state.createGroup.status = 'failed'; state.createGroup.status = 'failed';
state.createGroup.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// updateGroup // updateGroup
@@ -370,7 +368,13 @@ const groupsSlice = createSlice({
); );
builder.addCase(updateGroup.rejected, (state, action: any) => { builder.addCase(updateGroup.rejected, (state, action: any) => {
state.updateGroup.status = 'failed'; state.updateGroup.status = 'failed';
state.updateGroup.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// deleteGroup // deleteGroup
@@ -391,7 +395,13 @@ const groupsSlice = createSlice({
); );
builder.addCase(deleteGroup.rejected, (state, action: any) => { builder.addCase(deleteGroup.rejected, (state, action: any) => {
state.deleteGroup.status = 'failed'; state.deleteGroup.status = 'failed';
state.deleteGroup.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// addGroupMember // addGroupMember
@@ -403,7 +413,13 @@ const groupsSlice = createSlice({
}); });
builder.addCase(addGroupMember.rejected, (state, action: any) => { builder.addCase(addGroupMember.rejected, (state, action: any) => {
state.addGroupMember.status = 'failed'; state.addGroupMember.status = 'failed';
state.addGroupMember.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// removeGroupMember // removeGroupMember
@@ -430,7 +446,13 @@ const groupsSlice = createSlice({
); );
builder.addCase(removeGroupMember.rejected, (state, action: any) => { builder.addCase(removeGroupMember.rejected, (state, action: any) => {
state.removeGroupMember.status = 'failed'; state.removeGroupMember.status = 'failed';
state.removeGroupMember.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// fetchGroupJoinLink // fetchGroupJoinLink
@@ -449,7 +471,13 @@ const groupsSlice = createSlice({
); );
builder.addCase(fetchGroupJoinLink.rejected, (state, action: any) => { builder.addCase(fetchGroupJoinLink.rejected, (state, action: any) => {
state.fetchGroupJoinLink.status = 'failed'; state.fetchGroupJoinLink.status = 'failed';
state.fetchGroupJoinLink.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
// joinGroupByToken // joinGroupByToken
@@ -466,7 +494,13 @@ const groupsSlice = createSlice({
); );
builder.addCase(joinGroupByToken.rejected, (state, action: any) => { builder.addCase(joinGroupByToken.rejected, (state, action: any) => {
state.joinGroupByToken.status = 'failed'; state.joinGroupByToken.status = 'failed';
state.joinGroupByToken.error = action.payload;
const errors = action.payload.errors as Record<string, string[]>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}); });
}, },
}); });

View File

@@ -1,5 +1,6 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from '../../axios'; import axios from '../../axios';
import { toastError } from '../../lib/toastNotification';
// ─── Типы ──────────────────────────────────────────── // ─── Типы ────────────────────────────────────────────
@@ -29,6 +30,9 @@ interface MissionsState {
missions: Mission[]; missions: Mission[];
currentMission: Mission | null; currentMission: Mission | null;
hasNextPage: boolean; hasNextPage: boolean;
create: {
errors?: Record<string, string[]>;
};
statuses: { statuses: {
fetchList: Status; fetchList: Status;
fetchById: Status; fetchById: Status;
@@ -45,6 +49,7 @@ const initialState: MissionsState = {
missions: [], missions: [],
currentMission: null, currentMission: null,
hasNextPage: false, hasNextPage: false,
create: {},
statuses: { statuses: {
fetchList: 'idle', fetchList: 'idle',
fetchById: 'idle', fetchById: 'idle',
@@ -79,9 +84,7 @@ export const fetchMissions = createAsyncThunk(
}); });
return response.data; // { missions, hasNextPage } return response.data; // { missions, hasNextPage }
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при получении миссий',
);
} }
}, },
); );
@@ -94,9 +97,7 @@ export const fetchMissionById = createAsyncThunk(
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( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при получении миссии',
);
} }
}, },
); );
@@ -109,10 +110,7 @@ export const fetchMyMissions = createAsyncThunk(
const response = await axios.get('/missions/my'); const response = await axios.get('/missions/my');
return response.data as Mission[]; // массив миссий пользователя return response.data as Mission[]; // массив миссий пользователя
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message ||
'Ошибка при получении моих миссий',
);
} }
}, },
); );
@@ -141,9 +139,7 @@ export const uploadMission = createAsyncThunk(
}); });
return response.data; // Mission return response.data; // Mission
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при загрузке миссии',
);
} }
}, },
); );
@@ -156,9 +152,7 @@ export const deleteMission = createAsyncThunk(
await axios.delete(`/missions/${id}`); await axios.delete(`/missions/${id}`);
return id; // возвращаем id удалённой миссии return id; // возвращаем id удалённой миссии
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data);
err.response?.data?.message || 'Ошибка при удалении миссии',
);
} }
}, },
); );
@@ -204,7 +198,16 @@ const missionsSlice = createSlice({
fetchMissions.rejected, fetchMissions.rejected,
(state, action: PayloadAction<any>) => { (state, action: PayloadAction<any>) => {
state.statuses.fetchList = 'failed'; state.statuses.fetchList = 'failed';
state.error = action.payload;
const errors = action.payload.errors as Record<
string,
string[]
>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}, },
); );
@@ -224,7 +227,16 @@ const missionsSlice = createSlice({
fetchMissionById.rejected, fetchMissionById.rejected,
(state, action: PayloadAction<any>) => { (state, action: PayloadAction<any>) => {
state.statuses.fetchById = 'failed'; state.statuses.fetchById = 'failed';
state.error = action.payload;
const errors = action.payload.errors as Record<
string,
string[]
>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}, },
); );
@@ -244,7 +256,16 @@ const missionsSlice = createSlice({
fetchMyMissions.rejected, fetchMyMissions.rejected,
(state, action: PayloadAction<any>) => { (state, action: PayloadAction<any>) => {
state.statuses.fetchMy = 'failed'; state.statuses.fetchMy = 'failed';
state.error = action.payload;
const errors = action.payload.errors as Record<
string,
string[]
>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}, },
); );
@@ -264,7 +285,18 @@ const missionsSlice = createSlice({
uploadMission.rejected, uploadMission.rejected,
(state, action: PayloadAction<any>) => { (state, action: PayloadAction<any>) => {
state.statuses.upload = 'failed'; state.statuses.upload = 'failed';
state.error = action.payload;
const errors = action.payload.errors as Record<
string,
string[]
>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
state.create.errors = errors;
}, },
); );
@@ -290,7 +322,16 @@ const missionsSlice = createSlice({
deleteMission.rejected, deleteMission.rejected,
(state, action: PayloadAction<any>) => { (state, action: PayloadAction<any>) => {
state.statuses.delete = 'failed'; state.statuses.delete = 'failed';
state.error = action.payload;
const errors = action.payload.errors as Record<
string,
string[]
>;
Object.values(errors).forEach((messages) => {
messages.forEach((msg) => {
toastError(msg);
});
});
}, },
); );
}, },

View File

@@ -23,7 +23,11 @@ const Account = () => {
const username = query.get('username') ?? myname ?? ''; const username = query.get('username') ?? myname ?? '';
useEffect(() => { useEffect(() => {
if (username == myname) dispatch(setMenuActivePage('account')); if (username == myname) {
dispatch(setMenuActivePage('account'));
} else {
dispatch(setMenuActivePage(''));
}
dispatch( dispatch(
fetchProfileMissions({ fetchProfileMissions({
username: username, username: username,

View File

@@ -1,4 +1,3 @@
import { PrimaryButton } from '../../../components/button/PrimaryButton';
import { ReverseButton } from '../../../components/button/ReverseButton'; import { ReverseButton } from '../../../components/button/ReverseButton';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import { logout } from '../../../redux/slices/auth'; import { logout } from '../../../redux/slices/auth';
@@ -77,13 +76,13 @@ const RightPanel = () => {
)}`} )}`}
</div> </div>
{username == myname && ( {/* {username == myname && (
<PrimaryButton <PrimaryButton
onClick={() => {}} onClick={() => {}}
text="Редактировать" text="Редактировать"
className="w-full" className="w-full"
/> />
)} )} */}
<div className="h-[1px] w-full bg-liquid-lighter"></div> <div className="h-[1px] w-full bg-liquid-lighter"></div>

View File

@@ -8,6 +8,8 @@ import {
setMissionsStatus, setMissionsStatus,
} from '../../../../redux/slices/missions'; } from '../../../../redux/slices/missions';
import ConfirmModal from '../../../../components/modal/ConfirmModal'; import ConfirmModal from '../../../../components/modal/ConfirmModal';
import { fetchProfileMissions } from '../../../../redux/slices/profile';
import { useQuery } from '../../../../hooks/useQuery';
interface ItemProps { interface ItemProps {
count: number; count: number;
@@ -50,6 +52,10 @@ const Missions = () => {
(state) => state.profile.missions, (state) => state.profile.missions,
); );
const myname = useAppSelector((state) => state.auth.username);
const query = useQuery();
const username = query.get('username') ?? myname ?? '';
useEffect(() => { useEffect(() => {
dispatch(setMenuActiveProfilePage('missions')); dispatch(setMenuActiveProfilePage('missions'));
}, []); }, []);
@@ -115,7 +121,17 @@ const Missions = () => {
confirmColor="error" confirmColor="error"
confirmText="Удалить" confirmText="Удалить"
onConfirmClick={() => { onConfirmClick={() => {
dispatch(deleteMission(taskdeleteId)); dispatch(deleteMission(taskdeleteId))
.unwrap()
.then(() => {
dispatch(
fetchProfileMissions({
username: username,
recentPageSize: 1,
authoredPageSize: 100,
}),
);
});
}} }}
/> />
</div> </div>

View File

@@ -7,6 +7,7 @@ import GroupMenu from './GroupMenu';
import { Posts } from './posts/Posts'; import { Posts } from './posts/Posts';
import { Chat } from './chat/Chat'; import { Chat } from './chat/Chat';
import { Contests } from './contests/Contests'; import { Contests } from './contests/Contests';
import { setMenuActivePage } from '../../../redux/slices/store';
interface GroupsBlockProps {} interface GroupsBlockProps {}
@@ -20,6 +21,7 @@ const Group: FC<GroupsBlockProps> = () => {
const group = useAppSelector((state) => state.groups.fetchGroupById.group); const group = useAppSelector((state) => state.groups.fetchGroupById.group);
useEffect(() => { useEffect(() => {
dispatch(setMenuActivePage('groups'));
dispatch(fetchGroupById(groupId)); dispatch(fetchGroupById(groupId));
}, [groupId]); }, [groupId]);

View File

@@ -2,7 +2,6 @@ import { FC, useEffect, useState } from 'react';
import { useAppSelector, useAppDispatch } from '../../../../redux/hooks'; import { useAppSelector, useAppDispatch } from '../../../../redux/hooks';
import { fetchGroupPosts } from '../../../../redux/slices/groupfeed'; import { fetchGroupPosts } from '../../../../redux/slices/groupfeed';
import { SearchInput } from '../../../../components/input/SearchInput';
import { setMenuActiveGroupPage } from '../../../../redux/slices/store'; import { setMenuActiveGroupPage } from '../../../../redux/slices/store';
import { fetchGroupById } from '../../../../redux/slices/groups'; import { fetchGroupById } from '../../../../redux/slices/groups';
import { SecondaryButton } from '../../../../components/button/SecondaryButton'; import { SecondaryButton } from '../../../../components/button/SecondaryButton';
@@ -57,13 +56,6 @@ export const Posts: FC<PostsProps> = ({ groupId }) => {
<div className="h-full relative"> <div className="h-full relative">
<div className="grid grid-rows-[40px,1fr,40px] h-full relative min-h-0 gap-[20px]"> <div className="grid grid-rows-[40px,1fr,40px] h-full relative min-h-0 gap-[20px]">
<div className="h-[40px] mb-[20px] relative"> <div className="h-[40px] mb-[20px] relative">
<SearchInput
className="w-[216px]"
onChange={(v) => {
v;
}}
placeholder="Поиск сообщений"
/>
{isAdmin && ( {isAdmin && (
<div className=" h-[40px] w-[180px] absolute top-0 right-0 flex items-center"> <div className=" h-[40px] w-[180px] absolute top-0 right-0 flex items-center">
<SecondaryButton <SecondaryButton

View File

@@ -3,7 +3,6 @@ import {
Account, Account,
Clipboard, Clipboard,
Cup, Cup,
Home,
Openbook, Openbook,
Users, Users,
} from '../../../assets/icons/menu'; } from '../../../assets/icons/menu';
@@ -12,7 +11,6 @@ import { useAppSelector } from '../../../redux/hooks';
const Menu = () => { const Menu = () => {
const menuItems = [ const menuItems = [
{ text: 'Главная', href: '/home', icon: Home, page: 'home' },
{ {
text: 'Задачи', text: 'Задачи',
href: '/home/missions', href: '/home/missions',

View File

@@ -8,6 +8,10 @@ import {
setMissionsStatus, setMissionsStatus,
uploadMission, uploadMission,
} from '../../../redux/slices/missions'; } from '../../../redux/slices/missions';
import { toastSuccess } from '../../../lib/toastNotification';
import { cn } from '../../../lib/cn';
import { Link } from 'react-router-dom';
import { NumberInput } from '../../../components/input/NumberInput';
interface ModalCreateProps { interface ModalCreateProps {
active: boolean; active: boolean;
@@ -24,6 +28,8 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
const status = useAppSelector((state) => state.missions.statuses.upload); const status = useAppSelector((state) => state.missions.statuses.upload);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [clickSubmit, setClickSubmit] = useState<boolean>(false);
const addTag = () => { const addTag = () => {
const newTag = tagInput.trim(); const newTag = tagInput.trim();
if (newTag && !tags.includes(newTag)) { if (newTag && !tags.includes(newTag)) {
@@ -43,13 +49,14 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) return alert('Выберите файл миссии!'); setClickSubmit(true);
if (!file) return;
dispatch(uploadMission({ file, name, difficulty, tags })); dispatch(uploadMission({ file, name, difficulty, tags }));
}; };
useEffect(() => { useEffect(() => {
if (status === 'successful') { if (status === 'successful') {
alert('Миссия успешно загружена!'); toastSuccess('Миссия создана!');
setName(''); setName('');
setDifficulty(1); setDifficulty(1);
setTags([]); setTags([]);
@@ -60,9 +67,18 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
}, [status]); }, [status]);
useEffect(() => { useEffect(() => {
if (active == true) {
setClickSubmit(false);
}
dispatch(setMissionsStatus({ key: 'upload', status: 'idle' })); dispatch(setMissionsStatus({ key: 'upload', status: 'idle' }));
}, [active]); }, [active]);
const getNameErrorMessage = (): string => {
if (!clickSubmit) return '';
if (name == '') return 'Поле не может быть пустым';
return '';
};
return ( return (
<Modal <Modal
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white" className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
@@ -82,16 +98,17 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
defaultState={name} defaultState={name}
onChange={setName} onChange={setName}
placeholder="В яблочко" placeholder="В яблочко"
error={getNameErrorMessage()}
/> />
<Input <NumberInput
name="difficulty" name="difficulty"
autocomplete="difficulty"
className="mt-[10px]" className="mt-[10px]"
type="number"
label="Сложность" label="Сложность"
defaultState={'' + difficulty} defaultState={difficulty}
onChange={(v) => setDifficulty(Number(v))} minValue={1}
maxValue={3500}
onChange={(v) => setDifficulty(v)}
placeholder="1" placeholder="1"
/> />
@@ -106,6 +123,16 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
className="hidden" className="hidden"
/> />
</label> </label>
{
<div
className={cn(
'text-liquid-red text-[14px] h-auto text-left mt-[5px] whitespace-pre-line overflow-hidden ',
(!clickSubmit || file) && 'h-0 mt-0',
)}
>
Необходимо выбрать файл задачи
</div>
}
</div> </div>
{/* Теги */} {/* Теги */}
@@ -148,6 +175,17 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
</div> </div>
</div> </div>
<div>
Создать пакет задачи можно на платформе{' '}
<Link
to={'https://polygon.codeforces.com'}
target="_blank"
className="text-[#7489ff] hover:text-[#8c9dfd] transition-color duration-300"
>
polygon
</Link>
</div>
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]"> <div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
<PrimaryButton <PrimaryButton
onClick={handleSubmit} onClick={handleSubmit}
@@ -159,8 +197,6 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
text="Отмена" text="Отмена"
/> />
</div> </div>
{status == 'failed' && <div>error</div>}
</div> </div>
</Modal> </Modal>
); );

View File

@@ -1,5 +1,5 @@
import { FC, Fragment, useEffect, useState } from 'react'; import { FC, Fragment, useEffect, useState } from 'react';
import { Navigate, useParams } from 'react-router-dom'; import { Navigate, useNavigate, useParams } from 'react-router-dom';
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
import { fetchGroupById, GroupMember } from '../../../../redux/slices/groups'; import { fetchGroupById, GroupMember } from '../../../../redux/slices/groups';
import { Edit } from '../../../../assets/icons/input'; import { Edit } from '../../../../assets/icons/input';
@@ -13,6 +13,7 @@ export const GroupRightPanel: FC = () => {
return <Navigate to="/home/groups" replace />; return <Navigate to="/home/groups" replace />;
} }
const navigate = useNavigate();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [user, setUser] = useState<GroupMember | undefined>(); const [user, setUser] = useState<GroupMember | undefined>();
@@ -56,7 +57,14 @@ export const GroupRightPanel: FC = () => {
return ( return (
<Fragment key={i}> <Fragment key={i}>
{ {
<div className="text-liquid-light text-[16px] grid grid-cols-[40px,1fr] gap-[10px] items-center cursor-pointer hover:bg-liquid-lighter transition-all duration-300 rounded-[10px] p-[5px] group"> <div
className="text-liquid-light text-[16px] grid grid-cols-[40px,1fr] gap-[10px] items-center cursor-pointer hover:bg-liquid-lighter transition-all duration-300 rounded-[10px] p-[5px] group"
onClick={() => {
navigate(
`/home/account/missions?username=${v.username}`,
);
}}
>
<div className="h-[40px] w-[40px] rounded-[10px] bg-[#D9D9D9]"></div> <div className="h-[40px] w-[40px] rounded-[10px] bg-[#D9D9D9]"></div>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="text-liquid-white font-bold text-[16px] leading-5"> <div className="text-liquid-white font-bold text-[16px] leading-5">
@@ -73,7 +81,8 @@ export const GroupRightPanel: FC = () => {
!v.role.includes('Creator') && ( !v.role.includes('Creator') && (
<div <div
className="h-[34px] w-[34px] absolute right-[34px] opacity-0 group-hover:opacity-100 transition-all duration-300 hover:bg-liquid-light rounded-[10px] p-[5px] active:scale-90" className="h-[34px] w-[34px] absolute right-[34px] opacity-0 group-hover:opacity-100 transition-all duration-300 hover:bg-liquid-light rounded-[10px] p-[5px] active:scale-90"
onClick={() => { onClick={(e) => {
e.stopPropagation();
if ( if (
Number(userId) == v.userId Number(userId) == v.userId
) { ) {