From 4a65aa4b5349f66cbb2ba0f1f3a707c62b6e77f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Wed, 5 Nov 2025 23:54:40 +0300 Subject: [PATCH 01/17] profile contests --- src/views/home/account/Account.tsx | 11 +- src/views/home/account/ContestsBlock.tsx | 18 --- .../account/{ => articles}/ArticlesBlock.tsx | 10 +- .../home/account/contests/ContestItem.tsx | 124 ++++++++++++++++++ src/views/home/account/contests/Contests.tsx | 62 +++++++++ .../home/account/contests/ContestsBlock.tsx | 76 +++++++++++ .../account/{ => missions}/MissionsBlock.tsx | 4 +- 7 files changed, 273 insertions(+), 32 deletions(-) delete mode 100644 src/views/home/account/ContestsBlock.tsx rename src/views/home/account/{ => articles}/ArticlesBlock.tsx (93%) create mode 100644 src/views/home/account/contests/ContestItem.tsx create mode 100644 src/views/home/account/contests/Contests.tsx create mode 100644 src/views/home/account/contests/ContestsBlock.tsx rename src/views/home/account/{ => missions}/MissionsBlock.tsx (75%) diff --git a/src/views/home/account/Account.tsx b/src/views/home/account/Account.tsx index 7dbf7af..5d4530a 100644 --- a/src/views/home/account/Account.tsx +++ b/src/views/home/account/Account.tsx @@ -1,9 +1,9 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import AccountMenu from './AccoutMenu'; import RightPanel from './RightPanel'; -import MissionsBlock from './MissionsBlock'; -import ContestsBlock from './ContestsBlock'; -import ArticlesBlock from './ArticlesBlock'; +import MissionsBlock from './missions/MissionsBlock'; +import Contests from './contests/Contests'; +import ArticlesBlock from './articles/ArticlesBlock'; import { useAppDispatch } from '../../../redux/hooks'; import { useEffect } from 'react'; import { setMenuActivePage } from '../../../redux/slices/store'; @@ -32,10 +32,7 @@ const Account = () => { path="articles" element={} /> - } - /> + } /> { - const dispatch = useAppDispatch(); - - useEffect(() => { - dispatch(setMenuActiveProfilePage('contests')); - }, []); - return ( -
- Пока пусто :( -
- ); -}; - -export default ContestsBlock; diff --git a/src/views/home/account/ArticlesBlock.tsx b/src/views/home/account/articles/ArticlesBlock.tsx similarity index 93% rename from src/views/home/account/ArticlesBlock.tsx rename to src/views/home/account/articles/ArticlesBlock.tsx index 1d4f141..eba0bc8 100644 --- a/src/views/home/account/ArticlesBlock.tsx +++ b/src/views/home/account/articles/ArticlesBlock.tsx @@ -1,9 +1,9 @@ import { FC, useEffect, useState } from 'react'; -import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; -import { setMenuActiveProfilePage } from '../../../redux/slices/store'; -import { cn } from '../../../lib/cn'; -import { ChevroneDown, Edit } from '../../../assets/icons/groups'; -import { fetchArticles } from '../../../redux/slices/articles'; +import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; +import { setMenuActiveProfilePage } from '../../../../redux/slices/store'; +import { cn } from '../../../../lib/cn'; +import { ChevroneDown, Edit } from '../../../../assets/icons/groups'; +import { fetchArticles } from '../../../../redux/slices/articles'; import { useNavigate } from 'react-router-dom'; diff --git a/src/views/home/account/contests/ContestItem.tsx b/src/views/home/account/contests/ContestItem.tsx new file mode 100644 index 0000000..2245aa1 --- /dev/null +++ b/src/views/home/account/contests/ContestItem.tsx @@ -0,0 +1,124 @@ +import { cn } from '../../../../lib/cn'; +import { Account } from '../../../../assets/icons/auth'; +import { PrimaryButton } from '../../../../components/button/PrimaryButton'; +import { ReverseButton } from '../../../../components/button/ReverseButton'; +import { useNavigate } from 'react-router-dom'; + +export interface ContestItemProps { + id: number; + name: string; + startAt: string; + duration: number; + members: number; + statusRegister: 'reg' | 'nonreg'; + type: 'first' | 'second'; +} + +function formatDate(dateString: string): string { + const date = new Date(dateString); + + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear(); + + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + return `${day}/${month}/${year}\n${hours}:${minutes}`; +} + +function formatWaitTime(ms: number): string { + const minutes = Math.floor(ms / 60000); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) { + const remainder = days % 10; + let suffix = 'дней'; + if (remainder === 1 && days !== 11) suffix = 'день'; + else if (remainder >= 2 && remainder <= 4 && (days < 10 || days > 20)) + suffix = 'дня'; + return `${days} ${suffix}`; + } else if (hours > 0) { + const mins = minutes % 60; + return mins > 0 ? `${hours} ч ${mins} мин` : `${hours} ч`; + } else { + return `${minutes} мин`; + } +} + +const ContestItem: React.FC = ({ + id, + name, + startAt, + duration, + members, + statusRegister, + type, +}) => { + const navigate = useNavigate(); + + const now = new Date(); + + const waitTime = new Date(startAt).getTime() - now.getTime(); + + return ( +
{ + navigate(`/contest/${id}`); + }} + > +
{name}
+
+ {/* {authors.map((v, i) =>

{v}

)} */} + valavshonok +
+
+ {formatDate(startAt)} +
+
{formatWaitTime(duration)}
+ {waitTime > 0 && ( +
+ {'До начала\n' + formatWaitTime(waitTime)} +
+ )} +
+
{members}
+ +
+
+ {statusRegister == 'reg' ? ( + <> + {' '} + { + e.stopPropagation(); + }} + text="Регистрация" + /> + + ) : ( + <> + {' '} + { + e.stopPropagation(); + }} + text="Вы записаны" + /> + + )} +
+
+ ); +}; + +export default ContestItem; diff --git a/src/views/home/account/contests/Contests.tsx b/src/views/home/account/contests/Contests.tsx new file mode 100644 index 0000000..a68f224 --- /dev/null +++ b/src/views/home/account/contests/Contests.tsx @@ -0,0 +1,62 @@ +import { useEffect, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; +import { setMenuActiveProfilePage } from '../../../../redux/slices/store'; +import { fetchContests } from '../../../../redux/slices/contests'; +import ContestsBlock from './ContestsBlock'; + +const Contests = () => { + const dispatch = useAppDispatch(); + const now = new Date(); + + const [modalActive, setModalActive] = useState(false); + + // Берём данные из Redux + const contests = useAppSelector((state) => state.contests.contests); + const status = useAppSelector((state) => state.contests.statuses.create); + const error = useAppSelector((state) => state.contests.error); + + // При загрузке страницы — выставляем активную вкладку и подгружаем контесты + useEffect(() => { + dispatch(fetchContests({})); + }, []); + + useEffect(() => { + dispatch(setMenuActiveProfilePage('contests')); + }, []); + + if (status == 'loading') { + return ( +
Загрузка контестов...
+ ); + } + + if (error) { + return
Ошибка: {error}
; + } + + return ( +
+ { + const endTime = new Date(contest.endsAt).getTime(); + return endTime >= now.getTime(); + })} + /> + + { + const endTime = new Date(contest.endsAt).getTime(); + return endTime < now.getTime(); + })} + /> +
+ ); +}; + +export default Contests; diff --git a/src/views/home/account/contests/ContestsBlock.tsx b/src/views/home/account/contests/ContestsBlock.tsx new file mode 100644 index 0000000..fbe2ded --- /dev/null +++ b/src/views/home/account/contests/ContestsBlock.tsx @@ -0,0 +1,76 @@ +import { useState, FC } from 'react'; +import { cn } from '../../../../lib/cn'; +import { ChevroneDown } from '../../../../assets/icons/groups'; +import ContestItem from './ContestItem'; +import { Contest } from '../../../../redux/slices/contests'; + +interface ContestsBlockProps { + contests: Contest[]; + title: string; + className?: string; + type?: string; +} + +const ContestsBlock: FC = ({ + contests, + title, + className, +}) => { + const [active, setActive] = useState(title != 'Скрытые'); + + return ( +
+
{ + setActive(!active); + }} + > + {title} + +
+
+
+
+ {contests.map((v, i) => ( + + ))} +
+
+
+
+ ); +}; + +export default ContestsBlock; diff --git a/src/views/home/account/MissionsBlock.tsx b/src/views/home/account/missions/MissionsBlock.tsx similarity index 75% rename from src/views/home/account/MissionsBlock.tsx rename to src/views/home/account/missions/MissionsBlock.tsx index 1fce2a8..c20a241 100644 --- a/src/views/home/account/MissionsBlock.tsx +++ b/src/views/home/account/missions/MissionsBlock.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -import { useAppDispatch } from '../../../redux/hooks'; -import { setMenuActiveProfilePage } from '../../../redux/slices/store'; +import { useAppDispatch } from '../../../../redux/hooks'; +import { setMenuActiveProfilePage } from '../../../../redux/slices/store'; const MissionsBlock = () => { const dispatch = useAppDispatch(); From dc6df1480e1ac8ace5aba173ed1e365e6d608ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:41:01 +0300 Subject: [PATCH 02/17] my contests --- src/redux/slices/contests.ts | 217 +++++++++++++----- src/views/home/account/contests/Contests.tsx | 88 +++---- .../home/account/contests/ContestsBlock.tsx | 52 +++-- .../home/account/contests/MyContestItem.tsx | 104 +++++++++ ...ontestItem.tsx => RegisterContestItem.tsx} | 0 src/views/home/contests/Contests.tsx | 10 +- src/views/home/contests/ModalCreate.tsx | 4 +- 7 files changed, 354 insertions(+), 121 deletions(-) create mode 100644 src/views/home/account/contests/MyContestItem.tsx rename src/views/home/account/contests/{ContestItem.tsx => RegisterContestItem.tsx} (100%) diff --git a/src/redux/slices/contests.ts b/src/redux/slices/contests.ts index a5302ec..1bbcae5 100644 --- a/src/redux/slices/contests.ts +++ b/src/redux/slices/contests.ts @@ -70,33 +70,70 @@ export interface CreateContestBody { type Status = 'idle' | 'loading' | 'successful' | 'failed'; interface ContestsState { - contests: Contest[]; - selectedContest: Contest | null; - hasNextPage: boolean; - statuses: { - fetchList: Status; - fetchById: Status; - create: Status; + fetchContests: { + contests: Contest[]; + hasNextPage: boolean; + status: Status; + error: string | null; + }; + fetchContestById: { + contest: Contest | null; + status: Status; + error: string | null; + }; + createContest: { + contest: Contest | null; + status: Status; + error: string | null; + }; + fetchMyContests: { + contests: Contest[]; + status: Status; + error: string | null; + }; + fetchRegisteredContests: { + contests: Contest[]; + hasNextPage: boolean; + status: Status; + error: string | null; }; - error: string | null; } const initialState: ContestsState = { - contests: [], - selectedContest: null, - hasNextPage: false, - statuses: { - fetchList: 'idle', - fetchById: 'idle', - create: 'idle', + fetchContests: { + contests: [], + hasNextPage: false, + status: 'idle', + error: null, + }, + fetchContestById: { + contest: null, + status: 'idle', + error: null, + }, + createContest: { + contest: null, + status: 'idle', + error: null, + }, + fetchMyContests: { + contests: [], + status: 'idle', + error: null, + }, + fetchRegisteredContests: { + contests: [], + hasNextPage: false, + status: 'idle', + error: null, }, - error: null, }; // ===================== // Async Thunks // ===================== +// Все контесты export const fetchContests = createAsyncThunk( 'contests/fetchAll', async ( @@ -121,6 +158,7 @@ export const fetchContests = createAsyncThunk( }, ); +// Контест по ID export const fetchContestById = createAsyncThunk( 'contests/fetchById', async (id: number, { rejectWithValue }) => { @@ -135,6 +173,7 @@ export const fetchContestById = createAsyncThunk( }, ); +// Создание контеста export const createContest = createAsyncThunk( 'contests/create', async (contestData: CreateContestBody, { rejectWithValue }) => { @@ -152,6 +191,45 @@ export const createContest = createAsyncThunk( }, ); +// Контесты, созданные мной +export const fetchMyContests = createAsyncThunk( + 'contests/fetchMyContests', + async (_, { rejectWithValue }) => { + try { + const response = await axios.get('/contests/my'); + // Возвращаем просто массив контестов + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Failed to fetch my contests', + ); + } + }, +); + +// Контесты, где я зарегистрирован +export const fetchRegisteredContests = createAsyncThunk( + 'contests/fetchRegisteredContests', + async ( + params: { page?: number; pageSize?: number } = {}, + { rejectWithValue }, + ) => { + try { + const { page = 0, pageSize = 10 } = params; + const response = await axios.get( + '/contests/registered', + { params: { page, pageSize } }, + ); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || + 'Failed to fetch registered contests', + ); + } + }, +); + // ===================== // Slice // ===================== @@ -161,77 +239,100 @@ const contestsSlice = createSlice({ initialState, reducers: { clearSelectedContest: (state) => { - state.selectedContest = null; - }, - setContestStatus: ( - state, - action: PayloadAction<{ - key: keyof ContestsState['statuses']; - status: Status; - }>, - ) => { - state.statuses[action.payload.key] = action.payload.status; + state.fetchContestById.contest = null; }, }, extraReducers: (builder) => { // fetchContests builder.addCase(fetchContests.pending, (state) => { - state.statuses.fetchList = 'loading'; - state.error = null; + state.fetchContests.status = 'loading'; + state.fetchContests.error = null; }); builder.addCase( fetchContests.fulfilled, (state, action: PayloadAction) => { - state.statuses.fetchList = 'successful'; - state.contests = action.payload.contests; - state.hasNextPage = action.payload.hasNextPage; - }, - ); - builder.addCase( - fetchContests.rejected, - (state, action: PayloadAction) => { - state.statuses.fetchList = 'failed'; - state.error = action.payload; + state.fetchContests.status = 'successful'; + state.fetchContests.contests = action.payload.contests; + state.fetchContests.hasNextPage = action.payload.hasNextPage; }, ); + builder.addCase(fetchContests.rejected, (state, action: any) => { + state.fetchContests.status = 'failed'; + state.fetchContests.error = action.payload; + }); // fetchContestById builder.addCase(fetchContestById.pending, (state) => { - state.statuses.fetchById = 'loading'; - state.error = null; + state.fetchContestById.status = 'loading'; + state.fetchContestById.error = null; }); builder.addCase( fetchContestById.fulfilled, (state, action: PayloadAction) => { - state.statuses.fetchById = 'successful'; - state.selectedContest = action.payload; - }, - ); - builder.addCase( - fetchContestById.rejected, - (state, action: PayloadAction) => { - state.statuses.fetchById = 'failed'; - state.error = action.payload; + state.fetchContestById.status = 'successful'; + state.fetchContestById.contest = action.payload; }, ); + builder.addCase(fetchContestById.rejected, (state, action: any) => { + state.fetchContestById.status = 'failed'; + state.fetchContestById.error = action.payload; + }); // createContest builder.addCase(createContest.pending, (state) => { - state.statuses.create = 'loading'; - state.error = null; + state.createContest.status = 'loading'; + state.createContest.error = null; }); builder.addCase( createContest.fulfilled, (state, action: PayloadAction) => { - state.statuses.create = 'successful'; - state.contests.unshift(action.payload); + state.createContest.status = 'successful'; + state.createContest.contest = action.payload; + }, + ); + builder.addCase(createContest.rejected, (state, action: any) => { + state.createContest.status = 'failed'; + state.createContest.error = action.payload; + }); + + // fetchMyContests + // fetchMyContests + builder.addCase(fetchMyContests.pending, (state) => { + state.fetchMyContests.status = 'loading'; + state.fetchMyContests.error = null; + }); + builder.addCase( + fetchMyContests.fulfilled, + (state, action: PayloadAction) => { + state.fetchMyContests.status = 'successful'; + state.fetchMyContests.contests = action.payload; + }, + ); + builder.addCase(fetchMyContests.rejected, (state, action: any) => { + state.fetchMyContests.status = 'failed'; + state.fetchMyContests.error = action.payload; + }); + + // fetchRegisteredContests + builder.addCase(fetchRegisteredContests.pending, (state) => { + state.fetchRegisteredContests.status = 'loading'; + state.fetchRegisteredContests.error = null; + }); + builder.addCase( + fetchRegisteredContests.fulfilled, + (state, action: PayloadAction) => { + state.fetchRegisteredContests.status = 'successful'; + state.fetchRegisteredContests.contests = + action.payload.contests; + state.fetchRegisteredContests.hasNextPage = + action.payload.hasNextPage; }, ); builder.addCase( - createContest.rejected, - (state, action: PayloadAction) => { - state.statuses.create = 'failed'; - state.error = action.payload; + fetchRegisteredContests.rejected, + (state, action: any) => { + state.fetchRegisteredContests.status = 'failed'; + state.fetchRegisteredContests.error = action.payload; }, ); }, @@ -241,5 +342,5 @@ const contestsSlice = createSlice({ // Экспорты // ===================== -export const { clearSelectedContest, setContestStatus } = contestsSlice.actions; +export const { clearSelectedContest } = contestsSlice.actions; export const contestsReducer = contestsSlice.reducer; diff --git a/src/views/home/account/contests/Contests.tsx b/src/views/home/account/contests/Contests.tsx index a68f224..8f96a04 100644 --- a/src/views/home/account/contests/Contests.tsx +++ b/src/views/home/account/contests/Contests.tsx @@ -1,60 +1,64 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; import { setMenuActiveProfilePage } from '../../../../redux/slices/store'; -import { fetchContests } from '../../../../redux/slices/contests'; +import { + fetchMyContests, + fetchRegisteredContests, +} from '../../../../redux/slices/contests'; import ContestsBlock from './ContestsBlock'; const Contests = () => { const dispatch = useAppDispatch(); - const now = new Date(); - const [modalActive, setModalActive] = useState(false); - - // Берём данные из Redux - const contests = useAppSelector((state) => state.contests.contests); - const status = useAppSelector((state) => state.contests.statuses.create); - const error = useAppSelector((state) => state.contests.error); - - // При загрузке страницы — выставляем активную вкладку и подгружаем контесты - useEffect(() => { - dispatch(fetchContests({})); - }, []); + // Redux-состояния + const myContestsState = useAppSelector( + (state) => state.contests.fetchMyContests, + ); + const regContestsState = useAppSelector( + (state) => state.contests.fetchRegisteredContests, + ); + // При загрузке страницы — выставляем вкладку и подгружаем контесты useEffect(() => { dispatch(setMenuActiveProfilePage('contests')); + dispatch(fetchMyContests()); + dispatch(fetchRegisteredContests({})); }, []); - if (status == 'loading') { - return ( -
Загрузка контестов...
- ); - } - - if (error) { - return
Ошибка: {error}
; - } + console.log(myContestsState); return ( -
- { - const endTime = new Date(contest.endsAt).getTime(); - return endTime >= now.getTime(); - })} - /> +
+ {/* Контесты, в которых я участвую */} +
+ +
- { - const endTime = new Date(contest.endsAt).getTime(); - return endTime < now.getTime(); - })} - /> + {/* Контесты, которые я создал */} +
+ {myContestsState.status === 'loading' ? ( +
+ Загрузка ваших контестов... +
+ ) : myContestsState.error ? ( +
+ Ошибка: {myContestsState.error} +
+ ) : ( + + )} +
); }; diff --git a/src/views/home/account/contests/ContestsBlock.tsx b/src/views/home/account/contests/ContestsBlock.tsx index fbe2ded..f946fd0 100644 --- a/src/views/home/account/contests/ContestsBlock.tsx +++ b/src/views/home/account/contests/ContestsBlock.tsx @@ -1,20 +1,22 @@ import { useState, FC } from 'react'; import { cn } from '../../../../lib/cn'; import { ChevroneDown } from '../../../../assets/icons/groups'; -import ContestItem from './ContestItem'; +import MyContestItem from './MyContestItem'; +import RegisterContestItem from './RegisterContestItem'; import { Contest } from '../../../../redux/slices/contests'; interface ContestsBlockProps { contests: Contest[]; title: string; className?: string; - type?: string; + type?: 'my' | 'reg'; } const ContestsBlock: FC = ({ contests, title, className, + type = 'my', }) => { const [active, setActive] = useState(title != 'Скрытые'); @@ -51,21 +53,37 @@ const ContestsBlock: FC = ({ >
- {contests.map((v, i) => ( - - ))} + {contests.map((v, i) => { + return type == 'my' ? ( + + ) : ( + + ); + })}
diff --git a/src/views/home/account/contests/MyContestItem.tsx b/src/views/home/account/contests/MyContestItem.tsx new file mode 100644 index 0000000..3d2e56e --- /dev/null +++ b/src/views/home/account/contests/MyContestItem.tsx @@ -0,0 +1,104 @@ +import { cn } from '../../../../lib/cn'; +import { Account } from '../../../../assets/icons/auth'; +import { PrimaryButton } from '../../../../components/button/PrimaryButton'; +import { ReverseButton } from '../../../../components/button/ReverseButton'; +import { useNavigate } from 'react-router-dom'; +import { Edit } from '../../../../assets/icons/input'; + +export interface ContestItemProps { + id: number; + name: string; + startAt: string; + duration: number; + members: number; + type: 'first' | 'second'; +} + +function formatDate(dateString: string): string { + const date = new Date(dateString); + + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear(); + + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + return `${day}/${month}/${year}\n${hours}:${minutes}`; +} + +function formatWaitTime(ms: number): string { + const minutes = Math.floor(ms / 60000); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) { + const remainder = days % 10; + let suffix = 'дней'; + if (remainder === 1 && days !== 11) suffix = 'день'; + else if (remainder >= 2 && remainder <= 4 && (days < 10 || days > 20)) + suffix = 'дня'; + return `${days} ${suffix}`; + } else if (hours > 0) { + const mins = minutes % 60; + return mins > 0 ? `${hours} ч ${mins} мин` : `${hours} ч`; + } else { + return `${minutes} мин`; + } +} + +const ContestItem: React.FC = ({ + id, + name, + startAt, + duration, + members, + type, +}) => { + const navigate = useNavigate(); + + const now = new Date(); + + const waitTime = new Date(startAt).getTime() - now.getTime(); + + return ( +
{ + navigate(`/contest/${id}`); + }} + > +
{name}
+
+ {/* {authors.map((v, i) =>

{v}

)} */} + valavshonok +
+
+ {formatDate(startAt)} +
+
{formatWaitTime(duration)}
+
+
{members}
+ +
+ + { + e.stopPropagation(); + navigate( + `/contest/editor?back=/home/account/articles&articleId=${id}`, + ); + }} + /> +
+ ); +}; + +export default ContestItem; diff --git a/src/views/home/account/contests/ContestItem.tsx b/src/views/home/account/contests/RegisterContestItem.tsx similarity index 100% rename from src/views/home/account/contests/ContestItem.tsx rename to src/views/home/account/contests/RegisterContestItem.tsx diff --git a/src/views/home/contests/Contests.tsx b/src/views/home/contests/Contests.tsx index d3ad1eb..7af6f32 100644 --- a/src/views/home/contests/Contests.tsx +++ b/src/views/home/contests/Contests.tsx @@ -14,9 +14,13 @@ const Contests = () => { const [modalActive, setModalActive] = useState(false); // Берём данные из Redux - const contests = useAppSelector((state) => state.contests.contests); - const status = useAppSelector((state) => state.contests.statuses.create); - const error = useAppSelector((state) => state.contests.error); + const contests = useAppSelector( + (state) => state.contests.fetchContests.contests, + ); + const status = useAppSelector( + (state) => state.contests.fetchContests.status, + ); + const error = useAppSelector((state) => state.contests.fetchContests.error); // При загрузке страницы — выставляем активную вкладку и подгружаем контесты useEffect(() => { diff --git a/src/views/home/contests/ModalCreate.tsx b/src/views/home/contests/ModalCreate.tsx index ac9cc1c..54abe1a 100644 --- a/src/views/home/contests/ModalCreate.tsx +++ b/src/views/home/contests/ModalCreate.tsx @@ -18,7 +18,9 @@ const ModalCreateContest: FC = ({ setActive, }) => { const dispatch = useAppDispatch(); - const status = useAppSelector((state) => state.contests.statuses.create); + const status = useAppSelector( + (state) => state.contests.createContest.status, + ); const [form, setForm] = useState({ name: '', From 1b39b8c77f314c42a200be388f9f6d297b33b69d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:09:10 +0300 Subject: [PATCH 03/17] dont work --- src/App.tsx | 5 + src/pages/ContestEditor.tsx | 336 ++++++++++++++++++ src/redux/slices/contests.ts | 205 +++++++++-- .../home/account/contests/ContestsBlock.tsx | 1 - .../home/account/contests/MyContestItem.tsx | 2 +- src/views/home/contests/ModalCreate.tsx | 39 +- 6 files changed, 546 insertions(+), 42 deletions(-) create mode 100644 src/pages/ContestEditor.tsx diff --git a/src/App.tsx b/src/App.tsx index 6d0ec91..609203e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import Home from './pages/Home'; import Mission from './pages/Mission'; import ArticleEditor from './pages/ArticleEditor'; import Article from './pages/Article'; +import ContestEditor from './pages/ContestEditor'; function App() { return ( @@ -20,6 +21,10 @@ function App() { path="/article/create/*" element={} /> + } + /> } /> } /> diff --git a/src/pages/ContestEditor.tsx b/src/pages/ContestEditor.tsx new file mode 100644 index 0000000..669adc7 --- /dev/null +++ b/src/pages/ContestEditor.tsx @@ -0,0 +1,336 @@ +import { useEffect, useState } from 'react'; +import Header from '../views/articleeditor/Header'; +import { PrimaryButton } from '../components/button/PrimaryButton'; +import { Input } from '../components/input/Input'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { + createContest, + CreateContestBody, + fetchContestById, +} from '../redux/slices/contests'; +import DateRangeInput from '../components/input/DateRangeInput'; +import { useQuery } from '../hooks/useQuery'; +import { useNavigate } from 'react-router-dom'; +import { fetchMissionById, Mission } from '../redux/slices/missions'; +import { ReverseButton } from '../components/button/ReverseButton'; + +/** + * Страница создания / редактирования контеста + */ +const ContestEditor = () => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const status = useAppSelector( + (state) => state.contests.createContest.status, + ); + + const [missionIdInput, setMissionIdInput] = useState(''); + + const query = useQuery(); + const back = query.get('back') ?? undefined; + const contestId = Number(query.get('contestId') ?? undefined); + const refactor = !!contestId; + + const [contest, setContest] = useState({ + name: '', + description: '', + scheduleType: 'AlwaysOpen', + visibility: 'Public', + startsAt: null, + endsAt: null, + attemptDurationMinutes: null, + maxAttempts: null, + allowEarlyFinish: false, + groupId: null, + missionIds: [], + articleIds: [], + }); + + const [missions, setMissions] = useState([]); + + const { contest: contestById, status: contestByIdstatus } = useAppSelector( + (state) => state.contests.fetchContestById, + ); + console.log(contestByIdstatus, contestById); + useEffect(() => { + if (status === 'successful') { + } + }, [status]); + + const handleChange = (key: keyof CreateContestBody, value: any) => { + setContest((prev) => ({ ...prev, [key]: value })); + }; + + const handleSubmit = () => { + dispatch(createContest(contest)); + }; + + const addMission = () => { + const id = Number(missionIdInput.trim()); + if (!id || contest.missionIds?.includes(id)) return; + dispatch(fetchMissionById(id)) + .unwrap() + .then((mission) => { + setMissions((prev) => [...prev, mission]); + setContest((prev) => ({ + ...prev, + missionIds: [...(prev.missionIds ?? []), id], + })); + setMissionIdInput(''); + }) + .catch((err) => { + console.error('Ошибка при загрузке миссии:', err); + }); + }; + + const removeMission = (removeId: number) => { + setContest({ + ...contest, + missionIds: contest.missionIds?.filter((v) => v !== removeId), + }); + setMissions(missions.filter((v) => v.id != removeId)); + }; + + useEffect(() => { + if (refactor) { + dispatch(fetchContestById(contestId)); + } + }, [refactor]); + + useEffect(() => { + if (refactor && contestByIdstatus == 'successful' && contestById) { + setContest({ + ...contestById, + missionIds: [], + visibility: 'Public', + scheduleType: 'AlwaysOpen', + }); + } + }, [contestById]); + + return ( +
+
navigate(back || '/home/contests')} /> + +
+ {/* Левая панешь */} +
+
+

+ +
+
+ {refactor + ? `Редактирвоание контеста #${contestId} \"${contestById?.name}\"` + : 'Создать контест'} +
+ + handleChange('name', v)} + defaultState={contest.name ?? ''} + /> + + handleChange('description', v)} + defaultState={contest.description ?? ''} + /> + +
+
+ + +
+ +
+ + +
+
+ + {/* Даты начала и конца */} +
+ +
+ + {/* Продолжительность и лимиты */} +
+ + handleChange( + 'attemptDurationMinutes', + Number(v), + ) + } + /> + + handleChange('maxAttempts', Number(v)) + } + /> +
+ + {/* Разрешить раннее завершение */} +
+ + handleChange( + 'allowEarlyFinish', + e.target.checked, + ) + } + /> + +
+ + {/* Кнопки */} +
+ {refactor ? ( + <> + + + + ) : ( + + )} +
+
+
+
+ + {/* Правая панель */} +
+
+

