add error toasts
This commit is contained in:
@@ -58,7 +58,7 @@ export const Input: React.FC<inputProps> = ({
|
||||
)}
|
||||
value={value}
|
||||
name={name}
|
||||
autoComplete={autocomplete}
|
||||
autoComplete={autocomplete || undefined}
|
||||
type={
|
||||
type == 'password'
|
||||
? visible
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../redux/slices/articles';
|
||||
import { useQuery } from '../hooks/useQuery';
|
||||
import { ReverseButton } from '../components/button/ReverseButton';
|
||||
import { cn } from '../lib/cn';
|
||||
|
||||
const ArticleEditor = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -24,6 +25,7 @@ const ArticleEditor = () => {
|
||||
const back = query.get('back') ?? undefined;
|
||||
const articleId = Number(query.get('articleId') ?? undefined);
|
||||
const refactor = articleId && !isNaN(articleId);
|
||||
const [clickSubmit, setClickSubmit] = useState<boolean>(false);
|
||||
|
||||
// Достаём данные из redux
|
||||
const article = useAppSelector(
|
||||
@@ -61,7 +63,6 @@ const ArticleEditor = () => {
|
||||
const removeTag = (tagToRemove: string) => {
|
||||
setTags(tags.filter((tag) => tag !== tagToRemove));
|
||||
};
|
||||
|
||||
// ==========================
|
||||
// Эффекты по статусам
|
||||
// ==========================
|
||||
@@ -96,6 +97,7 @@ const ArticleEditor = () => {
|
||||
// Получение статьи
|
||||
// ==========================
|
||||
useEffect(() => {
|
||||
setClickSubmit(false);
|
||||
if (articleId) {
|
||||
dispatch(fetchArticleById(articleId));
|
||||
}
|
||||
@@ -110,6 +112,18 @@ const ArticleEditor = () => {
|
||||
}
|
||||
}, [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]">
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
setClickSubmit(true);
|
||||
dispatch(
|
||||
updateArticle({
|
||||
articleId,
|
||||
@@ -163,6 +178,7 @@ const ArticleEditor = () => {
|
||||
) : (
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
setClickSubmit(true);
|
||||
dispatch(
|
||||
createArticle({
|
||||
name,
|
||||
@@ -188,6 +204,7 @@ const ArticleEditor = () => {
|
||||
label="Название"
|
||||
onChange={setName}
|
||||
placeholder="Новая статья"
|
||||
error={getNameErrorMessage()}
|
||||
/>
|
||||
|
||||
{/* Теги */}
|
||||
@@ -236,6 +253,14 @@ const ArticleEditor = () => {
|
||||
text="Редактировать текст"
|
||||
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
|
||||
content={code}
|
||||
className="bg-transparent border-liquid-lighter border-[3px] rounded-[20px] mt-[20px]"
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
// 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 Register from '../views/home/auth/Register';
|
||||
import Menu from '../views/home/menu/Menu';
|
||||
import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
||||
import { useEffect } from 'react';
|
||||
import { fetchWhoAmI, logout } from '../redux/slices/auth';
|
||||
import { fetchWhoAmI } from '../redux/slices/auth';
|
||||
import Missions from '../views/home/missions/Missions';
|
||||
import Articles from '../views/home/articles/Articles';
|
||||
import Groups from '../views/home/groups/Groups';
|
||||
import Contests from '../views/home/contests/Contests';
|
||||
import { PrimaryButton } from '../components/button/PrimaryButton';
|
||||
import Group from '../views/home/group/Group';
|
||||
import Contest from '../views/home/contest/Contest';
|
||||
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 { GroupRightPanel } from '../views/home/rightpanel/group/Group';
|
||||
import GroupInvite from '../views/home/groupinviter/GroupInvite';
|
||||
import {
|
||||
toastError,
|
||||
toastSuccess,
|
||||
toastWarning,
|
||||
} from '../lib/toastNotification';
|
||||
|
||||
const Home = () => {
|
||||
const name = useAppSelector((state) => state.auth.username);
|
||||
const jwt = useAppSelector((state) => state.auth.jwt);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -59,53 +52,7 @@ const Home = () => {
|
||||
<Route path="contest/:contestId/*" element={<Contest />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
}
|
||||
element={<Navigate to="/home/account" replace />}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import axios from '../../axios';
|
||||
import { toastError } from '../../lib/toastNotification';
|
||||
|
||||
// =====================
|
||||
// Типы
|
||||
@@ -120,9 +121,7 @@ export const fetchArticles = createAsyncThunk(
|
||||
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при получении статей',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -135,10 +134,7 @@ export const fetchMyArticles = createAsyncThunk(
|
||||
const response = await axios.get<Article[]>('/articles/my');
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Ошибка при получении моих статей',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -151,9 +147,7 @@ export const fetchArticleById = createAsyncThunk(
|
||||
const response = await axios.get<Article>(`/articles/${articleId}`);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при получении статьи',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -177,9 +171,7 @@ export const createArticle = createAsyncThunk(
|
||||
});
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при создании статьи',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -207,9 +199,7 @@ export const updateArticle = createAsyncThunk(
|
||||
);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при обновлении статьи',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -222,9 +212,7 @@ export const deleteArticle = createAsyncThunk(
|
||||
await axios.delete(`/articles/${articleId}`);
|
||||
return articleId;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при удалении статьи',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -263,7 +251,12 @@ const articlesSlice = createSlice({
|
||||
);
|
||||
builder.addCase(fetchArticles.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -280,7 +273,12 @@ const articlesSlice = createSlice({
|
||||
);
|
||||
builder.addCase(fetchMyArticles.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -297,7 +295,12 @@ const articlesSlice = createSlice({
|
||||
);
|
||||
builder.addCase(fetchArticleById.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -314,7 +317,14 @@ const articlesSlice = createSlice({
|
||||
);
|
||||
builder.addCase(createArticle.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -331,7 +341,14 @@ const articlesSlice = createSlice({
|
||||
);
|
||||
builder.addCase(updateArticle.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -355,7 +372,12 @@ const articlesSlice = createSlice({
|
||||
);
|
||||
builder.addCase(deleteArticle.rejected, (state, action: any) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import axios from '../../axios';
|
||||
import { toastError } from '../../lib/toastNotification';
|
||||
|
||||
// =====================
|
||||
// Типы
|
||||
@@ -280,10 +281,7 @@ export const fetchParticipatingContests = createAsyncThunk(
|
||||
);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Failed to fetch participating contests',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -297,9 +295,7 @@ export const fetchMySubmissions = createAsyncThunk(
|
||||
);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to fetch my submissions',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -321,9 +317,7 @@ export const fetchContests = createAsyncThunk(
|
||||
});
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to fetch contests',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -335,9 +329,7 @@ export const fetchContestById = createAsyncThunk(
|
||||
const response = await axios.get<Contest>(`/contests/${id}`);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to fetch contest',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -352,9 +344,7 @@ export const createContest = createAsyncThunk(
|
||||
);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to create contest',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -375,9 +365,7 @@ export const updateContest = createAsyncThunk(
|
||||
);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to update contest',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -389,9 +377,7 @@ export const deleteContest = createAsyncThunk(
|
||||
await axios.delete(`/contests/${contestId}`);
|
||||
return contestId;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to delete contest',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -403,9 +389,7 @@ export const fetchMyContests = createAsyncThunk(
|
||||
const response = await axios.get<Contest[]>('/contests/my');
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to fetch my contests',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -424,10 +408,7 @@ export const fetchRegisteredContests = createAsyncThunk(
|
||||
);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Failed to fetch registered contests',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -451,10 +432,7 @@ export const addOrUpdateContestMember = createAsyncThunk(
|
||||
);
|
||||
return { contestId, members: response.data };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Failed to add or update contest member',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -470,10 +448,7 @@ export const deleteContestMember = createAsyncThunk(
|
||||
await axios.delete(`/contests/${contestId}/members/${memberId}`);
|
||||
return { contestId, memberId };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Failed to delete contest member',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -488,10 +463,7 @@ export const startContestAttempt = createAsyncThunk(
|
||||
);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Failed to start contest attempt',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -506,9 +478,7 @@ export const fetchMyAttemptsInContest = createAsyncThunk(
|
||||
);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to fetch my attempts',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -531,10 +501,7 @@ export const fetchContestMembers = createAsyncThunk(
|
||||
);
|
||||
return { contestId, ...response.data };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Failed to fetch contest members',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -549,9 +516,7 @@ export const checkContestRegistration = createAsyncThunk(
|
||||
);
|
||||
return { contestId, registered: response.data.registered };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to check registration',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -566,10 +531,7 @@ export const fetchUpcomingEligibleContests = createAsyncThunk(
|
||||
);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Failed to fetch upcoming eligible contests',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -584,9 +546,7 @@ export const fetchMyAllAttempts = createAsyncThunk(
|
||||
);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to fetch my attempts',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -601,9 +561,7 @@ export const fetchMyActiveAttempt = createAsyncThunk(
|
||||
);
|
||||
return { contestId, attempt: response.data };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Failed to fetch active attempt',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -642,7 +600,13 @@ const contestsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(fetchMySubmissions.rejected, (state, action: any) => {
|
||||
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) => {
|
||||
@@ -658,7 +622,13 @@ const contestsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(fetchContests.rejected, (state, action: any) => {
|
||||
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) => {
|
||||
@@ -673,7 +643,13 @@ const contestsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(fetchContestById.rejected, (state, action: any) => {
|
||||
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) => {
|
||||
@@ -688,7 +664,13 @@ const contestsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(createContest.rejected, (state, action: any) => {
|
||||
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) => {
|
||||
@@ -703,7 +685,13 @@ const contestsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(updateContest.rejected, (state, action: any) => {
|
||||
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) => {
|
||||
@@ -725,7 +713,13 @@ const contestsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(deleteContest.rejected, (state, action: any) => {
|
||||
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) => {
|
||||
@@ -740,7 +734,13 @@ const contestsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(fetchMyContests.rejected, (state, action: any) => {
|
||||
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) => {
|
||||
@@ -760,7 +760,15 @@ const contestsSlice = createSlice({
|
||||
fetchRegisteredContests.rejected,
|
||||
(state, action: any) => {
|
||||
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) => {
|
||||
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) => {
|
||||
@@ -800,7 +814,15 @@ const contestsSlice = createSlice({
|
||||
addOrUpdateContestMember.rejected,
|
||||
(state, action: any) => {
|
||||
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) => {
|
||||
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) => {
|
||||
@@ -827,7 +855,13 @@ const contestsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(startContestAttempt.rejected, (state, action: any) => {
|
||||
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) => {
|
||||
@@ -844,7 +878,15 @@ const contestsSlice = createSlice({
|
||||
fetchMyAttemptsInContest.rejected,
|
||||
(state, action: any) => {
|
||||
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) => {
|
||||
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) => {
|
||||
@@ -881,7 +929,13 @@ const contestsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(fetchMyActiveAttempt.rejected, (state, action: any) => {
|
||||
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) => {
|
||||
@@ -904,7 +958,15 @@ const contestsSlice = createSlice({
|
||||
checkContestRegistration.rejected,
|
||||
(state, action: any) => {
|
||||
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,
|
||||
(state, action: any) => {
|
||||
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,
|
||||
(state, action: any) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import axios from '../../axios';
|
||||
import { toastError } from '../../lib/toastNotification';
|
||||
|
||||
// =========================================
|
||||
// Типы
|
||||
@@ -81,10 +82,7 @@ export const fetchGroupMessages = createAsyncThunk(
|
||||
messages: response.data as ChatMessage[],
|
||||
};
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Ошибка при получении сообщений группы',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -99,9 +97,7 @@ export const sendGroupMessage = createAsyncThunk(
|
||||
});
|
||||
return response.data as ChatMessage;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при отправке сообщения',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -167,7 +163,12 @@ const groupChatSlice = createSlice({
|
||||
|
||||
builder.addCase(fetchGroupMessages.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -188,7 +189,12 @@ const groupChatSlice = createSlice({
|
||||
|
||||
builder.addCase(sendGroupMessage.rejected, (state, action: any) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import axios from '../../axios';
|
||||
import { toastError } from '../../lib/toastNotification';
|
||||
|
||||
// =====================
|
||||
// Типы
|
||||
@@ -104,9 +105,7 @@ export const fetchGroupPosts = createAsyncThunk(
|
||||
);
|
||||
return { page, data: response.data as PostsPage };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка загрузки постов',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -124,9 +123,7 @@ export const fetchPostById = createAsyncThunk(
|
||||
);
|
||||
return response.data as Post;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка загрузки поста',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -149,9 +146,7 @@ export const createPost = createAsyncThunk(
|
||||
});
|
||||
return response.data as Post;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка создания поста',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -183,9 +178,7 @@ export const updatePost = createAsyncThunk(
|
||||
);
|
||||
return response.data as Post;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка обновления поста',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -201,9 +194,7 @@ export const deletePost = createAsyncThunk(
|
||||
await axios.delete(`/groups/${groupId}/feed/${postId}`);
|
||||
return postId;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка удаления поста',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -244,7 +235,13 @@ const postsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(fetchGroupPosts.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -260,7 +257,13 @@ const postsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(fetchPostById.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -281,7 +284,13 @@ const postsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(createPost.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -310,7 +319,13 @@ const postsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(updatePost.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -338,7 +353,13 @@ const postsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(deletePost.rejected, (state, action: any) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
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 });
|
||||
return response.data as Group;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при создании группы',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -155,9 +154,7 @@ export const updateGroup = createAsyncThunk(
|
||||
});
|
||||
return response.data as Group;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при обновлении группы',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -169,9 +166,7 @@ export const deleteGroup = createAsyncThunk(
|
||||
await axios.delete(`/groups/${groupId}`);
|
||||
return groupId;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при удалении группы',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -183,9 +178,7 @@ export const fetchMyGroups = createAsyncThunk(
|
||||
const response = await axios.get('/groups/my');
|
||||
return response.data.groups as Group[];
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при получении групп',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -197,9 +190,7 @@ export const fetchGroupById = createAsyncThunk(
|
||||
const response = await axios.get(`/groups/${groupId}`);
|
||||
return response.data as Group;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при получении группы',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -221,10 +212,7 @@ export const addGroupMember = createAsyncThunk(
|
||||
});
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Ошибка при добавлении участника',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -239,9 +227,7 @@ export const removeGroupMember = createAsyncThunk(
|
||||
await axios.delete(`/groups/${groupId}/members/${memberId}`);
|
||||
return { groupId, memberId };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при удалении участника',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -258,10 +244,7 @@ export const fetchGroupJoinLink = createAsyncThunk(
|
||||
const response = await axios.get(`/groups/${groupId}/join-link`);
|
||||
return response.data as { token: string; expiresAt: string };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Ошибка при получении ссылки для присоединения',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -274,10 +257,7 @@ export const joinGroupByToken = createAsyncThunk(
|
||||
const response = await axios.post(`/groups/join/${token}`);
|
||||
return response.data as Group;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Ошибка при присоединении к группе по ссылке',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -314,7 +294,13 @@ const groupsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(fetchMyGroups.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -330,7 +316,13 @@ const groupsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(fetchGroupById.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -347,7 +339,13 @@ const groupsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(createGroup.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -370,7 +368,13 @@ const groupsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(updateGroup.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -391,7 +395,13 @@ const groupsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(deleteGroup.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -403,7 +413,13 @@ const groupsSlice = createSlice({
|
||||
});
|
||||
builder.addCase(addGroupMember.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -430,7 +446,13 @@ const groupsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(removeGroupMember.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -449,7 +471,13 @@ const groupsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(fetchGroupJoinLink.rejected, (state, action: any) => {
|
||||
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
|
||||
@@ -466,7 +494,13 @@ const groupsSlice = createSlice({
|
||||
);
|
||||
builder.addCase(joinGroupByToken.rejected, (state, action: any) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import axios from '../../axios';
|
||||
import { toastError } from '../../lib/toastNotification';
|
||||
|
||||
// ─── Типы ────────────────────────────────────────────
|
||||
|
||||
@@ -29,6 +30,9 @@ interface MissionsState {
|
||||
missions: Mission[];
|
||||
currentMission: Mission | null;
|
||||
hasNextPage: boolean;
|
||||
create: {
|
||||
errors?: Record<string, string[]>;
|
||||
};
|
||||
statuses: {
|
||||
fetchList: Status;
|
||||
fetchById: Status;
|
||||
@@ -45,6 +49,7 @@ const initialState: MissionsState = {
|
||||
missions: [],
|
||||
currentMission: null,
|
||||
hasNextPage: false,
|
||||
create: {},
|
||||
statuses: {
|
||||
fetchList: 'idle',
|
||||
fetchById: 'idle',
|
||||
@@ -79,9 +84,7 @@ export const fetchMissions = createAsyncThunk(
|
||||
});
|
||||
return response.data; // { missions, hasNextPage }
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при получении миссий',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -94,9 +97,7 @@ export const fetchMissionById = createAsyncThunk(
|
||||
const response = await axios.get(`/missions/${id}`);
|
||||
return response.data; // Mission
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при получении миссии',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -109,10 +110,7 @@ export const fetchMyMissions = createAsyncThunk(
|
||||
const response = await axios.get('/missions/my');
|
||||
return response.data as Mission[]; // массив миссий пользователя
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message ||
|
||||
'Ошибка при получении моих миссий',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -141,9 +139,7 @@ export const uploadMission = createAsyncThunk(
|
||||
});
|
||||
return response.data; // Mission
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при загрузке миссии',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -156,9 +152,7 @@ export const deleteMission = createAsyncThunk(
|
||||
await axios.delete(`/missions/${id}`);
|
||||
return id; // возвращаем id удалённой миссии
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка при удалении миссии',
|
||||
);
|
||||
return rejectWithValue(err.response?.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -204,7 +198,16 @@ const missionsSlice = createSlice({
|
||||
fetchMissions.rejected,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
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,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
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,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
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,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
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,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -23,7 +23,11 @@ const Account = () => {
|
||||
const username = query.get('username') ?? myname ?? '';
|
||||
|
||||
useEffect(() => {
|
||||
if (username == myname) dispatch(setMenuActivePage('account'));
|
||||
if (username == myname) {
|
||||
dispatch(setMenuActivePage('account'));
|
||||
} else {
|
||||
dispatch(setMenuActivePage(''));
|
||||
}
|
||||
dispatch(
|
||||
fetchProfileMissions({
|
||||
username: username,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||
import { ReverseButton } from '../../../components/button/ReverseButton';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import { logout } from '../../../redux/slices/auth';
|
||||
@@ -77,13 +76,13 @@ const RightPanel = () => {
|
||||
)}`}
|
||||
</div>
|
||||
|
||||
{username == myname && (
|
||||
{/* {username == myname && (
|
||||
<PrimaryButton
|
||||
onClick={() => {}}
|
||||
text="Редактировать"
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
<div className="h-[1px] w-full bg-liquid-lighter"></div>
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
setMissionsStatus,
|
||||
} from '../../../../redux/slices/missions';
|
||||
import ConfirmModal from '../../../../components/modal/ConfirmModal';
|
||||
import { fetchProfileMissions } from '../../../../redux/slices/profile';
|
||||
import { useQuery } from '../../../../hooks/useQuery';
|
||||
|
||||
interface ItemProps {
|
||||
count: number;
|
||||
@@ -50,6 +52,10 @@ const Missions = () => {
|
||||
(state) => state.profile.missions,
|
||||
);
|
||||
|
||||
const myname = useAppSelector((state) => state.auth.username);
|
||||
const query = useQuery();
|
||||
const username = query.get('username') ?? myname ?? '';
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActiveProfilePage('missions'));
|
||||
}, []);
|
||||
@@ -115,7 +121,17 @@ const Missions = () => {
|
||||
confirmColor="error"
|
||||
confirmText="Удалить"
|
||||
onConfirmClick={() => {
|
||||
dispatch(deleteMission(taskdeleteId));
|
||||
dispatch(deleteMission(taskdeleteId))
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
dispatch(
|
||||
fetchProfileMissions({
|
||||
username: username,
|
||||
recentPageSize: 1,
|
||||
authoredPageSize: 100,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import GroupMenu from './GroupMenu';
|
||||
import { Posts } from './posts/Posts';
|
||||
import { Chat } from './chat/Chat';
|
||||
import { Contests } from './contests/Contests';
|
||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||
|
||||
interface GroupsBlockProps {}
|
||||
|
||||
@@ -20,6 +21,7 @@ const Group: FC<GroupsBlockProps> = () => {
|
||||
const group = useAppSelector((state) => state.groups.fetchGroupById.group);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage('groups'));
|
||||
dispatch(fetchGroupById(groupId));
|
||||
}, [groupId]);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { FC, useEffect, useState } from 'react';
|
||||
|
||||
import { useAppSelector, useAppDispatch } from '../../../../redux/hooks';
|
||||
import { fetchGroupPosts } from '../../../../redux/slices/groupfeed';
|
||||
import { SearchInput } from '../../../../components/input/SearchInput';
|
||||
import { setMenuActiveGroupPage } from '../../../../redux/slices/store';
|
||||
import { fetchGroupById } from '../../../../redux/slices/groups';
|
||||
import { SecondaryButton } from '../../../../components/button/SecondaryButton';
|
||||
@@ -57,13 +56,6 @@ export const Posts: FC<PostsProps> = ({ groupId }) => {
|
||||
<div className="h-full relative">
|
||||
<div className="grid grid-rows-[40px,1fr,40px] h-full relative min-h-0 gap-[20px]">
|
||||
<div className="h-[40px] mb-[20px] relative">
|
||||
<SearchInput
|
||||
className="w-[216px]"
|
||||
onChange={(v) => {
|
||||
v;
|
||||
}}
|
||||
placeholder="Поиск сообщений"
|
||||
/>
|
||||
{isAdmin && (
|
||||
<div className=" h-[40px] w-[180px] absolute top-0 right-0 flex items-center">
|
||||
<SecondaryButton
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Account,
|
||||
Clipboard,
|
||||
Cup,
|
||||
Home,
|
||||
Openbook,
|
||||
Users,
|
||||
} from '../../../assets/icons/menu';
|
||||
@@ -12,7 +11,6 @@ import { useAppSelector } from '../../../redux/hooks';
|
||||
|
||||
const Menu = () => {
|
||||
const menuItems = [
|
||||
{ text: 'Главная', href: '/home', icon: Home, page: 'home' },
|
||||
{
|
||||
text: 'Задачи',
|
||||
href: '/home/missions',
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
setMissionsStatus,
|
||||
uploadMission,
|
||||
} 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 {
|
||||
active: boolean;
|
||||
@@ -24,6 +28,8 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
||||
const status = useAppSelector((state) => state.missions.statuses.upload);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [clickSubmit, setClickSubmit] = useState<boolean>(false);
|
||||
|
||||
const addTag = () => {
|
||||
const newTag = tagInput.trim();
|
||||
if (newTag && !tags.includes(newTag)) {
|
||||
@@ -43,13 +49,14 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!file) return alert('Выберите файл миссии!');
|
||||
setClickSubmit(true);
|
||||
if (!file) return;
|
||||
dispatch(uploadMission({ file, name, difficulty, tags }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'successful') {
|
||||
alert('Миссия успешно загружена!');
|
||||
toastSuccess('Миссия создана!');
|
||||
setName('');
|
||||
setDifficulty(1);
|
||||
setTags([]);
|
||||
@@ -60,9 +67,18 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (active == true) {
|
||||
setClickSubmit(false);
|
||||
}
|
||||
dispatch(setMissionsStatus({ key: 'upload', status: 'idle' }));
|
||||
}, [active]);
|
||||
|
||||
const getNameErrorMessage = (): string => {
|
||||
if (!clickSubmit) return '';
|
||||
if (name == '') return 'Поле не может быть пустым';
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
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}
|
||||
onChange={setName}
|
||||
placeholder="В яблочко"
|
||||
error={getNameErrorMessage()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
<NumberInput
|
||||
name="difficulty"
|
||||
autocomplete="difficulty"
|
||||
className="mt-[10px]"
|
||||
type="number"
|
||||
label="Сложность"
|
||||
defaultState={'' + difficulty}
|
||||
onChange={(v) => setDifficulty(Number(v))}
|
||||
defaultState={difficulty}
|
||||
minValue={1}
|
||||
maxValue={3500}
|
||||
onChange={(v) => setDifficulty(v)}
|
||||
placeholder="1"
|
||||
/>
|
||||
|
||||
@@ -106,6 +123,16 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
||||
className="hidden"
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* Теги */}
|
||||
@@ -148,6 +175,17 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
||||
</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]">
|
||||
<PrimaryButton
|
||||
onClick={handleSubmit}
|
||||
@@ -159,8 +197,6 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
||||
text="Отмена"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status == 'failed' && <div>error</div>}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { fetchGroupById, GroupMember } from '../../../../redux/slices/groups';
|
||||
import { Edit } from '../../../../assets/icons/input';
|
||||
@@ -13,6 +13,7 @@ export const GroupRightPanel: FC = () => {
|
||||
return <Navigate to="/home/groups" replace />;
|
||||
}
|
||||
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [user, setUser] = useState<GroupMember | undefined>();
|
||||
@@ -56,7 +57,14 @@ export const GroupRightPanel: FC = () => {
|
||||
return (
|
||||
<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="flex flex-col">
|
||||
<div className="text-liquid-white font-bold text-[16px] leading-5">
|
||||
@@ -73,7 +81,8 @@ export const GroupRightPanel: FC = () => {
|
||||
!v.role.includes('Creator') && (
|
||||
<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"
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (
|
||||
Number(userId) == v.userId
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user