diff --git a/src/redux/slices/articles.ts b/src/redux/slices/articles.ts index 421b214..d260f48 100644 --- a/src/redux/slices/articles.ts +++ b/src/redux/slices/articles.ts @@ -34,6 +34,12 @@ interface ArticlesState { status: Status; error?: string; }; + fetchNewArticles: { + articles: Article[]; + hasNextPage: boolean; + status: Status; + error?: string; + }; fetchArticleById: { article?: Article; status: Status; @@ -67,6 +73,12 @@ const initialState: ArticlesState = { status: 'idle', error: undefined, }, + fetchNewArticles: { + articles: [], + hasNextPage: false, + status: 'idle', + error: undefined, + }, fetchArticleById: { article: undefined, status: 'idle', @@ -97,13 +109,42 @@ const initialState: ArticlesState = { // Async Thunks // ===================== +// Новые статьи +export const fetchNewArticles = createAsyncThunk( + 'articles/fetchNewArticles', + async ( + { + page = 0, + pageSize = 5, + 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, + paramsSerializer: { + indexes: null, + }, + }); + + return response.data; + } catch (err: any) { + return rejectWithValue(err.response?.data); + } + }, +); + // Все статьи export const fetchArticles = createAsyncThunk( 'articles/fetchArticles', async ( { page = 0, - pageSize = 10, + pageSize = 100, tags, }: { page?: number; pageSize?: number; tags?: string[] } = {}, { rejectWithValue }, @@ -259,6 +300,29 @@ const articlesSlice = createSlice({ }); }); + // fetchNewArticles + builder.addCase(fetchNewArticles.pending, (state) => { + state.fetchNewArticles.status = 'loading'; + state.fetchNewArticles.error = undefined; + }); + builder.addCase( + fetchNewArticles.fulfilled, + (state, action: PayloadAction) => { + state.fetchNewArticles.status = 'successful'; + state.fetchNewArticles.articles = action.payload.articles; + state.fetchNewArticles.hasNextPage = action.payload.hasNextPage; + }, + ); + builder.addCase(fetchNewArticles.rejected, (state, action: any) => { + state.fetchNewArticles.status = 'failed'; + const errors = action.payload.errors as Record; + Object.values(errors).forEach((messages) => { + messages.forEach((msg) => { + toastError(msg); + }); + }); + }); + // fetchMyArticles builder.addCase(fetchMyArticles.pending, (state) => { state.fetchMyArticles.status = 'loading'; diff --git a/src/redux/slices/contests.ts b/src/redux/slices/contests.ts index 1c0f749..3bf6bcc 100644 --- a/src/redux/slices/contests.ts +++ b/src/redux/slices/contests.ts @@ -490,7 +490,7 @@ export const fetchContestMembers = createAsyncThunk( { contestId, page = 0, - pageSize = 25, + pageSize = 100, }: { contestId: number; page?: number; pageSize?: number }, { rejectWithValue }, ) => { diff --git a/src/redux/slices/missions.ts b/src/redux/slices/missions.ts index 0a4d7f3..d314f7b 100644 --- a/src/redux/slices/missions.ts +++ b/src/redux/slices/missions.ts @@ -28,6 +28,7 @@ export interface Mission { interface MissionsState { missions: Mission[]; + newMissions: Mission[]; currentMission: Mission | null; hasNextPage: boolean; create: { @@ -47,6 +48,7 @@ interface MissionsState { const initialState: MissionsState = { missions: [], + newMissions: [], currentMission: null, hasNextPage: false, create: {}, @@ -65,6 +67,33 @@ const initialState: MissionsState = { // GET /missions export const fetchMissions = createAsyncThunk( 'missions/fetchMissions', + async ( + { + page = 0, + pageSize = 100, + tags = [], + }: { page?: number; pageSize?: number; tags?: string[] }, + { rejectWithValue }, + ) => { + try { + const params: any = { page, pageSize }; + if (tags.length) params.tags = tags; + const response = await axios.get('/missions', { + params, + paramsSerializer: { + indexes: null, + }, + }); + return response.data; // { missions, hasNextPage } + } catch (err: any) { + return rejectWithValue(err.response?.data); + } + }, +); + +// GET /missions +export const fetchNewMissions = createAsyncThunk( + 'missions/fetchNewMissions', async ( { page = 0, @@ -211,6 +240,42 @@ const missionsSlice = createSlice({ }, ); + // ─── FETCH NEW MISSIONS ─── + builder.addCase(fetchNewMissions.pending, (state) => { + state.statuses.fetchList = 'loading'; + state.error = null; + }); + builder.addCase( + fetchNewMissions.fulfilled, + ( + state, + action: PayloadAction<{ + missions: Mission[]; + hasNextPage: boolean; + }>, + ) => { + state.statuses.fetchList = 'successful'; + state.newMissions = action.payload.missions; + state.hasNextPage = action.payload.hasNextPage; + }, + ); + builder.addCase( + fetchNewMissions.rejected, + (state, action: PayloadAction) => { + state.statuses.fetchList = 'failed'; + + const errors = action.payload.errors as Record< + string, + string[] + >; + Object.values(errors).forEach((messages) => { + messages.forEach((msg) => { + toastError(msg); + }); + }); + }, + ); + // ─── FETCH MISSION BY ID ─── builder.addCase(fetchMissionById.pending, (state) => { state.statuses.fetchById = 'loading'; diff --git a/src/redux/slices/profile.ts b/src/redux/slices/profile.ts index 466cded..399b900 100644 --- a/src/redux/slices/profile.ts +++ b/src/redux/slices/profile.ts @@ -197,9 +197,9 @@ export const fetchProfileMissions = createAsyncThunk( { username, recentPage = 0, - recentPageSize = 15, + recentPageSize = 100, authoredPage = 0, - authoredPageSize = 25, + authoredPageSize = 100, }: { username: string; recentPage?: number; @@ -237,7 +237,7 @@ export const fetchProfileArticles = createAsyncThunk( { username, page = 0, - pageSize = 25, + pageSize = 100, }: { username: string; page?: number; pageSize?: number }, { rejectWithValue }, ) => { @@ -262,11 +262,11 @@ export const fetchProfileContests = createAsyncThunk( { username, upcomingPage = 0, - upcomingPageSize = 10, + upcomingPageSize = 100, pastPage = 0, - pastPageSize = 10, + pastPageSize = 100, minePage = 0, - minePageSize = 10, + minePageSize = 100, }: { username: string; upcomingPage?: number; diff --git a/src/views/home/account/missions/MyMissionItem.tsx b/src/views/home/account/missions/MyMissionItem.tsx index c52b050..0cbf3dc 100644 --- a/src/views/home/account/missions/MyMissionItem.tsx +++ b/src/views/home/account/missions/MyMissionItem.tsx @@ -42,9 +42,12 @@ const MissionItem: React.FC = ({ setDeleteModalActive, }) => { const navigate = useNavigate(); - const difficultyItems = ['Easy', 'Medium', 'Hard']; - const difficultyString = - difficultyItems[Math.min(Math.max(0, difficulty - 1), 2)]; + const calcDifficulty = (d: number) => { + if (d <= 1200) return 'Easy'; + if (d <= 2000) return 'Medium'; + return 'Hard'; + }; + const difficultyString = calcDifficulty(difficulty); const deleteStatus = useAppSelector( (state) => state.missions.statuses.delete, ); diff --git a/src/views/home/missions/Missions.tsx b/src/views/home/missions/Missions.tsx index fb2f34d..1770cce 100644 --- a/src/views/home/missions/Missions.tsx +++ b/src/views/home/missions/Missions.tsx @@ -36,6 +36,12 @@ const Missions = () => { (state) => state.store.articles.articleTagFilter, ); + const calcDifficulty = (d: number) => { + if (d <= 1200) return 'Easy'; + if (d <= 2000) return 'Medium'; + return 'Hard'; + }; + useEffect(() => { dispatch(setMenuActivePage('missions')); dispatch(fetchMissions({ tags: tagsFilter })); @@ -83,7 +89,7 @@ const Missions = () => { id={v.id} authorId={v.authorId} name={v.name} - difficulty={'Easy'} + difficulty={calcDifficulty(v.difficulty)} tags={v.tags} timeLimit={1000} memoryLimit={256 * 1024 * 1024} diff --git a/src/views/home/rightpanel/Articles.tsx b/src/views/home/rightpanel/Articles.tsx index 5c2827e..fe70cb0 100644 --- a/src/views/home/rightpanel/Articles.tsx +++ b/src/views/home/rightpanel/Articles.tsx @@ -1,40 +1,63 @@ -import { FC, Fragment } from 'react'; +import { FC, Fragment, useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { fetchNewArticles } from '../../../redux/slices/articles'; export const ArticlesRightPanel: FC = () => { - const items = [ - { - name: 'Энтузиаст создал карточки с NFC-метками для знакомства ребёнка с музыкой', - }, - { - name: 'Алгоритм Древа Силы, Космический Сортировщик', - }, - { - name: 'Космический Сортировщик', - }, - { - name: 'Зеркала Многомерности', - }, - ]; + const dispatch = useAppDispatch(); + + const articles = useAppSelector( + (state) => state.articles.fetchNewArticles.articles, + ); + + useEffect(() => { + dispatch(fetchNewArticles({ pageSize: 10 })); + }, []); + + const getDaysAgo = (dateString: string) => { + const updatedDate = new Date(dateString); + const today = new Date(); + // Сбрасываем время для точного сравнения по дням + updatedDate.setHours(0, 0, 0, 0); + today.setHours(0, 0, 0, 0); + + const diffTime = today.getTime() - updatedDate.getTime(); + const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) return 'Сегодня'; + if (diffDays === 1) return '1 день назад'; + return `${diffDays} дня назад`; + }; return (
-
- Попоулярные статьи +
+ Новые статьи
- {items.map((v, i) => { - return ( - - { -
- {v.name} -
- } - {i + 1 != items.length && ( -
- )} -
- ); - })} + {articles && + articles + .filter((v) => { + const updatedDate = new Date(v.updatedAt); + const threeDaysAgo = new Date(); + threeDaysAgo.setDate(threeDaysAgo.getDate() - 3); + return updatedDate >= threeDaysAgo; + }) + .map((v, i) => { + return ( + + { +
+ {v.name} +
+ {getDaysAgo(v.updatedAt)} +
+
+ } + {i + 1 != articles.length && ( +
+ )} +
+ ); + })}
); }; diff --git a/src/views/home/rightpanel/Missions.tsx b/src/views/home/rightpanel/Missions.tsx index fc94021..ce048ad 100644 --- a/src/views/home/rightpanel/Missions.tsx +++ b/src/views/home/rightpanel/Missions.tsx @@ -1,5 +1,7 @@ -import { FC, Fragment } from 'react'; +import { FC, Fragment, useEffect } from 'react'; import { cn } from '../../../lib/cn'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { fetchNewMissions } from '../../../redux/slices/missions'; export const MissionsRightPanel: FC = () => { const items = [ @@ -24,30 +26,61 @@ export const MissionsRightPanel: FC = () => { tags: ['matrix', 'geometry', 'simulation'], }, ]; + + const dispatch = useAppDispatch(); + + const missions = useAppSelector((state) => state.missions.newMissions); + + useEffect(() => { + dispatch(fetchNewMissions({ pageSize: 10 })); + }, []); + + const getDaysAgo = (dateString: string) => { + const updatedDate = new Date(dateString); + const today = new Date(); + // Сбрасываем время для точного сравнения по дням + updatedDate.setHours(0, 0, 0, 0); + today.setHours(0, 0, 0, 0); + + const diffTime = today.getTime() - updatedDate.getTime(); + const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) return 'Сегодня'; + if (diffDays === 1) return '1 день назад'; + return `${diffDays} дня назад`; + }; + + const calcDifficulty = (d: number) => { + if (d <= 1200) return 'Easy'; + if (d <= 2000) return 'Medium'; + return 'Hard'; + }; return (
Новые задачи
- {items.map((v, i) => { + {missions.map((v, i) => { return ( {
-
{v.name}
+
+ {v.name} +
- {v.difficulty} + {calcDifficulty(v.difficulty)}
{v.tags.slice(0, 2).map((v, i) => ( @@ -55,6 +88,9 @@ export const MissionsRightPanel: FC = () => { ))} {v.tags.length > 2 && '...'}
+
+ {getDaysAgo(v.updatedAt)} +
} {i + 1 != items.length && (