+ + {/* Блок для тегов */} +
+
+ { + setMissionIdInput(v); + }} + defaultState={missionIdInput} + placeholder="458" + onKeyDown={(e) => { + if (e.key == 'Enter') addMission(); + }} + /> + +
+
+ {missions.map((v, i) => ( +
+ {v.id} + {v.name} + +
+ ))} +
+
+
+
+
+
+ ); +}; + +export default ContestEditor; diff --git a/src/redux/slices/contests.ts b/src/redux/slices/contests.ts index 1bbcae5..a5c0656 100644 --- a/src/redux/slices/contests.ts +++ b/src/redux/slices/contests.ts @@ -15,7 +15,7 @@ export interface Mission { updatedAt: string; timeLimitMilliseconds: number; memoryLimitBytes: number; - statements: null; + statements: string; } export interface Member { @@ -24,18 +24,22 @@ export interface Member { role: string; } +export interface Group { + groupId: number; + groupName: string; +} + export interface Contest { id: number; name: string; description: string; - scheduleType: string; + scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; startsAt: string; endsAt: string; - attemptDurationMinutes: number | null; - maxAttempts: number | null; - allowEarlyFinish: boolean | null; - groupId: number | null; - groupName: string | null; + attemptDurationMinutes: number; + maxAttempts: number; + allowEarlyFinish: boolean; + groups: Group[]; missions: Mission[]; articles: any[]; members: Member[]; @@ -47,20 +51,18 @@ interface ContestsResponse { } export interface CreateContestBody { - name?: string | null; - description?: string | null; + name: string; + description: string; scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; visibility: 'Public' | 'GroupPrivate'; - startsAt?: string | null; - endsAt?: string | null; - attemptDurationMinutes?: number | null; - maxAttempts?: number | null; - allowEarlyFinish?: boolean | null; - groupId?: number | null; - missionIds?: number[] | null; - articleIds?: number[] | null; - participantIds?: number[] | null; - organizerIds?: number[] | null; + startsAt: string; + endsAt: string; + attemptDurationMinutes: number; + maxAttempts: number; + allowEarlyFinish: boolean; + groupId: number; + missionIds: number[]; + articleIds: number[]; } // ===================== @@ -77,12 +79,22 @@ interface ContestsState { error: string | null; }; fetchContestById: { - contest: Contest | null; + contest: Contest; status: Status; error: string | null; }; createContest: { - contest: Contest | null; + contest: Contest; + status: Status; + error: string | null; + }; + // 🆕 Добавляем updateContest и deleteContest + updateContest: { + contest: Contest; + status: Status; + error: string | null; + }; + deleteContest: { status: Status; error: string | null; }; @@ -107,12 +119,63 @@ const initialState: ContestsState = { error: null, }, fetchContestById: { - contest: null, + contest: { + id: 0, + name: '', + description: '', + scheduleType: 'AlwaysOpen', + startsAt: '', + endsAt: '', + attemptDurationMinutes: 0, + maxAttempts: 0, + allowEarlyFinish: false, + groups: [], + missions: [], + articles: [], + members: [], + }, status: 'idle', error: null, }, createContest: { - contest: null, + contest: { + id: 0, + name: '', + description: '', + scheduleType: 'AlwaysOpen', + startsAt: '', + endsAt: '', + attemptDurationMinutes: 0, + maxAttempts: 0, + allowEarlyFinish: false, + groups: [], + missions: [], + articles: [], + members: [], + }, + status: 'idle', + error: null, + }, + updateContest: { + contest: { + id: 0, + name: '', + description: '', + scheduleType: 'AlwaysOpen', + startsAt: '', + endsAt: '', + attemptDurationMinutes: 0, + maxAttempts: 0, + allowEarlyFinish: false, + groups: [], + missions: [], + articles: [], + members: [], + }, + status: 'idle', + error: null, + }, + deleteContest: { status: 'idle', error: null, }, @@ -191,13 +254,51 @@ export const createContest = createAsyncThunk( }, ); +// 🆕 Обновление контеста +export const updateContest = createAsyncThunk( + 'contests/update', + async ( + { + contestId, + ...contestData + }: { contestId: number } & CreateContestBody, + { rejectWithValue }, + ) => { + try { + const response = await axios.patch( + `/contests/${contestId}`, + contestData, + ); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Failed to update contest', + ); + } + }, +); + +// 🆕 Удаление контеста +export const deleteContest = createAsyncThunk( + 'contests/delete', + async (contestId: number, { rejectWithValue }) => { + try { + await axios.delete(`/contests/${contestId}`); + return contestId; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Failed to delete contest', + ); + } + }, +); + // Контесты, созданные мной export const fetchMyContests = createAsyncThunk( 'contests/fetchMyContests', async (_, { rejectWithValue }) => { try { const response = await axios.get('/contests/my'); - // Возвращаем просто массив контестов return response.data; } catch (err: any) { return rejectWithValue( @@ -238,8 +339,15 @@ const contestsSlice = createSlice({ name: 'contests', initialState, reducers: { - clearSelectedContest: (state) => { - state.fetchContestById.contest = null; + // 🆕 Сброс статусов + setContestStatus: ( + state, + action: PayloadAction<{ key: keyof ContestsState; status: Status }>, + ) => { + const { key, status } = action.payload; + if (state[key]) { + (state[key] as any).status = status; + } }, }, extraReducers: (builder) => { @@ -295,7 +403,48 @@ const contestsSlice = createSlice({ state.createContest.error = action.payload; }); - // fetchMyContests + // 🆕 updateContest + builder.addCase(updateContest.pending, (state) => { + state.updateContest.status = 'loading'; + state.updateContest.error = null; + }); + builder.addCase( + updateContest.fulfilled, + (state, action: PayloadAction) => { + state.updateContest.status = 'successful'; + state.updateContest.contest = action.payload; + }, + ); + builder.addCase(updateContest.rejected, (state, action: any) => { + state.updateContest.status = 'failed'; + state.updateContest.error = action.payload; + }); + + // 🆕 deleteContest + builder.addCase(deleteContest.pending, (state) => { + state.deleteContest.status = 'loading'; + state.deleteContest.error = null; + }); + builder.addCase( + deleteContest.fulfilled, + (state, action: PayloadAction) => { + state.deleteContest.status = 'successful'; + // Удалим контест из списков + state.fetchContests.contests = + state.fetchContests.contests.filter( + (c) => c.id !== action.payload, + ); + state.fetchMyContests.contests = + state.fetchMyContests.contests.filter( + (c) => c.id !== action.payload, + ); + }, + ); + builder.addCase(deleteContest.rejected, (state, action: any) => { + state.deleteContest.status = 'failed'; + state.deleteContest.error = action.payload; + }); + // fetchMyContests builder.addCase(fetchMyContests.pending, (state) => { state.fetchMyContests.status = 'loading'; @@ -342,5 +491,5 @@ const contestsSlice = createSlice({ // Экспорты // ===================== -export const { clearSelectedContest } = contestsSlice.actions; +export const { setContestStatus } = contestsSlice.actions; export const contestsReducer = contestsSlice.reducer; diff --git a/src/views/home/account/contests/ContestsBlock.tsx b/src/views/home/account/contests/ContestsBlock.tsx index f946fd0..879c1a3 100644 --- a/src/views/home/account/contests/ContestsBlock.tsx +++ b/src/views/home/account/contests/ContestsBlock.tsx @@ -60,7 +60,6 @@ const ContestsBlock: FC = ({ id={v.id} name={v.name} startAt={v.startsAt} - statusRegister={'reg'} duration={ new Date(v.endsAt).getTime() - new Date(v.startsAt).getTime() diff --git a/src/views/home/account/contests/MyContestItem.tsx b/src/views/home/account/contests/MyContestItem.tsx index 3d2e56e..1fc3785 100644 --- a/src/views/home/account/contests/MyContestItem.tsx +++ b/src/views/home/account/contests/MyContestItem.tsx @@ -93,7 +93,7 @@ const ContestItem: React.FC = ({ onClick={(e) => { e.stopPropagation(); navigate( - `/contest/editor?back=/home/account/articles&articleId=${id}`, + `/contest/create?back=/home/account/contests&contestId=${id}`, ); }} /> diff --git a/src/views/home/contests/ModalCreate.tsx b/src/views/home/contests/ModalCreate.tsx index 54abe1a..39d9998 100644 --- a/src/views/home/contests/ModalCreate.tsx +++ b/src/views/home/contests/ModalCreate.tsx @@ -4,9 +4,13 @@ import { PrimaryButton } from '../../../components/button/PrimaryButton'; import { SecondaryButton } from '../../../components/button/SecondaryButton'; import { Input } from '../../../components/input/Input'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; -import { createContest } from '../../../redux/slices/contests'; +import { + createContest, + setContestStatus, +} from '../../../redux/slices/contests'; import { CreateContestBody } from '../../../redux/slices/contests'; import DateRangeInput from '../../../components/input/DateRangeInput'; +import { useNavigate } from 'react-router-dom'; interface ModalCreateContestProps { active: boolean; @@ -18,6 +22,7 @@ const ModalCreateContest: FC = ({ setActive, }) => { const dispatch = useAppDispatch(); + const navigate = useNavigate(); const status = useAppSelector( (state) => state.contests.createContest.status, ); @@ -27,21 +32,29 @@ const ModalCreateContest: FC = ({ description: '', scheduleType: 'AlwaysOpen', visibility: 'Public', - startsAt: null, - endsAt: null, - attemptDurationMinutes: null, - maxAttempts: null, + startsAt: '', + endsAt: '', + attemptDurationMinutes: 0, + maxAttempts: 0, allowEarlyFinish: false, - groupId: null, - missionIds: null, - articleIds: null, - participantIds: null, - organizerIds: null, + groupId: 0, + missionIds: [], + articleIds: [], }); + const contest = useAppSelector( + (state) => state.contests.createContest.contest, + ); + useEffect(() => { if (status === 'successful') { - setActive(false); + console.log('navigate'); + navigate( + `/contest/create?back=/home/account/contests&contestId=${contest.id}`, + ); + dispatch( + setContestStatus({ key: 'createContest', status: 'idle' }), + ); } }, [status]); @@ -176,7 +189,9 @@ const ModalCreateContest: FC = ({ {/* Кнопки */}
{ + handleSubmit(); + }} text="Создать" disabled={status === 'loading'} /> From 6c92c789d091cdac5040d4480953dd40c289947f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Thu, 6 Nov 2025 18:27:28 +0300 Subject: [PATCH 04/17] contests update --- src/pages/ContestEditor.tsx | 90 +++++++++++++++-------- src/redux/slices/contests.ts | 88 +++++++++++----------- src/views/home/contests/Contests.tsx | 49 ++++++------ src/views/home/contests/ContestsBlock.tsx | 8 +- src/views/home/contests/ModalCreate.tsx | 9 +-- 5 files changed, 136 insertions(+), 108 deletions(-) diff --git a/src/pages/ContestEditor.tsx b/src/pages/ContestEditor.tsx index 669adc7..aa46a50 100644 --- a/src/pages/ContestEditor.tsx +++ b/src/pages/ContestEditor.tsx @@ -6,48 +6,71 @@ import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { createContest, CreateContestBody, + deleteContest, fetchContestById, + setContestStatus, + updateContest, } from '../redux/slices/contests'; import DateRangeInput from '../components/input/DateRangeInput'; import { useQuery } from '../hooks/useQuery'; -import { useNavigate } from 'react-router-dom'; -import { fetchMissionById, Mission } from '../redux/slices/missions'; +import { Navigate, useNavigate } from 'react-router-dom'; +import { fetchMissionById } from '../redux/slices/missions'; import { ReverseButton } from '../components/button/ReverseButton'; + +interface Mission { + id: number; + name: string; +} + /** * Страница создания / редактирования контеста */ const ContestEditor = () => { + const dispatch = useAppDispatch(); const navigate = useNavigate(); - const status = useAppSelector( - (state) => state.contests.createContest.status, - ); - - const [missionIdInput, setMissionIdInput] = useState(''); const query = useQuery(); const back = query.get('back') ?? undefined; const contestId = Number(query.get('contestId') ?? undefined); const refactor = !!contestId; + if (!refactor){ + return + } + + + + const status = useAppSelector( + (state) => state.contests.createContest.status, + ); + + + + const [missionIdInput, setMissionIdInput] = useState(''); + + const [contest, setContest] = useState({ name: '', description: '', scheduleType: 'AlwaysOpen', visibility: 'Public', - startsAt: null, - endsAt: null, - attemptDurationMinutes: null, - maxAttempts: null, - allowEarlyFinish: false, - groupId: null, + startsAt: '', + endsAt: '', + attemptDurationMinutes: 60, + maxAttempts: 1, + allowEarlyFinish: true, + groupIds: [], missionIds: [], articleIds: [], }); const [missions, setMissions] = useState([]); + + const statusDelete = useAppSelector((state) => state.contests.deleteContest.status) + const { contest: contestById, status: contestByIdstatus } = useAppSelector( (state) => state.contests.fetchContestById, ); @@ -61,9 +84,15 @@ const ContestEditor = () => { setContest((prev) => ({ ...prev, [key]: value })); }; - const handleSubmit = () => { - dispatch(createContest(contest)); + const handleUpdateContest = () => { + dispatch(updateContest({...contest, contestId})); }; + + const handleDeleteContest = () => { + dispatch(deleteContest(contestId)); + }; + + const addMission = () => { const id = Number(missionIdInput.trim()); @@ -91,6 +120,13 @@ const ContestEditor = () => { setMissions(missions.filter((v) => v.id != removeId)); }; + useEffect(() => { + if (statusDelete == "successful"){ + dispatch(setContestStatus({key: "deleteContest", status: "idle"})) + navigate('/home/account/contests') + } + }, [statusDelete]) + useEffect(() => { if (refactor) { dispatch(fetchContestById(contestId)); @@ -101,10 +137,14 @@ const ContestEditor = () => { if (refactor && contestByIdstatus == 'successful' && contestById) { setContest({ ...contestById, - missionIds: [], + // groupIds: contestById.groups.map(group => group.groupId), + groupIds: [], + missionIds: contestById.missions?.map(mission => mission.id), + articleIds: contestById.articles?.map(article => article.articleId), visibility: 'Public', scheduleType: 'AlwaysOpen', }); + setMissions(contestById.missions ?? []); } }, [contestById]); @@ -253,27 +293,19 @@ const ContestEditor = () => { {/* Кнопки */}
- {refactor ? ( - <> + - - ) : ( - - )} +
diff --git a/src/redux/slices/contests.ts b/src/redux/slices/contests.ts index a5c0656..f6e9d4c 100644 --- a/src/redux/slices/contests.ts +++ b/src/redux/slices/contests.ts @@ -11,8 +11,6 @@ export interface Mission { name: string; difficulty: number; tags: string[]; - createdAt: string; - updatedAt: string; timeLimitMilliseconds: number; memoryLimitBytes: number; statements: string; @@ -32,17 +30,18 @@ export interface Group { export interface Contest { id: number; name: string; - description: string; + description?: string; scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; - startsAt: string; - endsAt: string; - attemptDurationMinutes: number; - maxAttempts: number; - allowEarlyFinish: boolean; - groups: Group[]; - missions: Mission[]; - articles: any[]; - members: Member[]; + visibility: 'Public' | 'GroupPrivate'; + startsAt?: string; + endsAt?: string; + attemptDurationMinutes?: number; + maxAttempts?: number; + allowEarlyFinish?: boolean; + groups?: Group[]; + missions?: Mission[]; + articles?: any[]; + members?: Member[]; } interface ContestsResponse { @@ -52,17 +51,17 @@ interface ContestsResponse { export interface CreateContestBody { name: string; - description: string; + description?: string; scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; visibility: 'Public' | 'GroupPrivate'; - startsAt: string; - endsAt: string; - attemptDurationMinutes: number; - maxAttempts: number; - allowEarlyFinish: boolean; - groupId: number; - missionIds: number[]; - articleIds: number[]; + startsAt?: string; + endsAt?: string; + attemptDurationMinutes?: number; + maxAttempts?: number; + allowEarlyFinish?: boolean; + groupIds?: number[]; + missionIds?: number[]; + articleIds?: number[]; } // ===================== @@ -76,38 +75,38 @@ interface ContestsState { contests: Contest[]; hasNextPage: boolean; status: Status; - error: string | null; + error?: string; }; fetchContestById: { contest: Contest; status: Status; - error: string | null; + error?: string; }; createContest: { contest: Contest; status: Status; - error: string | null; + error?: string; }; // 🆕 Добавляем updateContest и deleteContest updateContest: { contest: Contest; status: Status; - error: string | null; + error?: string; }; deleteContest: { status: Status; - error: string | null; + error?: string; }; fetchMyContests: { contests: Contest[]; status: Status; - error: string | null; + error?: string; }; fetchRegisteredContests: { contests: Contest[]; hasNextPage: boolean; status: Status; - error: string | null; + error?: string; }; } @@ -116,7 +115,7 @@ const initialState: ContestsState = { contests: [], hasNextPage: false, status: 'idle', - error: null, + error: undefined, }, fetchContestById: { contest: { @@ -124,6 +123,7 @@ const initialState: ContestsState = { name: '', description: '', scheduleType: 'AlwaysOpen', + visibility: 'Public', startsAt: '', endsAt: '', attemptDurationMinutes: 0, @@ -135,7 +135,7 @@ const initialState: ContestsState = { members: [], }, status: 'idle', - error: null, + error: undefined, }, createContest: { contest: { @@ -143,6 +143,7 @@ const initialState: ContestsState = { name: '', description: '', scheduleType: 'AlwaysOpen', + visibility: 'Public', startsAt: '', endsAt: '', attemptDurationMinutes: 0, @@ -154,7 +155,7 @@ const initialState: ContestsState = { members: [], }, status: 'idle', - error: null, + error: undefined, }, updateContest: { contest: { @@ -162,6 +163,7 @@ const initialState: ContestsState = { name: '', description: '', scheduleType: 'AlwaysOpen', + visibility: 'Public', startsAt: '', endsAt: '', attemptDurationMinutes: 0, @@ -173,22 +175,22 @@ const initialState: ContestsState = { members: [], }, status: 'idle', - error: null, + error: undefined, }, deleteContest: { status: 'idle', - error: null, + error: undefined, }, fetchMyContests: { contests: [], status: 'idle', - error: null, + error: undefined, }, fetchRegisteredContests: { contests: [], hasNextPage: false, status: 'idle', - error: null, + error: undefined, }, }; @@ -265,7 +267,7 @@ export const updateContest = createAsyncThunk( { rejectWithValue }, ) => { try { - const response = await axios.patch( + const response = await axios.put( `/contests/${contestId}`, contestData, ); @@ -354,7 +356,7 @@ const contestsSlice = createSlice({ // fetchContests builder.addCase(fetchContests.pending, (state) => { state.fetchContests.status = 'loading'; - state.fetchContests.error = null; + state.fetchContests.error = undefined; }); builder.addCase( fetchContests.fulfilled, @@ -372,7 +374,7 @@ const contestsSlice = createSlice({ // fetchContestById builder.addCase(fetchContestById.pending, (state) => { state.fetchContestById.status = 'loading'; - state.fetchContestById.error = null; + state.fetchContestById.error = undefined; }); builder.addCase( fetchContestById.fulfilled, @@ -389,7 +391,7 @@ const contestsSlice = createSlice({ // createContest builder.addCase(createContest.pending, (state) => { state.createContest.status = 'loading'; - state.createContest.error = null; + state.createContest.error = undefined; }); builder.addCase( createContest.fulfilled, @@ -406,7 +408,7 @@ const contestsSlice = createSlice({ // 🆕 updateContest builder.addCase(updateContest.pending, (state) => { state.updateContest.status = 'loading'; - state.updateContest.error = null; + state.updateContest.error = undefined; }); builder.addCase( updateContest.fulfilled, @@ -423,7 +425,7 @@ const contestsSlice = createSlice({ // 🆕 deleteContest builder.addCase(deleteContest.pending, (state) => { state.deleteContest.status = 'loading'; - state.deleteContest.error = null; + state.deleteContest.error = undefined; }); builder.addCase( deleteContest.fulfilled, @@ -448,7 +450,7 @@ const contestsSlice = createSlice({ // fetchMyContests builder.addCase(fetchMyContests.pending, (state) => { state.fetchMyContests.status = 'loading'; - state.fetchMyContests.error = null; + state.fetchMyContests.error = undefined; }); builder.addCase( fetchMyContests.fulfilled, @@ -465,7 +467,7 @@ const contestsSlice = createSlice({ // fetchRegisteredContests builder.addCase(fetchRegisteredContests.pending, (state) => { state.fetchRegisteredContests.status = 'loading'; - state.fetchRegisteredContests.error = null; + state.fetchRegisteredContests.error = undefined; }); builder.addCase( fetchRegisteredContests.fulfilled, diff --git a/src/views/home/contests/Contests.tsx b/src/views/home/contests/Contests.tsx index 7af6f32..999ee29 100644 --- a/src/views/home/contests/Contests.tsx +++ b/src/views/home/contests/Contests.tsx @@ -28,16 +28,6 @@ const Contests = () => { dispatch(fetchContests({})); }, []); - if (status == 'loading') { - return ( -
Загрузка контестов...
- ); - } - - if (error) { - return
Ошибка: {error}
; - } - return (
@@ -59,24 +49,29 @@ const Contests = () => {
+ {status == 'loading' &&
Загрузка контестов...
} + {status == 'failed' &&
Ошибка: {error}
} + {status == 'successful' && + <> + { + const endTime = new Date(contest.endsAt ?? new Date().toDateString()).getTime(); + return endTime >= now.getTime(); + })} + /> - { - const endTime = new Date(contest.endsAt).getTime(); - return endTime >= now.getTime(); - })} - /> - - { - const endTime = new Date(contest.endsAt).getTime(); - return endTime < now.getTime(); - })} - /> + { + const endTime = new Date(contest.endsAt ?? new Date().toDateString()).getTime(); + return endTime < now.getTime(); + })} + /> + + }
= ({ key={i} id={v.id} name={v.name} - startAt={v.startsAt} + startAt={v.startsAt ?? new Date().toString()} statusRegister={'reg'} duration={ - new Date(v.endsAt).getTime() - - new Date(v.startsAt).getTime() + new Date(v.endsAt ?? new Date().toString()).getTime() - + new Date(v.startsAt ?? new Date().toString()).getTime() } - members={v.members.length} + members={v.members?.length ?? 0} type={i % 2 ? 'second' : 'first'} /> ))} diff --git a/src/views/home/contests/ModalCreate.tsx b/src/views/home/contests/ModalCreate.tsx index 39d9998..6863b0d 100644 --- a/src/views/home/contests/ModalCreate.tsx +++ b/src/views/home/contests/ModalCreate.tsx @@ -37,7 +37,7 @@ const ModalCreateContest: FC = ({ attemptDurationMinutes: 0, maxAttempts: 0, allowEarlyFinish: false, - groupId: 0, + groupIds: [], missionIds: [], articleIds: [], }); @@ -48,13 +48,12 @@ const ModalCreateContest: FC = ({ useEffect(() => { if (status === 'successful') { - console.log('navigate'); - navigate( - `/contest/create?back=/home/account/contests&contestId=${contest.id}`, - ); dispatch( setContestStatus({ key: 'createContest', status: 'idle' }), ); + navigate( + `/contest/create?back=/home/account/contests&contestId=${contest.id}`, + ); } }, [status]); From 046e5d16931576b4fdb3780a9b0f7e4ff42b6ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Thu, 6 Nov 2025 18:54:06 +0300 Subject: [PATCH 05/17] fix null --- src/redux/slices/submit.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/redux/slices/submit.ts b/src/redux/slices/submit.ts index b70efe8..21522b1 100644 --- a/src/redux/slices/submit.ts +++ b/src/redux/slices/submit.ts @@ -8,7 +8,7 @@ export interface Submit { language: string; languageVersion: string; sourceCode: string; - contestId: number | null; + contestId?: number; } export interface Solution { @@ -30,8 +30,8 @@ export interface MissionSubmit { id: number; userId: number; solution: Solution; - contestId: number | null; - contestName: string | null; + contestId?: number; + contestName?: string; sourceType: string; } @@ -40,7 +40,7 @@ interface SubmitState { submitsById: Record; // ✅ добавлено currentSubmit?: Submit; status: 'idle' | 'loading' | 'successful' | 'failed'; - error: string | null; + error?: string; } // Начальное состояние @@ -49,7 +49,7 @@ const initialState: SubmitState = { submitsById: {}, // ✅ инициализация currentSubmit: undefined, status: 'idle', - error: null, + error: undefined, }; // AsyncThunk: Отправка решения @@ -123,7 +123,7 @@ const submitSlice = createSlice({ clearCurrentSubmit: (state) => { state.currentSubmit = undefined; state.status = 'idle'; - state.error = null; + state.error = undefined; }, clearSubmitsByMission: (state, action: PayloadAction) => { delete state.submitsById[action.payload]; @@ -133,7 +133,7 @@ const submitSlice = createSlice({ // Отправка решения builder.addCase(submitMission.pending, (state) => { state.status = 'loading'; - state.error = null; + state.error = undefined; }); builder.addCase( submitMission.fulfilled, @@ -153,7 +153,7 @@ const submitSlice = createSlice({ // Получить все свои отправки builder.addCase(fetchMySubmits.pending, (state) => { state.status = 'loading'; - state.error = null; + state.error = undefined; }); builder.addCase( fetchMySubmits.fulfilled, @@ -173,7 +173,7 @@ const submitSlice = createSlice({ // Получить отправку по ID builder.addCase(fetchSubmitById.pending, (state) => { state.status = 'loading'; - state.error = null; + state.error = undefined; }); builder.addCase( fetchSubmitById.fulfilled, @@ -193,7 +193,7 @@ const submitSlice = createSlice({ // ✅ Получить отправки по миссии builder.addCase(fetchMySubmitsByMission.pending, (state) => { state.status = 'loading'; - state.error = null; + state.error = undefined; }); builder.addCase( fetchMySubmitsByMission.fulfilled, From 93a5366fd5ebdc5eba05f533074d80e57e675b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:57:27 +0300 Subject: [PATCH 06/17] contest submisssions --- src/pages/ContestEditor.tsx | 11 ++- src/pages/Mission.tsx | 6 +- src/redux/slices/contests.ts | 94 ++++++++++++++++++- src/redux/slices/submit.ts | 1 + .../home/account/contests/ContestsBlock.tsx | 16 ++-- src/views/home/contest/Contest.tsx | 16 +++- src/views/home/contest/MissionItem.tsx | 4 +- src/views/home/contest/Missions.tsx | 6 +- src/views/home/contest/SubmissionItem.tsx | 83 ++++++++++++++++ src/views/home/contest/Submissions.tsx | 73 ++++++++++++++ src/views/home/contests/ModalCreate.tsx | 1 - .../mission/statement/MissionSubmissions.tsx | 46 ++++++++- 12 files changed, 329 insertions(+), 28 deletions(-) create mode 100644 src/views/home/contest/SubmissionItem.tsx diff --git a/src/pages/ContestEditor.tsx b/src/pages/ContestEditor.tsx index aa46a50..03d06b0 100644 --- a/src/pages/ContestEditor.tsx +++ b/src/pages/ContestEditor.tsx @@ -61,7 +61,6 @@ const ContestEditor = () => { attemptDurationMinutes: 60, maxAttempts: 1, allowEarlyFinish: true, - groupIds: [], missionIds: [], articleIds: [], }); @@ -70,6 +69,7 @@ const ContestEditor = () => { const statusDelete = useAppSelector((state) => state.contests.deleteContest.status) + const statusUpdate = useAppSelector((state) => state.contests.updateContest.status); const { contest: contestById, status: contestByIdstatus } = useAppSelector( (state) => state.contests.fetchContestById, @@ -127,6 +127,14 @@ const ContestEditor = () => { } }, [statusDelete]) + + useEffect(() => { + if (statusUpdate == "successful"){ + dispatch(setContestStatus({key: "updateContest", status: "idle"})) + navigate('/home/account/contests') + } + }, [statusUpdate]) + useEffect(() => { if (refactor) { dispatch(fetchContestById(contestId)); @@ -138,7 +146,6 @@ const ContestEditor = () => { setContest({ ...contestById, // groupIds: contestById.groups.map(group => group.groupId), - groupIds: [], missionIds: contestById.missions?.map(mission => mission.id), articleIds: contestById.articles?.map(article => article.articleId), visibility: 'Public', diff --git a/src/pages/Mission.tsx b/src/pages/Mission.tsx index b09c25a..161d516 100644 --- a/src/pages/Mission.tsx +++ b/src/pages/Mission.tsx @@ -20,6 +20,7 @@ const Mission = () => { const query = useQuery(); const back = query.get('back') ?? undefined; + const contestId = Number(query.get('contestId') ?? undefined); if (!missionId || isNaN(missionIdNumber)) { if (back) return ; @@ -179,13 +180,14 @@ const Mission = () => { { + console.log(contestId); await dispatch( submitMission({ missionId: missionIdNumber, language: language, languageVersion: 'latest', sourceCode: code, - contestId: null, + contestId: contestId, }), ).unwrap(); dispatch( @@ -198,7 +200,7 @@ const Mission = () => {
- +
diff --git a/src/redux/slices/contests.ts b/src/redux/slices/contests.ts index f6e9d4c..1e2970c 100644 --- a/src/redux/slices/contests.ts +++ b/src/redux/slices/contests.ts @@ -5,6 +5,36 @@ import axios from '../../axios'; // Типы // ===================== +// ===================== +// Типы для посылок +// ===================== + +export interface Solution { + id: number; + missionId: number; + language: string; + languageVersion: string; + sourceCode: string; + status: string; + time: string; + testerState: string; + testerErrorCode: string; + testerMessage: string; + currentTest: number; + amountOfTests: number; +} + +export interface Submission { + id: number; + userId: number; + solution: Solution; + contestId: number; + contestName: string; + sourceType: string; +} + + + export interface Mission { id: number; authorId: number; @@ -38,7 +68,8 @@ export interface Contest { attemptDurationMinutes?: number; maxAttempts?: number; allowEarlyFinish?: boolean; - groups?: Group[]; + groupId?: number; + groupName?: string; missions?: Mission[]; articles?: any[]; members?: Member[]; @@ -59,7 +90,8 @@ export interface CreateContestBody { attemptDurationMinutes?: number; maxAttempts?: number; allowEarlyFinish?: boolean; - groupIds?: number[]; + groupId?: number; + groupName?: string; missionIds?: number[]; articleIds?: number[]; } @@ -87,6 +119,12 @@ interface ContestsState { status: Status; error?: string; }; + fetchMySubmissions: { + submissions: Submission[]; + status: Status; + error?: string; + }; + // 🆕 Добавляем updateContest и deleteContest updateContest: { contest: Contest; @@ -129,7 +167,8 @@ const initialState: ContestsState = { attemptDurationMinutes: 0, maxAttempts: 0, allowEarlyFinish: false, - groups: [], + groupId: undefined, + groupName: undefined, missions: [], articles: [], members: [], @@ -137,6 +176,12 @@ const initialState: ContestsState = { status: 'idle', error: undefined, }, + fetchMySubmissions: { + submissions: [], + status: 'idle', + error: undefined, + }, + createContest: { contest: { id: 0, @@ -149,7 +194,8 @@ const initialState: ContestsState = { attemptDurationMinutes: 0, maxAttempts: 0, allowEarlyFinish: false, - groups: [], + groupId: undefined, + groupName: undefined, missions: [], articles: [], members: [], @@ -169,7 +215,8 @@ const initialState: ContestsState = { attemptDurationMinutes: 0, maxAttempts: 0, allowEarlyFinish: false, - groups: [], + groupId: undefined, + groupName: undefined, missions: [], articles: [], members: [], @@ -198,6 +245,24 @@ const initialState: ContestsState = { // Async Thunks // ===================== +// Мои посылки в контесте +export const fetchMySubmissions = createAsyncThunk( + 'contests/fetchMySubmissions', + async (contestId: number, { rejectWithValue }) => { + try { + const response = await axios.get( + `/contests/${contestId}/submissions/my`, + ); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Failed to fetch my submissions', + ); + } + }, +); + + // Все контесты export const fetchContests = createAsyncThunk( 'contests/fetchAll', @@ -353,6 +418,25 @@ const contestsSlice = createSlice({ }, }, extraReducers: (builder) => { + // 🆕 fetchMySubmissions + builder.addCase(fetchMySubmissions.pending, (state) => { + state.fetchMySubmissions.status = 'loading'; + state.fetchMySubmissions.error = undefined; + }); + builder.addCase( + fetchMySubmissions.fulfilled, + (state, action: PayloadAction) => { + state.fetchMySubmissions.status = 'successful'; + state.fetchMySubmissions.submissions = action.payload; + }, + ); + builder.addCase(fetchMySubmissions.rejected, (state, action: any) => { + state.fetchMySubmissions.status = 'failed'; + state.fetchMySubmissions.error = action.payload; + }); + + + // fetchContests builder.addCase(fetchContests.pending, (state) => { state.fetchContests.status = 'loading'; diff --git a/src/redux/slices/submit.ts b/src/redux/slices/submit.ts index 21522b1..1b627f2 100644 --- a/src/redux/slices/submit.ts +++ b/src/redux/slices/submit.ts @@ -56,6 +56,7 @@ const initialState: SubmitState = { export const submitMission = createAsyncThunk( 'submit/submitMission', async (submitData: Submit, { rejectWithValue }) => { + console.log(submitData); try { const response = await axios.post('/submits', submitData); return response.data; diff --git a/src/views/home/account/contests/ContestsBlock.tsx b/src/views/home/account/contests/ContestsBlock.tsx index 879c1a3..f732ffe 100644 --- a/src/views/home/account/contests/ContestsBlock.tsx +++ b/src/views/home/account/contests/ContestsBlock.tsx @@ -59,12 +59,12 @@ const ContestsBlock: FC = ({ key={i} id={v.id} name={v.name} - startAt={v.startsAt} + startAt={v.startsAt ?? ''} duration={ - new Date(v.endsAt).getTime() - - new Date(v.startsAt).getTime() + new Date(v.endsAt ?? '').getTime() - + new Date(v.startsAt ?? '').getTime() } - members={v.members.length} + members={(v.members??[]).length} type={i % 2 ? 'second' : 'first'} /> ) : ( @@ -72,13 +72,13 @@ const ContestsBlock: FC = ({ key={i} id={v.id} name={v.name} - startAt={v.startsAt} + startAt={v.startsAt ?? ''} statusRegister={'reg'} duration={ - new Date(v.endsAt).getTime() - - new Date(v.startsAt).getTime() + new Date(v.endsAt ?? '').getTime() - + new Date(v.startsAt ?? '').getTime() } - members={v.members.length} + members={(v.members??[]).length} type={i % 2 ? 'second' : 'first'} /> ); diff --git a/src/views/home/contest/Contest.tsx b/src/views/home/contest/Contest.tsx index d184dd4..79e7fcf 100644 --- a/src/views/home/contest/Contest.tsx +++ b/src/views/home/contest/Contest.tsx @@ -1,9 +1,11 @@ import { useEffect } from 'react'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { setMenuActivePage } from '../../../redux/slices/store'; -import { Navigate, Route, Routes, useParams } from 'react-router-dom'; +import { Navigate, Route, Routes, useNavigate, useParams } from 'react-router-dom'; import { fetchContestById } from '../../../redux/slices/contests'; import ContestMissions from './Missions'; +import { PrimaryButton } from '../../../components/button/PrimaryButton'; +import Submissions from './Submissions'; export interface Article { id: number; @@ -12,14 +14,15 @@ export interface Article { } const Contest = () => { + const navigate = useNavigate(); const { contestId } = useParams<{ contestId: string }>(); const contestIdNumber = contestId && /^\d+$/.test(contestId) ? parseInt(contestId, 10) : null; - if (contestIdNumber === null) { + if (!contestIdNumber) { return ; } const dispatch = useAppDispatch(); - const contest = useAppSelector((state) => state.contests.selectedContest); + const contest = useAppSelector((state) => state.contests.fetchContestById.contest); useEffect(() => { dispatch(setMenuActivePage('contest')); @@ -31,12 +34,19 @@ const Contest = () => { return (
+ {navigate(`/contest/${contestIdNumber}/submissions`)}} text='Мои посылки' /> + + } + /> } /> +
); }; diff --git a/src/views/home/contest/MissionItem.tsx b/src/views/home/contest/MissionItem.tsx index ee9579b..8f4e6cb 100644 --- a/src/views/home/contest/MissionItem.tsx +++ b/src/views/home/contest/MissionItem.tsx @@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { useLocation } from 'react-router-dom'; export interface MissionItemProps { + contestId: number; id: number; name: string; timeLimit?: number; @@ -24,6 +25,7 @@ export function formatBytesToMB(bytes: number): string { } const MissionItem: React.FC = ({ + contestId, id, name, timeLimit = 1000, @@ -48,7 +50,7 @@ const MissionItem: React.FC = ({ 'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300', )} onClick={() => { - navigate(`/mission/${id}?back=${path}`); + navigate(`/mission/${id}?back=${path}&contestId=${contestId}`); }} >
#{id}
diff --git a/src/views/home/contest/Missions.tsx b/src/views/home/contest/Missions.tsx index 535cf05..f18511a 100644 --- a/src/views/home/contest/Missions.tsx +++ b/src/views/home/contest/Missions.tsx @@ -9,7 +9,7 @@ export interface Article { } interface ContestMissionsProps { - contest: Contest | null; + contest?: Contest; } const ContestMissions: FC = ({ contest }) => { @@ -25,8 +25,10 @@ const ContestMissions: FC = ({ contest }) => { {contest?.name} {contest.id}
- {contest.missions.map((v, i) => ( + {(contest.missions ?? []).map((v, i) => ( = ({ + id, + language, + time, + verdict, + type, + status, +}) => { + // const navigate = useNavigate(); + + return ( +
{}} + > +
#{id}
+
+ {formatDate(time)} +
+
{language}
+
+ {verdict} +
+
+ ); +}; + +export default SubmissionItem; diff --git a/src/views/home/contest/Submissions.tsx b/src/views/home/contest/Submissions.tsx index e69de29..24024a2 100644 --- a/src/views/home/contest/Submissions.tsx +++ b/src/views/home/contest/Submissions.tsx @@ -0,0 +1,73 @@ +import SubmissionItem from './SubmissionItem'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { FC, useEffect } from 'react'; +import { fetchMySubmissions, setContestStatus } from '../../../redux/slices/contests'; + +export interface Mission { + id: number; + authorId: number; + name: string; + difficulty: 'Easy' | 'Medium' | 'Hard'; + tags: string[]; + timeLimit: number; + memoryLimit: number; + createdAt: string; + updatedAt: string; +} + +interface SubmissionsProps { + contestId: number; +} + +const Submissions: FC = ({ contestId }) => { + const dispatch = useAppDispatch(); + + const {submissions, status} = useAppSelector( + (state) => state.contests.fetchMySubmissions + ); + + useEffect(() => { + dispatch(fetchMySubmissions(contestId)); + }, [contestId]); + + useEffect(() => { + if (status == "successful"){ + dispatch(setContestStatus({key:"fetchMySubmissions", status: "idle"})); + } + }, [status]) + + const checkStatus = (status: string) => { + if (status == 'IncorrectAnswer') return 'wronganswer'; + if (status == 'TimeLimitError') return 'timelimit'; + return undefined; + }; + + return ( +
+ {submissions && + submissions.map((v, i) => ( + + ))} +
+ ); +}; + +export default Submissions; diff --git a/src/views/home/contests/ModalCreate.tsx b/src/views/home/contests/ModalCreate.tsx index 6863b0d..0c30465 100644 --- a/src/views/home/contests/ModalCreate.tsx +++ b/src/views/home/contests/ModalCreate.tsx @@ -37,7 +37,6 @@ const ModalCreateContest: FC = ({ attemptDurationMinutes: 0, maxAttempts: 0, allowEarlyFinish: false, - groupIds: [], missionIds: [], articleIds: [], }); diff --git a/src/views/mission/statement/MissionSubmissions.tsx b/src/views/mission/statement/MissionSubmissions.tsx index cdd725d..9d8db13 100644 --- a/src/views/mission/statement/MissionSubmissions.tsx +++ b/src/views/mission/statement/MissionSubmissions.tsx @@ -1,6 +1,7 @@ import SubmissionItem from './SubmissionItem'; -import { useAppSelector } from '../../../redux/hooks'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { FC, useEffect } from 'react'; +import { fetchMySubmissions } from '../../../redux/slices/contests'; export interface Mission { id: number; @@ -16,13 +17,18 @@ export interface Mission { interface MissionSubmissionsProps { missionId: number; + contestId?: number; } -const MissionSubmissions: FC = ({ missionId }) => { +const MissionSubmissions: FC = ({ missionId, contestId }) => { + const dispatch = useAppDispatch(); const submissions = useAppSelector( (state) => state.submin.submitsById[missionId], ); + + const {submissions: contestSubmission, status: contestStatus} = useAppSelector((state) => state.contests.fetchMySubmissions); + useEffect(() => {}, []); const checkStatus = (status: string) => { @@ -31,9 +37,40 @@ const MissionSubmissions: FC = ({ missionId }) => { return undefined; }; + + useEffect(() => { + if (contestId){ + dispatch(fetchMySubmissions(contestId)); + } + }, [contestId, missionId]) return (
- {submissions && + +{contestId ? +contestSubmission && + contestSubmission.filter(v => v.solution.missionId == missionId).map((v, i) => ( + + )) +: + submissions && submissions.map((v, i) => ( = ({ missionId }) => { : checkStatus(v.solution.testerErrorCode) } /> - ))} + )) + }
); }; From 69655dda8212181a8a6cd99f28d56a548d2b34d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Fri, 7 Nov 2025 19:04:45 +0300 Subject: [PATCH 07/17] contests --- .../home/account/missions/MissionsBlock.tsx | 69 +++++-- src/views/home/contest/Contest.tsx | 8 +- src/views/home/contest/MissionItem.tsx | 2 +- src/views/home/contest/Missions.tsx | 143 +++++++++++---- src/views/home/contest/SubmissionItem.tsx | 23 ++- src/views/home/contest/Submissions.tsx | 172 ++++++++++++------ .../mission/statement/MissionSubmissions.tsx | 95 +++------- 7 files changed, 334 insertions(+), 178 deletions(-) diff --git a/src/views/home/account/missions/MissionsBlock.tsx b/src/views/home/account/missions/MissionsBlock.tsx index c20a241..8cd10b0 100644 --- a/src/views/home/account/missions/MissionsBlock.tsx +++ b/src/views/home/account/missions/MissionsBlock.tsx @@ -1,19 +1,66 @@ -import { useEffect } from 'react'; -import { useAppDispatch } from '../../../../redux/hooks'; -import { setMenuActiveProfilePage } from '../../../../redux/slices/store'; +import { FC, useEffect } from "react"; +import { useAppDispatch } from "../../../../redux/hooks"; +import { setMenuActiveProfilePage } from "../../../../redux/slices/store"; +import { cn } from "../../../../lib/cn"; + + +interface ItemProps { + count: number; + totalCount: number; + title: string; + color?: "default" | "red" | "green" | "orange"; +} + +const Item: FC = ({count, totalCount, title, color = "default"}) => { + + return
+
{count}/{totalCount}
+
{title}
+
+}; const MissionsBlock = () => { - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - useEffect(() => { - dispatch(setMenuActiveProfilePage('missions')); - }, []); + useEffect(() => { + dispatch(setMenuActiveProfilePage("missions")); + }, []); - return ( -
- Пока пусто :( + return ( +
+
+
+
Решенные задачи
+
+ +
+ +
+
+ + + +
+ +
+
Компетенции
+ +
+ + + +
- ); +
Недавиние задачи
+
Мои задачи
+
+
+ ); }; export default MissionsBlock; diff --git a/src/views/home/contest/Contest.tsx b/src/views/home/contest/Contest.tsx index 79e7fcf..b30de29 100644 --- a/src/views/home/contest/Contest.tsx +++ b/src/views/home/contest/Contest.tsx @@ -24,6 +24,7 @@ const Contest = () => { const dispatch = useAppDispatch(); const contest = useAppSelector((state) => state.contests.fetchContestById.contest); + useEffect(() => { dispatch(setMenuActivePage('contest')); }, []); @@ -33,17 +34,16 @@ const Contest = () => { }, [contestIdNumber]); return ( -
- {navigate(`/contest/${contestIdNumber}/submissions`)}} text='Мои посылки' /> +
} + element={} /> } + element={} /> diff --git a/src/views/home/contest/MissionItem.tsx b/src/views/home/contest/MissionItem.tsx index 8f4e6cb..bf5f29e 100644 --- a/src/views/home/contest/MissionItem.tsx +++ b/src/views/home/contest/MissionItem.tsx @@ -10,7 +10,7 @@ export interface MissionItemProps { timeLimit?: number; memoryLimit?: number; type?: 'first' | 'second'; - status?: 'empty' | 'success' | 'error'; + status?: 'success' | 'error'; } export function formatMilliseconds(ms: number): string { diff --git a/src/views/home/contest/Missions.tsx b/src/views/home/contest/Missions.tsx index f18511a..1893df9 100644 --- a/src/views/home/contest/Missions.tsx +++ b/src/views/home/contest/Missions.tsx @@ -1,45 +1,124 @@ -import { FC } from 'react'; -import MissionItem from './MissionItem'; -import { Contest } from '../../../redux/slices/contests'; +import { FC, useEffect } from "react"; +import MissionItem from "./MissionItem"; +import { + Contest, + fetchMySubmissions, + setContestStatus, +} from "../../../redux/slices/contests"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; +import { PrimaryButton } from "../../../components/button/PrimaryButton"; +import { useNavigate } from "react-router-dom"; +import { arrowLeft } from "../../../assets/icons/header"; export interface Article { - id: number; - name: string; - tags: string[]; + id: number; + name: string; + tags: string[]; } interface ContestMissionsProps { - contest?: Contest; + contest?: Contest; } const ContestMissions: FC = ({ contest }) => { - if (!contest) { - return <>; - } + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const { submissions, status } = useAppSelector( + (state) => state.contests.fetchMySubmissions + ); - return ( -
-
-
-
- {contest?.name} {contest.id} -
-
- {(contest.missions ?? []).map((v, i) => ( - - ))} -
-
+ useEffect(() => { + if (contest) dispatch(fetchMySubmissions(contest.id)); + }, [contest]); + + useEffect(() => { + if (status == "successful") { + dispatch(setContestStatus({ key: "fetchMySubmissions", status: "idle" })); + } + }, [status]); + + if (!contest) { + return <>; + } + + const solvedCount = (contest.missions ?? []).filter((mission) => + submissions.some( + (s) => + s.solution.missionId === mission.id && + s.solution.status === "Accepted: All tests passed" + ) + ).length; + + const totalCount = contest.missions?.length ?? 0; + + return ( +
+
+
+ {contest.name}
- ); +
+
+ { + navigate(`/home/contests`); + }} + /> + + Контест #{contest.id} + +
+
{contest.attemptDurationMinutes ?? 0} минут
+
+
+
+
{`${solvedCount}/${totalCount} Решено`}
+ { + navigate(`/contest/${contest.id}/submissions`); + }} + text="Мои посылки" + /> +
+ +
+
+ {(contest.missions ?? []).map((v, i) => { + const missionSubmissions = submissions.filter( + (s) => s.solution.missionId === v.id + ); + + const hasSuccess = missionSubmissions.some( + (s) => s.solution.status == "Accepted: All tests passed" + ); + + console.log(missionSubmissions); + + const status = hasSuccess + ? "success" + : missionSubmissions.length > 0 + ? "error" + : undefined; + + return ( + + ); + })} +
+
+
+ ); }; export default ContestMissions; diff --git a/src/views/home/contest/SubmissionItem.tsx b/src/views/home/contest/SubmissionItem.tsx index c4b86a3..6d90537 100644 --- a/src/views/home/contest/SubmissionItem.tsx +++ b/src/views/home/contest/SubmissionItem.tsx @@ -4,9 +4,12 @@ import { cn } from '../../../lib/cn'; export interface SubmissionItemProps { id: number; + datetime: string; + missionId: number; language: string; - time: string; verdict: string; + duration: number; + memory: number; type: 'first' | 'second'; status?: 'success' | 'wronganswer' | 'timelimit'; } @@ -37,20 +40,23 @@ function formatDate(dateString: string): string { const SubmissionItem: React.FC = ({ id, + datetime, + missionId, language, - time, verdict, + duration, + memory, type, - status, + status }) => { // const navigate = useNavigate(); return (
= ({ >
#{id}
- {formatDate(time)} + {formatDate(datetime)}
+
{missionId}
{language}
= ({ )} > {verdict} +
+
{formatMilliseconds(duration)}
+
+ {formatBytesToMB(memory)}
); diff --git a/src/views/home/contest/Submissions.tsx b/src/views/home/contest/Submissions.tsx index 24024a2..7143099 100644 --- a/src/views/home/contest/Submissions.tsx +++ b/src/views/home/contest/Submissions.tsx @@ -1,73 +1,129 @@ -import SubmissionItem from './SubmissionItem'; -import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; -import { FC, useEffect } from 'react'; -import { fetchMySubmissions, setContestStatus } from '../../../redux/slices/contests'; +import SubmissionItem from "./SubmissionItem"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; +import { FC, useEffect } from "react"; +import { + Contest, + fetchMySubmissions, + setContestStatus, +} from "../../../redux/slices/contests"; +import { arrowLeft } from "../../../assets/icons/header"; +import { useNavigate } from "react-router-dom"; export interface Mission { - id: number; - authorId: number; - name: string; - difficulty: 'Easy' | 'Medium' | 'Hard'; - tags: string[]; - timeLimit: number; - memoryLimit: number; - createdAt: string; - updatedAt: string; + id: number; + authorId: number; + name: string; + difficulty: "Easy" | "Medium" | "Hard"; + tags: string[]; + timeLimit: number; + memoryLimit: number; + createdAt: string; + updatedAt: string; } interface SubmissionsProps { - contestId: number; + contest: Contest; } -const Submissions: FC = ({ contestId }) => { - const dispatch = useAppDispatch(); - - const {submissions, status} = useAppSelector( - (state) => state.contests.fetchMySubmissions - ); +const Submissions: FC = ({ contest }) => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); - useEffect(() => { - dispatch(fetchMySubmissions(contestId)); - }, [contestId]); + const { submissions, status } = useAppSelector( + (state) => state.contests.fetchMySubmissions + ); - useEffect(() => { - if (status == "successful"){ - dispatch(setContestStatus({key:"fetchMySubmissions", status: "idle"})); - } - }, [status]) + useEffect(() => { + if (contest && contest.id) dispatch(fetchMySubmissions(contest.id)); + }, [contest]); - const checkStatus = (status: string) => { - if (status == 'IncorrectAnswer') return 'wronganswer'; - if (status == 'TimeLimitError') return 'timelimit'; - return undefined; - }; + useEffect(() => { + if (status == "successful") { + dispatch(setContestStatus({ key: "fetchMySubmissions", status: "idle" })); + } + }, [status]); - return ( -
- {submissions && - submissions.map((v, i) => ( - - ))} + const checkStatus = (status: string) => { + if (status == "IncorrectAnswer") return "wronganswer"; + if (status == "TimeLimitError") return "timelimit"; + return undefined; + }; + + const solvedCount = (contest.missions ?? []).filter((mission) => + submissions.some( + (s) => + s.solution.missionId === mission.id && + s.solution.status === "Accepted: All tests passed" + ) + ).length; + + const totalCount = contest.missions?.length ?? 0; + + return ( +
+
+
+ {contest.name}
- ); +
+
+ { + navigate(`/contest/${contest.id}`); + }} + /> + + Контест #{contest.id} + +
+
{`${solvedCount}/${totalCount} Решено`}
+
+
+ +
+
+
Посылка
+
Когда
+
Задача
+
Язык
+
Вердикт
+
Время
+
Память
+
+ + {!submissions || submissions.length == 0 ? ( +
Вы еще ничего не отсылали
+ ) : ( + <> + {submissions.map((v, i) => ( + + ))} + + )} +
+
+ ); }; export default Submissions; diff --git a/src/views/mission/statement/MissionSubmissions.tsx b/src/views/mission/statement/MissionSubmissions.tsx index 9d8db13..04dca84 100644 --- a/src/views/mission/statement/MissionSubmissions.tsx +++ b/src/views/mission/statement/MissionSubmissions.tsx @@ -1,7 +1,6 @@ import SubmissionItem from './SubmissionItem'; -import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; -import { FC, useEffect } from 'react'; -import { fetchMySubmissions } from '../../../redux/slices/contests'; +import { useAppSelector } from '../../../redux/hooks'; +import { FC } from 'react'; export interface Mission { id: number; @@ -21,78 +20,42 @@ interface MissionSubmissionsProps { } const MissionSubmissions: FC = ({ missionId, contestId }) => { - const dispatch = useAppDispatch(); const submissions = useAppSelector( - (state) => state.submin.submitsById[missionId], + (state) => state.submin.submitsById[missionId] || [] ); - - const {submissions: contestSubmission, status: contestStatus} = useAppSelector((state) => state.contests.fetchMySubmissions); - - useEffect(() => {}, []); - const checkStatus = (status: string) => { - if (status == 'IncorrectAnswer') return 'wronganswer'; - if (status == 'TimeLimitError') return 'timelimit'; + if (status === 'IncorrectAnswer') return 'wronganswer'; + if (status === 'TimeLimitError') return 'timelimit'; return undefined; }; + // Если contestId передан, фильтруем по нему, иначе показываем все + const filteredSubmissions = contestId + ? submissions.filter((v) => v.contestId === contestId) + : submissions; - useEffect(() => { - if (contestId){ - dispatch(fetchMySubmissions(contestId)); - } - }, [contestId, missionId]) return ( -
- -{contestId ? -contestSubmission && - contestSubmission.filter(v => v.solution.missionId == missionId).map((v, i) => ( - - )) -: - submissions && - submissions.map((v, i) => ( - - )) - } +
+ {filteredSubmissions.map((v, i) => ( + + ))}
); }; From b12a3acf1d711885b31293a0a99a07310c3e7274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Sat, 8 Nov 2025 06:58:56 +0300 Subject: [PATCH 08/17] missions and filter --- src/assets/icons/filters/filters-active.svg | 3 + src/assets/icons/filters/filters.svg | 3 + src/assets/icons/filters/index.ts | 6 + src/assets/icons/filters/sort-active.svg | 3 + src/assets/icons/filters/sort.svg | 3 + src/components/button/PrimaryButton.tsx | 11 +- src/components/button/ReverseButton.tsx | 11 +- src/components/button/SecondaryButton.tsx | 9 +- src/components/drop-down-list/Filter.tsx | 124 +++++++++++++++++ src/components/drop-down-list/Sorter.tsx | 127 ++++++++++++++++++ src/pages/ContestEditor.tsx | 78 +++++------ src/pages/Home.tsx | 4 +- src/redux/slices/account.ts | 0 src/redux/slices/missions.ts | 40 ++++++ src/views/home/account/Account.tsx | 7 +- src/views/home/account/contests/Contests.tsx | 3 - .../home/account/contests/MyContestItem.tsx | 6 - .../account/contests/RegisterContestItem.tsx | 14 +- src/views/home/account/missions/Missions.tsx | 109 +++++++++++++++ .../home/account/missions/MissionsBlock.tsx | 119 ++++++++-------- .../home/account/missions/MyMissionItem.tsx | 90 +++++++++++++ src/views/home/contest/Contest.tsx | 17 +-- src/views/home/contests/ContestItem.tsx | 12 +- src/views/home/missions/Filter.tsx | 48 +++++++ src/views/home/missions/Missions.tsx | 3 +- tsconfig.app.tsbuildinfo | 2 +- 26 files changed, 694 insertions(+), 158 deletions(-) create mode 100644 src/assets/icons/filters/filters-active.svg create mode 100644 src/assets/icons/filters/filters.svg create mode 100644 src/assets/icons/filters/index.ts create mode 100644 src/assets/icons/filters/sort-active.svg create mode 100644 src/assets/icons/filters/sort.svg create mode 100644 src/components/drop-down-list/Filter.tsx create mode 100644 src/components/drop-down-list/Sorter.tsx delete mode 100644 src/redux/slices/account.ts create mode 100644 src/views/home/account/missions/Missions.tsx create mode 100644 src/views/home/account/missions/MyMissionItem.tsx create mode 100644 src/views/home/missions/Filter.tsx diff --git a/src/assets/icons/filters/filters-active.svg b/src/assets/icons/filters/filters-active.svg new file mode 100644 index 0000000..4c120fa --- /dev/null +++ b/src/assets/icons/filters/filters-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/filters/filters.svg b/src/assets/icons/filters/filters.svg new file mode 100644 index 0000000..00b357b --- /dev/null +++ b/src/assets/icons/filters/filters.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/filters/index.ts b/src/assets/icons/filters/index.ts new file mode 100644 index 0000000..2eca117 --- /dev/null +++ b/src/assets/icons/filters/index.ts @@ -0,0 +1,6 @@ +import FilterActive from './filters-active.svg'; +import Filter from './filters.svg'; +import Sort from './sort.svg'; +import SortActive from './sort-active.svg'; + +export { Filter, FilterActive, Sort, SortActive }; diff --git a/src/assets/icons/filters/sort-active.svg b/src/assets/icons/filters/sort-active.svg new file mode 100644 index 0000000..971008b --- /dev/null +++ b/src/assets/icons/filters/sort-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/filters/sort.svg b/src/assets/icons/filters/sort.svg new file mode 100644 index 0000000..5035447 --- /dev/null +++ b/src/assets/icons/filters/sort.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/button/PrimaryButton.tsx b/src/components/button/PrimaryButton.tsx index 6a9423f..415ad1f 100644 --- a/src/components/button/PrimaryButton.tsx +++ b/src/components/button/PrimaryButton.tsx @@ -5,7 +5,7 @@ interface ButtonProps { disabled?: boolean; text?: string; className?: string; - onClick: (e: React.MouseEvent) => void; + onClick: () => void; children?: React.ReactNode; color?: 'primary' | 'secondary' | 'error' | 'warning' | 'success'; } @@ -41,6 +41,9 @@ export const PrimaryButton: React.FC = ({ disabled && 'pointer-events-none', className, )} + onClick={(e) => { + e.stopPropagation(); + }} > {/* Основной контейнер, */}
= ({ '[&:focus-visible+*]:outline-liquid-brightmain', )} disabled={disabled} - onClick={( - e: React.MouseEvent, - ) => { - onClick(e); + onClick={() => { + onClick(); }} /> diff --git a/src/components/button/ReverseButton.tsx b/src/components/button/ReverseButton.tsx index 67ddceb..142924a 100644 --- a/src/components/button/ReverseButton.tsx +++ b/src/components/button/ReverseButton.tsx @@ -5,7 +5,7 @@ interface ButtonProps { disabled?: boolean; text?: string; className?: string; - onClick: (e: React.MouseEvent) => void; + onClick: () => void; children?: React.ReactNode; color?: 'primary' | 'secondary' | 'error' | 'warning' | 'success'; } @@ -41,6 +41,9 @@ export const ReverseButton: React.FC = ({ disabled && 'pointer-events-none', className, )} + onClick={(e) => { + e.stopPropagation(); + }} > {/* Основной контейнер, */}
= ({ '[&:focus-visible+*]:outline-liquid-brightmain', )} disabled={disabled} - onClick={( - e: React.MouseEvent, - ) => { - onClick(e); + onClick={() => { + onClick(); }} /> diff --git a/src/components/button/SecondaryButton.tsx b/src/components/button/SecondaryButton.tsx index e71ab94..bbb2f36 100644 --- a/src/components/button/SecondaryButton.tsx +++ b/src/components/button/SecondaryButton.tsx @@ -5,7 +5,7 @@ interface ButtonProps { disabled?: boolean; text?: string; className?: string; - onClick: (e: React.MouseEvent) => void; + onClick: () => void; children?: React.ReactNode; } @@ -23,6 +23,9 @@ export const SecondaryButton: React.FC = ({ disabled && 'pointer-events-none', className, )} + onClick={(e) => { + e.stopPropagation(); + }} > {/* Основной контейнер, */}
= ({ '[&:focus-visible+*]:outline-liquid-brightmain', )} disabled={disabled} - onClick={(e) => { - onClick(e); + onClick={() => { + onClick(); }} /> diff --git a/src/components/drop-down-list/Filter.tsx b/src/components/drop-down-list/Filter.tsx new file mode 100644 index 0000000..a3816c2 --- /dev/null +++ b/src/components/drop-down-list/Filter.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { cn } from '../../lib/cn'; +import { checkMark, chevroneDropDownList } from '../../assets/icons/input'; +import { useClickOutside } from '../../hooks/useClickOutside'; + +export interface FilterItem { + text: string; + value: string; +} + +interface FilterProps { + disabled?: boolean; + className?: string; + onChange: (state: string[]) => void; // теперь массив выбранных значений + defaultState?: FilterItem[]; + items: FilterItem[]; +} + +export const Filter: React.FC = ({ + className = '', + onChange, + defaultState = [], + items = [{ text: '', value: '' }], +}) => { + if (items.length === 0) items.push({ text: '', value: '' }); + + const [selectedValues, setSelectedValues] = + React.useState(defaultState); + const [active, setActive] = React.useState(false); + + React.useEffect(() => { + onChange(selectedValues.map((v) => v.value)); + }, [selectedValues]); + + const ref = React.useRef(null); + useClickOutside(ref, () => setActive(false)); + + const toggleItem = (item: FilterItem) => { + const exists = selectedValues.some((v) => v.value === item.value); + if (exists) { + setSelectedValues( + selectedValues.filter((v) => v.value !== item.value), + ); + } else { + setSelectedValues([...selectedValues, item]); + } + }; + + const displayText = + selectedValues.length > 0 + ? selectedValues.map((v) => v.text).join(', ') + : 'Выберите...'; + + return ( +
+
setActive(!active)} + title={displayText} + > + {displayText} +
+ + + +
+
+
+ {items.map((v, i) => { + const isSelected = selectedValues.some( + (sel) => sel.value === v.value, + ); + return ( +
toggleItem(v)} + > + {v.text} + + {isSelected && ( + + )} +
+ ); + })} +
+
+
+
+ ); +}; diff --git a/src/components/drop-down-list/Sorter.tsx b/src/components/drop-down-list/Sorter.tsx new file mode 100644 index 0000000..8c1e14d --- /dev/null +++ b/src/components/drop-down-list/Sorter.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { cn } from '../../lib/cn'; +import { checkMark } from '../../assets/icons/input'; +import { useClickOutside } from '../../hooks/useClickOutside'; +import { Sort, SortActive } from '../../assets/icons/filters'; + +export interface SorterItem { + text: string; + value: string; +} + +interface SorterProps { + disabled?: boolean; + className?: string; + onChange: (state: string) => void; + defaultState?: SorterItem; + items: SorterItem[]; +} + +export const Sorter: React.FC = ({ + // disabled = false, + className = '', + onChange, + defaultState, + items = [{ text: '', value: '' }], +}) => { + if (items.length == 0) items.push({ text: '', value: '' }); + + const [value, setValue] = React.useState( + defaultState != undefined ? defaultState : items[0], + ); + const [active, setActive] = React.useState(false); + + React.useEffect(() => onChange(value.value), [value]); + + const ref = React.useRef(null); + + useClickOutside(ref, () => { + setActive(false); + }); + + return ( +
+
{ + setActive(!active); + }} + > +
+ {' '} + {value.text} +
+
+ +
+ + + +
+
+
+ {items.map((v, i) => ( +
{ + setValue(v); + setActive(false); + }} + > + {v.text} + + {v.text == value.text && ( + + )} +
+ ))} +
+
+
+
+ ); +}; diff --git a/src/pages/ContestEditor.tsx b/src/pages/ContestEditor.tsx index 03d06b0..6b28820 100644 --- a/src/pages/ContestEditor.tsx +++ b/src/pages/ContestEditor.tsx @@ -4,7 +4,6 @@ import { PrimaryButton } from '../components/button/PrimaryButton'; import { Input } from '../components/input/Input'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { - createContest, CreateContestBody, deleteContest, fetchContestById, @@ -17,7 +16,6 @@ import { Navigate, useNavigate } from 'react-router-dom'; import { fetchMissionById } from '../redux/slices/missions'; import { ReverseButton } from '../components/button/ReverseButton'; - interface Mission { id: number; name: string; @@ -27,7 +25,6 @@ interface Mission { * Страница создания / редактирования контеста */ const ContestEditor = () => { - const dispatch = useAppDispatch(); const navigate = useNavigate(); @@ -36,21 +33,16 @@ const ContestEditor = () => { const contestId = Number(query.get('contestId') ?? undefined); const refactor = !!contestId; - if (!refactor){ - return + if (!refactor) { + return ; } - - const status = useAppSelector( (state) => state.contests.createContest.status, ); - - const [missionIdInput, setMissionIdInput] = useState(''); - const [contest, setContest] = useState({ name: '', description: '', @@ -67,9 +59,12 @@ const ContestEditor = () => { const [missions, setMissions] = useState([]); - - const statusDelete = useAppSelector((state) => state.contests.deleteContest.status) - const statusUpdate = useAppSelector((state) => state.contests.updateContest.status); + const statusDelete = useAppSelector( + (state) => state.contests.deleteContest.status, + ); + const statusUpdate = useAppSelector( + (state) => state.contests.updateContest.status, + ); const { contest: contestById, status: contestByIdstatus } = useAppSelector( (state) => state.contests.fetchContestById, @@ -85,15 +80,13 @@ const ContestEditor = () => { }; const handleUpdateContest = () => { - dispatch(updateContest({...contest, contestId})); + dispatch(updateContest({ ...contest, contestId })); }; - + const handleDeleteContest = () => { dispatch(deleteContest(contestId)); }; - - const addMission = () => { const id = Number(missionIdInput.trim()); if (!id || contest.missionIds?.includes(id)) return; @@ -121,19 +114,22 @@ const ContestEditor = () => { }; useEffect(() => { - if (statusDelete == "successful"){ - dispatch(setContestStatus({key: "deleteContest", status: "idle"})) - navigate('/home/account/contests') + if (statusDelete == 'successful') { + dispatch( + setContestStatus({ key: 'deleteContest', status: 'idle' }), + ); + navigate('/home/account/contests'); } - }, [statusDelete]) + }, [statusDelete]); - useEffect(() => { - if (statusUpdate == "successful"){ - dispatch(setContestStatus({key: "updateContest", status: "idle"})) - navigate('/home/account/contests') + if (statusUpdate == 'successful') { + dispatch( + setContestStatus({ key: 'updateContest', status: 'idle' }), + ); + navigate('/home/account/contests'); } - }, [statusUpdate]) + }, [statusUpdate]); useEffect(() => { if (refactor) { @@ -146,8 +142,10 @@ const ContestEditor = () => { setContest({ ...contestById, // groupIds: contestById.groups.map(group => group.groupId), - missionIds: contestById.missions?.map(mission => mission.id), - articleIds: contestById.articles?.map(article => article.articleId), + missionIds: contestById.missions?.map((mission) => mission.id), + articleIds: contestById.articles?.map( + (article) => article.articleId, + ), visibility: 'Public', scheduleType: 'AlwaysOpen', }); @@ -300,19 +298,17 @@ const ContestEditor = () => { {/* Кнопки */}
- - - - + +
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 9cebabb..18e78ff 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -51,8 +51,10 @@ const Home = () => {

{jwt}

{ - if (jwt) + if (jwt) { navigator.clipboard.writeText(jwt); + alert(jwt); + } }} text="скопировать токен" className="pt-[20px]" diff --git a/src/redux/slices/account.ts b/src/redux/slices/account.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/redux/slices/missions.ts b/src/redux/slices/missions.ts index 93f24eb..9125402 100644 --- a/src/redux/slices/missions.ts +++ b/src/redux/slices/missions.ts @@ -20,6 +20,8 @@ export interface Mission { tags: string[]; createdAt: string; updatedAt: string; + timeLimit: number; + memoryLimit: number; statements?: Statement[]; } @@ -31,6 +33,7 @@ interface MissionsState { fetchList: Status; fetchById: Status; upload: Status; + fetchMy: Status; }; error: string | null; } @@ -45,6 +48,7 @@ const initialState: MissionsState = { fetchList: 'idle', fetchById: 'idle', upload: 'idle', + fetchMy: 'idle', }, error: null, }; @@ -90,6 +94,22 @@ export const fetchMissionById = createAsyncThunk( }, ); +// ✅ GET /missions/my +export const fetchMyMissions = createAsyncThunk( + 'missions/fetchMyMissions', + async (_, { rejectWithValue }) => { + try { + const response = await axios.get('/missions/my'); + return response.data as Mission[]; // массив миссий пользователя + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || + 'Ошибка при получении моих миссий', + ); + } + }, +); + // POST /missions/upload export const uploadMission = createAsyncThunk( 'missions/uploadMission', @@ -189,6 +209,26 @@ const missionsSlice = createSlice({ }, ); + // ✅ FETCH MY MISSIONS ─── + builder.addCase(fetchMyMissions.pending, (state) => { + state.statuses.fetchMy = 'loading'; + state.error = null; + }); + builder.addCase( + fetchMyMissions.fulfilled, + (state, action: PayloadAction) => { + state.statuses.fetchMy = 'successful'; + state.missions = action.payload; + }, + ); + builder.addCase( + fetchMyMissions.rejected, + (state, action: PayloadAction) => { + state.statuses.fetchMy = 'failed'; + state.error = action.payload; + }, + ); + // ─── UPLOAD MISSION ─── builder.addCase(uploadMission.pending, (state) => { state.statuses.upload = 'loading'; diff --git a/src/views/home/account/Account.tsx b/src/views/home/account/Account.tsx index 5d4530a..527dc5c 100644 --- a/src/views/home/account/Account.tsx +++ b/src/views/home/account/Account.tsx @@ -1,7 +1,7 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import AccountMenu from './AccoutMenu'; import RightPanel from './RightPanel'; -import MissionsBlock from './missions/MissionsBlock'; +import Missions from './missions/Missions'; import Contests from './contests/Contests'; import ArticlesBlock from './articles/ArticlesBlock'; import { useAppDispatch } from '../../../redux/hooks'; @@ -24,10 +24,7 @@ const Account = () => {
- } - /> + } /> } diff --git a/src/views/home/account/contests/Contests.tsx b/src/views/home/account/contests/Contests.tsx index 8f96a04..67fdbb7 100644 --- a/src/views/home/account/contests/Contests.tsx +++ b/src/views/home/account/contests/Contests.tsx @@ -14,9 +14,6 @@ const Contests = () => { const myContestsState = useAppSelector( (state) => state.contests.fetchMyContests, ); - const regContestsState = useAppSelector( - (state) => state.contests.fetchRegisteredContests, - ); // При загрузке страницы — выставляем вкладку и подгружаем контесты useEffect(() => { diff --git a/src/views/home/account/contests/MyContestItem.tsx b/src/views/home/account/contests/MyContestItem.tsx index 1fc3785..eef1bf4 100644 --- a/src/views/home/account/contests/MyContestItem.tsx +++ b/src/views/home/account/contests/MyContestItem.tsx @@ -1,7 +1,5 @@ import { cn } from '../../../../lib/cn'; import { Account } from '../../../../assets/icons/auth'; -import { PrimaryButton } from '../../../../components/button/PrimaryButton'; -import { ReverseButton } from '../../../../components/button/ReverseButton'; import { useNavigate } from 'react-router-dom'; import { Edit } from '../../../../assets/icons/input'; @@ -57,10 +55,6 @@ const ContestItem: React.FC = ({ }) => { const navigate = useNavigate(); - const now = new Date(); - - const waitTime = new Date(startAt).getTime() - now.getTime(); - return (
= ({ {statusRegister == 'reg' ? ( <> {' '} - { - e.stopPropagation(); - }} - text="Регистрация" - /> + {}} text="Регистрация" /> ) : ( <> {' '} - { - e.stopPropagation(); - }} - text="Вы записаны" - /> + {}} text="Вы записаны" /> )}
diff --git a/src/views/home/account/missions/Missions.tsx b/src/views/home/account/missions/Missions.tsx new file mode 100644 index 0000000..1d8d2fb --- /dev/null +++ b/src/views/home/account/missions/Missions.tsx @@ -0,0 +1,109 @@ +import { FC, useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; +import { setMenuActiveProfilePage } from '../../../../redux/slices/store'; +import { cn } from '../../../../lib/cn'; +import MissionsBlock from './MissionsBlock'; +import { + fetchMyMissions, + setMissionsStatus, +} from '../../../../redux/slices/missions'; + +interface ItemProps { + count: number; + totalCount: number; + title: string; + color?: 'default' | 'red' | 'green' | 'orange'; +} + +const Item: FC = ({ + count, + totalCount, + title, + color = 'default', +}) => { + return ( +
+
+ {count}/{totalCount} +
+
{title}
+
+ ); +}; + +const Missions = () => { + const dispatch = useAppDispatch(); + const missions = useAppSelector((state) => state.missions.missions); + const status = useAppSelector((state) => state.missions.statuses.fetchMy); + + useEffect(() => { + dispatch(setMenuActiveProfilePage('missions')); + dispatch(fetchMyMissions()); + }, []); + + useEffect(() => { + dispatch(setMissionsStatus({ key: 'fetchMy', status: 'idle' })); + }, [status]); + + return ( +
+
+
+
+ Решенные задачи +
+
+
+ +
+
+ + + +
+
+
+ Компетенции +
+ +
+ + + +
+
+
+ +
+
+
+ ); +}; + +export default Missions; diff --git a/src/views/home/account/missions/MissionsBlock.tsx b/src/views/home/account/missions/MissionsBlock.tsx index 8cd10b0..b9b2efe 100644 --- a/src/views/home/account/missions/MissionsBlock.tsx +++ b/src/views/home/account/missions/MissionsBlock.tsx @@ -1,66 +1,71 @@ -import { FC, useEffect } from "react"; -import { useAppDispatch } from "../../../../redux/hooks"; -import { setMenuActiveProfilePage } from "../../../../redux/slices/store"; -import { cn } from "../../../../lib/cn"; +import { useState, FC } from 'react'; +import { cn } from '../../../../lib/cn'; +import { ChevroneDown } from '../../../../assets/icons/groups'; +import MyMissionItem from './MyMissionItem'; +import { Mission } from '../../../../redux/slices/missions'; - -interface ItemProps { - count: number; - totalCount: number; +interface MissionsBlockProps { + missions: Mission[]; title: string; - color?: "default" | "red" | "green" | "orange"; + className?: string; } -const Item: FC = ({count, totalCount, title, color = "default"}) => { +const MissionsBlock: FC = ({ + missions, + title, + className, +}) => { + const [active, setActive] = useState(true); - return
-
{count}/{totalCount}
-
{title}
-
-}; - -const MissionsBlock = () => { - const dispatch = useAppDispatch(); - - useEffect(() => { - dispatch(setMenuActiveProfilePage("missions")); - }, []); - - return ( -
-
-
-
Решенные задачи
-
- -
- -
-
- - - -
- -
-
Компетенции
- -
- - - -
+ return ( +
+
{ + setActive(!active); + }} + > + {title} + +
+
+
+
+ {missions.map((v, i) => ( + + ))} +
+
+
-
Недавиние задачи
-
Мои задачи
-
-
- ); + ); }; export default MissionsBlock; diff --git a/src/views/home/account/missions/MyMissionItem.tsx b/src/views/home/account/missions/MyMissionItem.tsx new file mode 100644 index 0000000..18b1ef3 --- /dev/null +++ b/src/views/home/account/missions/MyMissionItem.tsx @@ -0,0 +1,90 @@ +import { cn } from '../../../../lib/cn'; +import { IconError, IconSuccess } from '../../../../assets/icons/missions'; +import { useNavigate } from 'react-router-dom'; +import { Edit } from '../../../../assets/icons/input'; + +export interface MissionItemProps { + id: number; + authorId?: number; + name: string; + difficulty: number; + tags?: string[]; + timeLimit?: number; + memoryLimit?: number; + createdAt?: string; + updatedAt?: string; + type?: 'first' | 'second'; + status?: 'empty' | 'success' | 'error'; +} + +export function formatMilliseconds(ms: number): string { + const rounded = Math.round(ms) / 1000; + const formatted = rounded.toString().replace(/\.?0+$/, ''); + return `${formatted} c`; +} + +export function formatBytesToMB(bytes: number): string { + const megabytes = Math.floor(bytes / (1024 * 1024)); + return `${megabytes} МБ`; +} + +const MissionItem: React.FC = ({ + id, + name, + difficulty, + timeLimit = 1000, + memoryLimit = 256 * 1024 * 1024, + type, + status, +}) => { + const navigate = useNavigate(); + const difficultyItems = ['Easy', 'Medium', 'Hard']; + const difficultyString = + difficultyItems[Math.min(Math.max(0, difficulty - 1), 2)]; + + return ( +
{ + navigate(`/mission/${id}?back=/home/account/missions`); + }} + > +
#{id}
+
{name}
+
+ стандартный ввод/вывод {formatMilliseconds(timeLimit)},{' '} + {formatBytesToMB(memoryLimit)} +
+
+ {difficultyString} +
+
+ { + e.stopPropagation(); + }} + /> +
+
+ ); +}; + +export default MissionItem; diff --git a/src/views/home/contest/Contest.tsx b/src/views/home/contest/Contest.tsx index b30de29..5a026bc 100644 --- a/src/views/home/contest/Contest.tsx +++ b/src/views/home/contest/Contest.tsx @@ -1,10 +1,9 @@ import { useEffect } from 'react'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { setMenuActivePage } from '../../../redux/slices/store'; -import { Navigate, Route, Routes, useNavigate, useParams } from 'react-router-dom'; +import { Navigate, Route, Routes, useParams } from 'react-router-dom'; import { fetchContestById } from '../../../redux/slices/contests'; import ContestMissions from './Missions'; -import { PrimaryButton } from '../../../components/button/PrimaryButton'; import Submissions from './Submissions'; export interface Article { @@ -14,7 +13,6 @@ export interface Article { } const Contest = () => { - const navigate = useNavigate(); const { contestId } = useParams<{ contestId: string }>(); const contestIdNumber = contestId && /^\d+$/.test(contestId) ? parseInt(contestId, 10) : null; @@ -22,8 +20,9 @@ const Contest = () => { return ; } const dispatch = useAppDispatch(); - const contest = useAppSelector((state) => state.contests.fetchContestById.contest); - + const contest = useAppSelector( + (state) => state.contests.fetchContestById.contest, + ); useEffect(() => { dispatch(setMenuActivePage('contest')); @@ -34,19 +33,17 @@ const Contest = () => { }, [contestIdNumber]); return ( -
+
- } + element={} /> } + element={} /> -
); }; diff --git a/src/views/home/contests/ContestItem.tsx b/src/views/home/contests/ContestItem.tsx index c2fc6fd..edf948e 100644 --- a/src/views/home/contests/ContestItem.tsx +++ b/src/views/home/contests/ContestItem.tsx @@ -73,6 +73,7 @@ const ContestItem: React.FC = ({ : ' bg-liquid-background', )} onClick={() => { + console.log(456); navigate(`/contest/${id}`); }} > @@ -99,8 +100,8 @@ const ContestItem: React.FC = ({ <> {' '} { - e.stopPropagation(); + onClick={() => { + console.log(123); }} text="Регистрация" /> @@ -108,12 +109,7 @@ const ContestItem: React.FC = ({ ) : ( <> {' '} - { - e.stopPropagation(); - }} - text="Вы записаны" - /> + {}} text="Вы записаны" /> )}
diff --git a/src/views/home/missions/Filter.tsx b/src/views/home/missions/Filter.tsx new file mode 100644 index 0000000..e698815 --- /dev/null +++ b/src/views/home/missions/Filter.tsx @@ -0,0 +1,48 @@ +import { Filter, FilterItem } from '../../../components/drop-down-list/Filter'; +import { Sorter } from '../../../components/drop-down-list/Sorter'; + +const Filters = () => { + const items: FilterItem[] = [ + { text: 'React', value: 'react' }, + { text: 'Vue', value: 'vue' }, + { text: 'Angular', value: 'angular' }, + { text: 'Svelte', value: 'svelte' }, + { text: 'Next.js', value: 'next' }, + { text: 'Nuxt', value: 'nuxt' }, + { text: 'Solid', value: 'solid' }, + { text: 'Qwik', value: 'qwik' }, + ]; + + return ( +
+
+ + console.log(v)} + /> + + {/* console.log(values)} // обработчик изменения + className="w-[240px]" + /> */} +
+ ); +}; + +export default Filters; diff --git a/src/views/home/missions/Missions.tsx b/src/views/home/missions/Missions.tsx index 3b5203e..28d71b0 100644 --- a/src/views/home/missions/Missions.tsx +++ b/src/views/home/missions/Missions.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'; import { setMenuActivePage } from '../../../redux/slices/store'; import { fetchMissions } from '../../../redux/slices/missions'; import ModalCreate from './ModalCreate'; +import Filters from './Filter'; export interface Mission { id: number; @@ -45,7 +46,7 @@ const Missions = () => { />
-
+
{missions.map((v, i) => ( diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo index dc32943..88b15bb 100644 --- a/tsconfig.app.tsbuildinfo +++ b/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/axios.ts","./src/main.tsx","./src/vite-env.d.ts","./src/assets/icons/account/index.ts","./src/assets/icons/auth/index.ts","./src/assets/icons/groups/index.ts","./src/assets/icons/header/index.ts","./src/assets/icons/input/index.ts","./src/assets/icons/menu/index.ts","./src/assets/icons/missions/index.ts","./src/assets/logos/index.ts","./src/components/button/primarybutton.tsx","./src/components/button/reversebutton.tsx","./src/components/button/secondarybutton.tsx","./src/components/checkbox/checkbox.tsx","./src/components/drop-down-list/dropdownlist.tsx","./src/components/input/daterangeinput.tsx","./src/components/input/input.tsx","./src/components/modal/modal.tsx","./src/components/router/protectedroute.tsx","./src/components/switch/switch.tsx","./src/config/colors.ts","./src/hooks/useclickoutside.ts","./src/hooks/usequery.ts","./src/lib/cn.ts","./src/pages/article.tsx","./src/pages/articleeditor.tsx","./src/pages/home.tsx","./src/pages/mission.tsx","./src/redux/hooks.ts","./src/redux/store.ts","./src/redux/slices/account.ts","./src/redux/slices/articles.ts","./src/redux/slices/auth.ts","./src/redux/slices/contests.ts","./src/redux/slices/groups.ts","./src/redux/slices/missions.ts","./src/redux/slices/store.ts","./src/redux/slices/submit.ts","./src/views/article/header.tsx","./src/views/articleeditor/editor.tsx","./src/views/articleeditor/header.tsx","./src/views/articleeditor/marckdownpreview.tsx","./src/views/home/account/account.tsx","./src/views/home/account/accoutmenu.tsx","./src/views/home/account/articlesblock.tsx","./src/views/home/account/contestsblock.tsx","./src/views/home/account/missionsblock.tsx","./src/views/home/account/rightpanel.tsx","./src/views/home/articles/articleitem.tsx","./src/views/home/articles/articles.tsx","./src/views/home/auth/login.tsx","./src/views/home/auth/register.tsx","./src/views/home/contest/contest.tsx","./src/views/home/contest/missionitem.tsx","./src/views/home/contest/missions.tsx","./src/views/home/contest/submissions.tsx","./src/views/home/contests/contestitem.tsx","./src/views/home/contests/contests.tsx","./src/views/home/contests/contestsblock.tsx","./src/views/home/contests/modalcreate.tsx","./src/views/home/groups/group.tsx","./src/views/home/groups/groupitem.tsx","./src/views/home/groups/groups.tsx","./src/views/home/groups/groupsblock.tsx","./src/views/home/groups/modalcreate.tsx","./src/views/home/groups/modalupdate.tsx","./src/views/home/menu/menu.tsx","./src/views/home/menu/menuitem.tsx","./src/views/home/missions/missionitem.tsx","./src/views/home/missions/missions.tsx","./src/views/home/missions/modalcreate.tsx","./src/views/mission/codeeditor/codeeditor.tsx","./src/views/mission/statement/header.tsx","./src/views/mission/statement/latextcontainer.tsx","./src/views/mission/statement/missionsubmissions.tsx","./src/views/mission/statement/statement.tsx","./src/views/mission/statement/submissionitem.tsx","./src/views/mission/submission/submission.tsx"],"version":"5.6.2"} \ No newline at end of file +{"root":["./src/app.tsx","./src/axios.ts","./src/main.tsx","./src/vite-env.d.ts","./src/assets/icons/account/index.ts","./src/assets/icons/auth/index.ts","./src/assets/icons/groups/index.ts","./src/assets/icons/header/index.ts","./src/assets/icons/input/index.ts","./src/assets/icons/menu/index.ts","./src/assets/icons/missions/index.ts","./src/assets/logos/index.ts","./src/components/button/primarybutton.tsx","./src/components/button/reversebutton.tsx","./src/components/button/secondarybutton.tsx","./src/components/checkbox/checkbox.tsx","./src/components/drop-down-list/dropdownlist.tsx","./src/components/input/daterangeinput.tsx","./src/components/input/input.tsx","./src/components/modal/modal.tsx","./src/components/router/protectedroute.tsx","./src/components/switch/switch.tsx","./src/config/colors.ts","./src/hooks/useclickoutside.ts","./src/hooks/usequery.ts","./src/lib/cn.ts","./src/pages/article.tsx","./src/pages/articleeditor.tsx","./src/pages/contesteditor.tsx","./src/pages/home.tsx","./src/pages/mission.tsx","./src/redux/hooks.ts","./src/redux/store.ts","./src/redux/slices/account.ts","./src/redux/slices/articles.ts","./src/redux/slices/auth.ts","./src/redux/slices/contests.ts","./src/redux/slices/groups.ts","./src/redux/slices/missions.ts","./src/redux/slices/store.ts","./src/redux/slices/submit.ts","./src/views/article/header.tsx","./src/views/articleeditor/editor.tsx","./src/views/articleeditor/header.tsx","./src/views/articleeditor/marckdownpreview.tsx","./src/views/home/account/account.tsx","./src/views/home/account/accoutmenu.tsx","./src/views/home/account/rightpanel.tsx","./src/views/home/account/articles/articlesblock.tsx","./src/views/home/account/contests/contests.tsx","./src/views/home/account/contests/contestsblock.tsx","./src/views/home/account/contests/mycontestitem.tsx","./src/views/home/account/contests/registercontestitem.tsx","./src/views/home/account/missions/missionsblock.tsx","./src/views/home/articles/articleitem.tsx","./src/views/home/articles/articles.tsx","./src/views/home/auth/login.tsx","./src/views/home/auth/register.tsx","./src/views/home/contest/contest.tsx","./src/views/home/contest/missionitem.tsx","./src/views/home/contest/missions.tsx","./src/views/home/contest/submissionitem.tsx","./src/views/home/contest/submissions.tsx","./src/views/home/contests/contestitem.tsx","./src/views/home/contests/contests.tsx","./src/views/home/contests/contestsblock.tsx","./src/views/home/contests/modalcreate.tsx","./src/views/home/groups/group.tsx","./src/views/home/groups/groupitem.tsx","./src/views/home/groups/groups.tsx","./src/views/home/groups/groupsblock.tsx","./src/views/home/groups/modalcreate.tsx","./src/views/home/groups/modalupdate.tsx","./src/views/home/menu/menu.tsx","./src/views/home/menu/menuitem.tsx","./src/views/home/missions/missionitem.tsx","./src/views/home/missions/missions.tsx","./src/views/home/missions/modalcreate.tsx","./src/views/mission/codeeditor/codeeditor.tsx","./src/views/mission/statement/header.tsx","./src/views/mission/statement/latextcontainer.tsx","./src/views/mission/statement/missionsubmissions.tsx","./src/views/mission/statement/statement.tsx","./src/views/mission/statement/submissionitem.tsx","./src/views/mission/submission/submission.tsx"],"version":"5.6.2"} \ No newline at end of file From f7924cd564e753a2ca3989ee971444b559590fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Sat, 8 Nov 2025 15:11:53 +0300 Subject: [PATCH 09/17] filter --- src/components/drop-down-list/Filter.tsx | 206 +++++++++++------------ src/components/drop-down-list/Sorter.tsx | 8 +- src/views/home/missions/Filter.tsx | 11 +- 3 files changed, 110 insertions(+), 115 deletions(-) diff --git a/src/components/drop-down-list/Filter.tsx b/src/components/drop-down-list/Filter.tsx index a3816c2..d0d47f8 100644 --- a/src/components/drop-down-list/Filter.tsx +++ b/src/components/drop-down-list/Filter.tsx @@ -1,124 +1,120 @@ import React from 'react'; import { cn } from '../../lib/cn'; -import { checkMark, chevroneDropDownList } from '../../assets/icons/input'; +import { checkMark } from '../../assets/icons/input'; import { useClickOutside } from '../../hooks/useClickOutside'; +import { Sort, SortActive } from '../../assets/icons/filters'; export interface FilterItem { - text: string; - value: string; + text: string; + value: string; } interface FilterProps { - disabled?: boolean; - className?: string; - onChange: (state: string[]) => void; // теперь массив выбранных значений - defaultState?: FilterItem[]; - items: FilterItem[]; + disabled?: boolean; + className?: string; + onChange: (items: FilterItem[]) => void; + defaultState?: FilterItem[]; + items: FilterItem[]; } export const Filter: React.FC = ({ - className = '', - onChange, - defaultState = [], - items = [{ text: '', value: '' }], + disabled = false, + className = '', + onChange, + defaultState = [], + items = [], }) => { - if (items.length === 0) items.push({ text: '', value: '' }); + const [value, setValue] = React.useState(defaultState); + const [active, setActive] = React.useState(false); - const [selectedValues, setSelectedValues] = - React.useState(defaultState); - const [active, setActive] = React.useState(false); + const ref = React.useRef(null); - React.useEffect(() => { - onChange(selectedValues.map((v) => v.value)); - }, [selectedValues]); + useClickOutside(ref, () => { + setActive(false); + }); - const ref = React.useRef(null); - useClickOutside(ref, () => setActive(false)); + React.useEffect(() => { + onChange(value); + }, [value]); - const toggleItem = (item: FilterItem) => { - const exists = selectedValues.some((v) => v.value === item.value); - if (exists) { - setSelectedValues( - selectedValues.filter((v) => v.value !== item.value), - ); - } else { - setSelectedValues([...selectedValues, item]); - } - }; + const toggleItem = (item: FilterItem) => { + const exists = value.some((val) => val.value === item.value); + if (exists) { + setValue(value.filter((val) => val.value !== item.value)); + } else { + setValue([...value, item]); + } + }; - const displayText = - selectedValues.length > 0 - ? selectedValues.map((v) => v.text).join(', ') - : 'Выберите...'; - - return ( -
-
setActive(!active)} - title={displayText} - > - {displayText} -
- - - -
-
-
- {items.map((v, i) => { - const isSelected = selectedValues.some( - (sel) => sel.value === v.value, - ); - return ( -
toggleItem(v)} - > - {v.text} - - {isSelected && ( - - )} -
- ); - })} -
-
-
+ return ( +
+
0) && 'w-fit border-liquid-brightmain border-[1px] border-solid', + )} + onClick={() => { + if (!disabled) setActive(!active); + }} + > +
+ {value.length}
- ); +
+ + {/* Sort icons */} + + 0) && 'opacity-100', + )} + /> + + {/* Dropdown */} +
+
+
+ {items.map((v) => { + const selected = value.some((val) => val.value === v.value); + return ( +
toggleItem(v)} + > + {v.text} + {selected && ( + + )} +
+ ); + })} +
+
+
+
+ ); }; diff --git a/src/components/drop-down-list/Sorter.tsx b/src/components/drop-down-list/Sorter.tsx index 8c1e14d..7ce3b85 100644 --- a/src/components/drop-down-list/Sorter.tsx +++ b/src/components/drop-down-list/Sorter.tsx @@ -45,8 +45,8 @@ export const Sorter: React.FC = ({ className={cn( 'grid items-center h-[40px] rounded-full bg-liquid-lighter grid-cols-[40px]', 'text-[18px] font-bold cursor-pointer select-none', - 'transitin-all active:scale-95 duration-300 overflow-hidden', - active && ' grid-cols-[1fr]', + 'overflow-hidden', + active && ' grid-cols-[1fr] border-liquid-brightmain border-[1px] border-solid', )} onClick={() => { setActive(!active); @@ -54,7 +54,7 @@ export const Sorter: React.FC = ({ >
@@ -63,7 +63,7 @@ export const Sorter: React.FC = ({
-
+
{ ]; return ( -
+
{ onChange={(v) => console.log(v)} /> - {/* console.log(values)} // обработчик изменения - className="w-[240px]" - /> */} + defaultState={[]} + onChange={(values) => console.log(values)} + />
); }; From 1cbd2dc0b3c532f5504615d08733f75afdb8db66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Sat, 8 Nov 2025 15:54:49 +0300 Subject: [PATCH 10/17] add filter --- src/assets/icons/filters/index.ts | 11 +- src/assets/icons/filters/search.svg | 3 + src/components/drop-down-list/Filter.tsx | 205 +++++++++--------- src/components/drop-down-list/Sorter.tsx | 26 ++- src/components/input/SearchInput.tsx | 59 +++++ .../home/account/missions/MyMissionItem.tsx | 1 - src/views/home/articles/Articles.tsx | 3 +- src/views/home/articles/Filter.tsx | 51 +++++ src/views/home/contests/Contests.tsx | 25 ++- src/views/home/contests/Filter.tsx | 51 +++++ src/views/home/groups/Filter.tsx | 51 +++++ src/views/home/groups/Groups.tsx | 3 +- src/views/home/missions/Filter.tsx | 16 +- tsconfig.app.tsbuildinfo | 2 +- 14 files changed, 373 insertions(+), 134 deletions(-) create mode 100644 src/assets/icons/filters/search.svg create mode 100644 src/components/input/SearchInput.tsx create mode 100644 src/views/home/articles/Filter.tsx create mode 100644 src/views/home/contests/Filter.tsx create mode 100644 src/views/home/groups/Filter.tsx diff --git a/src/assets/icons/filters/index.ts b/src/assets/icons/filters/index.ts index 2eca117..4a4b8a6 100644 --- a/src/assets/icons/filters/index.ts +++ b/src/assets/icons/filters/index.ts @@ -1,6 +1,7 @@ -import FilterActive from './filters-active.svg'; -import Filter from './filters.svg'; -import Sort from './sort.svg'; -import SortActive from './sort-active.svg'; +import iconFilterActive from './filters-active.svg'; +import iconFilter from './filters.svg'; +import iconSort from './sort.svg'; +import iconSortActive from './sort-active.svg'; +import iconSearch from './search.svg'; -export { Filter, FilterActive, Sort, SortActive }; +export { iconFilter, iconFilterActive, iconSort, iconSortActive, iconSearch }; diff --git a/src/assets/icons/filters/search.svg b/src/assets/icons/filters/search.svg new file mode 100644 index 0000000..7827a6e --- /dev/null +++ b/src/assets/icons/filters/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/drop-down-list/Filter.tsx b/src/components/drop-down-list/Filter.tsx index d0d47f8..5ef647f 100644 --- a/src/components/drop-down-list/Filter.tsx +++ b/src/components/drop-down-list/Filter.tsx @@ -2,119 +2,124 @@ import React from 'react'; import { cn } from '../../lib/cn'; import { checkMark } from '../../assets/icons/input'; import { useClickOutside } from '../../hooks/useClickOutside'; -import { Sort, SortActive } from '../../assets/icons/filters'; +import { iconFilter, iconFilterActive } from '../../assets/icons/filters'; export interface FilterItem { - text: string; - value: string; + text: string; + value: string; } interface FilterProps { - disabled?: boolean; - className?: string; - onChange: (items: FilterItem[]) => void; - defaultState?: FilterItem[]; - items: FilterItem[]; + disabled?: boolean; + className?: string; + onChange: (items: FilterItem[]) => void; + defaultState?: FilterItem[]; + items: FilterItem[]; } -export const Filter: React.FC = ({ - disabled = false, - className = '', - onChange, - defaultState = [], - items = [], +export const FilterDropDown: React.FC = ({ + disabled = false, + className = '', + onChange, + defaultState = [], + items = [], }) => { - const [value, setValue] = React.useState(defaultState); - const [active, setActive] = React.useState(false); + const [value, setValue] = React.useState(defaultState); + const [active, setActive] = React.useState(false); - const ref = React.useRef(null); + const ref = React.useRef(null); - useClickOutside(ref, () => { - setActive(false); - }); + useClickOutside(ref, () => { + setActive(false); + }); - React.useEffect(() => { - onChange(value); - }, [value]); + React.useEffect(() => { + onChange(value); + }, [value]); - const toggleItem = (item: FilterItem) => { - const exists = value.some((val) => val.value === item.value); - if (exists) { - setValue(value.filter((val) => val.value !== item.value)); - } else { - setValue([...value, item]); - } - }; + const toggleItem = (item: FilterItem) => { + const exists = value.some((val) => val.value === item.value); + if (exists) { + setValue(value.filter((val) => val.value !== item.value)); + } else { + setValue([...value, item]); + } + }; - return ( -
-
0) && 'w-fit border-liquid-brightmain border-[1px] border-solid', - )} - onClick={() => { - if (!disabled) setActive(!active); - }} - > -
- {value.length} -
-
- - {/* Sort icons */} - - 0) && 'opacity-100', - )} - /> - - {/* Dropdown */} -
-
-
- {items.map((v) => { - const selected = value.some((val) => val.value === v.value); - return ( -
toggleItem(v)} - > - {v.text} - {selected && ( - - )} + return ( +
+
0) && + 'w-fit border-liquid-brightmain border-[1px] border-solid', + )} + onClick={() => { + if (!disabled) setActive(!active); + }} + > +
+ {value.length}
- ); - })} -
+
+ + {/* Filter icons */} + + 0) && 'opacity-100', + )} + /> + + {/* Dropdown */} +
+
+
+ {items.map((v) => { + const selected = value.some( + (val) => val.value === v.value, + ); + return ( +
toggleItem(v)} + > + {v.text} + {selected && ( + + )} +
+ ); + })} +
+
+
-
-
- ); + ); }; diff --git a/src/components/drop-down-list/Sorter.tsx b/src/components/drop-down-list/Sorter.tsx index 7ce3b85..49ca751 100644 --- a/src/components/drop-down-list/Sorter.tsx +++ b/src/components/drop-down-list/Sorter.tsx @@ -1,8 +1,8 @@ -import React from 'react'; +import { FC, useEffect, useRef, useState } from 'react'; import { cn } from '../../lib/cn'; import { checkMark } from '../../assets/icons/input'; import { useClickOutside } from '../../hooks/useClickOutside'; -import { Sort, SortActive } from '../../assets/icons/filters'; +import { iconSort, iconSortActive } from '../../assets/icons/filters'; export interface SorterItem { text: string; @@ -17,7 +17,7 @@ interface SorterProps { items: SorterItem[]; } -export const Sorter: React.FC = ({ +export const SorterDropDown: FC = ({ // disabled = false, className = '', onChange, @@ -26,14 +26,15 @@ export const Sorter: React.FC = ({ }) => { if (items.length == 0) items.push({ text: '', value: '' }); - const [value, setValue] = React.useState( + const [value, setValue] = useState( defaultState != undefined ? defaultState : items[0], ); - const [active, setActive] = React.useState(false); + const [active, setActive] = useState(false); + const [activate, setActivate] = useState(false); - React.useEffect(() => onChange(value.value), [value]); + useEffect(() => onChange(value.value), [value]); - const ref = React.useRef(null); + const ref = useRef(null); useClickOutside(ref, () => { setActive(false); @@ -46,7 +47,8 @@ export const Sorter: React.FC = ({ 'grid items-center h-[40px] rounded-full bg-liquid-lighter grid-cols-[40px]', 'text-[18px] font-bold cursor-pointer select-none', 'overflow-hidden', - active && ' grid-cols-[1fr] border-liquid-brightmain border-[1px] border-solid', + (active || activate) && + ' grid-cols-[1fr] border-liquid-brightmain border-[1px] border-solid', )} onClick={() => { setActive(!active); @@ -63,18 +65,17 @@ export const Sorter: React.FC = ({
-
@@ -107,6 +108,7 @@ export const Sorter: React.FC = ({ onClick={() => { setValue(v); setActive(false); + setActivate(true); }} > {v.text} diff --git a/src/components/input/SearchInput.tsx b/src/components/input/SearchInput.tsx new file mode 100644 index 0000000..f091b4f --- /dev/null +++ b/src/components/input/SearchInput.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { cn } from '../../lib/cn'; +import { iconSearch } from '../../assets/icons/filters'; + +interface searchInputProps { + name?: string; + error?: string; + disabled?: boolean; + required?: boolean; + label?: string; + placeholder?: string; + className?: string; + onChange: (state: string) => void; + defaultState?: string; + autocomplete?: string; + onKeyDown?: (e: React.KeyboardEvent) => void; +} + +export const SearchInput: React.FC = ({ + placeholder = '', + className = '', + onChange, + defaultState = '', + name = '', + autocomplete = '', + onKeyDown, +}) => { + const [value, setValue] = React.useState(defaultState); + + React.useEffect(() => onChange(value), [value]); + React.useEffect(() => setValue(defaultState), [defaultState]); + + return ( + + ); +}; diff --git a/src/views/home/account/missions/MyMissionItem.tsx b/src/views/home/account/missions/MyMissionItem.tsx index 18b1ef3..5fa27f0 100644 --- a/src/views/home/account/missions/MyMissionItem.tsx +++ b/src/views/home/account/missions/MyMissionItem.tsx @@ -1,5 +1,4 @@ import { cn } from '../../../../lib/cn'; -import { IconError, IconSuccess } from '../../../../assets/icons/missions'; import { useNavigate } from 'react-router-dom'; import { Edit } from '../../../../assets/icons/input'; diff --git a/src/views/home/articles/Articles.tsx b/src/views/home/articles/Articles.tsx index 13519fd..5d124a8 100644 --- a/src/views/home/articles/Articles.tsx +++ b/src/views/home/articles/Articles.tsx @@ -5,6 +5,7 @@ import ArticleItem from './ArticleItem'; import { setMenuActivePage } from '../../../redux/slices/store'; import { useNavigate } from 'react-router-dom'; import { fetchArticles } from '../../../redux/slices/articles'; +import Filters from './Filter'; export interface Article { id: number; @@ -42,7 +43,7 @@ const Articles = () => { />
-
+
{articles.map((v, i) => ( diff --git a/src/views/home/articles/Filter.tsx b/src/views/home/articles/Filter.tsx new file mode 100644 index 0000000..ca01a9d --- /dev/null +++ b/src/views/home/articles/Filter.tsx @@ -0,0 +1,51 @@ +import { + FilterDropDown, + FilterItem, +} from '../../../components/drop-down-list/Filter'; +import { SorterDropDown } from '../../../components/drop-down-list/Sorter'; +import { SearchInput } from '../../../components/input/SearchInput'; + +const Filters = () => { + const items: FilterItem[] = [ + { text: 'React', value: 'react' }, + { text: 'Vue', value: 'vue' }, + { text: 'Angular', value: 'angular' }, + { text: 'Svelte', value: 'svelte' }, + { text: 'Next.js', value: 'next' }, + { text: 'Nuxt', value: 'nuxt' }, + { text: 'Solid', value: 'solid' }, + { text: 'Qwik', value: 'qwik' }, + ]; + + return ( +
+ {}} placeholder="Поиск задачи" /> + + console.log(v)} + /> + + console.log(values)} + /> +
+ ); +}; + +export default Filters; diff --git a/src/views/home/contests/Contests.tsx b/src/views/home/contests/Contests.tsx index 999ee29..7e33094 100644 --- a/src/views/home/contests/Contests.tsx +++ b/src/views/home/contests/Contests.tsx @@ -6,6 +6,7 @@ import ContestsBlock from './ContestsBlock'; import { setMenuActivePage } from '../../../redux/slices/store'; import { fetchContests } from '../../../redux/slices/contests'; import ModalCreateContest from './ModalCreate'; +import Filters from './Filter'; const Contests = () => { const dispatch = useAppDispatch(); @@ -48,16 +49,24 @@ const Contests = () => { />
-
- {status == 'loading' &&
Загрузка контестов...
} - {status == 'failed' &&
Ошибка: {error}
} - {status == 'successful' && + + {status == 'loading' && ( +
+ Загрузка контестов... +
+ )} + {status == 'failed' && ( +
Ошибка: {error}
+ )} + {status == 'successful' && ( <> { - const endTime = new Date(contest.endsAt ?? new Date().toDateString()).getTime(); + const endTime = new Date( + contest.endsAt ?? new Date().toDateString(), + ).getTime(); return endTime >= now.getTime(); })} /> @@ -66,12 +75,14 @@ const Contests = () => { className="mb-[20px]" title="Прошедшие" contests={contests.filter((contest) => { - const endTime = new Date(contest.endsAt ?? new Date().toDateString()).getTime(); + const endTime = new Date( + contest.endsAt ?? new Date().toDateString(), + ).getTime(); return endTime < now.getTime(); })} /> - } + )}
{ + const items: FilterItem[] = [ + { text: 'React', value: 'react' }, + { text: 'Vue', value: 'vue' }, + { text: 'Angular', value: 'angular' }, + { text: 'Svelte', value: 'svelte' }, + { text: 'Next.js', value: 'next' }, + { text: 'Nuxt', value: 'nuxt' }, + { text: 'Solid', value: 'solid' }, + { text: 'Qwik', value: 'qwik' }, + ]; + + return ( +
+ {}} placeholder="Поиск задачи" /> + + console.log(v)} + /> + + console.log(values)} + /> +
+ ); +}; + +export default Filters; diff --git a/src/views/home/groups/Filter.tsx b/src/views/home/groups/Filter.tsx new file mode 100644 index 0000000..ca01a9d --- /dev/null +++ b/src/views/home/groups/Filter.tsx @@ -0,0 +1,51 @@ +import { + FilterDropDown, + FilterItem, +} from '../../../components/drop-down-list/Filter'; +import { SorterDropDown } from '../../../components/drop-down-list/Sorter'; +import { SearchInput } from '../../../components/input/SearchInput'; + +const Filters = () => { + const items: FilterItem[] = [ + { text: 'React', value: 'react' }, + { text: 'Vue', value: 'vue' }, + { text: 'Angular', value: 'angular' }, + { text: 'Svelte', value: 'svelte' }, + { text: 'Next.js', value: 'next' }, + { text: 'Nuxt', value: 'nuxt' }, + { text: 'Solid', value: 'solid' }, + { text: 'Qwik', value: 'qwik' }, + ]; + + return ( +
+ {}} placeholder="Поиск задачи" /> + + console.log(v)} + /> + + console.log(values)} + /> +
+ ); +}; + +export default Filters; diff --git a/src/views/home/groups/Groups.tsx b/src/views/home/groups/Groups.tsx index 64b3692..51650e6 100644 --- a/src/views/home/groups/Groups.tsx +++ b/src/views/home/groups/Groups.tsx @@ -7,6 +7,7 @@ import { setMenuActivePage } from '../../../redux/slices/store'; import { fetchMyGroups } from '../../../redux/slices/groups'; import ModalCreate from './ModalCreate'; import ModalUpdate from './ModalUpdate'; +import Filters from './Filter'; export interface GroupUpdate { id: number; @@ -86,7 +87,7 @@ const Groups = () => { />
-
+ { const items: FilterItem[] = [ @@ -14,10 +18,10 @@ const Filters = () => { ]; return ( -
-
+
+ {}} placeholder="Поиск задачи" /> - { onChange={(v) => console.log(v)} /> - console.log(values)} diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo index 88b15bb..f725851 100644 --- a/tsconfig.app.tsbuildinfo +++ b/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/axios.ts","./src/main.tsx","./src/vite-env.d.ts","./src/assets/icons/account/index.ts","./src/assets/icons/auth/index.ts","./src/assets/icons/groups/index.ts","./src/assets/icons/header/index.ts","./src/assets/icons/input/index.ts","./src/assets/icons/menu/index.ts","./src/assets/icons/missions/index.ts","./src/assets/logos/index.ts","./src/components/button/primarybutton.tsx","./src/components/button/reversebutton.tsx","./src/components/button/secondarybutton.tsx","./src/components/checkbox/checkbox.tsx","./src/components/drop-down-list/dropdownlist.tsx","./src/components/input/daterangeinput.tsx","./src/components/input/input.tsx","./src/components/modal/modal.tsx","./src/components/router/protectedroute.tsx","./src/components/switch/switch.tsx","./src/config/colors.ts","./src/hooks/useclickoutside.ts","./src/hooks/usequery.ts","./src/lib/cn.ts","./src/pages/article.tsx","./src/pages/articleeditor.tsx","./src/pages/contesteditor.tsx","./src/pages/home.tsx","./src/pages/mission.tsx","./src/redux/hooks.ts","./src/redux/store.ts","./src/redux/slices/account.ts","./src/redux/slices/articles.ts","./src/redux/slices/auth.ts","./src/redux/slices/contests.ts","./src/redux/slices/groups.ts","./src/redux/slices/missions.ts","./src/redux/slices/store.ts","./src/redux/slices/submit.ts","./src/views/article/header.tsx","./src/views/articleeditor/editor.tsx","./src/views/articleeditor/header.tsx","./src/views/articleeditor/marckdownpreview.tsx","./src/views/home/account/account.tsx","./src/views/home/account/accoutmenu.tsx","./src/views/home/account/rightpanel.tsx","./src/views/home/account/articles/articlesblock.tsx","./src/views/home/account/contests/contests.tsx","./src/views/home/account/contests/contestsblock.tsx","./src/views/home/account/contests/mycontestitem.tsx","./src/views/home/account/contests/registercontestitem.tsx","./src/views/home/account/missions/missionsblock.tsx","./src/views/home/articles/articleitem.tsx","./src/views/home/articles/articles.tsx","./src/views/home/auth/login.tsx","./src/views/home/auth/register.tsx","./src/views/home/contest/contest.tsx","./src/views/home/contest/missionitem.tsx","./src/views/home/contest/missions.tsx","./src/views/home/contest/submissionitem.tsx","./src/views/home/contest/submissions.tsx","./src/views/home/contests/contestitem.tsx","./src/views/home/contests/contests.tsx","./src/views/home/contests/contestsblock.tsx","./src/views/home/contests/modalcreate.tsx","./src/views/home/groups/group.tsx","./src/views/home/groups/groupitem.tsx","./src/views/home/groups/groups.tsx","./src/views/home/groups/groupsblock.tsx","./src/views/home/groups/modalcreate.tsx","./src/views/home/groups/modalupdate.tsx","./src/views/home/menu/menu.tsx","./src/views/home/menu/menuitem.tsx","./src/views/home/missions/missionitem.tsx","./src/views/home/missions/missions.tsx","./src/views/home/missions/modalcreate.tsx","./src/views/mission/codeeditor/codeeditor.tsx","./src/views/mission/statement/header.tsx","./src/views/mission/statement/latextcontainer.tsx","./src/views/mission/statement/missionsubmissions.tsx","./src/views/mission/statement/statement.tsx","./src/views/mission/statement/submissionitem.tsx","./src/views/mission/submission/submission.tsx"],"version":"5.6.2"} \ No newline at end of file +{"root":["./src/app.tsx","./src/axios.ts","./src/main.tsx","./src/vite-env.d.ts","./src/assets/icons/account/index.ts","./src/assets/icons/auth/index.ts","./src/assets/icons/filters/index.ts","./src/assets/icons/groups/index.ts","./src/assets/icons/header/index.ts","./src/assets/icons/input/index.ts","./src/assets/icons/menu/index.ts","./src/assets/icons/missions/index.ts","./src/assets/logos/index.ts","./src/components/button/primarybutton.tsx","./src/components/button/reversebutton.tsx","./src/components/button/secondarybutton.tsx","./src/components/checkbox/checkbox.tsx","./src/components/drop-down-list/dropdownlist.tsx","./src/components/drop-down-list/filter.tsx","./src/components/drop-down-list/sorter.tsx","./src/components/input/daterangeinput.tsx","./src/components/input/input.tsx","./src/components/input/searchinput.tsx","./src/components/modal/modal.tsx","./src/components/router/protectedroute.tsx","./src/components/switch/switch.tsx","./src/config/colors.ts","./src/hooks/useclickoutside.ts","./src/hooks/usequery.ts","./src/lib/cn.ts","./src/pages/article.tsx","./src/pages/articleeditor.tsx","./src/pages/contesteditor.tsx","./src/pages/home.tsx","./src/pages/mission.tsx","./src/redux/hooks.ts","./src/redux/store.ts","./src/redux/slices/articles.ts","./src/redux/slices/auth.ts","./src/redux/slices/contests.ts","./src/redux/slices/groups.ts","./src/redux/slices/missions.ts","./src/redux/slices/store.ts","./src/redux/slices/submit.ts","./src/views/article/header.tsx","./src/views/articleeditor/editor.tsx","./src/views/articleeditor/header.tsx","./src/views/articleeditor/marckdownpreview.tsx","./src/views/home/account/account.tsx","./src/views/home/account/accoutmenu.tsx","./src/views/home/account/rightpanel.tsx","./src/views/home/account/articles/articlesblock.tsx","./src/views/home/account/contests/contests.tsx","./src/views/home/account/contests/contestsblock.tsx","./src/views/home/account/contests/mycontestitem.tsx","./src/views/home/account/contests/registercontestitem.tsx","./src/views/home/account/missions/missions.tsx","./src/views/home/account/missions/missionsblock.tsx","./src/views/home/account/missions/mymissionitem.tsx","./src/views/home/articles/articleitem.tsx","./src/views/home/articles/articles.tsx","./src/views/home/articles/filter.tsx","./src/views/home/auth/login.tsx","./src/views/home/auth/register.tsx","./src/views/home/contest/contest.tsx","./src/views/home/contest/missionitem.tsx","./src/views/home/contest/missions.tsx","./src/views/home/contest/submissionitem.tsx","./src/views/home/contest/submissions.tsx","./src/views/home/contests/contestitem.tsx","./src/views/home/contests/contests.tsx","./src/views/home/contests/contestsblock.tsx","./src/views/home/contests/filter.tsx","./src/views/home/contests/modalcreate.tsx","./src/views/home/groups/filter.tsx","./src/views/home/groups/group.tsx","./src/views/home/groups/groupitem.tsx","./src/views/home/groups/groups.tsx","./src/views/home/groups/groupsblock.tsx","./src/views/home/groups/modalcreate.tsx","./src/views/home/groups/modalupdate.tsx","./src/views/home/menu/menu.tsx","./src/views/home/menu/menuitem.tsx","./src/views/home/missions/filter.tsx","./src/views/home/missions/missionitem.tsx","./src/views/home/missions/missions.tsx","./src/views/home/missions/modalcreate.tsx","./src/views/mission/codeeditor/codeeditor.tsx","./src/views/mission/statement/header.tsx","./src/views/mission/statement/latextcontainer.tsx","./src/views/mission/statement/missionsubmissions.tsx","./src/views/mission/statement/statement.tsx","./src/views/mission/statement/submissionitem.tsx","./src/views/mission/submission/submission.tsx"],"version":"5.6.2"} \ No newline at end of file From 0b0c5c36e1f10a800a8a732c89922461c4b1d4a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Sat, 8 Nov 2025 16:40:30 +0300 Subject: [PATCH 11/17] FASIE --- src/assets/logos/LogoFASIE.png | Bin 0 -> 5131 bytes src/assets/logos/index.ts | 3 ++- src/views/home/menu/Menu.tsx | 13 +++++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 src/assets/logos/LogoFASIE.png diff --git a/src/assets/logos/LogoFASIE.png b/src/assets/logos/LogoFASIE.png new file mode 100644 index 0000000000000000000000000000000000000000..44ee9ab4389e4ca2e1ba6880abf6b49412fe1dc5 GIT binary patch literal 5131 zcmeAS@N?(olHy`uVBq!ia0y~yU|7n)z!1p6#=yX^StIQ#0|NtRfk$L90|U1(2s1Lw znj^u$z`$4>`5hI&5m5vm?+Mr8Xgr!EBU*g_}(ioZ!_P1 zr~hXE?#G_L84mB9J9qBf{`C9b=a#2UXV45<;vvX#EJEzCQk&X z@}CNhNrydom?e%W+I+H^cc_3v>+5CNpG7ujk6bw2JhgwPxA(-dw(D<&1$r(A-4yX- zdvgBw9O1v#hffRDItQJfG;P*0(S^Ho54`p|epxSdR$rAZKV!nSBWJ7TTwZu+d12f) z%k`VK?G$lf?e`E}KKr7>)D4AuEB*=nTc)nxe%$!d(pnq4_Lr(@-X2*zXJtx#j1}$D zRy0R?G|zu7E?d_2sy8#qpsVkVQ|h@ctK@r?J>Q&L*W?*EUs)@%$-eLB9ATHmb>@}( zUrThE@F`8cA~xYwc9rO<^Ao$&ElFX1$Hl-hRyrK`Gx;o@uWq?`){qyWy)~+mywdmda0C`D%AF4A-R>-!zQv3Z~?234bdpEn{iPIPP z?fCi3HQAOd(fP6Bi`(QP3GFqFyHrdb9z3u^?V$SngO4*;=zGLoKGL>F!)1j`_{BU0 zr&j_o?uw~Hg+$p_H?}%`|)29s~+ikk@7+5Mx`aiSvH+yJa`D!pfW!3q8 zJEIR?`hRYD^X~3nKNoVAGVEV2@#6@ayV*I}b!#`Q_{uEoUi$STChQP}a8Jv4~x6Hikv^Sq`R1zXO4IVlz5KKJ<2h@kn?`fogY zYIZsIWao4FiJv;ZraE0z&2OB4`g8L2RJX9bI@?cZyC|n7CE4 zF82I@LCCzahuVudk58G?x!UGp+T7{>{@X9>DxEudT#2K?_ra#VKXGaeT~lP5U)8D4 zX+JBaz_Y1u6I;!vRX6r5T9dMcbF=x3S7%bqeI4p-eP-re_qcRW;Q3MomCq7aR@m0P zoiOj8;PhRRekUL7*lwH>`hN8VJ=?6=O84|%Pk%Q3U2CqRgs#vSMw!Wig=u2Yrb~#WdHMP3SP=rlbn(h z*zU|fr@DAu&=QY{O0Hd6JM;<{`kHWkTQlL!(#b);6hdr1tM5&VJyYhx{r{Qd!y~EN ztQXrq`TF=$oWs)Bv9DHtRxx5%YkH%VwmbNj%*kbU#6%5l$Ev;U9xfVr(nsVzo|UcUl*RLm8m>7^-WCXwbx(Ho~$#t`~26QDf;?L{att5 z!+Ck%uqzMZf!9-7EW&$@S=K~M=mpFY&*gsv_>f4MY%O_n%1_71Jmwq|0uV5lW z;mXoj*EX**1>i_VkGyM3p~a(&k# z_n&bB{B18Mh2^(YR{F0?7E!W#Sku64+9sqK`SnnZu|wGUz$<;yUUK{QDzJs$GtY3C zE13A^$v$I6yS9kbTpNBa{w=GPm{iQU!eFla+9T|~ix0EZ z;(dqv=KeY0oAf)gxQjFEx7J*PgKFOdzDsG$Y-n6E<;Z@{ZLy-$rZfxWPnyzxtg$1* z}Y11b^vHsB5VN~{KYnIZY59M0*tw&hDDA=5TZScaQ|Lda}2Olfh zSa2k_&3WGFFlpU_2ST?dOsZ*0^?J^;LY_xXT#@C+Hy2T1RgU0G`5RwWurZwqQ(RiH zYtgwQZA-Co%>oaEg#!Jh zzS|0{eZO*kci$EVIo63mE7tFLztM(c!tM3!Peiw^kGLvPVi}%&B00RJK;r4g$~|Q+ ziNfpUkI5cCx`4YW$7|uTa|fOIj&}z-y6vA}#Fi6w{p+MDNpHFYnAZH7yF}?o`v<## zoxy*Oy4>duiFFFT5VKm8ITvGPe6?82l!pLCizCE)KhrK!?iHmWQ3rppMkR5pZ`^Ss>f zlh>2s>(pr)#%Y27BQpf{sHl3nXYZbL_@eiISLeSjyXW}t+o$n2Z0|mihgGMqw+S6( zseU%Q_0*|TQx4?>ZHaGtwRN-28H-oWZ2P?K@4Oq`koL|osO)i7`ztfY+&5{Jl75<& zyi+%R*jcW*gYr~g_;`>bxVT5&z!y-7N#qFWiyt{<&d=4TOWRVL(M(sAid{C*Xu(s z*!`aAab(ZVYWxf5m_q4x}FYV^kewMQNz9nzl_J-AFm;Z3*=ooUk_;3X} z9NDvV6JPnYQay#K z>pxy-;&xBcyHgQa{jk_-&u_C9SD_;|d-BzL{@%2E;kH$d_5J!a2QU3KTKT!~O^2pO zpfJbE8;czG)mFRR->J)NZBX-Az5Bz|nfDtK{|U7H|K-2n&q4PI6Xwpdp1|apdH>D2 zd67|C8UO9p9e&Nq!L9VHp07^s*|%>_Art0$3Voh=yv$LAV`XKv>0B07&T6(=^YxiC zlRX@gpMT>iNji}s6|<-Cy3%9+=k>hD>;GBVe12o7@%@eTySq>IC8l^}-k0f|c~WG{ z|G%5~{(rX@U`@BLV~YEA`LO`I`@|_O=T-!LK9?ma_I~ZewU(_ z-g$pdeqZp{TliFqN{iCc1Ao>|HT;s?m-XL_t9KTIV8@)r8QGK0N{YJpg}i*>mSo&B z)t~vzjIv!Ca&863%g%jL&$_j7Tkb5sRXb&KU1oN$s7qRh-{plR>tZo3(s*DhFUq^n4|rAP0Rk}r&BGi?_REZhVSdL zcH87M%Sml7Zk$_~J0tgOO2YH9TxZkM6RvJKF!{RPVf(u8t~Y+S%QJnG|DQW;_bq$D zwh1jKmOSb0$W2#MHRbf2FxjC@eI3_C?UhF)6dX@~QM<>ywM0`zu}^;Q=VSrSK(4gy zi=)aWw;A0zu|rW-e;-r9pWp*-cH)&zlHF5SZu4%iV!LnoeUICb9u3oz*ZzESpYPc- z=e*4}>uFKq3$7{#z5jmk#}SA7HyxWUWwSk z(iUJ#XU{N)Y%cp6{T%e^)EI8poZzAV}PXtR#jQ+9mw+0M1g_*mV&{Q9#_mkR|ptNh#+ zox<0y22cgei&_qvadJiVs2(nG=ge0qHCMK$GzFBuc`dlvn@G4(`Tvx}$Z zgqABRG5;<&&f$@5*OpLcoitCy((|bM-46?yl(bltUP>8F-WL*-_Gd@T-6hR{hZGgl+9#i~%bdc+58SU~J2n5$mC9clVL{J) zd_6pGOqlhY+xgZ@eYa_|Sd^Y;v$G3Lp2Or4Flo-#2@9fSuTQ@|p~YJwPg(JOrO-r& ztY0UdI<@w+IJY~;O|iV~+wS+Q&@e;odOOSdQ} z##{3ihXAMkg4iEotJ1pGFU~jQ2sK;G;N`BOykU8-%hJU?vnB^6I4`aLyoOJ|R`yZD z3C~ND!`ddzSi8H?28SlA*~HJ6 zjz*X*=W)DH<#8dcN#T)T$VHRcWv-@@$6xJ@T)J?H$b&k|<{ZBndqt|9wygZKcki@W z+dXD2s9q`QAoL;m{l3JT?MvpxsxES9KB=)m)kCvc&E?B}k*(Le|K3`CpQ&Omhi5ZK zmf+Wm>bjGzMPI!xr1E!O&3mIS_e>_X|~jpiju{~3fn zt@qln`gMt|n!&0Yo0d%3sA#)>-{tjl|4ON3R#!DHQJ*-K!z*QLUGjo2Rf5wv+#L5? zvdezYi+=JtBC@Udee6vOaV`~6`_8<%{WVioXZ%Y4w)W`SZS`9;Jv0?^{JR%S*ZY?J zoZI69H^cjV%wO3i1`A9tDE_Woai??P-lDqp?U4>D!b@I7>`b1x_VGPg?cODEDwlRT zu1KHMCI2%yra%4pwn-r+&cPFd+(aGoW8SxK*K7DDyTx(6lH>Gg4;HemN>j+p@IF%c zM!D+g;u|{aYfN-CQ#<}P&FuNAnW?lQZ=%RyXU9wL@>CZ2I{o~pOW00WQnKiikVwSse;aSsdicqTiRENgB_Et;k!!kX5{pg4k=xgkg(n

uUZzaMhkIYPp_r z=2o3`aj!juZoTYnOx?Ql>D_|rlX1uSiu@;@Qr#qIwd=&Q-AB*QSW)Jyvzg`TCfUPQ vf*ZaV)T=0}rZN>&)+P#H(l1=`xIFrJ=@*$LpXC`C7#KWV{an^LB{Ts5p%I&- literal 0 HcmV?d00001 diff --git a/src/assets/logos/index.ts b/src/assets/logos/index.ts index 5ea6c09..c01722d 100644 --- a/src/assets/logos/index.ts +++ b/src/assets/logos/index.ts @@ -1,3 +1,4 @@ import Logo from './Logo.svg'; +import LogoFASIE from './LogoFASIE.png'; -export { Logo }; +export { Logo, LogoFASIE }; diff --git a/src/views/home/menu/Menu.tsx b/src/views/home/menu/Menu.tsx index e7b6ce4..3329c04 100644 --- a/src/views/home/menu/Menu.tsx +++ b/src/views/home/menu/Menu.tsx @@ -1,4 +1,4 @@ -import { Logo } from '../../../assets/logos'; +import { Logo, LogoFASIE } from '../../../assets/logos'; import { Account, Clipboard, @@ -42,7 +42,7 @@ const Menu = () => { const activePage = useAppSelector((state) => state.store.menu.activePage); return ( -

+
{menuItems.map((v, i) => ( @@ -56,6 +56,15 @@ const Menu = () => { /> ))}
+ +
+ +
+ { + 'Проект «LiquidCode» создан при поддержке Федерального государственного бюджетного учреждения «Фонд содействия развитию малых форм предприятий в научно-технической сфере» в рамках программы «Студенческий стартап» федерального проекта «Платформа университетского технологического предпринимательства»' + } +
+
); }; From 18d17f895d891d8b7bb80dfd3572eb3a0f1d84de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Sat, 8 Nov 2025 19:24:53 +0300 Subject: [PATCH 12/17] right panel --- src/pages/Home.tsx | 5 +- src/views/home/rightpanel/Articles.tsx | 40 +++++++++++++++ src/views/home/rightpanel/Missions.tsx | 68 ++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/views/home/rightpanel/Articles.tsx create mode 100644 src/views/home/rightpanel/Missions.tsx diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 18e78ff..939635f 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -15,6 +15,8 @@ import Group from '../views/home/groups/Group'; import Contest from '../views/home/contest/Contest'; import Account from '../views/home/account/Account'; import ProtectedRoute from '../components/router/ProtectedRoute'; +import { MissionsRightPanel } from '../views/home/rightpanel/Missions'; +import { ArticlesRightPanel } from '../views/home/rightpanel/Articles'; const Home = () => { const name = useAppSelector((state) => state.auth.username); @@ -74,7 +76,8 @@ const Home = () => {
{ -
} /> + } /> + } /> }
diff --git a/src/views/home/rightpanel/Articles.tsx b/src/views/home/rightpanel/Articles.tsx new file mode 100644 index 0000000..48a9511 --- /dev/null +++ b/src/views/home/rightpanel/Articles.tsx @@ -0,0 +1,40 @@ +import { FC } from 'react'; + +export const ArticlesRightPanel: FC = () => { + const items = [ + { + name: 'Энтузиаст создал карточки с NFC-метками для знакомства ребёнка с музыкой', + }, + { + name: 'Алгоритм Древа Силы, Космический Сортировщик', + }, + { + name: 'Космический Сортировщик', + }, + { + name: 'Зеркала Многомерности', + }, + ]; + return ( +
+
+ Попоулярные статьи +
+ + {items.map((v, i) => { + return ( + <> + { +
+ {v.name} +
+ } + {i + 1 != items.length && ( +
+ )} + + ); + })} +
+ ); +}; diff --git a/src/views/home/rightpanel/Missions.tsx b/src/views/home/rightpanel/Missions.tsx new file mode 100644 index 0000000..2d2abe8 --- /dev/null +++ b/src/views/home/rightpanel/Missions.tsx @@ -0,0 +1,68 @@ +import { FC } from 'react'; +import { cn } from '../../../lib/cn'; + +export const MissionsRightPanel: FC = () => { + const items = [ + { + name: 'Кромсатели металла v4', + difficulty: 'Easy', + tags: ['strings', 'arrays', 'math'], + }, + { + name: 'Алгоритм Древа Силы', + difficulty: 'Medium', + tags: ['trees', 'dfs', 'recursion'], + }, + { + name: 'Космический Сортировщик', + difficulty: 'Hard', + tags: ['sorting', 'optimization', 'greedy'], + }, + { + name: 'Зеркала Многомерности', + difficulty: 'Medium', + tags: ['matrix', 'geometry', 'simulation'], + }, + ]; + return ( +
+
+ Новые задачи +
+ + {items.map((v, i) => { + return ( + <> + { +
+
{v.name}
+
+ {v.difficulty} +
+
+ {v.tags.slice(0, 2).map((v, i) => ( +
{v}
+ ))} + {v.tags.length > 2 && '...'} +
+
+ } + {i + 1 != items.length && ( +
+ )} + + ); + })} +
+ ); +}; From ded41ba7f0f414cb7662e88681823be5eacaad5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:32:32 +0300 Subject: [PATCH 13/17] update articles slice --- src/pages/Article.tsx | 11 +- src/pages/ArticleEditor.tsx | 122 ++--- src/pages/Home.tsx | 7 +- src/redux/slices/articles.ts | 433 ++++++++++-------- .../home/account/articles/ArticlesBlock.tsx | 77 +++- src/views/home/articles/Articles.tsx | 71 ++- src/views/home/{groups => group}/Group.tsx | 0 src/views/home/rightpanel/Group.tsx | 60 +++ 8 files changed, 496 insertions(+), 285 deletions(-) rename src/views/home/{groups => group}/Group.tsx (100%) create mode 100644 src/views/home/rightpanel/Group.tsx diff --git a/src/pages/Article.tsx b/src/pages/Article.tsx index 44b910a..3282ab0 100644 --- a/src/pages/Article.tsx +++ b/src/pages/Article.tsx @@ -5,6 +5,7 @@ import { useEffect } from 'react'; import { fetchArticleById } from '../redux/slices/articles'; import MarkdownPreview from '../views/articleeditor/MarckDownPreview'; import { useQuery } from '../hooks/useQuery'; +import { ArticlesRightPanel } from '../views/home/rightpanel/Articles'; const Article = () => { // Получаем параметры из URL @@ -19,8 +20,12 @@ const Article = () => { return ; } const dispatch = useAppDispatch(); - const article = useAppSelector((state) => state.articles.currentArticle); - const status = useAppSelector((state) => state.articles.statuses.fetchById); + const article = useAppSelector( + (state) => state.articles.fetchArticleById.article, + ); + const status = useAppSelector( + (state) => state.articles.fetchArticleById.status, + ); useEffect(() => { dispatch(fetchArticleById(articleIdNumber)); @@ -65,7 +70,7 @@ const Article = () => { )}
-
+
); }; diff --git a/src/pages/ArticleEditor.tsx b/src/pages/ArticleEditor.tsx index 79002b8..8461e02 100644 --- a/src/pages/ArticleEditor.tsx +++ b/src/pages/ArticleEditor.tsx @@ -23,26 +23,33 @@ const ArticleEditor = () => { const query = useQuery(); const back = query.get('back') ?? undefined; const articleId = Number(query.get('articleId') ?? undefined); - const article = useAppSelector((state) => state.articles.currentArticle); - const refactor = articleId != undefined && !isNaN(articleId); + const refactor = articleId && !isNaN(articleId); + // Достаём данные из redux + const article = useAppSelector( + (state) => state.articles.fetchArticleById.article, + ); + + const statusCreate = useAppSelector( + (state) => state.articles.createArticle.status, + ); + const statusUpdate = useAppSelector( + (state) => state.articles.updateArticle.status, + ); + const statusDelete = useAppSelector( + (state) => state.articles.deleteArticle.status, + ); + + // Локальные состояния const [code, setCode] = useState(article?.content || ''); const [name, setName] = useState(article?.name || ''); const [tagInput, setTagInput] = useState(''); const [tags, setTags] = useState(article?.tags || []); - const [activeEditor, setActiveEditor] = useState(false); - const statusCreate = useAppSelector( - (state) => state.articles.statuses.create, - ); - const statusUpdate = useAppSelector( - (state) => state.articles.statuses.update, - ); - const statusDelete = useAppSelector( - (state) => state.articles.statuses.delete, - ); - + // ========================== + // Теги + // ========================== const addTag = () => { const newTag = tagInput.trim(); if (newTag && !tags.includes(newTag)) { @@ -55,53 +62,63 @@ const ArticleEditor = () => { setTags(tags.filter((tag) => tag !== tagToRemove)); }; + // ========================== + // Эффекты по статусам + // ========================== useEffect(() => { - if (statusCreate == 'successful') { - dispatch(setArticlesStatus({ key: 'create', status: 'idle' })); - navigate(back ? back : '/home/articles'); + if (statusCreate === 'successful') { + dispatch( + setArticlesStatus({ key: 'createArticle', status: 'idle' }), + ); + navigate(back ?? '/home/articles'); } }, [statusCreate]); useEffect(() => { - if (statusDelete == 'successful') { - dispatch(setArticlesStatus({ key: 'delete', status: 'idle' })); - navigate(back ? back : '/home/articles'); - } - }, [statusDelete]); - - useEffect(() => { - if (statusUpdate == 'successful') { - dispatch(setArticlesStatus({ key: 'update', status: 'idle' })); - navigate(back ? back : '/home/articles'); + if (statusUpdate === 'successful') { + dispatch( + setArticlesStatus({ key: 'updateArticle', status: 'idle' }), + ); + navigate(back ?? '/home/articles'); } }, [statusUpdate]); + useEffect(() => { + if (statusDelete === 'successful') { + dispatch( + setArticlesStatus({ key: 'deleteArticle', status: 'idle' }), + ); + navigate(back ?? '/home/articles'); + } + }, [statusDelete]); + + // ========================== + // Получение статьи + // ========================== useEffect(() => { if (articleId) { dispatch(fetchArticleById(articleId)); } }, [articleId]); + // Обновление локального состояния после загрузки статьи useEffect(() => { if (article && refactor) { - setCode(article?.content || ''); - setName(article?.name || ''); - setTags(article?.tags || []); + setCode(article.content || ''); + setName(article.name || ''); + setTags(article.tags || []); } }, [article]); + // ========================== + // Рендер + // ========================== return (
{activeEditor ? ( -
{ - setActiveEditor(false); - }} - /> +
setActiveEditor(false)} /> ) : ( -
navigate(back ? back : '/home/articles')} - /> +
navigate(back ?? '/home/articles')} /> )} {activeEditor ? ( @@ -113,6 +130,8 @@ const ArticleEditor = () => { ? `Редактирование статьи: \"${article?.name}\"` : 'Создание статьи'}
+ + {/* Кнопки действий */}
{refactor ? (
@@ -129,16 +148,16 @@ const ArticleEditor = () => { }} text="Обновить" className="mt-[20px]" - disabled={statusUpdate == 'loading'} + disabled={statusUpdate === 'loading'} /> { - dispatch(deleteArticle(articleId)); - }} + onClick={() => + dispatch(deleteArticle(articleId)) + } color="error" text="Удалить" className="mt-[20px]" - disabled={statusDelete == 'loading'} + disabled={statusDelete === 'loading'} />
) : ( @@ -154,11 +173,12 @@ const ArticleEditor = () => { }} text="Опубликовать" className="mt-[20px]" - disabled={statusCreate == 'loading'} + disabled={statusCreate === 'loading'} /> )}
+ {/* Название */} { className="mt-[20px] max-w-[600px]" type="text" label="Название" - onChange={(v) => { - setName(v); - }} + onChange={setName} placeholder="Новая статья" /> - {/* Блок для тегов */} + {/* Теги */}
{ className="mt-[20px] max-w-[600px]" type="text" label="Теги" - onChange={(v) => { - setTagInput(v); - }} + onChange={setTagInput} defaultState={tagInput} placeholder="arrays" onKeyDown={(e) => { - console.log(e.key); - if (e.key == 'Enter') addTag(); + if (e.key === 'Enter') addTag(); }} /> {
+ {/* Просмотр и переход в редактор */} setActiveEditor(true)} text="Редактировать текст" @@ -222,7 +238,7 @@ const ArticleEditor = () => { />
)} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 939635f..5b3114c 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -11,12 +11,13 @@ 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/groups/Group'; +import Group from '../views/home/group/Group'; import Contest from '../views/home/contest/Contest'; import Account from '../views/home/account/Account'; import ProtectedRoute from '../components/router/ProtectedRoute'; import { MissionsRightPanel } from '../views/home/rightpanel/Missions'; import { ArticlesRightPanel } from '../views/home/rightpanel/Articles'; +import { GroupRightPanel } from '../views/home/rightpanel/Group'; const Home = () => { const name = useAppSelector((state) => state.auth.username); @@ -78,6 +79,10 @@ const Home = () => { } /> } /> + } + /> }
diff --git a/src/redux/slices/articles.ts b/src/redux/slices/articles.ts index 73c59df..e2346dc 100644 --- a/src/redux/slices/articles.ts +++ b/src/redux/slices/articles.ts @@ -1,7 +1,9 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import axios from '../../axios'; -// ─── Типы ──────────────────────────────────────────── +// ===================== +// Типы +// ===================== type Status = 'idle' | 'loading' | 'successful' | 'failed'; @@ -15,39 +17,145 @@ export interface Article { updatedAt: string; } -interface ArticlesState { - articles: Article[]; - currentArticle?: Article; +interface ArticlesResponse { hasNextPage: boolean; - statuses: { - create: Status; - update: Status; - delete: Status; - fetchAll: Status; - fetchById: Status; + articles: Article[]; +} + +// ===================== +// Состояние +// ===================== + +interface ArticlesState { + fetchArticles: { + articles: Article[]; + hasNextPage: boolean; + status: Status; + error?: string; + }; + fetchArticleById: { + article?: Article; + status: Status; + error?: string; + }; + createArticle: { + article?: Article; + status: Status; + error?: string; + }; + updateArticle: { + article?: Article; + status: Status; + error?: string; + }; + deleteArticle: { + status: Status; + error?: string; + }; + fetchMyArticles: { + articles: Article[]; + status: Status; + error?: string; }; - error: string | null; } const initialState: ArticlesState = { - articles: [], - currentArticle: undefined, - hasNextPage: false, - statuses: { - create: 'idle', - update: 'idle', - delete: 'idle', - fetchAll: 'idle', - fetchById: 'idle', + fetchArticles: { + articles: [], + hasNextPage: false, + status: 'idle', + error: undefined, + }, + fetchArticleById: { + article: undefined, + status: 'idle', + error: undefined, + }, + createArticle: { + article: undefined, + status: 'idle', + error: undefined, + }, + updateArticle: { + article: undefined, + status: 'idle', + error: undefined, + }, + deleteArticle: { + status: 'idle', + error: undefined, + }, + fetchMyArticles: { + articles: [], + status: 'idle', + error: undefined, }, - error: null, }; -// ─── Async Thunks ───────────────────────────────────── +// ===================== +// Async Thunks +// ===================== -// POST /articles +// Все статьи +export const fetchArticles = createAsyncThunk( + 'articles/fetchArticles', + async ( + { + page = 0, + pageSize = 10, + tags, + }: { page?: number; pageSize?: number; tags?: string[] } = {}, + { rejectWithValue }, + ) => { + try { + const params: any = { page, pageSize }; + if (tags && tags.length > 0) params.tags = tags; + const response = await axios.get('/articles', { + params, + }); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка при получении статей', + ); + } + }, +); + +// Мои статьи +export const fetchMyArticles = createAsyncThunk( + 'articles/fetchMyArticles', + async (_, { rejectWithValue }) => { + try { + const response = await axios.get('/articles/my'); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || + 'Ошибка при получении моих статей', + ); + } + }, +); + +// Статья по ID +export const fetchArticleById = createAsyncThunk( + 'articles/fetchById', + async (articleId: number, { rejectWithValue }) => { + try { + const response = await axios.get
(`/articles/${articleId}`); + return response.data; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка при получении статьи', + ); + } + }, +); + +// Создание статьи export const createArticle = createAsyncThunk( - 'articles/createArticle', + 'articles/create', async ( { name, @@ -57,12 +165,12 @@ export const createArticle = createAsyncThunk( { rejectWithValue }, ) => { try { - const response = await axios.post('/articles', { + const response = await axios.post
('/articles', { name, content, tags, }); - return response.data as Article; + return response.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Ошибка при создании статьи', @@ -71,9 +179,9 @@ export const createArticle = createAsyncThunk( }, ); -// PUT /articles/{articleId} +// Обновление статьи export const updateArticle = createAsyncThunk( - 'articles/updateArticle', + 'articles/update', async ( { articleId, @@ -84,12 +192,15 @@ export const updateArticle = createAsyncThunk( { rejectWithValue }, ) => { try { - const response = await axios.put(`/articles/${articleId}`, { - name, - content, - tags, - }); - return response.data as Article; + const response = await axios.put
( + `/articles/${articleId}`, + { + name, + content, + tags, + }, + ); + return response.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Ошибка при обновлении статьи', @@ -98,9 +209,9 @@ export const updateArticle = createAsyncThunk( }, ); -// DELETE /articles/{articleId} +// Удаление статьи export const deleteArticle = createAsyncThunk( - 'articles/deleteArticle', + 'articles/delete', async (articleId: number, { rejectWithValue }) => { try { await axios.delete(`/articles/${articleId}`); @@ -113,186 +224,136 @@ export const deleteArticle = createAsyncThunk( }, ); -// GET /articles -export const fetchArticles = createAsyncThunk( - 'articles/fetchArticles', - async ( - { - page = 0, - pageSize = 10, - tags, - }: { page?: number; pageSize?: number; tags?: string[] }, - { rejectWithValue }, - ) => { - try { - const params: any = { page, pageSize }; - if (tags && tags.length > 0) params.tags = tags; - const response = await axios.get('/articles', { params }); - return response.data as { - hasNextPage: boolean; - articles: Article[]; - }; - } catch (err: any) { - return rejectWithValue( - err.response?.data?.message || 'Ошибка при получении статей', - ); - } - }, -); - -// GET /articles/{articleId} -export const fetchArticleById = createAsyncThunk( - 'articles/fetchArticleById', - async (articleId: number, { rejectWithValue }) => { - try { - const response = await axios.get(`/articles/${articleId}`); - return response.data as Article; - } catch (err: any) { - return rejectWithValue( - err.response?.data?.message || 'Ошибка при получении статьи', - ); - } - }, -); - -// ─── Slice ──────────────────────────────────────────── +// ===================== +// Slice +// ===================== const articlesSlice = createSlice({ name: 'articles', initialState, reducers: { - clearCurrentArticle: (state) => { - state.currentArticle = undefined; - }, setArticlesStatus: ( state, - action: PayloadAction<{ - key: keyof ArticlesState['statuses']; - status: Status; - }>, + action: PayloadAction<{ key: keyof ArticlesState; status: Status }>, ) => { const { key, status } = action.payload; - state.statuses[key] = status; + if (state[key]) { + (state[key] as any).status = status; + } }, }, extraReducers: (builder) => { - // ─── CREATE ARTICLE ─── - builder.addCase(createArticle.pending, (state) => { - state.statuses.create = 'loading'; - state.error = null; - }); - builder.addCase( - createArticle.fulfilled, - (state, action: PayloadAction
) => { - state.statuses.create = 'successful'; - state.articles.push(action.payload); - }, - ); - builder.addCase( - createArticle.rejected, - (state, action: PayloadAction) => { - state.statuses.create = 'failed'; - state.error = action.payload; - }, - ); - - // ─── UPDATE ARTICLE ─── - builder.addCase(updateArticle.pending, (state) => { - state.statuses.update = 'loading'; - state.error = null; - }); - builder.addCase( - updateArticle.fulfilled, - (state, action: PayloadAction
) => { - state.statuses.update = 'successful'; - const index = state.articles.findIndex( - (a) => a.id === action.payload.id, - ); - if (index !== -1) state.articles[index] = action.payload; - if (state.currentArticle?.id === action.payload.id) - state.currentArticle = action.payload; - }, - ); - builder.addCase( - updateArticle.rejected, - (state, action: PayloadAction) => { - state.statuses.update = 'failed'; - state.error = action.payload; - }, - ); - - // ─── DELETE ARTICLE ─── - builder.addCase(deleteArticle.pending, (state) => { - state.statuses.delete = 'loading'; - state.error = null; - }); - builder.addCase( - deleteArticle.fulfilled, - (state, action: PayloadAction) => { - state.statuses.delete = 'successful'; - state.articles = state.articles.filter( - (a) => a.id !== action.payload, - ); - if (state.currentArticle?.id === action.payload) - state.currentArticle = undefined; - }, - ); - builder.addCase( - deleteArticle.rejected, - (state, action: PayloadAction) => { - state.statuses.delete = 'failed'; - state.error = action.payload; - }, - ); - - // ─── FETCH ARTICLES ─── + // fetchArticles builder.addCase(fetchArticles.pending, (state) => { - state.statuses.fetchAll = 'loading'; - state.error = null; + state.fetchArticles.status = 'loading'; + state.fetchArticles.error = undefined; }); builder.addCase( fetchArticles.fulfilled, - ( - state, - action: PayloadAction<{ - hasNextPage: boolean; - articles: Article[]; - }>, - ) => { - state.statuses.fetchAll = 'successful'; - state.articles = action.payload.articles; - state.hasNextPage = action.payload.hasNextPage; - }, - ); - builder.addCase( - fetchArticles.rejected, - (state, action: PayloadAction) => { - state.statuses.fetchAll = 'failed'; - state.error = action.payload; + (state, action: PayloadAction) => { + state.fetchArticles.status = 'successful'; + state.fetchArticles.articles = action.payload.articles; + state.fetchArticles.hasNextPage = action.payload.hasNextPage; }, ); + builder.addCase(fetchArticles.rejected, (state, action: any) => { + state.fetchArticles.status = 'failed'; + state.fetchArticles.error = action.payload; + }); - // ─── FETCH ARTICLE BY ID ─── + // fetchMyArticles + builder.addCase(fetchMyArticles.pending, (state) => { + state.fetchMyArticles.status = 'loading'; + state.fetchMyArticles.error = undefined; + }); + builder.addCase( + fetchMyArticles.fulfilled, + (state, action: PayloadAction) => { + state.fetchMyArticles.status = 'successful'; + state.fetchMyArticles.articles = action.payload; + }, + ); + builder.addCase(fetchMyArticles.rejected, (state, action: any) => { + state.fetchMyArticles.status = 'failed'; + state.fetchMyArticles.error = action.payload; + }); + + // fetchArticleById builder.addCase(fetchArticleById.pending, (state) => { - state.statuses.fetchById = 'loading'; - state.error = null; + state.fetchArticleById.status = 'loading'; + state.fetchArticleById.error = undefined; }); builder.addCase( fetchArticleById.fulfilled, (state, action: PayloadAction
) => { - state.statuses.fetchById = 'successful'; - state.currentArticle = action.payload; + state.fetchArticleById.status = 'successful'; + state.fetchArticleById.article = action.payload; }, ); + builder.addCase(fetchArticleById.rejected, (state, action: any) => { + state.fetchArticleById.status = 'failed'; + state.fetchArticleById.error = action.payload; + }); + + // createArticle + builder.addCase(createArticle.pending, (state) => { + state.createArticle.status = 'loading'; + state.createArticle.error = undefined; + }); builder.addCase( - fetchArticleById.rejected, - (state, action: PayloadAction) => { - state.statuses.fetchById = 'failed'; - state.error = action.payload; + createArticle.fulfilled, + (state, action: PayloadAction
) => { + state.createArticle.status = 'successful'; + state.createArticle.article = action.payload; }, ); + builder.addCase(createArticle.rejected, (state, action: any) => { + state.createArticle.status = 'failed'; + state.createArticle.error = action.payload; + }); + + // updateArticle + builder.addCase(updateArticle.pending, (state) => { + state.updateArticle.status = 'loading'; + state.updateArticle.error = undefined; + }); + builder.addCase( + updateArticle.fulfilled, + (state, action: PayloadAction
) => { + state.updateArticle.status = 'successful'; + state.updateArticle.article = action.payload; + }, + ); + builder.addCase(updateArticle.rejected, (state, action: any) => { + state.updateArticle.status = 'failed'; + state.updateArticle.error = action.payload; + }); + + // deleteArticle + builder.addCase(deleteArticle.pending, (state) => { + state.deleteArticle.status = 'loading'; + state.deleteArticle.error = undefined; + }); + builder.addCase( + deleteArticle.fulfilled, + (state, action: PayloadAction) => { + state.deleteArticle.status = 'successful'; + state.fetchArticles.articles = + state.fetchArticles.articles.filter( + (a) => a.id !== action.payload, + ); + state.fetchMyArticles.articles = + state.fetchMyArticles.articles.filter( + (a) => a.id !== action.payload, + ); + }, + ); + builder.addCase(deleteArticle.rejected, (state, action: any) => { + state.deleteArticle.status = 'failed'; + state.deleteArticle.error = action.payload; + }); }, }); -export const { clearCurrentArticle, setArticlesStatus } = articlesSlice.actions; +export const { setArticlesStatus } = articlesSlice.actions; export const articlesReducer = articlesSlice.reducer; diff --git a/src/views/home/account/articles/ArticlesBlock.tsx b/src/views/home/account/articles/ArticlesBlock.tsx index eba0bc8..eb91f9b 100644 --- a/src/views/home/account/articles/ArticlesBlock.tsx +++ b/src/views/home/account/articles/ArticlesBlock.tsx @@ -3,8 +3,7 @@ import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; import { setMenuActiveProfilePage } from '../../../../redux/slices/store'; import { cn } from '../../../../lib/cn'; import { ChevroneDown, Edit } from '../../../../assets/icons/groups'; -import { fetchArticles } from '../../../../redux/slices/articles'; - +import { fetchMyArticles } from '../../../../redux/slices/articles'; import { useNavigate } from 'react-router-dom'; export interface ArticleItemProps { @@ -13,21 +12,21 @@ export interface ArticleItemProps { tags: string[]; } -const ArticleItem: React.FC = ({ id, name, tags }) => { +const ArticleItem: FC = ({ id, name, tags }) => { const navigate = useNavigate(); + return (
{ - navigate(`/article/${id}?back=/home/account/articles`); - }} + onClick={() => + navigate(`/article/${id}?back=/home/account/articles`) + } > -
+
#{id}
@@ -35,13 +34,14 @@ const ArticleItem: React.FC = ({ id, name, tags }) => { {name}
+
{tags.map((v, i) => (
{v} @@ -50,8 +50,9 @@ const ArticleItem: React.FC = ({ id, name, tags }) => {
Редактировать { e.stopPropagation(); navigate( @@ -69,49 +70,79 @@ interface ArticlesBlockProps { const ArticlesBlock: FC = ({ className = '' }) => { const dispatch = useAppDispatch(); - const articles = useAppSelector((state) => state.articles.articles); const [active, setActive] = useState(true); + // ✅ Берём только "мои статьи" + const articles = useAppSelector( + (state) => state.articles.fetchMyArticles.articles, + ); + const status = useAppSelector( + (state) => state.articles.fetchMyArticles.status, + ); + const error = useAppSelector( + (state) => state.articles.fetchMyArticles.error, + ); + useEffect(() => { dispatch(setMenuActiveProfilePage('articles')); - dispatch(fetchArticles({})); - }, []); + dispatch(fetchMyArticles()); + }, [dispatch]); + return (
+ {/* Заголовок */}
{ - setActive(!active); - }} + onClick={() => setActive(!active)} > Мои статьи toggle
+ + {/* Контент */}
- {articles.map((v, i) => ( - + {status === 'loading' && ( +
+ Загрузка статей... +
+ )} + {status === 'failed' && ( +
+ Ошибка:{' '} + {error || 'Не удалось загрузить статьи'} +
+ )} + {status === 'successful' && + articles.length === 0 && ( +
+ У вас пока нет статей +
+ )} + {articles.map((v) => ( + ))}
diff --git a/src/views/home/articles/Articles.tsx b/src/views/home/articles/Articles.tsx index 5d124a8..5c3caad 100644 --- a/src/views/home/articles/Articles.tsx +++ b/src/views/home/articles/Articles.tsx @@ -7,51 +7,84 @@ import { useNavigate } from 'react-router-dom'; import { fetchArticles } from '../../../redux/slices/articles'; import Filters from './Filter'; -export interface Article { - id: number; - name: string; - tags: string[]; -} - const Articles = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); - const articles = useAppSelector((state) => state.articles.articles); - const status = useAppSelector((state) => state.articles.statuses.fetchAll); + // ✅ Берём данные из нового состояния + const articles = useAppSelector( + (state) => state.articles.fetchArticles.articles, + ); + const status = useAppSelector( + (state) => state.articles.fetchArticles.status, + ); + const error = useAppSelector((state) => state.articles.fetchArticles.error); useEffect(() => { dispatch(setMenuActivePage('articles')); dispatch(fetchArticles({})); - }, []); + }, [dispatch]); - if (status == 'loading') return
Загрузка...
; + // ======================== + // Состояния загрузки / ошибки + // ======================== + if (status === 'loading') { + return ( +
+ Загрузка статей... +
+ ); + } + if (status === 'failed') { + return ( +
+ Ошибка при загрузке статей + {error && ( +
+ {error} +
+ )} +
+ ); + } + + // ======================== + // Основной контент + // ======================== return ( -
+
+ {/* Заголовок */}
Статьи
{ - navigate('/article/create'); - }} + onClick={() => navigate('/article/create')} text="Создать статью" className="absolute right-0" />
+ {/* Фильтры */} -
- {articles.map((v, i) => ( - - ))} + {/* Список статей */} +
+ {articles.length === 0 ? ( +
+ Пока нет статей +
+ ) : ( + articles.map((v) => ) + )}
-
pages
+ {/* Пагинация (пока заглушка) */} +
+ pages +
); diff --git a/src/views/home/groups/Group.tsx b/src/views/home/group/Group.tsx similarity index 100% rename from src/views/home/groups/Group.tsx rename to src/views/home/group/Group.tsx diff --git a/src/views/home/rightpanel/Group.tsx b/src/views/home/rightpanel/Group.tsx new file mode 100644 index 0000000..28b26fb --- /dev/null +++ b/src/views/home/rightpanel/Group.tsx @@ -0,0 +1,60 @@ +import { FC } from 'react'; + +export const GroupRightPanel: FC = () => { + const items = [ + { + name: 'Игнат Герасименко', + role: 'Администратор', + }, + { + name: 'Алиса Макаренко', + role: 'Модератор', + }, + { + name: 'Федор Картман', + role: 'Модератор', + }, + { + name: 'Карина Механаджанович', + role: 'Участник', + }, + { + name: 'Михаил Ангрский', + role: 'Участник', + }, + { + name: 'newuser', + role: 'Участник (Вы)', + }, + ]; + return ( +
+
+ Пользователи +
+ + {items.map((v, i) => { + return ( + <> + { +
+
+
+
+ {v.name} +
+
+ {v.role} +
+
+
+ } + {i + 1 != items.length && ( +
+ )} + + ); + })} +
+ ); +}; From dfc298520931543796de43f3557df5582ec2a134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:37:47 +0300 Subject: [PATCH 14/17] auth + groups invite --- src/App.tsx | 21 +- src/components/router/ProtectedRoute.tsx | 8 +- src/pages/Home.tsx | 11 +- src/redux/slices/auth.ts | 34 +- src/redux/slices/contests.ts | 9 +- src/redux/slices/groups.ts | 435 +++++++++++++------- src/redux/slices/missions.ts | 5 +- src/views/home/auth/Login.tsx | 8 +- src/views/home/auth/Register.tsx | 8 +- src/views/home/groupinviter/GroupInvite.tsx | 111 +++++ src/views/home/groups/GroupItem.tsx | 20 +- src/views/home/groups/Groups.tsx | 109 +++-- src/views/home/groups/GroupsBlock.tsx | 11 +- src/views/home/groups/ModalCreate.tsx | 2 +- src/views/home/groups/ModalInvite.tsx | 102 +++++ src/views/home/groups/ModalUpdate.tsx | 4 +- 16 files changed, 673 insertions(+), 225 deletions(-) create mode 100644 src/views/home/groupinviter/GroupInvite.tsx create mode 100644 src/views/home/groups/ModalInvite.tsx diff --git a/src/App.tsx b/src/App.tsx index 609203e..009f229 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,22 +9,27 @@ import Mission from './pages/Mission'; import ArticleEditor from './pages/ArticleEditor'; import Article from './pages/Article'; import ContestEditor from './pages/ContestEditor'; +import ProtectedRoute from './components/router/ProtectedRoute'; function App() { return (
+ }> + } + /> + } + /> + + } /> } /> - } - /> - } - /> + } /> } /> diff --git a/src/components/router/ProtectedRoute.tsx b/src/components/router/ProtectedRoute.tsx index 775a461..c78e309 100644 --- a/src/components/router/ProtectedRoute.tsx +++ b/src/components/router/ProtectedRoute.tsx @@ -1,11 +1,15 @@ // src/routes/ProtectedRoute.tsx -import { Navigate, Outlet } from 'react-router-dom'; +import { Navigate, Outlet, useLocation } from 'react-router-dom'; import { useAppSelector } from '../../redux/hooks'; export default function ProtectedRoute() { const isAuthenticated = useAppSelector((state) => !!state.auth.jwt); + const location = useLocation(); + + console.log('location', location); + if (!isAuthenticated) { - return ; + return ; } return ; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 5b3114c..b133b62 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,4 +1,4 @@ -// import React from "react"; +// src/pages/Home.tsx import { Route, Routes } from 'react-router-dom'; import Login from '../views/home/auth/Login'; import Register from '../views/home/auth/Register'; @@ -18,6 +18,7 @@ import ProtectedRoute from '../components/router/ProtectedRoute'; import { MissionsRightPanel } from '../views/home/rightpanel/Missions'; import { ArticlesRightPanel } from '../views/home/rightpanel/Articles'; import { GroupRightPanel } from '../views/home/rightpanel/Group'; +import GroupInvite from '../views/home/groupinviter/GroupInvite'; const Home = () => { const name = useAppSelector((state) => state.auth.username); @@ -37,14 +38,18 @@ const Home = () => { }> } /> + } + /> + } /> + } /> } /> } /> } /> } /> - } /> - } /> } /> } /> { + async (_, { dispatch, getState, rejectWithValue }) => { try { const response = await axios.get('/authentication/whoami'); return response.data; } catch (err: any) { + const state: any = getState(); + const refresh = state.auth.refreshToken; + + if (refresh) { + // пробуем refresh + const result = await dispatch( + refreshToken({ refreshToken: refresh }), + ); + + // если успешный, повторить whoami + if (refreshToken.fulfilled.match(result)) { + const retry = await axios.get('/authentication/whoami'); + return retry.data; + } + } return rejectWithValue( err.response?.data?.message || 'Failed to fetch user info', ); @@ -269,6 +284,23 @@ const authSlice = createSlice({ builder.addCase(fetchWhoAmI.rejected, (state, action) => { state.status = 'failed'; state.error = action.payload as string; + + // Если пользователь не авторизован (401), делаем logout и пытаемся refresh + console.log(action); + if ( + action.payload === 'Unauthorized' || + action.payload === 'Failed to fetch user info' + ) { + // Вызов logout + state.jwt = null; + state.refreshToken = null; + state.username = null; + state.email = null; + state.id = null; + localStorage.removeItem('jwt'); + localStorage.removeItem('refreshToken'); + delete axios.defaults.headers.common['Authorization']; + } }); }, }); diff --git a/src/redux/slices/contests.ts b/src/redux/slices/contests.ts index 1e2970c..5ddff69 100644 --- a/src/redux/slices/contests.ts +++ b/src/redux/slices/contests.ts @@ -33,8 +33,6 @@ export interface Submission { sourceType: string; } - - export interface Mission { id: number; authorId: number; @@ -124,8 +122,6 @@ interface ContestsState { status: Status; error?: string; }; - - // 🆕 Добавляем updateContest и deleteContest updateContest: { contest: Contest; status: Status; @@ -176,7 +172,7 @@ const initialState: ContestsState = { status: 'idle', error: undefined, }, - fetchMySubmissions: { + fetchMySubmissions: { submissions: [], status: 'idle', error: undefined, @@ -262,7 +258,6 @@ export const fetchMySubmissions = createAsyncThunk( }, ); - // Все контесты export const fetchContests = createAsyncThunk( 'contests/fetchAll', @@ -435,8 +430,6 @@ const contestsSlice = createSlice({ state.fetchMySubmissions.error = action.payload; }); - - // fetchContests builder.addCase(fetchContests.pending, (state) => { state.fetchContests.status = 'loading'; diff --git a/src/redux/slices/groups.ts b/src/redux/slices/groups.ts index e9c586d..38350bb 100644 --- a/src/redux/slices/groups.ts +++ b/src/redux/slices/groups.ts @@ -1,7 +1,9 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import axios from '../../axios'; -// ─── Типы ──────────────────────────────────────────── +// ===================== +// Типы +// ===================== type Status = 'idle' | 'loading' | 'successful' | 'failed'; @@ -19,39 +21,106 @@ export interface Group { contests: any[]; } +// ===================== +// Состояние +// ===================== + interface GroupsState { - groups: Group[]; - currentGroup: Group | null; - statuses: { - create: Status; - update: Status; - delete: Status; - fetchMy: Status; - fetchById: Status; - addMember: Status; - removeMember: Status; + fetchMyGroups: { + groups: Group[]; + status: Status; + error?: string; + }; + fetchGroupById: { + group?: Group; + status: Status; + error?: string; + }; + createGroup: { + group?: Group; + status: Status; + error?: string; + }; + updateGroup: { + group?: Group; + status: Status; + error?: string; + }; + deleteGroup: { + deletedId?: number; + status: Status; + error?: string; + }; + addGroupMember: { + status: Status; + error?: string; + }; + removeGroupMember: { + status: Status; + error?: string; + }; + fetchGroupJoinLink: { + joinLink?: { token: string; expiresAt: string }; + status: Status; + error?: string; + }; + joinGroupByToken: { + group?: Group; + status: Status; + error?: string; }; - error: string | null; } const initialState: GroupsState = { - groups: [], - currentGroup: null, - statuses: { - create: 'idle', - update: 'idle', - delete: 'idle', - fetchMy: 'idle', - fetchById: 'idle', - addMember: 'idle', - removeMember: 'idle', + fetchMyGroups: { + groups: [], + status: 'idle', + error: undefined, + }, + fetchGroupById: { + group: undefined, + status: 'idle', + error: undefined, + }, + createGroup: { + group: undefined, + status: 'idle', + error: undefined, + }, + updateGroup: { + group: undefined, + status: 'idle', + error: undefined, + }, + deleteGroup: { + deletedId: undefined, + status: 'idle', + error: undefined, + }, + addGroupMember: { + status: 'idle', + error: undefined, + }, + removeGroupMember: { + status: 'idle', + error: undefined, + }, + fetchGroupJoinLink: { + joinLink: undefined, + status: 'idle', + error: undefined, + }, + joinGroupByToken: { + group: undefined, + status: 'idle', + error: undefined, }, - error: null, }; -// ─── Async Thunks ───────────────────────────────────── +// ===================== +// Async Thunks +// ===================== -// POST /groups export const createGroup = createAsyncThunk( 'groups/createGroup', async ( @@ -69,7 +138,6 @@ export const createGroup = createAsyncThunk( }, ); -// PUT /groups/{groupId} export const updateGroup = createAsyncThunk( 'groups/updateGroup', async ( @@ -94,7 +162,6 @@ export const updateGroup = createAsyncThunk( }, ); -// DELETE /groups/{groupId} export const deleteGroup = createAsyncThunk( 'groups/deleteGroup', async (groupId: number, { rejectWithValue }) => { @@ -109,7 +176,6 @@ export const deleteGroup = createAsyncThunk( }, ); -// GET /groups/my export const fetchMyGroups = createAsyncThunk( 'groups/fetchMyGroups', async (_, { rejectWithValue }) => { @@ -124,7 +190,6 @@ export const fetchMyGroups = createAsyncThunk( }, ); -// GET /groups/{groupId} export const fetchGroupById = createAsyncThunk( 'groups/fetchGroupById', async (groupId: number, { rejectWithValue }) => { @@ -139,16 +204,22 @@ export const fetchGroupById = createAsyncThunk( }, ); -// POST /groups/members export const addGroupMember = createAsyncThunk( 'groups/addGroupMember', async ( - { userId, role }: { userId: number; role: string }, + { + groupId, + userId, + role, + }: { groupId: number; userId: number; role: string }, { rejectWithValue }, ) => { try { - await axios.post('/groups/members', { userId, role }); - return { userId, role }; + const response = await axios.post(`/groups/${groupId}/members`, { + userId, + role, + }); + return response.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || @@ -158,7 +229,6 @@ export const addGroupMember = createAsyncThunk( }, ); -// DELETE /groups/{groupId}/members/{memberId} export const removeGroupMember = createAsyncThunk( 'groups/removeGroupMember', async ( @@ -176,147 +246,169 @@ export const removeGroupMember = createAsyncThunk( }, ); -// ─── Slice ──────────────────────────────────────────── +// ===================== +// Новые Async Thunks +// ===================== + +// Получение актуальной ссылки для присоединения к группе +export const fetchGroupJoinLink = createAsyncThunk( + 'groups/fetchGroupJoinLink', + async (groupId: number, { rejectWithValue }) => { + try { + 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 || + 'Ошибка при получении ссылки для присоединения', + ); + } + }, +); + +// Присоединение к группе по токену приглашения +export const joinGroupByToken = createAsyncThunk( + 'groups/joinGroupByToken', + async (token: string, { rejectWithValue }) => { + try { + const response = await axios.post(`/groups/join/${token}`); + return response.data as Group; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || + 'Ошибка при присоединении к группе по ссылке', + ); + } + }, +); + +// ===================== +// Slice +// ===================== const groupsSlice = createSlice({ name: 'groups', initialState, reducers: { - clearCurrentGroup: (state) => { - state.currentGroup = null; + setGroupsStatus: ( + state, + action: PayloadAction<{ key: keyof GroupsState; status: Status }>, + ) => { + const { key, status } = action.payload; + if (state[key]) { + (state[key] as any).status = status; + } }, }, extraReducers: (builder) => { - // ─── CREATE GROUP ─── - builder.addCase(createGroup.pending, (state) => { - state.statuses.create = 'loading'; - state.error = null; - }); - builder.addCase( - createGroup.fulfilled, - (state, action: PayloadAction) => { - state.statuses.create = 'successful'; - state.groups.push(action.payload); - }, - ); - builder.addCase( - createGroup.rejected, - (state, action: PayloadAction) => { - state.statuses.create = 'failed'; - state.error = action.payload; - }, - ); - - // ─── UPDATE GROUP ─── - builder.addCase(updateGroup.pending, (state) => { - state.statuses.update = 'loading'; - state.error = null; - }); - builder.addCase( - updateGroup.fulfilled, - (state, action: PayloadAction) => { - state.statuses.update = 'successful'; - const index = state.groups.findIndex( - (g) => g.id === action.payload.id, - ); - if (index !== -1) state.groups[index] = action.payload; - if (state.currentGroup?.id === action.payload.id) { - state.currentGroup = action.payload; - } - }, - ); - builder.addCase( - updateGroup.rejected, - (state, action: PayloadAction) => { - state.statuses.update = 'failed'; - state.error = action.payload; - }, - ); - - // ─── DELETE GROUP ─── - builder.addCase(deleteGroup.pending, (state) => { - state.statuses.delete = 'loading'; - state.error = null; - }); - builder.addCase( - deleteGroup.fulfilled, - (state, action: PayloadAction) => { - state.statuses.delete = 'successful'; - state.groups = state.groups.filter( - (g) => g.id !== action.payload, - ); - if (state.currentGroup?.id === action.payload) - state.currentGroup = null; - }, - ); - builder.addCase( - deleteGroup.rejected, - (state, action: PayloadAction) => { - state.statuses.delete = 'failed'; - state.error = action.payload; - }, - ); - - // ─── FETCH MY GROUPS ─── + // fetchMyGroups builder.addCase(fetchMyGroups.pending, (state) => { - state.statuses.fetchMy = 'loading'; - state.error = null; + state.fetchMyGroups.status = 'loading'; }); builder.addCase( fetchMyGroups.fulfilled, (state, action: PayloadAction) => { - state.statuses.fetchMy = 'successful'; - state.groups = action.payload; - }, - ); - builder.addCase( - fetchMyGroups.rejected, - (state, action: PayloadAction) => { - state.statuses.fetchMy = 'failed'; - state.error = action.payload; + state.fetchMyGroups.status = 'successful'; + state.fetchMyGroups.groups = action.payload; }, ); + builder.addCase(fetchMyGroups.rejected, (state, action: any) => { + state.fetchMyGroups.status = 'failed'; + state.fetchMyGroups.error = action.payload; + }); - // ─── FETCH GROUP BY ID ─── + // fetchGroupById builder.addCase(fetchGroupById.pending, (state) => { - state.statuses.fetchById = 'loading'; - state.error = null; + state.fetchGroupById.status = 'loading'; }); builder.addCase( fetchGroupById.fulfilled, (state, action: PayloadAction) => { - state.statuses.fetchById = 'successful'; - state.currentGroup = action.payload; - }, - ); - builder.addCase( - fetchGroupById.rejected, - (state, action: PayloadAction) => { - state.statuses.fetchById = 'failed'; - state.error = action.payload; + state.fetchGroupById.status = 'successful'; + state.fetchGroupById.group = action.payload; }, ); + builder.addCase(fetchGroupById.rejected, (state, action: any) => { + state.fetchGroupById.status = 'failed'; + state.fetchGroupById.error = action.payload; + }); - // ─── ADD MEMBER ─── + // createGroup + builder.addCase(createGroup.pending, (state) => { + state.createGroup.status = 'loading'; + }); + builder.addCase( + createGroup.fulfilled, + (state, action: PayloadAction) => { + state.createGroup.status = 'successful'; + state.createGroup.group = action.payload; + state.fetchMyGroups.groups.push(action.payload); + }, + ); + builder.addCase(createGroup.rejected, (state, action: any) => { + state.createGroup.status = 'failed'; + state.createGroup.error = action.payload; + }); + + // updateGroup + builder.addCase(updateGroup.pending, (state) => { + state.updateGroup.status = 'loading'; + }); + builder.addCase( + updateGroup.fulfilled, + (state, action: PayloadAction) => { + state.updateGroup.status = 'successful'; + state.updateGroup.group = action.payload; + const index = state.fetchMyGroups.groups.findIndex( + (g) => g.id === action.payload.id, + ); + if (index !== -1) + state.fetchMyGroups.groups[index] = action.payload; + if (state.fetchGroupById.group?.id === action.payload.id) + state.fetchGroupById.group = action.payload; + }, + ); + builder.addCase(updateGroup.rejected, (state, action: any) => { + state.updateGroup.status = 'failed'; + state.updateGroup.error = action.payload; + }); + + // deleteGroup + builder.addCase(deleteGroup.pending, (state) => { + state.deleteGroup.status = 'loading'; + }); + builder.addCase( + deleteGroup.fulfilled, + (state, action: PayloadAction) => { + state.deleteGroup.status = 'successful'; + state.deleteGroup.deletedId = action.payload; + state.fetchMyGroups.groups = state.fetchMyGroups.groups.filter( + (g) => g.id !== action.payload, + ); + if (state.fetchGroupById.group?.id === action.payload) + state.fetchGroupById.group = undefined; + }, + ); + builder.addCase(deleteGroup.rejected, (state, action: any) => { + state.deleteGroup.status = 'failed'; + state.deleteGroup.error = action.payload; + }); + + // addGroupMember builder.addCase(addGroupMember.pending, (state) => { - state.statuses.addMember = 'loading'; - state.error = null; + state.addGroupMember.status = 'loading'; }); builder.addCase(addGroupMember.fulfilled, (state) => { - state.statuses.addMember = 'successful'; + state.addGroupMember.status = 'successful'; + }); + builder.addCase(addGroupMember.rejected, (state, action: any) => { + state.addGroupMember.status = 'failed'; + state.addGroupMember.error = action.payload; }); - builder.addCase( - addGroupMember.rejected, - (state, action: PayloadAction) => { - state.statuses.addMember = 'failed'; - state.error = action.payload; - }, - ); - // ─── REMOVE MEMBER ─── + // removeGroupMember builder.addCase(removeGroupMember.pending, (state) => { - state.statuses.removeMember = 'loading'; - state.error = null; + state.removeGroupMember.status = 'loading'; }); builder.addCase( removeGroupMember.fulfilled, @@ -324,27 +416,60 @@ const groupsSlice = createSlice({ state, action: PayloadAction<{ groupId: number; memberId: number }>, ) => { - state.statuses.removeMember = 'successful'; + state.removeGroupMember.status = 'successful'; if ( - state.currentGroup && - state.currentGroup.id === action.payload.groupId + state.fetchGroupById.group && + state.fetchGroupById.group.id === action.payload.groupId ) { - state.currentGroup.members = - state.currentGroup.members.filter( + state.fetchGroupById.group.members = + state.fetchGroupById.group.members.filter( (m) => m.userId !== action.payload.memberId, ); } }, ); + builder.addCase(removeGroupMember.rejected, (state, action: any) => { + state.removeGroupMember.status = 'failed'; + state.removeGroupMember.error = action.payload; + }); + + // fetchGroupJoinLink + builder.addCase(fetchGroupJoinLink.pending, (state) => { + state.fetchGroupJoinLink.status = 'loading'; + }); builder.addCase( - removeGroupMember.rejected, - (state, action: PayloadAction) => { - state.statuses.removeMember = 'failed'; - state.error = action.payload; + fetchGroupJoinLink.fulfilled, + ( + state, + action: PayloadAction<{ token: string; expiresAt: string }>, + ) => { + state.fetchGroupJoinLink.status = 'successful'; + state.fetchGroupJoinLink.joinLink = action.payload; }, ); + builder.addCase(fetchGroupJoinLink.rejected, (state, action: any) => { + state.fetchGroupJoinLink.status = 'failed'; + state.fetchGroupJoinLink.error = action.payload; + }); + + // joinGroupByToken + builder.addCase(joinGroupByToken.pending, (state) => { + state.joinGroupByToken.status = 'loading'; + }); + builder.addCase( + joinGroupByToken.fulfilled, + (state, action: PayloadAction) => { + state.joinGroupByToken.status = 'successful'; + state.joinGroupByToken.group = action.payload; + state.fetchMyGroups.groups.push(action.payload); // добавим новую группу в список + }, + ); + builder.addCase(joinGroupByToken.rejected, (state, action: any) => { + state.joinGroupByToken.status = 'failed'; + state.joinGroupByToken.error = action.payload; + }); }, }); -export const { clearCurrentGroup } = groupsSlice.actions; +export const { setGroupsStatus } = groupsSlice.actions; export const groupsReducer = groupsSlice.reducer; diff --git a/src/redux/slices/missions.ts b/src/redux/slices/missions.ts index 9125402..f3c84ee 100644 --- a/src/redux/slices/missions.ts +++ b/src/redux/slices/missions.ts @@ -147,9 +147,6 @@ const missionsSlice = createSlice({ name: 'missions', initialState, reducers: { - clearCurrentMission: (state) => { - state.currentMission = null; - }, setMissionsStatus: ( state, action: PayloadAction<{ @@ -251,5 +248,5 @@ const missionsSlice = createSlice({ }, }); -export const { clearCurrentMission, setMissionsStatus } = missionsSlice.actions; +export const { setMissionsStatus } = missionsSlice.actions; export const missionsReducer = missionsSlice.reducer; diff --git a/src/views/home/auth/Login.tsx b/src/views/home/auth/Login.tsx index 548be70..d83cea5 100644 --- a/src/views/home/auth/Login.tsx +++ b/src/views/home/auth/Login.tsx @@ -1,8 +1,9 @@ +// src/views/home/auth/Login.tsx import { useState, useEffect } from 'react'; import { PrimaryButton } from '../../../components/button/PrimaryButton'; import { Input } from '../../../components/input/Input'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import { loginUser } from '../../../redux/slices/auth'; // import { cn } from "../../../lib/cn"; import { setMenuActivePage } from '../../../redux/slices/store'; @@ -13,6 +14,7 @@ import { googleLogo } from '../../../assets/icons/input'; const Login = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); + const location = useLocation(); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); @@ -30,7 +32,9 @@ const Login = () => { useEffect(() => { if (jwt) { - navigate('/home/account'); // или другая страница после входа + const from = location.state?.from; + const path = from ? from.pathname + from.search : '/home/account'; + navigate(path, { replace: true }); } }, [jwt]); diff --git a/src/views/home/auth/Register.tsx b/src/views/home/auth/Register.tsx index d341b39..d096422 100644 --- a/src/views/home/auth/Register.tsx +++ b/src/views/home/auth/Register.tsx @@ -1,8 +1,9 @@ +// src/views/home/auth/Register.tsx import { useState, useEffect } from 'react'; import { PrimaryButton } from '../../../components/button/PrimaryButton'; import { Input } from '../../../components/input/Input'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { registerUser } from '../../../redux/slices/auth'; // import { cn } from "../../../lib/cn"; import { setMenuActivePage } from '../../../redux/slices/store'; @@ -15,6 +16,7 @@ import { googleLogo } from '../../../assets/icons/input'; const Register = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); + const location = useLocation(); const [username, setUsername] = useState(''); const [email, setEmail] = useState(''); @@ -32,7 +34,9 @@ const Register = () => { useEffect(() => { if (jwt) { - navigate('/home/account'); + const from = location.state?.from; + const path = from ? from.pathname + from.search : '/home/account'; + navigate(path, { replace: true }); } console.log(submitClicked); }, [jwt]); diff --git a/src/views/home/groupinviter/GroupInvite.tsx b/src/views/home/groupinviter/GroupInvite.tsx new file mode 100644 index 0000000..893d817 --- /dev/null +++ b/src/views/home/groupinviter/GroupInvite.tsx @@ -0,0 +1,111 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { setMenuActivePage } from '../../../redux/slices/store'; +import { useQuery } from '../../../hooks/useQuery'; +import { PrimaryButton } from '../../../components/button/PrimaryButton'; +import { SecondaryButton } from '../../../components/button/SecondaryButton'; +import { + joinGroupByToken, + setGroupsStatus, +} from '../../../redux/slices/groups'; + +const GroupInvite = () => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const query = useQuery(); + const token = query.get('token') ?? undefined; + const expiresAt = query.get('expiresAt') ?? undefined; + const groupName = query.get('groupName') ?? undefined; + const groupId = Number(query.get('groupId') ?? undefined); + + const username = useAppSelector((state) => state.auth.username); + const joinStatus = useAppSelector( + (state) => state.groups.joinGroupByToken.status, + ); + const joinError = useAppSelector( + (state) => state.groups.joinGroupByToken.error, + ); + + useEffect(() => { + dispatch(setMenuActivePage('groups')); + }, []); + + useEffect(() => { + if (joinStatus == 'successful') { + dispatch( + setGroupsStatus({ key: 'joinGroupByToken', status: 'idle' }), + ); + navigate(`/group/${groupId}`); + } + }, [joinStatus]); + + if (!token || !expiresAt || !groupName || !groupId) { + return ( +
+ Приглашение признано недействительным. +
+ ); + } + + const isExpired = new Date(expiresAt) < new Date(); + + if (isExpired) { + return ( +
+ Период действия приглашения истек. +
+ ); + } + + const handleJoin = async () => { + if (!token) return; + try { + await dispatch(joinGroupByToken(token)).unwrap(); + } catch (err) { + console.error('Failed to join group', err); + } + }; + + const handleCancel = () => { + navigate('/home/account'); + }; + + return ( +
+
+
+ Привет, {username}! +
+
+ Вы действительно хотите присоединиться к группе: +
+
+ "{groupName}" +
+ + {joinError && ( +
+ Ошибка присоединения: {joinError} +
+ )} + +
+ + +
+
+
+ ); +}; + +export default GroupInvite; diff --git a/src/views/home/groups/GroupItem.tsx b/src/views/home/groups/GroupItem.tsx index d7f579f..4a979b0 100644 --- a/src/views/home/groups/GroupItem.tsx +++ b/src/views/home/groups/GroupItem.tsx @@ -7,7 +7,7 @@ import { EyeOpen, } from '../../../assets/icons/groups'; import { useNavigate } from 'react-router-dom'; -import { GroupUpdate } from './Groups'; +import { GroupInvite, GroupUpdate } from './Groups'; export interface GroupItemProps { id: number; @@ -17,6 +17,9 @@ export interface GroupItemProps { description: string; setUpdateActive: (value: any) => void; setUpdateGroup: (value: GroupUpdate) => void; + setInviteActive: (value: any) => void; + setInviteGroup: (value: GroupInvite) => void; + type: 'manage' | 'member'; } interface IconComponentProps { @@ -45,6 +48,9 @@ const GroupItem: React.FC = ({ description, setUpdateGroup, setUpdateActive, + setInviteActive, + setInviteGroup, + type, }) => { const navigate = useNavigate(); @@ -63,10 +69,16 @@ const GroupItem: React.FC = ({
{name}
- {(role == 'menager' || role == 'owner') && ( - + {type == 'manage' && ( + { + setInviteActive(true); + setInviteGroup({ id, name }); + }} + /> )} - {(role == 'menager' || role == 'owner') && ( + {type == 'manage' && ( { diff --git a/src/views/home/groups/Groups.tsx b/src/views/home/groups/Groups.tsx index 51650e6..2752d3c 100644 --- a/src/views/home/groups/Groups.tsx +++ b/src/views/home/groups/Groups.tsx @@ -8,6 +8,7 @@ import { fetchMyGroups } from '../../../redux/slices/groups'; import ModalCreate from './ModalCreate'; import ModalUpdate from './ModalUpdate'; import Filters from './Filter'; +import ModalInvite from './ModalInvite'; export interface GroupUpdate { id: number; @@ -15,19 +16,35 @@ export interface GroupUpdate { description: string; } +export interface GroupInvite { + id: number; + name: string; +} + const Groups = () => { - const [modalActive, setModalActive] = useState(false); - const [modelUpdateActive, setModalUpdateActive] = useState(false); + const [modalActive, setModalActive] = useState(false); + const [modalUpdateActive, setModalUpdateActive] = useState(false); const [updateGroup, setUpdateGroup] = useState({ id: 0, name: '', description: '', }); + const [modalInviteActive, setModalInviteActive] = useState(false); + const [inviteGroup, setInviteGroup] = useState({ + id: 0, + name: '', + }); const dispatch = useAppDispatch(); - // Берём группы из стора - const groups = useAppSelector((store) => store.groups.groups); + // ✅ Берём группы и статус из нового слайса + const groups = useAppSelector((store) => store.groups.fetchMyGroups.groups); + const groupsStatus = useAppSelector( + (store) => store.groups.fetchMyGroups.status, + ); + const groupsError = useAppSelector( + (store) => store.groups.fetchMyGroups.error, + ); // Берём текущего пользователя const currentUserName = useAppSelector((store) => store.auth.username); @@ -52,8 +69,8 @@ const Groups = () => { (m) => m.username === currentUserName, ); if (!me) return; - - if (me.role === 'Administrator') { + const roles = me.role.split(',').map((r) => r.trim()); + if (roles.includes('Administrator')) { managed.push(group); } else { current.push(group); @@ -68,7 +85,7 @@ const Groups = () => { }, [groups, currentUserName]); return ( -
+
{ Группы
{ - setModalActive(true); - }} + onClick={() => setModalActive(true)} text="Создать группу" className="absolute right-0" /> @@ -89,37 +104,67 @@ const Groups = () => { - - - + {groupsStatus === 'loading' && ( +
+ Загрузка групп... +
+ )} + {groupsStatus === 'failed' && ( +
+ Ошибка: {groupsError || 'Не удалось загрузить группы'} +
+ )} + + {groupsStatus === 'successful' && ( + <> + + + + + )}
+
); }; diff --git a/src/views/home/groups/GroupsBlock.tsx b/src/views/home/groups/GroupsBlock.tsx index e66d713..2f64f17 100644 --- a/src/views/home/groups/GroupsBlock.tsx +++ b/src/views/home/groups/GroupsBlock.tsx @@ -3,7 +3,7 @@ import GroupItem from './GroupItem'; import { cn } from '../../../lib/cn'; import { ChevroneDown } from '../../../assets/icons/groups'; import { Group } from '../../../redux/slices/groups'; -import { GroupUpdate } from './Groups'; +import { GroupInvite, GroupUpdate } from './Groups'; interface GroupsBlockProps { groups: Group[]; @@ -11,6 +11,9 @@ interface GroupsBlockProps { className?: string; setUpdateActive: (value: any) => void; setUpdateGroup: (value: GroupUpdate) => void; + setInviteActive: (value: any) => void; + setInviteGroup: (value: GroupInvite) => void; + type: 'manage' | 'member'; } const GroupsBlock: FC = ({ @@ -19,6 +22,9 @@ const GroupsBlock: FC = ({ className, setUpdateActive, setUpdateGroup, + setInviteActive, + setInviteGroup, + type, }) => { const [active, setActive] = useState(title != 'Скрытые'); @@ -63,8 +69,11 @@ const GroupsBlock: FC = ({ description={v.description} setUpdateActive={setUpdateActive} setUpdateGroup={setUpdateGroup} + setInviteActive={setInviteActive} + setInviteGroup={setInviteGroup} role={'owner'} name={v.name} + type={type} /> ))}
diff --git a/src/views/home/groups/ModalCreate.tsx b/src/views/home/groups/ModalCreate.tsx index 458c491..ecda215 100644 --- a/src/views/home/groups/ModalCreate.tsx +++ b/src/views/home/groups/ModalCreate.tsx @@ -14,7 +14,7 @@ interface ModalCreateProps { const ModalCreate: FC = ({ active, setActive }) => { const [name, setName] = useState(''); const [description, setDescription] = useState(''); - const status = useAppSelector((state) => state.groups.statuses.create); + const status = useAppSelector((state) => state.groups.createGroup.status); const dispatch = useAppDispatch(); useEffect(() => { diff --git a/src/views/home/groups/ModalInvite.tsx b/src/views/home/groups/ModalInvite.tsx new file mode 100644 index 0000000..a00cfec --- /dev/null +++ b/src/views/home/groups/ModalInvite.tsx @@ -0,0 +1,102 @@ +import { FC, useEffect, useMemo } from 'react'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { fetchGroupJoinLink } from '../../../redux/slices/groups'; +import { Modal } from '../../../components/modal/Modal'; +import { PrimaryButton } from '../../../components/button/PrimaryButton'; +import { SecondaryButton } from '../../../components/button/SecondaryButton'; +import { Input } from '../../../components/input/Input'; + +interface ModalInviteProps { + active: boolean; + setActive: (value: boolean) => void; + groupId: number; + groupName: string; +} + +const ModalInvite: FC = ({ + active, + setActive, + groupId, + groupName, +}) => { + const dispatch = useAppDispatch(); + const baseUrl = window.location.origin; + + // Получаем токен и дату из Redux + const { joinLink, status } = useAppSelector( + (state) => state.groups.fetchGroupJoinLink, + ); + + // При открытии модалки запрашиваем join link + useEffect(() => { + if (active) { + dispatch(fetchGroupJoinLink(groupId)); + } + }, [active, groupId, dispatch]); + + // Генерация полной ссылки с query параметрами + const inviteLink = useMemo(() => { + if (!joinLink) return ''; + const params = new URLSearchParams({ + token: joinLink.token, + expiresAt: joinLink.expiresAt, + groupName, + groupId: `${groupId}`, + }); + return `${baseUrl}/home/group-invite?${params.toString()}`; + }, [joinLink, groupName, baseUrl, groupId]); + + // Копирование и закрытие модалки + const handleCopy = async () => { + if (!inviteLink) return; + try { + await navigator.clipboard.writeText(inviteLink); + setActive(false); + } catch (err) { + console.error('Не удалось скопировать ссылку:', err); + } + }; + + return ( + +
+
+ Приглашение в группу "{groupName}" +
+ +
+
+ Ссылка для приглашения +
+
+ {inviteLink} +
+
+ +
+ + setActive(false)} + text="Отмена" + /> +
+
+
+ ); +}; + +export default ModalInvite; diff --git a/src/views/home/groups/ModalUpdate.tsx b/src/views/home/groups/ModalUpdate.tsx index 9233c9f..5d4ec0a 100644 --- a/src/views/home/groups/ModalUpdate.tsx +++ b/src/views/home/groups/ModalUpdate.tsx @@ -24,10 +24,10 @@ const ModalUpdate: FC = ({ const [name, setName] = useState(''); const [description, setDescription] = useState(''); const statusUpdate = useAppSelector( - (state) => state.groups.statuses.update, + (state) => state.groups.updateGroup.status, ); const statusDelete = useAppSelector( - (state) => state.groups.statuses.delete, + (state) => state.groups.deleteGroup.status, ); const dispatch = useAppDispatch(); From 56b6f9b3390b6ca5eea3db4f047ef7d23e771477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:23:26 +0300 Subject: [PATCH 15/17] group posts --- src/assets/icons/group/cup.svg | 3 + src/assets/icons/group/home.svg | 3 + src/assets/icons/group/index.ts | 5 + src/assets/icons/group/message-chat.svg | 3 + src/components/input/SearchInput.tsx | 7 +- src/components/router/ProtectedRoute.tsx | 2 - src/pages/ContestEditor.tsx | 5 +- src/pages/Home.tsx | 4 +- src/pages/Mission.tsx | 10 +- src/redux/slices/auth.ts | 1 - src/redux/slices/groupfeed.ts | 336 +++++++++++++++++++ src/redux/slices/store.ts | 16 +- src/redux/slices/submit.ts | 1 - src/redux/store.ts | 2 + src/views/home/account/AccoutMenu.tsx | 2 - src/views/home/account/contests/Contests.tsx | 2 - src/views/home/articles/Filter.tsx | 4 +- src/views/home/auth/Login.tsx | 2 +- src/views/home/auth/Register.tsx | 2 +- src/views/home/contests/ContestItem.tsx | 8 +- src/views/home/group/Group.tsx | 42 ++- src/views/home/group/GroupMenu.tsx | 96 ++++++ src/views/home/group/chat/Chat.tsx | 12 + src/views/home/group/contests/Contests.tsx | 12 + src/views/home/group/posts/PostItem.tsx | 0 src/views/home/group/posts/Posts.tsx | 83 +++++ src/views/home/groupinviter/GroupInvite.tsx | 4 +- src/views/home/groups/Filter.tsx | 4 +- 28 files changed, 624 insertions(+), 47 deletions(-) create mode 100644 src/assets/icons/group/cup.svg create mode 100644 src/assets/icons/group/home.svg create mode 100644 src/assets/icons/group/index.ts create mode 100644 src/assets/icons/group/message-chat.svg create mode 100644 src/redux/slices/groupfeed.ts create mode 100644 src/views/home/group/GroupMenu.tsx create mode 100644 src/views/home/group/chat/Chat.tsx create mode 100644 src/views/home/group/contests/Contests.tsx create mode 100644 src/views/home/group/posts/PostItem.tsx create mode 100644 src/views/home/group/posts/Posts.tsx diff --git a/src/assets/icons/group/cup.svg b/src/assets/icons/group/cup.svg new file mode 100644 index 0000000..65b15e5 --- /dev/null +++ b/src/assets/icons/group/cup.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/group/home.svg b/src/assets/icons/group/home.svg new file mode 100644 index 0000000..76e7b28 --- /dev/null +++ b/src/assets/icons/group/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/group/index.ts b/src/assets/icons/group/index.ts new file mode 100644 index 0000000..1faa753 --- /dev/null +++ b/src/assets/icons/group/index.ts @@ -0,0 +1,5 @@ +import Cup from './cup.svg'; +import Home from './home.svg'; +import MessageChat from './message-chat.svg'; + +export { Cup, MessageChat, Home }; diff --git a/src/assets/icons/group/message-chat.svg b/src/assets/icons/group/message-chat.svg new file mode 100644 index 0000000..1e32098 --- /dev/null +++ b/src/assets/icons/group/message-chat.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/input/SearchInput.tsx b/src/components/input/SearchInput.tsx index f091b4f..a006e99 100644 --- a/src/components/input/SearchInput.tsx +++ b/src/components/input/SearchInput.tsx @@ -39,7 +39,7 @@ export const SearchInput: React.FC = ({ > = ({ if (onKeyDown) onKeyDown(e); }} /> - + ); }; diff --git a/src/components/router/ProtectedRoute.tsx b/src/components/router/ProtectedRoute.tsx index c78e309..b704bf7 100644 --- a/src/components/router/ProtectedRoute.tsx +++ b/src/components/router/ProtectedRoute.tsx @@ -6,8 +6,6 @@ export default function ProtectedRoute() { const isAuthenticated = useAppSelector((state) => !!state.auth.jwt); const location = useLocation(); - console.log('location', location); - if (!isAuthenticated) { return ; } diff --git a/src/pages/ContestEditor.tsx b/src/pages/ContestEditor.tsx index 6b28820..a2abf04 100644 --- a/src/pages/ContestEditor.tsx +++ b/src/pages/ContestEditor.tsx @@ -69,7 +69,6 @@ const ContestEditor = () => { const { contest: contestById, status: contestByIdstatus } = useAppSelector( (state) => state.contests.fetchContestById, ); - console.log(contestByIdstatus, contestById); useEffect(() => { if (status === 'successful') { } @@ -100,9 +99,7 @@ const ContestEditor = () => { })); setMissionIdInput(''); }) - .catch((err) => { - console.error('Ошибка при загрузке миссии:', err); - }); + .catch((err) => {}); }; const removeMission = (removeId: number) => { diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index b133b62..71d967c 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -42,7 +42,7 @@ const Home = () => { path="group-invite/*" element={} /> - } /> + } /> } /> @@ -85,7 +85,7 @@ const Home = () => { } /> } /> } /> diff --git a/src/pages/Mission.tsx b/src/pages/Mission.tsx index 161d516..0a6435b 100644 --- a/src/pages/Mission.tsx +++ b/src/pages/Mission.tsx @@ -149,9 +149,7 @@ const Mission = () => { html: htmlStatement.statementTexts['problem.html'], mediaFiles: latexStatement.mediaFiles, }; - } catch (err) { - console.error('Ошибка парсинга statementTexts:', err); - } + } catch (err) {} return (
@@ -180,7 +178,6 @@ const Mission = () => { { - console.log(contestId); await dispatch( submitMission({ missionId: missionIdNumber, @@ -200,7 +197,10 @@ const Mission = () => {
- +
diff --git a/src/redux/slices/auth.ts b/src/redux/slices/auth.ts index c4f8930..eeabf20 100644 --- a/src/redux/slices/auth.ts +++ b/src/redux/slices/auth.ts @@ -286,7 +286,6 @@ const authSlice = createSlice({ state.error = action.payload as string; // Если пользователь не авторизован (401), делаем logout и пытаемся refresh - console.log(action); if ( action.payload === 'Unauthorized' || action.payload === 'Failed to fetch user info' diff --git a/src/redux/slices/groupfeed.ts b/src/redux/slices/groupfeed.ts new file mode 100644 index 0000000..011267b --- /dev/null +++ b/src/redux/slices/groupfeed.ts @@ -0,0 +1,336 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from '../../axios'; + +// ===================== +// Типы +// ===================== + +type Status = 'idle' | 'loading' | 'successful' | 'failed'; + +export interface Post { + id: number; + groupId: number; + authorId: number; + authorUsername: string; + name: string; + content: string; + createdAt: string; + updatedAt: string; +} + +export interface PostsPage { + items: Post[]; + hasNext: boolean; +} + +// ===================== +// Состояние +// ===================== + +interface PostsState { + fetchPosts: { + pages: Record; // страница => данные + status: Status; + error?: string; + }; + fetchPostById: { + post?: Post; + status: Status; + error?: string; + }; + createPost: { + post?: Post; + status: Status; + error?: string; + }; + updatePost: { + post?: Post; + status: Status; + error?: string; + }; + deletePost: { + deletedId?: number; + status: Status; + error?: string; + }; +} + +const initialState: PostsState = { + fetchPosts: { + pages: {}, + status: 'idle', + error: undefined, + }, + fetchPostById: { + post: undefined, + status: 'idle', + error: undefined, + }, + createPost: { + post: undefined, + status: 'idle', + error: undefined, + }, + updatePost: { + post: undefined, + status: 'idle', + error: undefined, + }, + deletePost: { + deletedId: undefined, + status: 'idle', + error: undefined, + }, +}; + +// ===================== +// Async Thunks +// ===================== + +// Получить посты группы (пагинация) +export const fetchGroupPosts = createAsyncThunk( + 'posts/fetchGroupPosts', + async ( + { + groupId, + page = 0, + pageSize = 20, + }: { groupId: number; page?: number; pageSize?: number }, + { rejectWithValue }, + ) => { + try { + const response = await axios.get( + `/groups/${groupId}/feed?page=${page}&pageSize=${pageSize}`, + ); + return { page, data: response.data as PostsPage }; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка загрузки постов', + ); + } + }, +); + +// Получить один пост +export const fetchPostById = createAsyncThunk( + 'posts/fetchPostById', + async ( + { groupId, postId }: { groupId: number; postId: number }, + { rejectWithValue }, + ) => { + try { + const response = await axios.get( + `/groups/${groupId}/feed/${postId}`, + ); + return response.data as Post; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка загрузки поста', + ); + } + }, +); + +// Создать пост +export const createPost = createAsyncThunk( + 'posts/createPost', + async ( + { + groupId, + name, + content, + }: { groupId: number; name: string; content: string }, + { rejectWithValue }, + ) => { + try { + const response = await axios.post(`/groups/${groupId}/feed`, { + name, + content, + }); + return response.data as Post; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка создания поста', + ); + } + }, +); + +// Обновить пост +export const updatePost = createAsyncThunk( + 'posts/updatePost', + async ( + { + groupId, + postId, + name, + content, + }: { + groupId: number; + postId: number; + name: string; + content: string; + }, + { rejectWithValue }, + ) => { + try { + const response = await axios.put( + `/groups/${groupId}/feed/${postId}`, + { + name, + content, + }, + ); + return response.data as Post; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка обновления поста', + ); + } + }, +); + +// Удалить пост +export const deletePost = createAsyncThunk( + 'posts/deletePost', + async ( + { groupId, postId }: { groupId: number; postId: number }, + { rejectWithValue }, + ) => { + try { + await axios.delete(`/groups/${groupId}/feed/${postId}`); + return postId; + } catch (err: any) { + return rejectWithValue( + err.response?.data?.message || 'Ошибка удаления поста', + ); + } + }, +); + +// ===================== +// Slice +// ===================== + +const postsSlice = createSlice({ + name: 'posts', + initialState, + reducers: {}, + extraReducers: (builder) => { + // fetchGroupPosts + builder.addCase(fetchGroupPosts.pending, (state) => { + state.fetchPosts.status = 'loading'; + }); + builder.addCase( + fetchGroupPosts.fulfilled, + ( + state, + action: PayloadAction<{ page: number; data: PostsPage }>, + ) => { + const { page, data } = action.payload; + state.fetchPosts.status = 'successful'; + state.fetchPosts.pages[page] = data; + }, + ); + builder.addCase(fetchGroupPosts.rejected, (state, action: any) => { + state.fetchPosts.status = 'failed'; + state.fetchPosts.error = action.payload; + }); + + // fetchPostById + builder.addCase(fetchPostById.pending, (state) => { + state.fetchPostById.status = 'loading'; + }); + builder.addCase( + fetchPostById.fulfilled, + (state, action: PayloadAction) => { + state.fetchPostById.status = 'successful'; + state.fetchPostById.post = action.payload; + }, + ); + builder.addCase(fetchPostById.rejected, (state, action: any) => { + state.fetchPostById.status = 'failed'; + state.fetchPostById.error = action.payload; + }); + + // createPost + builder.addCase(createPost.pending, (state) => { + state.createPost.status = 'loading'; + }); + builder.addCase( + createPost.fulfilled, + (state, action: PayloadAction) => { + state.createPost.status = 'successful'; + state.createPost.post = action.payload; + + // добавляем сразу в первую страницу (page = 0) + if (state.fetchPosts.pages[0]) { + state.fetchPosts.pages[0].items.unshift(action.payload); + } + }, + ); + builder.addCase(createPost.rejected, (state, action: any) => { + state.createPost.status = 'failed'; + state.createPost.error = action.payload; + }); + + // updatePost + builder.addCase(updatePost.pending, (state) => { + state.updatePost.status = 'loading'; + }); + builder.addCase( + updatePost.fulfilled, + (state, action: PayloadAction) => { + state.updatePost.status = 'successful'; + state.updatePost.post = action.payload; + + // обновим в списках + for (const page of Object.values(state.fetchPosts.pages)) { + const index = page.items.findIndex( + (p) => p.id === action.payload.id, + ); + if (index !== -1) page.items[index] = action.payload; + } + + // обновим если открыт одиночный пост + if (state.fetchPostById.post?.id === action.payload.id) { + state.fetchPostById.post = action.payload; + } + }, + ); + builder.addCase(updatePost.rejected, (state, action: any) => { + state.updatePost.status = 'failed'; + state.updatePost.error = action.payload; + }); + + // deletePost + builder.addCase(deletePost.pending, (state) => { + state.deletePost.status = 'loading'; + }); + builder.addCase( + deletePost.fulfilled, + (state, action: PayloadAction) => { + state.deletePost.status = 'successful'; + state.deletePost.deletedId = action.payload; + + // удалить из всех страниц + for (const page of Object.values(state.fetchPosts.pages)) { + page.items = page.items.filter( + (p) => p.id !== action.payload, + ); + } + + // если открыт индивидуальный пост + if (state.fetchPostById.post?.id === action.payload) { + state.fetchPostById.post = undefined; + } + }, + ); + builder.addCase(deletePost.rejected, (state, action: any) => { + state.deletePost.status = 'failed'; + state.deletePost.error = action.payload; + }); + }, +}); + +export const groupFeedReducer = postsSlice.reducer; diff --git a/src/redux/slices/store.ts b/src/redux/slices/store.ts index bd54ba6..cc713a0 100644 --- a/src/redux/slices/store.ts +++ b/src/redux/slices/store.ts @@ -5,6 +5,7 @@ interface StorState { menu: { activePage: string; activeProfilePage: string; + activeGroupPage: string; }; } @@ -13,6 +14,7 @@ const initialState: StorState = { menu: { activePage: '', activeProfilePage: '', + activeGroupPage: '', }, }; @@ -30,9 +32,19 @@ const storeSlice = createSlice({ ) => { state.menu.activeProfilePage = activeProfilePage.payload; }, + setMenuActiveGroupPage: ( + state, + activeGroupPage: PayloadAction, + ) => { + state.menu.activeGroupPage = activeGroupPage.payload; + }, }, }); -export const { setMenuActivePage, setMenuActiveProfilePage } = - storeSlice.actions; +export const { + setMenuActivePage, + setMenuActiveProfilePage, + setMenuActiveGroupPage, +} = storeSlice.actions; + export const storeReducer = storeSlice.reducer; diff --git a/src/redux/slices/submit.ts b/src/redux/slices/submit.ts index 1b627f2..21522b1 100644 --- a/src/redux/slices/submit.ts +++ b/src/redux/slices/submit.ts @@ -56,7 +56,6 @@ const initialState: SubmitState = { export const submitMission = createAsyncThunk( 'submit/submitMission', async (submitData: Submit, { rejectWithValue }) => { - console.log(submitData); try { const response = await axios.post('/submits', submitData); return response.data; diff --git a/src/redux/store.ts b/src/redux/store.ts index edbe49c..84a6fb3 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -6,6 +6,7 @@ import { submitReducer } from './slices/submit'; import { contestsReducer } from './slices/contests'; import { groupsReducer } from './slices/groups'; import { articlesReducer } from './slices/articles'; +import { groupFeedReducer } from './slices/groupfeed'; // использование // import { useAppDispatch, useAppSelector } from '../redux/hooks'; @@ -25,6 +26,7 @@ export const store = configureStore({ contests: contestsReducer, groups: groupsReducer, articles: articlesReducer, + groupfeed: groupFeedReducer, }, }); diff --git a/src/views/home/account/AccoutMenu.tsx b/src/views/home/account/AccoutMenu.tsx index f8a51b1..321defb 100644 --- a/src/views/home/account/AccoutMenu.tsx +++ b/src/views/home/account/AccoutMenu.tsx @@ -76,8 +76,6 @@ const AccountMenu = () => { (state) => state.store.menu.activeProfilePage, ); - console.log('active', [activeProfilePage]); - return (
{menuItems.map((v, i) => ( diff --git a/src/views/home/account/contests/Contests.tsx b/src/views/home/account/contests/Contests.tsx index 67fdbb7..0c62c1a 100644 --- a/src/views/home/account/contests/Contests.tsx +++ b/src/views/home/account/contests/Contests.tsx @@ -22,8 +22,6 @@ const Contests = () => { dispatch(fetchRegisteredContests({})); }, []); - console.log(myContestsState); - return (
{/* Контесты, в которых я участвую */} diff --git a/src/views/home/articles/Filter.tsx b/src/views/home/articles/Filter.tsx index ca01a9d..1d8a82f 100644 --- a/src/views/home/articles/Filter.tsx +++ b/src/views/home/articles/Filter.tsx @@ -36,13 +36,13 @@ const Filters = () => { text: 'ID', }, ]} - onChange={(v) => console.log(v)} + onChange={(v) => {}} /> console.log(values)} + onChange={(values) => {}} />
); diff --git a/src/views/home/auth/Login.tsx b/src/views/home/auth/Login.tsx index d83cea5..a660b35 100644 --- a/src/views/home/auth/Login.tsx +++ b/src/views/home/auth/Login.tsx @@ -27,7 +27,7 @@ const Login = () => { // После успешного логина useEffect(() => { dispatch(setMenuActivePage('account')); - console.log(submitClicked); + submitClicked; }, []); useEffect(() => { diff --git a/src/views/home/auth/Register.tsx b/src/views/home/auth/Register.tsx index d096422..4eee15a 100644 --- a/src/views/home/auth/Register.tsx +++ b/src/views/home/auth/Register.tsx @@ -38,7 +38,7 @@ const Register = () => { const path = from ? from.pathname + from.search : '/home/account'; navigate(path, { replace: true }); } - console.log(submitClicked); + submitClicked; }, [jwt]); const handleRegister = () => { diff --git a/src/views/home/contests/ContestItem.tsx b/src/views/home/contests/ContestItem.tsx index edf948e..1abbf4a 100644 --- a/src/views/home/contests/ContestItem.tsx +++ b/src/views/home/contests/ContestItem.tsx @@ -73,7 +73,6 @@ const ContestItem: React.FC = ({ : ' bg-liquid-background', )} onClick={() => { - console.log(456); navigate(`/contest/${id}`); }} > @@ -99,12 +98,7 @@ const ContestItem: React.FC = ({ {statusRegister == 'reg' ? ( <> {' '} - { - console.log(123); - }} - text="Регистрация" - /> + {}} text="Регистрация" /> ) : ( <> diff --git a/src/views/home/group/Group.tsx b/src/views/home/group/Group.tsx index cc6ad00..ef34d56 100644 --- a/src/views/home/group/Group.tsx +++ b/src/views/home/group/Group.tsx @@ -1,24 +1,50 @@ -import { FC } from 'react'; +import { FC, useEffect } from 'react'; import { cn } from '../../../lib/cn'; -import { useParams, Navigate } from 'react-router-dom'; +import { useParams, Navigate, Routes, Route } from 'react-router-dom'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { fetchGroupById } from '../../../redux/slices/groups'; +import GroupMenu from './GroupMenu'; +import { Posts } from './posts/Posts'; +import { SearchInput } from '../../../components/input/SearchInput'; +import { Chat } from './chat/Chat'; +import { Contests } from './contests/Contests'; interface GroupsBlockProps {} const Group: FC = () => { - const { groupId } = useParams<{ groupId: string }>(); - const groupIdNumber = Number(groupId); - - if (!groupId || isNaN(groupIdNumber) || !groupIdNumber) { + const groupId = Number(useParams<{ groupId: string }>().groupId); + if (!groupId) { return ; } + const dispatch = useAppDispatch(); + const group = useAppSelector((state) => state.groups.fetchGroupById.group); + + useEffect(() => { + dispatch(fetchGroupById(groupId)); + }, [groupId]); + + console.log(group); + return (
- {groupIdNumber} +
{group?.name}
+ + + + + } /> + } /> + } /> + } + /> +
); }; diff --git a/src/views/home/group/GroupMenu.tsx b/src/views/home/group/GroupMenu.tsx new file mode 100644 index 0000000..081c9f2 --- /dev/null +++ b/src/views/home/group/GroupMenu.tsx @@ -0,0 +1,96 @@ +import { MessageChat, Home, Cup } from '../../../assets/icons/group'; + +import React, { FC } from 'react'; +import { Link } from 'react-router-dom'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { + setMenuActivePage, + setMenuActiveProfilePage, +} from '../../../redux/slices/store'; + +interface MenuItemProps { + icon: string; + text: string; + href: string; + page: string; + profilePage: string; + active?: boolean; +} + +const MenuItem: React.FC = ({ + icon, + text = '', + href = '', + active = false, + page = '', + profilePage = '', +}) => { + const dispatch = useAppDispatch(); + + return ( + { + dispatch(setMenuActivePage(page)); + dispatch(setMenuActiveProfilePage(profilePage)); + }} + > + + {text} + + ); +}; + +interface GroupMenuProps { + groupId: number; +} + +const GroupMenu: FC = ({ groupId }) => { + const menuItems = [ + { + text: 'Главная', + href: `/group/${groupId}/home`, + icon: Home, + page: 'group', + profilePage: 'home', + }, + { + text: 'Чат', + href: `/group/${groupId}/chat`, + icon: MessageChat, + page: 'group', + profilePage: 'chat', + }, + { + text: 'Контесты', + href: `/group/${groupId}/contests`, + icon: Cup, + page: 'group', + profilePage: 'contests', + }, + ]; + + const activeGroupPage = useAppSelector( + (state) => state.store.menu.activeGroupPage, + ); + + return ( +
+ {menuItems.map((v, i) => ( + + ))} +
+ ); +}; + +export default GroupMenu; diff --git a/src/views/home/group/chat/Chat.tsx b/src/views/home/group/chat/Chat.tsx new file mode 100644 index 0000000..7464e22 --- /dev/null +++ b/src/views/home/group/chat/Chat.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; +import { useAppDispatch } from '../../../../redux/hooks'; +import { setMenuActiveGroupPage } from '../../../../redux/slices/store'; + +export const Chat = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(setMenuActiveGroupPage('chat')); + }, []); + return <>; +}; diff --git a/src/views/home/group/contests/Contests.tsx b/src/views/home/group/contests/Contests.tsx new file mode 100644 index 0000000..39faea4 --- /dev/null +++ b/src/views/home/group/contests/Contests.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; +import { useAppDispatch } from '../../../../redux/hooks'; +import { setMenuActiveGroupPage } from '../../../../redux/slices/store'; + +export const Contests = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(setMenuActiveGroupPage('contests')); + }, []); + return <>; +}; diff --git a/src/views/home/group/posts/PostItem.tsx b/src/views/home/group/posts/PostItem.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/views/home/group/posts/Posts.tsx b/src/views/home/group/posts/Posts.tsx new file mode 100644 index 0000000..8e9a0b0 --- /dev/null +++ b/src/views/home/group/posts/Posts.tsx @@ -0,0 +1,83 @@ +import { FC, useEffect } 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'; + +interface PostsProps { + groupId: number; +} + +export const Posts: FC = ({ groupId }) => { + const dispatch = useAppDispatch(); + + const { pages, status } = useAppSelector( + (state) => state.groupfeed.fetchPosts, + ); + + // Загружаем только первую страницу + useEffect(() => { + dispatch(fetchGroupPosts({ groupId, page: 0, pageSize: 20 })); + }, [groupId]); + + useEffect(() => { + dispatch(setMenuActiveGroupPage('home')); + }, []); + + const page0 = pages[0]; + + return ( +
+
+ {}} + placeholder="Поиск сообщений" + /> +
+ + {status === 'loading' &&
Загрузка...
} + {status === 'failed' &&
Ошибка загрузки постов
} + + {status == 'successful' && + page0?.items && + page0.items.length > 0 ? ( +
+ {page0.items.map((post) => ( +
+
+ ID: {post.id} +
+
+ Название: {post.name} +
+
+ Содержимое: {post.content} +
+
+ Автор: {post.authorUsername} +
+
+ Автор ID: {post.authorId} +
+
+ Group ID: {post.groupId} +
+
+ Создан: {post.createdAt} +
+
+ Обновлён: {post.updatedAt} +
+
+ ))} +
+ ) : status === 'successful' ? ( +
Постов пока нет
+ ) : null} +
+ ); +}; diff --git a/src/views/home/groupinviter/GroupInvite.tsx b/src/views/home/groupinviter/GroupInvite.tsx index 893d817..1b3cc7c 100644 --- a/src/views/home/groupinviter/GroupInvite.tsx +++ b/src/views/home/groupinviter/GroupInvite.tsx @@ -63,9 +63,7 @@ const GroupInvite = () => { if (!token) return; try { await dispatch(joinGroupByToken(token)).unwrap(); - } catch (err) { - console.error('Failed to join group', err); - } + } catch (err) {} }; const handleCancel = () => { diff --git a/src/views/home/groups/Filter.tsx b/src/views/home/groups/Filter.tsx index ca01a9d..1d8a82f 100644 --- a/src/views/home/groups/Filter.tsx +++ b/src/views/home/groups/Filter.tsx @@ -36,13 +36,13 @@ const Filters = () => { text: 'ID', }, ]} - onChange={(v) => console.log(v)} + onChange={(v) => {}} /> console.log(values)} + onChange={(values) => {}} />
); From b949837e136f88a0076ef55b1addcd7d6ae1440a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:03:07 +0300 Subject: [PATCH 16/17] group posts --- package-lock.json | 1436 ++++++++++++++------------ package.json | 5 +- src/components/input/SearchInput.tsx | 4 +- src/lib/toastNotification.ts | 34 + src/main.tsx | 2 + src/pages/Home.tsx | 29 + src/styles/index.css | 1 + src/styles/toast.css | 32 + src/views/home/group/posts/Posts.tsx | 37 +- 9 files changed, 937 insertions(+), 643 deletions(-) create mode 100644 src/lib/toastNotification.ts create mode 100644 src/styles/toast.css diff --git a/package-lock.json b/package-lock.json index ec3a875..9b339e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,13 +16,14 @@ "clsx": "^2.1.1", "framer-motion": "^11.9.0", "highlight.js": "^11.11.1", - "monaco-editor": "^0.54.0", + "monaco-editor": "^0.53.0", "postcss": "^8.4.47", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^10.1.0", "react-redux": "^9.2.0", "react-router-dom": "^7.9.4", + "react-toastify": "^11.0.5", "rehype-highlight": "^7.0.2", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", @@ -43,7 +44,7 @@ "globals": "^15.9.0", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", - "vite": "^5.4.1" + "vite": "^7.2.2" } }, "node_modules/@alloc/quick-lru": { @@ -58,38 +59,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", - "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, "license": "MIT", "engines": { @@ -97,22 +85,22 @@ } }, "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -128,31 +116,32 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", - "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.6", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -160,31 +149,40 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -194,33 +192,19 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -228,9 +212,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -238,9 +222,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -248,43 +232,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", - "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", - "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.6" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -294,13 +262,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", - "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -310,13 +278,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", - "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -326,80 +294,66 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", - "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.6", - "@babel/parser": "^7.25.6", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", - "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -410,13 +364,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -427,13 +381,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -444,13 +398,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -461,13 +415,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -478,13 +432,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -495,13 +449,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -512,13 +466,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -529,13 +483,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -546,13 +500,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -563,13 +517,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -580,13 +534,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -597,13 +551,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -614,13 +568,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -631,13 +585,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -648,13 +602,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -665,13 +619,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -682,13 +636,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -699,13 +670,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -716,13 +704,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -733,13 +738,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -750,13 +755,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -767,13 +772,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -784,21 +789,24 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -817,9 +825,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -827,13 +835,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -841,10 +849,36 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -879,19 +913,22 @@ } }, "node_modules/@eslint/js": { - "version": "9.10.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz", - "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -899,18 +936,43 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", - "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -926,9 +988,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -984,17 +1046,24 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1006,15 +1075,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -1022,9 +1082,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1125,10 +1185,17 @@ } } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz", - "integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", "cpu": [ "arm" ], @@ -1140,9 +1207,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz", - "integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", "cpu": [ "arm64" ], @@ -1154,9 +1221,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz", - "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", "cpu": [ "arm64" ], @@ -1168,9 +1235,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz", - "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", "cpu": [ "x64" ], @@ -1181,10 +1248,38 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz", - "integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", "cpu": [ "arm" ], @@ -1196,9 +1291,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz", - "integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", "cpu": [ "arm" ], @@ -1210,9 +1305,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz", - "integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", "cpu": [ "arm64" ], @@ -1224,9 +1319,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz", - "integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", "cpu": [ "arm64" ], @@ -1237,10 +1332,24 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz", - "integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", "cpu": [ "ppc64" ], @@ -1252,9 +1361,23 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz", - "integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", "cpu": [ "riscv64" ], @@ -1266,9 +1389,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz", - "integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", "cpu": [ "s390x" ], @@ -1280,9 +1403,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz", - "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", "cpu": [ "x64" ], @@ -1294,9 +1417,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz", - "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", "cpu": [ "x64" ], @@ -1307,10 +1430,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz", - "integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", "cpu": [ "arm64" ], @@ -1322,9 +1459,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz", - "integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", "cpu": [ "ia32" ], @@ -1335,10 +1472,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz", - "integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", "cpu": [ "x64" ], @@ -1441,9 +1592,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -1474,6 +1625,13 @@ "hoist-non-react-statics": "^3.3.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -1536,6 +1694,12 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/@types/trusted-types": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz", + "integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==", + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -1698,9 +1862,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1797,29 +1961,30 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", - "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-react-jsx-self": "^7.24.5", - "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" + "react-refresh": "^0.17.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1865,19 +2030,6 @@ "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1981,6 +2133,16 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", + "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1994,9 +2156,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2017,9 +2179,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", "dev": true, "funding": [ { @@ -2037,10 +2199,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -2082,9 +2245,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", "dev": true, "funding": [ { @@ -2112,21 +2275,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -2212,23 +2360,6 @@ "node": ">=6" } }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2284,9 +2415,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2395,12 +2526,6 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, - "node_modules/dompurify": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", - "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", - "license": "(MPL-2.0 OR Apache-2.0)" - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2422,9 +2547,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.26", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.26.tgz", - "integrity": "sha512-Z+OMe9M/V6Ep9n/52+b7lkvYEps26z4Yz3vjWL1V61W0q+VLF1pOHhMY17sa4roz4AWmULSI8E6SAojZA5L0YQ==", + "version": "1.5.253", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.253.tgz", + "integrity": "sha512-O0tpQ/35rrgdiGQ0/OFWhy1itmd9A6TY9uQzlqj3hKSu/aYpe7UIn5d7CU2N9myH6biZiWF3VMZVuup8pw5U9w==", "dev": true, "license": "ISC" }, @@ -2492,9 +2617,9 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2502,32 +2627,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -2540,40 +2668,33 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eslint": { - "version": "9.10.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz", - "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", - "@eslint/config-array": "^0.18.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.10.0", - "@eslint/plugin-kit": "^0.1.0", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.0", - "@nodelib/fs.walk": "^1.2.8", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.0.2", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2583,14 +2704,11 @@ "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -2634,9 +2752,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", - "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2651,9 +2769,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2753,15 +2871,15 @@ } }, "node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3147,9 +3265,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -3202,16 +3320,6 @@ "dev": true, "license": "MIT" }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -3498,9 +3606,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3640,16 +3748,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -3699,9 +3797,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3712,16 +3810,16 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -3877,18 +3975,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/marked": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", - "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4809,13 +4895,12 @@ } }, "node_modules/monaco-editor": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz", - "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.53.0.tgz", + "integrity": "sha512-0WNThgC6CMWNXXBxTbaYYcunj08iB5rnx4/G56UOPeL9UVIUGGHA1GR0EWIh9Ebabj7NpCRawQ5b0hfN1jQmYQ==", "license": "MIT", "dependencies": { - "dompurify": "3.1.7", - "marked": "14.0.0" + "@types/trusted-types": "^1.0.6" } }, "node_modules/ms": { @@ -4836,9 +4921,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -4861,9 +4946,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -5058,9 +5143,9 @@ "license": "ISC" }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { @@ -5094,9 +5179,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -5113,8 +5198,8 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -5386,9 +5471,9 @@ } }, "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, "license": "MIT", "engines": { @@ -5433,6 +5518,19 @@ "react-dom": ">=18" } }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5469,12 +5567,6 @@ "redux": "^5.0.0" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, "node_modules/rehype-highlight": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", @@ -5631,13 +5723,13 @@ } }, "node_modules/rollup": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz", - "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -5647,22 +5739,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.22.5", - "@rollup/rollup-android-arm64": "4.22.5", - "@rollup/rollup-darwin-arm64": "4.22.5", - "@rollup/rollup-darwin-x64": "4.22.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.5", - "@rollup/rollup-linux-arm-musleabihf": "4.22.5", - "@rollup/rollup-linux-arm64-gnu": "4.22.5", - "@rollup/rollup-linux-arm64-musl": "4.22.5", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5", - "@rollup/rollup-linux-riscv64-gnu": "4.22.5", - "@rollup/rollup-linux-s390x-gnu": "4.22.5", - "@rollup/rollup-linux-x64-gnu": "4.22.5", - "@rollup/rollup-linux-x64-musl": "4.22.5", - "@rollup/rollup-win32-arm64-msvc": "4.22.5", - "@rollup/rollup-win32-ia32-msvc": "4.22.5", - "@rollup/rollup-win32-x64-msvc": "4.22.5", + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" } }, @@ -5929,19 +6027,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -6011,13 +6096,6 @@ "node": ">=14.0.0" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6039,14 +6117,52 @@ "node": ">=0.8" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/to-regex-range": { @@ -6259,9 +6375,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "dev": true, "funding": [ { @@ -6279,8 +6395,8 @@ ], "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -6357,21 +6473,24 @@ } }, "node_modules/vite": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", - "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -6380,19 +6499,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -6413,9 +6538,46 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/package.json b/package.json index 946dd15..c836017 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,14 @@ "clsx": "^2.1.1", "framer-motion": "^11.9.0", "highlight.js": "^11.11.1", - "monaco-editor": "^0.54.0", + "monaco-editor": "^0.53.0", "postcss": "^8.4.47", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^10.1.0", "react-redux": "^9.2.0", "react-router-dom": "^7.9.4", + "react-toastify": "^11.0.5", "rehype-highlight": "^7.0.2", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", @@ -45,6 +46,6 @@ "globals": "^15.9.0", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", - "vite": "^5.4.1" + "vite": "^7.2.2" } } diff --git a/src/components/input/SearchInput.tsx b/src/components/input/SearchInput.tsx index a006e99..0e23057 100644 --- a/src/components/input/SearchInput.tsx +++ b/src/components/input/SearchInput.tsx @@ -33,13 +33,13 @@ export const SearchInput: React.FC = ({ return (