add contest thunck and remove google button

This commit is contained in:
Виталий Лавшонок
2025-12-02 22:34:28 +03:00
parent 2e5d4b2653
commit 95f7479375
6 changed files with 459 additions and 162 deletions

View File

@@ -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 };

View File

@@ -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;
},
);
}, },
}); });

View File

@@ -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]">

View File

@@ -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]">

View File

@@ -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 (

View File

@@ -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>
); );
}; };