add contest thunck and remove google button
This commit is contained in:
@@ -5,4 +5,5 @@ import Edit from './edit.svg';
|
|||||||
import UserAdd from './user-profile-add.svg';
|
import UserAdd from './user-profile-add.svg';
|
||||||
import ChevroneDown from './chevron-down.svg';
|
import ChevroneDown from './chevron-down.svg';
|
||||||
|
|
||||||
|
|
||||||
export { Book, Edit, EyeClosed, EyeOpen, UserAdd, ChevroneDown };
|
export { Book, Edit, EyeClosed, EyeOpen, UserAdd, ChevroneDown };
|
||||||
|
|||||||
@@ -5,10 +5,6 @@ import axios from '../../axios';
|
|||||||
// Типы
|
// Типы
|
||||||
// =====================
|
// =====================
|
||||||
|
|
||||||
// =====================
|
|
||||||
// Типы для посылок
|
|
||||||
// =====================
|
|
||||||
|
|
||||||
export interface Solution {
|
export interface Solution {
|
||||||
id: number;
|
id: number;
|
||||||
missionId: number;
|
missionId: number;
|
||||||
@@ -73,11 +69,25 @@ export interface Contest {
|
|||||||
members?: Member[];
|
members?: Member[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Attempt {
|
||||||
|
attemptId: number;
|
||||||
|
contestId: number;
|
||||||
|
startedAt: string;
|
||||||
|
expiresAt: string;
|
||||||
|
finished: boolean;
|
||||||
|
results?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
interface ContestsResponse {
|
interface ContestsResponse {
|
||||||
hasNextPage: boolean;
|
hasNextPage: boolean;
|
||||||
contests: Contest[];
|
contests: Contest[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MembersPage {
|
||||||
|
members: Member[];
|
||||||
|
hasNextPage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateContestBody {
|
export interface CreateContestBody {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -142,106 +152,106 @@ interface ContestsState {
|
|||||||
status: Status;
|
status: Status;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NEW:
|
||||||
|
fetchContestMembers: {
|
||||||
|
members: Member[];
|
||||||
|
hasNextPage: boolean;
|
||||||
|
status: Status;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
addOrUpdateMember: {
|
||||||
|
status: Status;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
deleteContestMember: {
|
||||||
|
status: Status;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
startAttempt: {
|
||||||
|
attempt?: Attempt;
|
||||||
|
status: Status;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
fetchMyAttemptsInContest: {
|
||||||
|
attempts: Attempt[];
|
||||||
|
status: Status;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
fetchMyAllAttempts: {
|
||||||
|
attempts: Attempt[];
|
||||||
|
status: Status;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
fetchMyActiveAttempt: {
|
||||||
|
attempt?: Attempt | null;
|
||||||
|
status: Status;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
checkRegistration: {
|
||||||
|
registered: boolean;
|
||||||
|
status: Status;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUpcomingEligible: {
|
||||||
|
contests: Contest[];
|
||||||
|
status: Status;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ContestsState = {
|
const emptyContest: Contest = {
|
||||||
fetchContests: {
|
id: 0,
|
||||||
contests: [],
|
name: '',
|
||||||
hasNextPage: false,
|
description: '',
|
||||||
status: 'idle',
|
scheduleType: 'AlwaysOpen',
|
||||||
error: undefined,
|
visibility: 'Public',
|
||||||
},
|
startsAt: '',
|
||||||
fetchContestById: {
|
endsAt: '',
|
||||||
contest: {
|
attemptDurationMinutes: 0,
|
||||||
id: 0,
|
maxAttempts: 0,
|
||||||
name: '',
|
allowEarlyFinish: false,
|
||||||
description: '',
|
missions: [],
|
||||||
scheduleType: 'AlwaysOpen',
|
articles: [],
|
||||||
visibility: 'Public',
|
members: [],
|
||||||
startsAt: '',
|
};
|
||||||
endsAt: '',
|
|
||||||
attemptDurationMinutes: 0,
|
|
||||||
maxAttempts: 0,
|
|
||||||
allowEarlyFinish: false,
|
|
||||||
groupId: undefined,
|
|
||||||
groupName: undefined,
|
|
||||||
missions: [],
|
|
||||||
articles: [],
|
|
||||||
members: [],
|
|
||||||
},
|
|
||||||
status: 'idle',
|
|
||||||
error: undefined,
|
|
||||||
},
|
|
||||||
fetchMySubmissions: {
|
|
||||||
submissions: [],
|
|
||||||
status: 'idle',
|
|
||||||
error: undefined,
|
|
||||||
},
|
|
||||||
|
|
||||||
createContest: {
|
const initialState: ContestsState = {
|
||||||
contest: {
|
fetchContests: { contests: [], hasNextPage: false, status: 'idle' },
|
||||||
id: 0,
|
fetchContestById: { contest: emptyContest, status: 'idle' },
|
||||||
name: '',
|
createContest: { contest: emptyContest, status: 'idle' },
|
||||||
description: '',
|
fetchMySubmissions: { submissions: [], status: 'idle' },
|
||||||
scheduleType: 'AlwaysOpen',
|
updateContest: { contest: emptyContest, status: 'idle' },
|
||||||
visibility: 'Public',
|
deleteContest: { status: 'idle' },
|
||||||
startsAt: '',
|
fetchMyContests: { contests: [], status: 'idle' },
|
||||||
endsAt: '',
|
|
||||||
attemptDurationMinutes: 0,
|
|
||||||
maxAttempts: 0,
|
|
||||||
allowEarlyFinish: false,
|
|
||||||
groupId: undefined,
|
|
||||||
groupName: undefined,
|
|
||||||
missions: [],
|
|
||||||
articles: [],
|
|
||||||
members: [],
|
|
||||||
},
|
|
||||||
status: 'idle',
|
|
||||||
error: undefined,
|
|
||||||
},
|
|
||||||
updateContest: {
|
|
||||||
contest: {
|
|
||||||
id: 0,
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
scheduleType: 'AlwaysOpen',
|
|
||||||
visibility: 'Public',
|
|
||||||
startsAt: '',
|
|
||||||
endsAt: '',
|
|
||||||
attemptDurationMinutes: 0,
|
|
||||||
maxAttempts: 0,
|
|
||||||
allowEarlyFinish: false,
|
|
||||||
groupId: undefined,
|
|
||||||
groupName: undefined,
|
|
||||||
missions: [],
|
|
||||||
articles: [],
|
|
||||||
members: [],
|
|
||||||
},
|
|
||||||
status: 'idle',
|
|
||||||
error: undefined,
|
|
||||||
},
|
|
||||||
deleteContest: {
|
|
||||||
status: 'idle',
|
|
||||||
error: undefined,
|
|
||||||
},
|
|
||||||
fetchMyContests: {
|
|
||||||
contests: [],
|
|
||||||
status: 'idle',
|
|
||||||
error: undefined,
|
|
||||||
},
|
|
||||||
fetchRegisteredContests: {
|
fetchRegisteredContests: {
|
||||||
contests: [],
|
contests: [],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
error: undefined,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fetchContestMembers: { members: [], hasNextPage: false, status: 'idle' },
|
||||||
|
addOrUpdateMember: { status: 'idle' },
|
||||||
|
deleteContestMember: { status: 'idle' },
|
||||||
|
|
||||||
|
startAttempt: { status: 'idle' },
|
||||||
|
fetchMyAttemptsInContest: { attempts: [], status: 'idle' },
|
||||||
|
fetchMyAllAttempts: { attempts: [], status: 'idle' },
|
||||||
|
fetchMyActiveAttempt: { attempt: null, status: 'idle' },
|
||||||
|
|
||||||
|
checkRegistration: { registered: false, status: 'idle' },
|
||||||
|
fetchUpcomingEligible: { contests: [], status: 'idle' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// Async Thunks
|
// Async Thunks
|
||||||
// =====================
|
// =====================
|
||||||
|
|
||||||
// Мои посылки в контесте
|
// Existing ----------------------------
|
||||||
|
|
||||||
export const fetchMySubmissions = createAsyncThunk(
|
export const fetchMySubmissions = createAsyncThunk(
|
||||||
'contests/fetchMySubmissions',
|
'contests/fetchMySubmissions',
|
||||||
async (contestId: number, { rejectWithValue }) => {
|
async (contestId: number, { rejectWithValue }) => {
|
||||||
@@ -258,7 +268,6 @@ export const fetchMySubmissions = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Все контесты
|
|
||||||
export const fetchContests = createAsyncThunk(
|
export const fetchContests = createAsyncThunk(
|
||||||
'contests/fetchAll',
|
'contests/fetchAll',
|
||||||
async (
|
async (
|
||||||
@@ -283,7 +292,6 @@ export const fetchContests = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Контест по ID
|
|
||||||
export const fetchContestById = createAsyncThunk(
|
export const fetchContestById = createAsyncThunk(
|
||||||
'contests/fetchById',
|
'contests/fetchById',
|
||||||
async (id: number, { rejectWithValue }) => {
|
async (id: number, { rejectWithValue }) => {
|
||||||
@@ -298,7 +306,6 @@ export const fetchContestById = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Создание контеста
|
|
||||||
export const createContest = createAsyncThunk(
|
export const createContest = createAsyncThunk(
|
||||||
'contests/create',
|
'contests/create',
|
||||||
async (contestData: CreateContestBody, { rejectWithValue }) => {
|
async (contestData: CreateContestBody, { rejectWithValue }) => {
|
||||||
@@ -316,7 +323,6 @@ export const createContest = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🆕 Обновление контеста
|
|
||||||
export const updateContest = createAsyncThunk(
|
export const updateContest = createAsyncThunk(
|
||||||
'contests/update',
|
'contests/update',
|
||||||
async (
|
async (
|
||||||
@@ -340,7 +346,6 @@ export const updateContest = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🆕 Удаление контеста
|
|
||||||
export const deleteContest = createAsyncThunk(
|
export const deleteContest = createAsyncThunk(
|
||||||
'contests/delete',
|
'contests/delete',
|
||||||
async (contestId: number, { rejectWithValue }) => {
|
async (contestId: number, { rejectWithValue }) => {
|
||||||
@@ -355,7 +360,6 @@ export const deleteContest = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Контесты, созданные мной
|
|
||||||
export const fetchMyContests = createAsyncThunk(
|
export const fetchMyContests = createAsyncThunk(
|
||||||
'contests/fetchMyContests',
|
'contests/fetchMyContests',
|
||||||
async (_, { rejectWithValue }) => {
|
async (_, { rejectWithValue }) => {
|
||||||
@@ -370,7 +374,6 @@ export const fetchMyContests = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Контесты, где я зарегистрирован
|
|
||||||
export const fetchRegisteredContests = createAsyncThunk(
|
export const fetchRegisteredContests = createAsyncThunk(
|
||||||
'contests/fetchRegisteredContests',
|
'contests/fetchRegisteredContests',
|
||||||
async (
|
async (
|
||||||
@@ -393,6 +396,182 @@ export const fetchRegisteredContests = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// NEW -----------------------------------
|
||||||
|
|
||||||
|
// Add or update member
|
||||||
|
export const addOrUpdateContestMember = createAsyncThunk(
|
||||||
|
'contests/addOrUpdateMember',
|
||||||
|
async (
|
||||||
|
{
|
||||||
|
contestId,
|
||||||
|
member,
|
||||||
|
}: { contestId: number; member: { userId: number; role: string } },
|
||||||
|
{ rejectWithValue },
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<Member[]>(
|
||||||
|
`/contests/${contestId}/members`,
|
||||||
|
member,
|
||||||
|
);
|
||||||
|
return { contestId, members: response.data };
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message ||
|
||||||
|
'Failed to add or update contest member',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete member
|
||||||
|
export const deleteContestMember = createAsyncThunk(
|
||||||
|
'contests/deleteContestMember',
|
||||||
|
async (
|
||||||
|
{ contestId, memberId }: { contestId: number; memberId: number },
|
||||||
|
{ rejectWithValue },
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await axios.delete(`/contests/${contestId}/members/${memberId}`);
|
||||||
|
return { contestId, memberId };
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message ||
|
||||||
|
'Failed to delete contest member',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start attempt
|
||||||
|
export const startContestAttempt = createAsyncThunk(
|
||||||
|
'contests/startContestAttempt',
|
||||||
|
async (contestId: number, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<Attempt>(
|
||||||
|
`/contests/${contestId}/attempts`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message ||
|
||||||
|
'Failed to start contest attempt',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// My attempts in contest
|
||||||
|
export const fetchMyAttemptsInContest = createAsyncThunk(
|
||||||
|
'contests/fetchMyAttemptsInContest',
|
||||||
|
async (contestId: number, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<Attempt[]>(
|
||||||
|
`/contests/${contestId}/attempts/my`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message || 'Failed to fetch my attempts',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Members with pagination
|
||||||
|
export const fetchContestMembers = createAsyncThunk(
|
||||||
|
'contests/fetchContestMembers',
|
||||||
|
async (
|
||||||
|
{
|
||||||
|
contestId,
|
||||||
|
page = 0,
|
||||||
|
pageSize = 25,
|
||||||
|
}: { contestId: number; page?: number; pageSize?: number },
|
||||||
|
{ rejectWithValue },
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<MembersPage>(
|
||||||
|
`/contests/${contestId}/members`,
|
||||||
|
{ params: { page, pageSize } },
|
||||||
|
);
|
||||||
|
return { contestId, ...response.data };
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message ||
|
||||||
|
'Failed to fetch contest members',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check registration
|
||||||
|
export const checkContestRegistration = createAsyncThunk(
|
||||||
|
'contests/checkRegistration',
|
||||||
|
async (contestId: number, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<{ registered: boolean }>(
|
||||||
|
`/contests/${contestId}/registered`,
|
||||||
|
);
|
||||||
|
return { contestId, registered: response.data.registered };
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message || 'Failed to check registration',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Upcoming eligible contests
|
||||||
|
export const fetchUpcomingEligibleContests = createAsyncThunk(
|
||||||
|
'contests/fetchUpcomingEligible',
|
||||||
|
async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<Contest[]>(
|
||||||
|
'/contests/upcoming/eligible',
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message ||
|
||||||
|
'Failed to fetch upcoming eligible contests',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// All my attempts
|
||||||
|
export const fetchMyAllAttempts = createAsyncThunk(
|
||||||
|
'contests/fetchMyAllAttempts',
|
||||||
|
async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<Attempt[]>(
|
||||||
|
'/contests/attempts/my',
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message || 'Failed to fetch my attempts',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Active attempt
|
||||||
|
export const fetchMyActiveAttempt = createAsyncThunk(
|
||||||
|
'contests/fetchMyActiveAttempt',
|
||||||
|
async (contestId: number, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<Attempt | null>(
|
||||||
|
`/contests/${contestId}/attempts/my/active`,
|
||||||
|
);
|
||||||
|
return { contestId, attempt: response.data };
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message || 'Failed to fetch active attempt',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// Slice
|
// Slice
|
||||||
// =====================
|
// =====================
|
||||||
@@ -401,7 +580,6 @@ const contestsSlice = createSlice({
|
|||||||
name: 'contests',
|
name: 'contests',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
// 🆕 Сброс статусов
|
|
||||||
setContestStatus: (
|
setContestStatus: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ key: keyof ContestsState; status: Status }>,
|
action: PayloadAction<{ key: keyof ContestsState; status: Status }>,
|
||||||
@@ -413,7 +591,8 @@ const contestsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
// 🆕 fetchMySubmissions
|
// ——— YOUR EXISTING HANDLERS (unchanged) ———
|
||||||
|
|
||||||
builder.addCase(fetchMySubmissions.pending, (state) => {
|
builder.addCase(fetchMySubmissions.pending, (state) => {
|
||||||
state.fetchMySubmissions.status = 'loading';
|
state.fetchMySubmissions.status = 'loading';
|
||||||
state.fetchMySubmissions.error = undefined;
|
state.fetchMySubmissions.error = undefined;
|
||||||
@@ -430,10 +609,8 @@ const contestsSlice = createSlice({
|
|||||||
state.fetchMySubmissions.error = action.payload;
|
state.fetchMySubmissions.error = action.payload;
|
||||||
});
|
});
|
||||||
|
|
||||||
// fetchContests
|
|
||||||
builder.addCase(fetchContests.pending, (state) => {
|
builder.addCase(fetchContests.pending, (state) => {
|
||||||
state.fetchContests.status = 'loading';
|
state.fetchContests.status = 'loading';
|
||||||
state.fetchContests.error = undefined;
|
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
fetchContests.fulfilled,
|
fetchContests.fulfilled,
|
||||||
@@ -448,10 +625,8 @@ const contestsSlice = createSlice({
|
|||||||
state.fetchContests.error = action.payload;
|
state.fetchContests.error = action.payload;
|
||||||
});
|
});
|
||||||
|
|
||||||
// fetchContestById
|
|
||||||
builder.addCase(fetchContestById.pending, (state) => {
|
builder.addCase(fetchContestById.pending, (state) => {
|
||||||
state.fetchContestById.status = 'loading';
|
state.fetchContestById.status = 'loading';
|
||||||
state.fetchContestById.error = undefined;
|
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
fetchContestById.fulfilled,
|
fetchContestById.fulfilled,
|
||||||
@@ -465,10 +640,8 @@ const contestsSlice = createSlice({
|
|||||||
state.fetchContestById.error = action.payload;
|
state.fetchContestById.error = action.payload;
|
||||||
});
|
});
|
||||||
|
|
||||||
// createContest
|
|
||||||
builder.addCase(createContest.pending, (state) => {
|
builder.addCase(createContest.pending, (state) => {
|
||||||
state.createContest.status = 'loading';
|
state.createContest.status = 'loading';
|
||||||
state.createContest.error = undefined;
|
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
createContest.fulfilled,
|
createContest.fulfilled,
|
||||||
@@ -482,10 +655,8 @@ const contestsSlice = createSlice({
|
|||||||
state.createContest.error = action.payload;
|
state.createContest.error = action.payload;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🆕 updateContest
|
|
||||||
builder.addCase(updateContest.pending, (state) => {
|
builder.addCase(updateContest.pending, (state) => {
|
||||||
state.updateContest.status = 'loading';
|
state.updateContest.status = 'loading';
|
||||||
state.updateContest.error = undefined;
|
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
updateContest.fulfilled,
|
updateContest.fulfilled,
|
||||||
@@ -499,16 +670,13 @@ const contestsSlice = createSlice({
|
|||||||
state.updateContest.error = action.payload;
|
state.updateContest.error = action.payload;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🆕 deleteContest
|
|
||||||
builder.addCase(deleteContest.pending, (state) => {
|
builder.addCase(deleteContest.pending, (state) => {
|
||||||
state.deleteContest.status = 'loading';
|
state.deleteContest.status = 'loading';
|
||||||
state.deleteContest.error = undefined;
|
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
deleteContest.fulfilled,
|
deleteContest.fulfilled,
|
||||||
(state, action: PayloadAction<number>) => {
|
(state, action: PayloadAction<number>) => {
|
||||||
state.deleteContest.status = 'successful';
|
state.deleteContest.status = 'successful';
|
||||||
// Удалим контест из списков
|
|
||||||
state.fetchContests.contests =
|
state.fetchContests.contests =
|
||||||
state.fetchContests.contests.filter(
|
state.fetchContests.contests.filter(
|
||||||
(c) => c.id !== action.payload,
|
(c) => c.id !== action.payload,
|
||||||
@@ -524,10 +692,8 @@ const contestsSlice = createSlice({
|
|||||||
state.deleteContest.error = action.payload;
|
state.deleteContest.error = action.payload;
|
||||||
});
|
});
|
||||||
|
|
||||||
// fetchMyContests
|
|
||||||
builder.addCase(fetchMyContests.pending, (state) => {
|
builder.addCase(fetchMyContests.pending, (state) => {
|
||||||
state.fetchMyContests.status = 'loading';
|
state.fetchMyContests.status = 'loading';
|
||||||
state.fetchMyContests.error = undefined;
|
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
fetchMyContests.fulfilled,
|
fetchMyContests.fulfilled,
|
||||||
@@ -541,10 +707,8 @@ const contestsSlice = createSlice({
|
|||||||
state.fetchMyContests.error = action.payload;
|
state.fetchMyContests.error = action.payload;
|
||||||
});
|
});
|
||||||
|
|
||||||
// fetchRegisteredContests
|
|
||||||
builder.addCase(fetchRegisteredContests.pending, (state) => {
|
builder.addCase(fetchRegisteredContests.pending, (state) => {
|
||||||
state.fetchRegisteredContests.status = 'loading';
|
state.fetchRegisteredContests.status = 'loading';
|
||||||
state.fetchRegisteredContests.error = undefined;
|
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
fetchRegisteredContests.fulfilled,
|
fetchRegisteredContests.fulfilled,
|
||||||
@@ -563,6 +727,168 @@ const contestsSlice = createSlice({
|
|||||||
state.fetchRegisteredContests.error = action.payload;
|
state.fetchRegisteredContests.error = action.payload;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// NEW HANDLERS
|
||||||
|
|
||||||
|
builder.addCase(fetchContestMembers.pending, (state) => {
|
||||||
|
state.fetchContestMembers.status = 'loading';
|
||||||
|
});
|
||||||
|
builder.addCase(
|
||||||
|
fetchContestMembers.fulfilled,
|
||||||
|
(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
contestId: number;
|
||||||
|
members: Member[];
|
||||||
|
hasNextPage: boolean;
|
||||||
|
}>,
|
||||||
|
) => {
|
||||||
|
state.fetchContestMembers.status = 'successful';
|
||||||
|
state.fetchContestMembers.members = action.payload.members;
|
||||||
|
state.fetchContestMembers.hasNextPage =
|
||||||
|
action.payload.hasNextPage;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(fetchContestMembers.rejected, (state, action: any) => {
|
||||||
|
state.fetchContestMembers.status = 'failed';
|
||||||
|
state.fetchContestMembers.error = action.payload;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(addOrUpdateContestMember.pending, (state) => {
|
||||||
|
state.addOrUpdateMember.status = 'loading';
|
||||||
|
});
|
||||||
|
builder.addCase(addOrUpdateContestMember.fulfilled, (state) => {
|
||||||
|
state.addOrUpdateMember.status = 'successful';
|
||||||
|
});
|
||||||
|
builder.addCase(
|
||||||
|
addOrUpdateContestMember.rejected,
|
||||||
|
(state, action: any) => {
|
||||||
|
state.addOrUpdateMember.status = 'failed';
|
||||||
|
state.addOrUpdateMember.error = action.payload;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.addCase(deleteContestMember.pending, (state) => {
|
||||||
|
state.deleteContestMember.status = 'loading';
|
||||||
|
});
|
||||||
|
builder.addCase(deleteContestMember.fulfilled, (state) => {
|
||||||
|
state.deleteContestMember.status = 'successful';
|
||||||
|
});
|
||||||
|
builder.addCase(deleteContestMember.rejected, (state, action: any) => {
|
||||||
|
state.deleteContestMember.status = 'failed';
|
||||||
|
state.deleteContestMember.error = action.payload;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(startContestAttempt.pending, (state) => {
|
||||||
|
state.startAttempt.status = 'loading';
|
||||||
|
});
|
||||||
|
builder.addCase(
|
||||||
|
startContestAttempt.fulfilled,
|
||||||
|
(state, action: PayloadAction<Attempt>) => {
|
||||||
|
state.startAttempt.status = 'successful';
|
||||||
|
state.startAttempt.attempt = action.payload;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(startContestAttempt.rejected, (state, action: any) => {
|
||||||
|
state.startAttempt.status = 'failed';
|
||||||
|
state.startAttempt.error = action.payload;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(fetchMyAttemptsInContest.pending, (state) => {
|
||||||
|
state.fetchMyAttemptsInContest.status = 'loading';
|
||||||
|
});
|
||||||
|
builder.addCase(
|
||||||
|
fetchMyAttemptsInContest.fulfilled,
|
||||||
|
(state, action: PayloadAction<Attempt[]>) => {
|
||||||
|
state.fetchMyAttemptsInContest.status = 'successful';
|
||||||
|
state.fetchMyAttemptsInContest.attempts = action.payload;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(
|
||||||
|
fetchMyAttemptsInContest.rejected,
|
||||||
|
(state, action: any) => {
|
||||||
|
state.fetchMyAttemptsInContest.status = 'failed';
|
||||||
|
state.fetchMyAttemptsInContest.error = action.payload;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.addCase(fetchMyAllAttempts.pending, (state) => {
|
||||||
|
state.fetchMyAllAttempts.status = 'loading';
|
||||||
|
});
|
||||||
|
builder.addCase(
|
||||||
|
fetchMyAllAttempts.fulfilled,
|
||||||
|
(state, action: PayloadAction<Attempt[]>) => {
|
||||||
|
state.fetchMyAllAttempts.status = 'successful';
|
||||||
|
state.fetchMyAllAttempts.attempts = action.payload;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(fetchMyAllAttempts.rejected, (state, action: any) => {
|
||||||
|
state.fetchMyAllAttempts.status = 'failed';
|
||||||
|
state.fetchMyAllAttempts.error = action.payload;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(fetchMyActiveAttempt.pending, (state) => {
|
||||||
|
state.fetchMyActiveAttempt.status = 'loading';
|
||||||
|
});
|
||||||
|
builder.addCase(
|
||||||
|
fetchMyActiveAttempt.fulfilled,
|
||||||
|
(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
contestId: number;
|
||||||
|
attempt: Attempt | null;
|
||||||
|
}>,
|
||||||
|
) => {
|
||||||
|
state.fetchMyActiveAttempt.status = 'successful';
|
||||||
|
state.fetchMyActiveAttempt.attempt = action.payload.attempt;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(fetchMyActiveAttempt.rejected, (state, action: any) => {
|
||||||
|
state.fetchMyActiveAttempt.status = 'failed';
|
||||||
|
state.fetchMyActiveAttempt.error = action.payload;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(checkContestRegistration.pending, (state) => {
|
||||||
|
state.checkRegistration.status = 'loading';
|
||||||
|
});
|
||||||
|
builder.addCase(
|
||||||
|
checkContestRegistration.fulfilled,
|
||||||
|
(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
contestId: number;
|
||||||
|
registered: boolean;
|
||||||
|
}>,
|
||||||
|
) => {
|
||||||
|
state.checkRegistration.status = 'successful';
|
||||||
|
state.checkRegistration.registered = action.payload.registered;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(
|
||||||
|
checkContestRegistration.rejected,
|
||||||
|
(state, action: any) => {
|
||||||
|
state.checkRegistration.status = 'failed';
|
||||||
|
state.checkRegistration.error = action.payload;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.addCase(fetchUpcomingEligibleContests.pending, (state) => {
|
||||||
|
state.fetchUpcomingEligible.status = 'loading';
|
||||||
|
});
|
||||||
|
builder.addCase(
|
||||||
|
fetchUpcomingEligibleContests.fulfilled,
|
||||||
|
(state, action: PayloadAction<Contest[]>) => {
|
||||||
|
state.fetchUpcomingEligible.status = 'successful';
|
||||||
|
state.fetchUpcomingEligible.contests = action.payload;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(
|
||||||
|
fetchUpcomingEligibleContests.rejected,
|
||||||
|
(state, action: any) => {
|
||||||
|
state.fetchUpcomingEligible.status = 'failed';
|
||||||
|
state.fetchUpcomingEligible.error = action.payload;
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -120,15 +120,6 @@ const Login = () => {
|
|||||||
text={status === 'loading' ? 'Вход...' : 'Вход'}
|
text={status === 'loading' ? 'Вход...' : 'Вход'}
|
||||||
disabled={status === 'loading'}
|
disabled={status === 'loading'}
|
||||||
/>
|
/>
|
||||||
<SecondaryButton className="w-full" onClick={() => {}}>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<img
|
|
||||||
src={googleLogo}
|
|
||||||
className="h-[24px] w-[24px] mr-[15px]"
|
|
||||||
/>
|
|
||||||
Вход с Google
|
|
||||||
</div>
|
|
||||||
</SecondaryButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-center mt-[10px]">
|
<div className="flex justify-center mt-[10px]">
|
||||||
|
|||||||
@@ -237,15 +237,6 @@ const Register = () => {
|
|||||||
}
|
}
|
||||||
disabled={status === 'loading'}
|
disabled={status === 'loading'}
|
||||||
/>
|
/>
|
||||||
<SecondaryButton className="w-full" onClick={() => {}}>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<img
|
|
||||||
src={googleLogo}
|
|
||||||
className="h-[24px] w-[24px] mr-[15px]"
|
|
||||||
/>
|
|
||||||
Регистрация с Google
|
|
||||||
</div>
|
|
||||||
</SecondaryButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-center mt-[10px]">
|
<div className="flex justify-center mt-[10px]">
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ import { CreateContestBody } from '../../../redux/slices/contests';
|
|||||||
import DateRangeInput from '../../../components/input/DateRangeInput';
|
import DateRangeInput from '../../../components/input/DateRangeInput';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
function toUtc(localDateTime?: string): string {
|
||||||
|
if (!localDateTime) return '';
|
||||||
|
|
||||||
|
// Создаём дату (она автоматически считается как локальная)
|
||||||
|
const date = new Date(localDateTime);
|
||||||
|
|
||||||
|
// Возвращаем ISO-строку с 'Z' (всегда в UTC)
|
||||||
|
return date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
interface ModalCreateContestProps {
|
interface ModalCreateContestProps {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
setActive: (value: boolean) => void;
|
setActive: (value: boolean) => void;
|
||||||
@@ -61,7 +71,13 @@ const ModalCreateContest: FC<ModalCreateContestProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
dispatch(createContest(form));
|
dispatch(
|
||||||
|
createContest({
|
||||||
|
...form,
|
||||||
|
endsAt: toUtc(form.endsAt),
|
||||||
|
startsAt: toUtc(form.startsAt),
|
||||||
|
}),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -19,35 +19,7 @@ const Filters = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className=" h-[50px] mb-[20px] flex gap-[20px] items-center">
|
<div className=" h-[50px] mb-[20px] flex gap-[20px] items-center">
|
||||||
<SearchInput onChange={() => {}} placeholder="Поиск задачи" />
|
<SearchInput onChange={() => {}} placeholder="Поиск группы" />
|
||||||
|
|
||||||
<SorterDropDown
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
value: '1',
|
|
||||||
text: 'Сложность',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: '2',
|
|
||||||
text: 'Дата создания',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: '3',
|
|
||||||
text: 'ID',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onChange={(v) => {
|
|
||||||
v;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FilterDropDown
|
|
||||||
items={items}
|
|
||||||
defaultState={[]}
|
|
||||||
onChange={(values) => {
|
|
||||||
values;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user