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}
name={name}
autoComplete={autocomplete}
autoComplete={autocomplete || undefined}
type={
type == 'password'
? visible

View File

@@ -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]"

View File

@@ -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>

View File

@@ -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);
});
});
});
},
});

View File

@@ -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);
});
});
},
);
},

View File

@@ -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);
});
});
});
},
});

View File

@@ -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);
});
});
});
},
});

View File

@@ -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);
});
});
});
},
});

View File

@@ -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);
});
});
},
);
},

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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]);

View File

@@ -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

View File

@@ -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',

View File

@@ -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>
);

View File

@@ -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
) {