diff --git a/public/locales/en/task.json b/public/locales/en/task.json index f10d2918b..cb254c626 100644 --- a/public/locales/en/task.json +++ b/public/locales/en/task.json @@ -78,6 +78,19 @@ "companies": { "title": "Companies" }, + "sort": { + "fields": { + "title": "Sort by field", + "name": "Name", + "difficulty": "Difficulty", + "createdAt": "Date created" + }, + "order": { + "title": "Sort by", + "asc": "Ascending", + "desc": "Descending" + } + }, "subscription.level": { "title": "Subscription level", "subtitle": "Specify the required subscription level for access to the task", diff --git a/public/locales/ru/task.json b/public/locales/ru/task.json index fa3211743..4acee9545 100644 --- a/public/locales/ru/task.json +++ b/public/locales/ru/task.json @@ -93,6 +93,19 @@ "companies": { "title": "Компании" }, + "sort": { + "fields": { + "title": "Сортировать по полю", + "name": "Название", + "difficulty": "Сложность", + "createdAt": "Дата создания" + }, + "order": { + "title": "Сортировать по", + "asc": "Возрастанию", + "desc": "Убыванию" + } + }, "subscription.level": { "title": "Уровень подписки", "subtitle": "Укажите требуемый уровень подписки для допуска к задаче", diff --git a/src/entities/task/index.ts b/src/entities/task/index.ts index 33deed466..f3a8470f7 100644 --- a/src/entities/task/index.ts +++ b/src/entities/task/index.ts @@ -12,6 +12,7 @@ export type { TaskSubscriptionLevel, TaskTestCaseResult, TaskTestCaseResultTest, + TaskFilterOrderBy, } from './model/types/task'; export type { TasksFilterParams } from './model/types/filters'; export { taskApiUrls } from './model/constants/task'; diff --git a/src/entities/task/model/types/filters.ts b/src/entities/task/model/types/filters.ts index eb1b223cd..1e2a77577 100644 --- a/src/entities/task/model/types/filters.ts +++ b/src/entities/task/model/types/filters.ts @@ -1,4 +1,6 @@ -import type { TaskCategoryCode, TaskDifficulty } from './task'; +import { SortOrder } from '@/shared/libs'; + +import type { TaskCategoryCode, TaskDifficulty, TaskFilterOrderBy } from './task'; export interface TasksFilterParams { page?: number; @@ -7,4 +9,6 @@ export interface TasksFilterParams { category?: TaskCategoryCode; langIds?: number[]; companyId?: string; + sortBy?: TaskFilterOrderBy; + sortOrder?: SortOrder; } diff --git a/src/entities/task/model/types/task.ts b/src/entities/task/model/types/task.ts index 2969a625d..2b0595f89 100644 --- a/src/entities/task/model/types/task.ts +++ b/src/entities/task/model/types/task.ts @@ -94,6 +94,8 @@ export interface TaskCategory { export type GetTaskCategoriesResponse = TaskCategory[]; +export type TaskFilterOrderBy = 'name' | 'difficulty' | 'createdAt' | 'updatedAt'; + export interface GetTasksListParams { page?: number; limit?: number; @@ -105,7 +107,7 @@ export interface GetTasksListParams { langIds?: number[]; isActive?: boolean; search?: string; - sortBy?: 'name' | 'difficulty' | 'createdAt' | 'updatedAt'; + sortBy?: TaskFilterOrderBy; sortOrder?: SortOrder; canSolve?: boolean; collectionId?: number; diff --git a/src/entities/topic/index.ts b/src/entities/topic/index.ts index 12d00321e..f09577f59 100644 --- a/src/entities/topic/index.ts +++ b/src/entities/topic/index.ts @@ -1,3 +1,5 @@ +export { TopicFilterField } from './ui/TopicFilterField/TopicFilterField'; + export { TopicForm } from './ui/TopicForm/TopicForm'; export { TopicFilterField } from './ui/TopicFilterField/TopicFilterField'; diff --git a/src/features/task/filterTasks/index.ts b/src/features/task/filterTasks/index.ts index 276ec6926..2b0308efe 100644 --- a/src/features/task/filterTasks/index.ts +++ b/src/features/task/filterTasks/index.ts @@ -3,3 +3,5 @@ export { useTasksFilters } from './model/hooks/useTasksFilters'; export { TasksFiltersSkeleton } from './ui/TaskFilters/TasksFilters.skeleton'; export { TaskDifficultyFilterSkeleton } from './ui/TaskDifficultyFilter/TaskDifficultyFilter.skeleton'; export { TaskLanguagesFilterSkeleton } from './ui/TaskLanguagesFilter/TaskLanguagesFilter.skeleton'; +export { SortTasksByOrder } from './ui/SortTasksByOrder/SortTasksByOrder'; +export { SortTasksByField } from './ui/SortTasksByField/SortTasksByField'; diff --git a/src/features/task/filterTasks/model/hooks/useTasksFilters.ts b/src/features/task/filterTasks/model/hooks/useTasksFilters.ts index 5a171a98c..3fe5c421e 100644 --- a/src/features/task/filterTasks/model/hooks/useTasksFilters.ts +++ b/src/features/task/filterTasks/model/hooks/useTasksFilters.ts @@ -18,7 +18,9 @@ export const useTasksFilters = (initialParams: TasksFilterParams) => { Boolean(filters.difficulty) || Boolean(filters.category) || Boolean(filters.companyId) || - (filters.langIds || []).length > 0; + (filters.langIds || []).length > 0 || + Boolean(filters.sortBy) || + Boolean(filters.sortOrder); const onChangePage = (page: TasksFilterParams['page']) => { onFilterChange({ page }); @@ -45,6 +47,14 @@ export const useTasksFilters = (initialParams: TasksFilterParams) => { onFilterChange({ langIds, page: 1 }); }; + const onChangeSortField = (sortBy?: TasksFilterParams['sortBy']) => { + onFilterChange({ sortBy, page: 1 }); + }; + + const onChangeSortOrder = (sortOrder?: TasksFilterParams['sortOrder']) => { + onFilterChange({ sortOrder, page: 1 }); + }; + return { filters, hasFilters, @@ -54,6 +64,8 @@ export const useTasksFilters = (initialParams: TasksFilterParams) => { onChangeLangIds, onChangeCategory, onChangeCompanyId, + onChangeSortField, + onChangeSortOrder, onResetFilters, }; }; diff --git a/src/features/task/filterTasks/ui/SortTasksByField/SortTasksByField.tsx b/src/features/task/filterTasks/ui/SortTasksByField/SortTasksByField.tsx new file mode 100644 index 000000000..4a397e5ba --- /dev/null +++ b/src/features/task/filterTasks/ui/SortTasksByField/SortTasksByField.tsx @@ -0,0 +1,38 @@ +import { useTranslation } from 'react-i18next'; + +import { i18Namespace, Tasks } from '@/shared/config'; +import { BaseFilterItem, BaseFilterSection } from '@/shared/ui/BaseFilterSection'; + +import { TaskFilterOrderBy } from '@/entities/task'; + +interface SortTasksFieldProps { + selectedOrderBy?: TaskFilterOrderBy; + onChangeSortField: (field?: TaskFilterOrderBy) => void; +} + +export const SortTasksByField = ({ onChangeSortField, selectedOrderBy }: SortTasksFieldProps) => { + const { t } = useTranslation(i18Namespace.task); + + const field: BaseFilterItem[] = [ + { id: 'name', title: t(Tasks.SORT_FIELDS_NAME) }, + { id: 'difficulty', title: t(Tasks.SORT_DIFFICULTY) }, + { id: 'createdAt', title: t(Tasks.SORT_CREATED_AT) }, + ]; + + const preparedData = field.map((item) => ({ + ...item, + active: selectedOrderBy === item.id, + })); + + const onChangeOrder = (orderBy: TaskFilterOrderBy) => { + onChangeSortField(orderBy === selectedOrderBy ? undefined : orderBy); + }; + + return ( + + ); +}; diff --git a/src/features/task/filterTasks/ui/SortTasksByOrder/SortTasksByOrder.tsx b/src/features/task/filterTasks/ui/SortTasksByOrder/SortTasksByOrder.tsx new file mode 100644 index 000000000..722505007 --- /dev/null +++ b/src/features/task/filterTasks/ui/SortTasksByOrder/SortTasksByOrder.tsx @@ -0,0 +1,36 @@ +import { useTranslation } from 'react-i18next'; + +import { i18Namespace, Tasks } from '@/shared/config'; +import { SortOrder } from '@/shared/libs'; +import { BaseFilterItem, BaseFilterSection } from '@/shared/ui/BaseFilterSection'; + +interface SortTasksByOrderProps { + selectedOrder?: SortOrder; + onChangeSortOrder: (sortOrder?: SortOrder) => void; +} + +export const SortTasksByOrder = ({ onChangeSortOrder, selectedOrder }: SortTasksByOrderProps) => { + const { t } = useTranslation(i18Namespace.task); + + const order: BaseFilterItem[] = [ + { id: 'ASC', title: t(Tasks.SORT_ASC) }, + { id: 'DESC', title: t(Tasks.SORT_DESC) }, + ]; + + const preparedData = order.map((item) => ({ + ...item, + active: selectedOrder === item.id, + })); + + const onChangeOrder = (orderBy: SortOrder) => { + onChangeSortOrder(orderBy === selectedOrder ? undefined : orderBy); + }; + + return ( + + ); +}; diff --git a/src/features/task/filterTasks/ui/TaskFilters/TasksFilters.tsx b/src/features/task/filterTasks/ui/TaskFilters/TasksFilters.tsx index 6d70b6a6f..7e3cd6a33 100644 --- a/src/features/task/filterTasks/ui/TaskFilters/TasksFilters.tsx +++ b/src/features/task/filterTasks/ui/TaskFilters/TasksFilters.tsx @@ -7,25 +7,33 @@ import { SearchInput } from '@/shared/ui/SearchInput'; import { PublicCompanySelect } from '@/entities/company'; import { TaskCategoryFilterList, TasksFilterParams } from '@/entities/task'; +import { SortTasksByField } from '../SortTasksByField/SortTasksByField'; +import { SortTasksByOrder } from '../SortTasksByOrder/SortTasksByOrder'; import { TaskDifficultyFilter } from '../TaskDifficultyFilter/TaskDifficultyFilter'; import { TaskLanguagesFilter } from '../TaskLanguagesFilter/TaskLanguagesFilter'; interface TasksFiltersProps { filters: TasksFilterParams; + withSorting?: boolean; onChangeTitle: (title: TasksFilterParams['title']) => void; onChangeDifficulty: (difficulty?: TasksFilterParams['difficulty']) => void; onChangeLangIds: (langIds?: TasksFilterParams['langIds']) => void; onChangeCategory: (category?: TasksFilterParams['category']) => void; onChangeCompanyId: (companyId?: TasksFilterParams['companyId']) => void; + onChangeSortField?: (sortBy?: TasksFilterParams['sortBy']) => void; + onChangeSortOrder?: (sortOrder?: TasksFilterParams['sortOrder']) => void; } export const TasksFilters = ({ filters, + withSorting, onChangeTitle, onChangeDifficulty, onChangeLangIds, onChangeCategory, onChangeCompanyId, + onChangeSortField, + onChangeSortOrder, }: TasksFiltersProps) => { const { title, difficulty, langIds, category, companyId } = filters; const { t } = useTranslation(i18Namespace.task); @@ -46,6 +54,18 @@ export const TasksFilters = ({ + {withSorting && onChangeSortField && onChangeSortOrder && ( + <> + + + + )} ); }; diff --git a/src/pages/admin/task/tasks/ui/TasksTablePage/TasksTablePage.tsx b/src/pages/admin/task/tasks/ui/TasksTablePage/TasksTablePage.tsx index 15f83dc78..065084b09 100644 --- a/src/pages/admin/task/tasks/ui/TasksTablePage/TasksTablePage.tsx +++ b/src/pages/admin/task/tasks/ui/TasksTablePage/TasksTablePage.tsx @@ -33,6 +33,8 @@ const TasksTablePage = () => { onChangeLangIds, onChangeCategory, onChangeCompanyId, + onChangeSortField, + onChangeSortOrder, } = useTasksFilters({ page: 1 }); const { @@ -47,6 +49,8 @@ const TasksTablePage = () => { langIds: filters.langIds, category: filters.category, companyId: filters.companyId, + sortBy: filters.sortBy, + sortOrder: filters.sortOrder, }); const onSelectTasks = (ids: SelectedAdminEntities) => { @@ -104,11 +108,14 @@ const TasksTablePage = () => { renderFilter={() => ( )} /> diff --git a/src/shared/config/i18n/i18nTranslations.ts b/src/shared/config/i18n/i18nTranslations.ts index 9acb9600e..8d4ccef51 100644 --- a/src/shared/config/i18n/i18nTranslations.ts +++ b/src/shared/config/i18n/i18nTranslations.ts @@ -1680,6 +1680,13 @@ export enum Tasks { DESCRIPTION_TAB_TITLE = 'description.tab.title', DESCRIPTION_CONSTRAINTS_TITLE = 'description.constraints.title', DESCRIPTION_TAB_SUBTITLE = 'description.tab.subtitle', + SORT_FIELDS_TITLE = 'sort.fields.title', + SORT_FIELDS_NAME = 'sort.fields.name', + SORT_DIFFICULTY = 'sort.fields.difficulty', + SORT_CREATED_AT = 'sort.fields.createdAt', + SORT_ORDER_TITLE = 'sort.order.title', + SORT_ASC = 'sort.order.asc', + SORT_DESC = 'sort.order.desc', STATUS_ATTEMPTED = 'status.attempted', STATUS_NOT_STARTED = 'status.not.started', STATUS_SOLVED = 'status.solved',