add contests
This commit is contained in:
@@ -273,7 +273,7 @@ export const fetchParticipatingContests = createAsyncThunk(
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const { page = 0, pageSize = 10 } = params;
|
||||
const { page = 0, pageSize = 100 } = params;
|
||||
const response = await axios.get<ContestsResponse>(
|
||||
'/contests/participating',
|
||||
{ params: { page, pageSize } },
|
||||
@@ -315,7 +315,7 @@ export const fetchContests = createAsyncThunk(
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const { page = 0, pageSize = 10, groupId } = params;
|
||||
const { page = 0, pageSize = 100, groupId } = params;
|
||||
const response = await axios.get<ContestsResponse>('/contests', {
|
||||
params: { page, pageSize, groupId },
|
||||
});
|
||||
@@ -417,7 +417,7 @@ export const fetchRegisteredContests = createAsyncThunk(
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const { page = 0, pageSize = 10 } = params;
|
||||
const { page = 0, pageSize = 100 } = params;
|
||||
const response = await axios.get<ContestsResponse>(
|
||||
'/contests/registered',
|
||||
{ params: { page, pageSize } },
|
||||
|
||||
@@ -94,7 +94,7 @@ export const fetchGroupPosts = createAsyncThunk(
|
||||
{
|
||||
groupId,
|
||||
page = 0,
|
||||
pageSize = 20,
|
||||
pageSize = 100,
|
||||
}: { groupId: number; page?: number; pageSize?: number },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
|
||||
395
src/redux/slices/profile.ts
Normal file
395
src/redux/slices/profile.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
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<ProfileResponse>(
|
||||
`/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 = 15,
|
||||
authoredPage = 0,
|
||||
authoredPageSize = 25,
|
||||
}: {
|
||||
username: string;
|
||||
recentPage?: number;
|
||||
recentPageSize?: number;
|
||||
authoredPage?: number;
|
||||
authoredPageSize?: number;
|
||||
},
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const res = await axios.get<MissionsResponse>(
|
||||
`/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 = 25,
|
||||
}: { username: string; page?: number; pageSize?: number },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const res = await axios.get<ProfileArticlesResponse>(
|
||||
`/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 = 10,
|
||||
pastPage = 0,
|
||||
pastPageSize = 10,
|
||||
minePage = 0,
|
||||
minePageSize = 10,
|
||||
}: {
|
||||
username: string;
|
||||
upcomingPage?: number;
|
||||
upcomingPageSize?: number;
|
||||
pastPage?: number;
|
||||
pastPageSize?: number;
|
||||
minePage?: number;
|
||||
minePageSize?: number;
|
||||
},
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const res = await axios.get<ProfileContestsResponse>(
|
||||
`/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<ProfileResponse>) => {
|
||||
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<MissionsResponse>) => {
|
||||
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<ProfileArticlesResponse>) => {
|
||||
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<ProfileContestsResponse>) => {
|
||||
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;
|
||||
@@ -7,6 +7,9 @@ interface StorState {
|
||||
activeProfilePage: string;
|
||||
activeGroupPage: string;
|
||||
};
|
||||
group: {
|
||||
groupFilter: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Инициализация состояния
|
||||
@@ -16,6 +19,9 @@ const initialState: StorState = {
|
||||
activeProfilePage: '',
|
||||
activeGroupPage: '',
|
||||
},
|
||||
group: {
|
||||
groupFilter: '',
|
||||
},
|
||||
};
|
||||
|
||||
// Slice
|
||||
@@ -38,6 +44,9 @@ const storeSlice = createSlice({
|
||||
) => {
|
||||
state.menu.activeGroupPage = activeGroupPage.payload;
|
||||
},
|
||||
setGroupFilter: (state, groupFilter: PayloadAction<string>) => {
|
||||
state.group.groupFilter = groupFilter.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -45,6 +54,7 @@ export const {
|
||||
setMenuActivePage,
|
||||
setMenuActiveProfilePage,
|
||||
setMenuActiveGroupPage,
|
||||
setGroupFilter,
|
||||
} = storeSlice.actions;
|
||||
|
||||
export const storeReducer = storeSlice.reducer;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { groupsReducer } from './slices/groups';
|
||||
import { articlesReducer } from './slices/articles';
|
||||
import { groupFeedReducer } from './slices/groupfeed';
|
||||
import { groupChatReducer } from './slices/groupChat';
|
||||
import { profileReducer } from './slices/profile';
|
||||
|
||||
// использование
|
||||
// import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
||||
@@ -29,6 +30,7 @@ export const store = configureStore({
|
||||
articles: articlesReducer,
|
||||
groupfeed: groupFeedReducer,
|
||||
groupchat: groupChatReducer,
|
||||
profile: profileReducer,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user