import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import axios from '../../axios'; // ===================== // Типы // ===================== type Status = 'idle' | 'loading' | 'successful' | 'failed'; // Основной профиль export interface ProfileIdentity { userId: number; username: string; email: string; createdAt: string; } export interface ProfileSolutions { totalSolved: number; solvedLast7Days: number; } export interface ProfileContestsInfo { totalParticipations: number; participationsLast7Days: number; } export interface ProfileCreationStats { missions: { total: number; last7Days: number }; contests: { total: number; last7Days: number }; articles: { total: number; last7Days: number }; } export interface ProfileResponse { identity: ProfileIdentity; solutions: ProfileSolutions; contests: ProfileContestsInfo; creation: ProfileCreationStats; } // Missions export interface MissionsBucket { key: string; label: string; solved: number; total: number; } export interface MissionItem { missionId: number; missionName: string; difficultyLabel: string; difficultyValue: number; createdAt: string; timeLimitMilliseconds: number; memoryLimitBytes: number; } export interface MissionsResponse { summary: { total: MissionsBucket; buckets: MissionsBucket[]; }; recent: { items: MissionItem[]; page: number; pageSize: number; hasNextPage: boolean; }; authored: { items: MissionItem[]; page: number; pageSize: number; hasNextPage: boolean; }; } // Articles export interface ProfileArticleItem { articleId: number; title: string; createdAt: string; updatedAt: string; } export interface ProfileArticlesResponse { articles: { items: ProfileArticleItem[]; page: number; pageSize: number; hasNextPage: boolean; }; } // Contests export interface ContestItem { contestId: number; name: string; scheduleType: 'AlwaysOpen' | 'FixedWindow' | 'RollingWindow'; visibility: string; startsAt: string; endsAt: string; attemptDurationMinutes: number; role: 'None' | 'Participant' | 'Organizer'; } export interface ContestsList { items: ContestItem[]; page: number; pageSize: number; hasNextPage: boolean; } export interface ProfileContestsResponse { upcoming: ContestsList; past: ContestsList; mine: ContestsList; } // ===================== // Состояние // ===================== interface ProfileState { profile: { data?: ProfileResponse; status: Status; error?: string; }; missions: { data?: MissionsResponse; status: Status; error?: string; }; articles: { data?: ProfileArticlesResponse; status: Status; error?: string; }; contests: { data?: ProfileContestsResponse; status: Status; error?: string; }; } const initialState: ProfileState = { profile: { data: undefined, status: 'idle', error: undefined, }, missions: { data: undefined, status: 'idle', error: undefined, }, articles: { data: undefined, status: 'idle', error: undefined, }, contests: { data: undefined, status: 'idle', error: undefined, }, }; // ===================== // Async Thunks // ===================== // Основной профиль export const fetchProfile = createAsyncThunk( 'profile/fetch', async (username: string, { rejectWithValue }) => { try { const res = await axios.get( `/profile/${username}`, ); return res.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Ошибка загрузки профиля', ); } }, ); // Missions export const fetchProfileMissions = createAsyncThunk( 'profile/fetchMissions', async ( { username, recentPage = 0, recentPageSize = 100, authoredPage = 0, authoredPageSize = 100, }: { username: string; recentPage?: number; recentPageSize?: number; authoredPage?: number; authoredPageSize?: number; }, { rejectWithValue }, ) => { try { const res = await axios.get( `/profile/${username}/missions`, { params: { recentPage, recentPageSize, authoredPage, authoredPageSize, }, }, ); return res.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Ошибка загрузки задач', ); } }, ); // Articles export const fetchProfileArticles = createAsyncThunk( 'profile/fetchArticles', async ( { username, page = 0, pageSize = 100, }: { username: string; page?: number; pageSize?: number }, { rejectWithValue }, ) => { try { const res = await axios.get( `/profile/${username}/articles`, { params: { page, pageSize } }, ); return res.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Ошибка загрузки статей', ); } }, ); // Contests export const fetchProfileContests = createAsyncThunk( 'profile/fetchContests', async ( { username, upcomingPage = 0, upcomingPageSize = 100, pastPage = 0, pastPageSize = 100, minePage = 0, minePageSize = 100, }: { username: string; upcomingPage?: number; upcomingPageSize?: number; pastPage?: number; pastPageSize?: number; minePage?: number; minePageSize?: number; }, { rejectWithValue }, ) => { try { const res = await axios.get( `/profile/${username}/contests`, { params: { upcomingPage, upcomingPageSize, pastPage, pastPageSize, minePage, minePageSize, }, }, ); return res.data; } catch (err: any) { return rejectWithValue( err.response?.data?.message || 'Ошибка загрузки контестов', ); } }, ); // ===================== // Slice // ===================== const profileSlice = createSlice({ name: 'profile', initialState, reducers: { setProfileStatus: ( state, action: PayloadAction<{ key: keyof ProfileState; status: Status; }>, ) => { state[action.payload.key].status = action.payload.status; }, }, extraReducers: (builder) => { // PROFILE builder.addCase(fetchProfile.pending, (state) => { state.profile.status = 'loading'; state.profile.error = undefined; }); builder.addCase( fetchProfile.fulfilled, (state, action: PayloadAction) => { state.profile.status = 'successful'; state.profile.data = action.payload; }, ); builder.addCase(fetchProfile.rejected, (state, action: any) => { state.profile.status = 'failed'; state.profile.error = action.payload; }); // MISSIONS builder.addCase(fetchProfileMissions.pending, (state) => { state.missions.status = 'loading'; state.missions.error = undefined; }); builder.addCase( fetchProfileMissions.fulfilled, (state, action: PayloadAction) => { state.missions.status = 'successful'; state.missions.data = action.payload; }, ); builder.addCase(fetchProfileMissions.rejected, (state, action: any) => { state.missions.status = 'failed'; state.missions.error = action.payload; }); // ARTICLES builder.addCase(fetchProfileArticles.pending, (state) => { state.articles.status = 'loading'; state.articles.error = undefined; }); builder.addCase( fetchProfileArticles.fulfilled, (state, action: PayloadAction) => { state.articles.status = 'successful'; state.articles.data = action.payload; }, ); builder.addCase(fetchProfileArticles.rejected, (state, action: any) => { state.articles.status = 'failed'; state.articles.error = action.payload; }); // CONTESTS builder.addCase(fetchProfileContests.pending, (state) => { state.contests.status = 'loading'; state.contests.error = undefined; }); builder.addCase( fetchProfileContests.fulfilled, (state, action: PayloadAction) => { state.contests.status = 'successful'; state.contests.data = action.payload; }, ); builder.addCase(fetchProfileContests.rejected, (state, action: any) => { state.contests.status = 'failed'; state.contests.error = action.payload; }); }, }); export const { setProfileStatus } = profileSlice.actions; export const profileReducer = profileSlice.reducer;