diff --git a/public/locales/en/programmingLanguage.json b/public/locales/en/programmingLanguage.json new file mode 100644 index 000000000..85fd7d0c8 --- /dev/null +++ b/public/locales/en/programmingLanguage.json @@ -0,0 +1,7 @@ +{ + "select": { + "choose": "Select a language from the list", + "empty": "There are no options", + "selected": "Selected programming language" + } +} diff --git a/public/locales/en/task.json b/public/locales/en/task.json index a7b7f9907..63d8873dd 100644 --- a/public/locales/en/task.json +++ b/public/locales/en/task.json @@ -2,22 +2,84 @@ "title": { "short": "Coding tasks" }, + "name": { + "title": "Name", + "subtitle": "Add a task name" + }, + "memory.limit": { + "title": "Memory", + "subtitle": "Add task memory limit", + "value": "{{count}}Kb" + }, + "time.limit": { + "title": "Execution time", + "subtitle": "Add a task execution time limit", + "value": "{{count}}s" + }, + "constraints": { + "title": "Constraints", + "subtitle": "Add task organization", + "add.button": "Add" + }, + "test.cases": { + "title": "Test cases", + "subtitle": "Add test cases of the task", + "input": "Input data", + "expected.output": "Expected output", + "hidden": "Active", + "add.button": "Add" + }, + "task.structures": { + "title": "Code templates", + "subtitle": "Add a task name", + "language": "Programming language", + "solution.stub": "Solution code template", + "text.fixture": "Solution code testing template", + "active": "Active", + "add.button": "Add" + }, + "edit.page.title": "Editing task", + "create.page.title": "Creating task", "description": { + "title": "Description", + "subtitle": "Add task description", "tab.title": "Description", "constraints": { "title": "Constraints" } }, + "select": { + "choose": "Select a task category from the list", + "empty": "There are no options", + "selected": "Selected task category" + }, "search": { "placeholder": "Enter a task..." }, "difficulty": { "title": "Question difficulty", + "subtitle": "Select a task difficulty", "title.short": "Difficulty" }, "languages": { "title": "Programming languages" }, + "status": { + "solved": "Solved", + "attempted": "Attempted", + "not.started": "Not started" + }, + "category": { + "title": "Task category", + "label": "Add a task category", + "placeholder": "Add a category", + "data.structures": "Data structures", + "algorithms": "algorithmics", + "arrays": "Arrays", + "databases": "Databases", + "strings": "Strings", + "dynamic.programming": "Dynamic programming" + }, "stub": { "empty.tasks": { "public": { @@ -34,22 +96,25 @@ } }, "empty.task": { - "public": { - "title": "Failed to load task data", - "subtitle": "Try again or refresh the page", - "buttonText": "Try again" - }, - "admin": {} + "title": "Failed to load task data", + "subtitle": "Try again or refresh the page", + "buttonText": "Try again" } }, "table": { - "task.title": "Title", - "difficulty.title": "Difficulty", - "status.title": "Status" + "task": "Title", + "difficulty": "Difficulty", + "status": "Status", + "status.solved": "Solved", + "status.not.solved": "Not solved", + "memory": "Memory", + "time": "Time", + "language": "Language" }, "solutions": { "tab.title": "My solutions", - "tab.subtitle": "Here will be your solutions to the task" + "tab.subtitle": "Here will be your solutions to the task", + "back.button": "Solutions list" }, "editor": { "actions": { diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index f18c269a8..efbce5a8c 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -228,6 +228,22 @@ } } }, + "tasks": { + "create": { + "success": "Task created successfully", + "failed": "Task could not be created" + }, + "edit": { + "success": "Task successfully changed", + "failed": "Task could not be changed" + }, + "delete": { + "single": { + "success": "Task successfully deleted", + "failed": "Task could not be deleted" + } + } + }, "questions": { "learned": { "success": "The question has been fully studied", diff --git a/public/locales/ru/programmingLanguage.json b/public/locales/ru/programmingLanguage.json new file mode 100644 index 000000000..5a6143796 --- /dev/null +++ b/public/locales/ru/programmingLanguage.json @@ -0,0 +1,7 @@ +{ + "select": { + "choose": "Выберите язык из списка", + "empty": "Нет вариантов", + "selected": "Выбранный язык программирования" + } +} diff --git a/public/locales/ru/task.json b/public/locales/ru/task.json index ee59a4df7..c583c864e 100644 --- a/public/locales/ru/task.json +++ b/public/locales/ru/task.json @@ -2,22 +2,84 @@ "title": { "short": "Задачи на код" }, + "name": { + "title": "Название", + "subtitle": "Добавьте название задачи" + }, + "memory.limit": { + "title": "Память", + "subtitle": "Добавьте ограничение по памяти задачи в Кб", + "value": "{{count}}Кб" + }, + "time.limit": { + "title": "Время выполнения", + "subtitle": "Добавьте ограничение по времени выполнения задачи в секундах", + "value": "{{count}}с" + }, + "constraints": { + "title": "Ограничения", + "subtitle": "Добавьте органичения задачи", + "add.button": "Добавить" + }, + "test.cases": { + "title": "Тест-кейсы", + "subtitle": "Добавьте тест-кейсы задачи", + "input": "Входные данные", + "expected.output": "Ожидаемый вывод", + "hidden": "Активный", + "add.button": "Добавить" + }, + "task.structures": { + "title": "Шаблоны кода", + "subtitle": "Добавьте шаблоны кода задачи", + "language": "Язык программирования", + "solution.stub": "Шаблон кода решения", + "text.fixture": "Шаблон тестирования кода решения", + "active": "Активный", + "add.button": "Добавить" + }, + "edit.page.title": "Редактирование задачи", + "create.page.title": "Создание задачи", "description": { + "title": "Описание", + "subtitle": "Добавьте описание задачи", "tab.title": "Описание", "constraints": { "title": "Ограничения" } }, + "select": { + "choose": "Выберите категорию задачи из списка", + "empty": "Нет вариантов", + "selected": "Выбранная категория задачи" + }, "search": { "placeholder": "Введите задачу..." }, "difficulty": { - "title": "Сложность вопросов", + "title": "Сложность задачи", + "subtitle": "Укажите сложность задачи", "title.short": "Сложность" }, "languages": { "title": "Языки программирования" }, + "status": { + "solved": "Решена", + "attempted": "В процессе", + "not.started": "Не начата" + }, + "category": { + "title": "Категория", + "subtitle": "Добавьте категорию задачи", + "placeholder": "Добавьте категорию", + "data.structures": "Структуры данных", + "algorithms": "Алгоритмы", + "arrays": "Массивы", + "databases": "Базы данных", + "strings": "Строки", + "dynamic.programming": "Динамическое программирование" + }, "stub": { "empty.tasks": { "public": { @@ -34,26 +96,29 @@ } }, "empty.task": { - "public": { - "title": "Не удалось загрузить данные задачи", - "subtitle": "Попробуйте обновить страницу или повторить попытку", - "submit": "Повторить попытку" - }, - "admin": {} + "title": "Не удалось загрузить данные задачи", + "subtitle": "Попробуйте обновить страницу или повторить попытку", + "submit": "Повторить попытку" } }, "table": { - "task.title": "Название задачи", - "difficulty.title": "Сложность", - "status.title": "Статус" + "task": "Название задачи", + "difficulty": "Сложность", + "status": "Статус", + "status.solved": "Решено верно", + "status.not.solved": "Решено не верно", + "memory": "Память", + "time": "Время", + "language": "Язык" }, "solutions": { "tab.title": "Мои решения", - "tab.subtitle": "Здесь будут ваши решения задачи" + "tab.subtitle": "Здесь будут ваши решения задачи", + "back.button": "Список решений" }, "editor": { "actions": { - "run": "Выполнить", + "run": "Проверить", "submit": "Отправить" } }, diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 964179b65..70d37b2f4 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -232,6 +232,22 @@ } } }, + "tasks": { + "create": { + "success": "Задача успешно создана", + "failed": "Не удалось создать задачу" + }, + "edit": { + "success": "Задача успешно изменена", + "failed": "Не удалось изменить задачу" + }, + "delete": { + "single": { + "success": "Задача успешно удалена", + "failed": "Не удалось удалить задачу" + } + } + }, "questions": { "learned": { "success": "Вопрос полностью изучен", diff --git a/src/app/providers/router/routeConfig.tsx b/src/app/providers/router/routeConfig.tsx index fe79ef566..df78acd14 100644 --- a/src/app/providers/router/routeConfig.tsx +++ b/src/app/providers/router/routeConfig.tsx @@ -54,6 +54,9 @@ import { SpecializationCreatePage } from '@/pages/admin/specialization/specializ import { SpecializationDetailPage } from '@/pages/admin/specialization/specializationDetail'; import { SpecializationEditPage } from '@/pages/admin/specialization/specializationEdit'; import { SpecializationsPage } from '@/pages/admin/specialization/specializations'; +import { TaskCreatePage } from '@/pages/admin/task/taskCreate'; +import { TaskPage as AdminTaskPage } from '@/pages/admin/task/taskDetail'; +import { TaskEditPage } from '@/pages/admin/task/taskEdit'; import { TasksTablePage } from '@/pages/admin/task/tasks'; import { TopicCreatePage } from '@/pages/admin/topic/topicCreate'; import { TopicDetailPage } from '@/pages/admin/topic/topicDetail'; @@ -566,6 +569,18 @@ export const router = createBrowserRouter([ index: true, element: , }, + { + path: ROUTES.admin.tasks.create.route, + element: , + }, + { + path: ROUTES.admin.tasks.edit.route, + element: , + }, + { + path: ROUTES.admin.tasks.details.route, + element: , + }, ], }, { diff --git a/src/entities/profile/@x/task.ts b/src/entities/profile/@x/task.ts new file mode 100644 index 000000000..cd4fa33a4 --- /dev/null +++ b/src/entities/profile/@x/task.ts @@ -0,0 +1 @@ +export { getProfileId } from '../model/selectors/profileSelectors'; diff --git a/src/entities/programmingLanguage/@x/task.ts b/src/entities/programmingLanguage/@x/task.ts new file mode 100644 index 000000000..085c64241 --- /dev/null +++ b/src/entities/programmingLanguage/@x/task.ts @@ -0,0 +1,7 @@ +export type { + ProgrammingLanguage, + ProgrammingLanguageCode, +} from '../model/types/programmingLanguage'; +export { ProgrammingLanguageList } from '../ui/ProgrammingLanguageList/ProgrammingLanguageList'; +export { ProgrammingLanguageSelect } from '../ui/ProgrammingLanguageSelect/ProgrammingLanguageSelect'; +export { useGetLanguagesQuery } from '../api/programmingLanguageApi'; diff --git a/src/entities/programmingLanguage/index.ts b/src/entities/programmingLanguage/index.ts index 57093cb8a..048f6ce6d 100644 --- a/src/entities/programmingLanguage/index.ts +++ b/src/entities/programmingLanguage/index.ts @@ -1,2 +1,5 @@ export { useGetLanguagesQuery } from './api/programmingLanguageApi'; export type { ProgrammingLanguage, GetLanguagesResponse } from './model/types/programmingLanguage'; +export { ProgrammingLanguageList } from './ui/ProgrammingLanguageList/ProgrammingLanguageList'; +export { ProgrammingLanguageSelect } from './ui/ProgrammingLanguageSelect/ProgrammingLanguageSelect'; +export { ProgrammingLanguageChipList } from './ui/ProgrammingLanguageChipList/ProgrammingLanguageChipList'; diff --git a/src/entities/programmingLanguage/model/types/programmingLanguage.ts b/src/entities/programmingLanguage/model/types/programmingLanguage.ts index 596e5958a..775a468c1 100644 --- a/src/entities/programmingLanguage/model/types/programmingLanguage.ts +++ b/src/entities/programmingLanguage/model/types/programmingLanguage.ts @@ -1,4 +1,4 @@ -type ProgrammingLanguageCode = 'cpp' | 'go' | 'java' | 'javascript' | 'python' | 'ruby'; +export type ProgrammingLanguageCode = 'cpp' | 'go' | 'java' | 'javascript' | 'python' | 'ruby'; export interface ProgrammingLanguage { id: number; @@ -7,6 +7,7 @@ export interface ProgrammingLanguage { monacoLangId: ProgrammingLanguageCode; fileExtension: string; isActive: boolean; + imageSrc: string; } export type GetLanguagesResponse = ProgrammingLanguage[]; diff --git a/src/entities/programmingLanguage/ui/ProgrammingLanguageChipList/ProgrammingLanguageChipList.tsx b/src/entities/programmingLanguage/ui/ProgrammingLanguageChipList/ProgrammingLanguageChipList.tsx new file mode 100644 index 000000000..d1c277659 --- /dev/null +++ b/src/entities/programmingLanguage/ui/ProgrammingLanguageChipList/ProgrammingLanguageChipList.tsx @@ -0,0 +1,40 @@ +import { Chip } from '@/shared/ui/Chip'; +import { Flex } from '@/shared/ui/Flex'; + +import { ProgrammingLanguage } from '../../model/types/programmingLanguage'; + +interface ProgrammingLanguageChipListProps { + languages: ProgrammingLanguage[]; + onClick?: (languageId: number) => void; +} + +export const ProgrammingLanguageChipList = ({ + languages, + onClick, +}: ProgrammingLanguageChipListProps) => { + return ( + + {languages?.map((language) => { + return ( +
  • + onClick?.(language.id)} + label={language.name} + theme="primary" + active + prefix={ + language.imageSrc && ( + {language.name} + ) + } + /> +
  • + ); + })} +
    + ); +}; diff --git a/src/entities/programmingLanguage/ui/ProgrammingLanguageList/ProgrammingLanguageList.module.css b/src/entities/programmingLanguage/ui/ProgrammingLanguageList/ProgrammingLanguageList.module.css new file mode 100644 index 000000000..d549031a2 --- /dev/null +++ b/src/entities/programmingLanguage/ui/ProgrammingLanguageList/ProgrammingLanguageList.module.css @@ -0,0 +1,10 @@ +.image { + border-radius: 3px; +} + +.list { + padding: 6px; + box-shadow: 0 4px 10px 0 rgb(106 99 118 / 10%); + border-radius: 8px; + background: var(--color-black-30); +} \ No newline at end of file diff --git a/src/entities/programmingLanguage/ui/ProgrammingLanguageList/ProgrammingLanguageList.tsx b/src/entities/programmingLanguage/ui/ProgrammingLanguageList/ProgrammingLanguageList.tsx new file mode 100644 index 000000000..ba1897093 --- /dev/null +++ b/src/entities/programmingLanguage/ui/ProgrammingLanguageList/ProgrammingLanguageList.tsx @@ -0,0 +1,31 @@ +import { Flex } from '@/shared/ui/Flex'; +import { Tooltip } from '@/shared/ui/Tooltip'; + +import { ProgrammingLanguage } from '../../model/types/programmingLanguage'; + +import styles from './ProgrammingLanguageList.module.css'; + +interface ProgrammingLanguageListProps { + languages: ProgrammingLanguage[]; +} + +export const ProgrammingLanguageList = ({ languages }: ProgrammingLanguageListProps) => { + const languagesTitles = languages.map((language) => language.name).join(', '); + + return ( + + + {languages.map((language) => ( + {language.name} + ))} + + + ); +}; diff --git a/src/entities/programmingLanguage/ui/ProgrammingLanguageSelect/ProgrammingLanguageSelect.tsx b/src/entities/programmingLanguage/ui/ProgrammingLanguageSelect/ProgrammingLanguageSelect.tsx new file mode 100644 index 000000000..6561e099c --- /dev/null +++ b/src/entities/programmingLanguage/ui/ProgrammingLanguageSelect/ProgrammingLanguageSelect.tsx @@ -0,0 +1,146 @@ +import { ComponentProps, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { i18Namespace, ProgrammingLanguages, Tasks } from '@/shared/config'; +import { Dropdown, Option } from '@/shared/ui/Dropdown'; +import { SelectWithChips } from '@/shared/ui/SelectWithChips'; + +import { useGetLanguagesQuery } from '../../api/programmingLanguageApi'; +import { ProgrammingLanguage } from '../../model/types/programmingLanguage'; + +type ProgrammingLanguageSelectProps = Omit< + ComponentProps, + 'options' | 'type' | 'value' | 'onChange' | 'children' +> & { + value: string | string[]; + onChange: (value: string[] | string) => void; + hasMultiple?: boolean; + disabled?: boolean; + selectedLanguageIds?: number[]; + supportedLanguages?: ProgrammingLanguage[]; + width?: number; +}; + +type LanguageType = { + id: string; + title: string; +}; + +export const ProgrammingLanguageSelect = ({ + onChange, + value, + hasMultiple, + disabled, + selectedLanguageIds, + supportedLanguages, + width, +}: ProgrammingLanguageSelectProps) => { + const { t } = useTranslation(i18Namespace.programmingLanguage); + + const { data } = useGetLanguagesQuery(undefined, { skip: !!supportedLanguages }); + + const languages = (supportedLanguages || data || [])?.map((language) => ({ + id: String(language.id), + title: language.name, + })); + + const [selectedLanguages, setSelectedLanguages] = useState( + Array.isArray(value) ? value : value !== undefined ? [value] : [], + ); + + const handleChangeLanguage = (newValue: string | undefined) => { + if (disabled || !newValue) return; + const strValue = newValue; + + if (hasMultiple) { + const updates = [...selectedLanguages, strValue]; + setSelectedLanguages(updates); + onChange(updates); + } else { + setSelectedLanguages([strValue]); + onChange(strValue); + } + }; + + const handleDeleteLanguage = (id: string) => () => { + if (disabled) return; + const updates = selectedLanguages.filter((languageId) => languageId !== id); + setSelectedLanguages(updates); + onChange(updates); + }; + + const options = useMemo(() => { + if (hasMultiple) { + return languages + .map((language) => ({ + label: language.title, + value: language.id, + limit: 100, + })) + .filter((language) => !selectedLanguages?.includes(language.value)); + } else { + return languages.map((language) => ({ + label: language.title, + value: language.id, + limit: 100, + })); + } + }, [selectedLanguages, languages]); + + const languagesDictionary = useMemo(() => { + const emptyLanguage: LanguageType = { + id: '0', + title: t(Tasks.SELECT_CHOOSE), + }; + return languages.reduce( + (acc, language) => { + acc[language.id] = language; + return acc; + }, + { 0: emptyLanguage } as Record, + ); + }, [languages]); + + const filteredOptions = options.filter( + (option) => !selectedLanguageIds?.includes(Number(option.value)), + ); + + if (!hasMultiple) { + return ( + <> + handleChangeLanguage(String(val))} + > + {filteredOptions.map((option) => ( + + + ); + } + + return ( + + ); +}; diff --git a/src/entities/question/model/types/question.ts b/src/entities/question/model/types/question.ts index 8ba33965d..6736a1b3c 100644 --- a/src/entities/question/model/types/question.ts +++ b/src/entities/question/model/types/question.ts @@ -67,7 +67,7 @@ export interface GetQuestionsListParamsRequest { rate?: number[]; keywords?: string[]; skillFilterMode?: skillFilterMode; - specialization?: number | number[]; + specializationId?: number | number[]; order?: string; orderBy?: string; random?: boolean; @@ -84,10 +84,8 @@ export type GetQuestionByIdParamsRequest = { }; export type GetQuestionByIdResponse = Question; -export interface GetQuestionsForLearnParamsRequest extends Omit< - GetQuestionsListParamsRequest, - 'order' | 'orderBy' | 'random' -> { +export interface GetQuestionsForLearnParamsRequest + extends Omit { profileId: string; isLearned?: boolean; areFavorites?: boolean; diff --git a/src/entities/question/ui/ChooseQuestionsDrawer/ChooseQuestionsDrawer.tsx b/src/entities/question/ui/ChooseQuestionsDrawer/ChooseQuestionsDrawer.tsx index 42473068a..96367b773 100644 --- a/src/entities/question/ui/ChooseQuestionsDrawer/ChooseQuestionsDrawer.tsx +++ b/src/entities/question/ui/ChooseQuestionsDrawer/ChooseQuestionsDrawer.tsx @@ -41,7 +41,7 @@ export const ChooseQuestionsDrawer = ({ const questions = useGetQuestionsListQuery({ title: collectionSearch, limit: COLLECTION_QUESTIONS_LIMIT, - specialization: specializations?.length ? specializations : undefined, + specializationId: specializations?.length ? specializations : undefined, }); const handleCollectionSearch = (e: ChangeEvent) => { diff --git a/src/entities/quiz/model/types/quiz.ts b/src/entities/quiz/model/types/quiz.ts index ea49ebf8f..ff4c5846e 100644 --- a/src/entities/quiz/model/types/quiz.ts +++ b/src/entities/quiz/model/types/quiz.ts @@ -87,10 +87,8 @@ export type ActiveQuiz = Omit; export type CreateNewQuizResponse = ActiveQuiz; -export interface CreateNewMockQuizParamsRequest extends Omit< - CreateNewQuizParamsRequest, - 'profileId' -> { +export interface CreateNewMockQuizParamsRequest + extends Omit { specialization?: number[] | number; } diff --git a/src/entities/task/api/taskApi.ts b/src/entities/task/api/taskApi.ts index 6e9664c74..16f9afd73 100644 --- a/src/entities/task/api/taskApi.ts +++ b/src/entities/task/api/taskApi.ts @@ -6,8 +6,11 @@ import { ExecuteCodeRequest, ExecuteCodeResponse, GetTaskByIdResponse, + GetTaskCategoriesResponse, GetTasksListParams, GetTasksListResponse, + GetTasksProfileSolutionsParamRequest, + GetTasksProfileSolutionsResponse, } from '../model/types/task'; const taskApi = baseApi.injectEndpoints({ @@ -25,12 +28,22 @@ const taskApi = baseApi.injectEndpoints({ }), providesTags: [ApiTags.TASK_DETAIL], }), + getTasksProfileSolutions: build.query< + GetTasksProfileSolutionsResponse, + GetTasksProfileSolutionsParamRequest + >({ + query: ({ taskId, profileId }) => ({ + url: route(taskApiUrls.getTasksProfileSolutions, taskId, profileId), + }), + providesTags: [ApiTags.TASK_SOLUTIONS], + }), executeCode: build.mutation({ query: (body) => ({ url: taskApiUrls.executeCode, method: 'POST', body, }), + invalidatesTags: [ApiTags.TASK_SOLUTIONS, ApiTags.TASKS, ApiTags.TASK_DETAIL], }), testCode: build.mutation({ query: (body) => ({ @@ -38,6 +51,13 @@ const taskApi = baseApi.injectEndpoints({ method: 'POST', body, }), + invalidatesTags: [ApiTags.TASKS, ApiTags.TASK_DETAIL], + }), + getTaskCategories: build.query({ + query: () => ({ + url: taskApiUrls.getTaskCategories, + }), + providesTags: [ApiTags.TASK_CATEGORIES], }), }), }); @@ -47,4 +67,6 @@ export const { useGetTaskByIdQuery, useExecuteCodeMutation, useTestCodeMutation, + useGetTasksProfileSolutionsQuery, + useGetTaskCategoriesQuery, } = taskApi; diff --git a/src/entities/task/index.ts b/src/entities/task/index.ts index 64d05eba2..12976c533 100644 --- a/src/entities/task/index.ts +++ b/src/entities/task/index.ts @@ -1,13 +1,31 @@ -export type { Task, TaskListItem, TaskDifficulty, ExecuteCodeResponse } from './model/types/task'; +export type { + Task, + TestCase, + TaskStructure, + TaskDifficulty, + ExecuteCodeResponse, + TaskSolution, + TaskCategory, + TaskCategoryCode, + CreateOrEditTaskFormValues, +} from './model/types/task'; export type { TasksFilterParams } from './model/types/filters'; -export { taskApiUrls, LANGUAGE_IDS } from './model/constants/task'; +export { taskApiUrls } from './model/constants/task'; export { useGetTasksListQuery, useGetTaskByIdQuery, useExecuteCodeMutation, useTestCodeMutation, + useGetTasksProfileSolutionsQuery, + useGetTaskCategoriesQuery, } from './api/taskApi'; export { TaskCard } from './ui/TaskCard/TaskCard'; export { TaskDescription } from './ui/TaskDescription/TaskDescription'; export { TaskSolutions } from './ui/TaskSolutions/TaskSolutions'; export { TaskDifficultyChip } from './ui/TaskDifficultyChip/TaskDifficultyChip'; +export { TaskStatusChip } from './ui/TaskStatusChip/TaskStatusChip'; +export { TaskForm } from './ui/TaskForm/TaskForm'; +export { TaskCategorySelect } from './ui/TaskCategorySelect/TaskCategorySelect'; +export { TaskConstraintsField } from './ui/TaskConstraintsField/TaskConstraintsField'; +export { TaskStructuresField } from './ui/TaskStructuresField/TaskStructuresField'; +export { taskCategories } from './model/constants/task'; diff --git a/src/entities/task/model/constants/task.ts b/src/entities/task/model/constants/task.ts index e00ed070c..e0cbb75c3 100644 --- a/src/entities/task/model/constants/task.ts +++ b/src/entities/task/model/constants/task.ts @@ -1,12 +1,21 @@ -import { API_VERSION } from '@/shared/config'; +import { API_VERSION, Tasks } from '@/shared/config'; + +import { TaskCategoryCode } from '../types/task'; export const taskApiUrls = { getTasksList: `${API_VERSION.V1}/live-coding/tasks`, getTaskById: `${API_VERSION.V1}/live-coding/tasks/:taskId`, executeCode: `${API_VERSION.V1}/live-coding/tasks/execute`, testCode: `${API_VERSION.V1}/live-coding/tasks/test`, + getTasksProfileSolutions: `${API_VERSION.V1}/live-coding/tasks/:taskId/solutions/:profileId`, + getTaskCategories: `${API_VERSION.V1}/live-coding/category`, }; -export const LANGUAGE_IDS = { - JAVASCRIPT: 63, -} as const; +export const taskCategories: Record = { + 'data-structures': Tasks.CATEGORY_DATA_STRUCTURES, + algorithms: Tasks.CATEGORY_ALGORITHMS, + arrays: Tasks.CATEGORY_ARRAYS, + databases: Tasks.CATEGORY_DATABASES, + strings: Tasks.CATEGORY_STRINGS, + 'dynamic-programming': Tasks.CATEGORY_DYNAMIC_PROGRAMMING, +}; diff --git a/src/entities/task/model/types/task.ts b/src/entities/task/model/types/task.ts index ddb3c6933..90ce3d330 100644 --- a/src/entities/task/model/types/task.ts +++ b/src/entities/task/model/types/task.ts @@ -1,18 +1,9 @@ import { Response, SortOrder } from '@/shared/libs'; -export type TaskStatus = 'SOLVED' | 'UNSOLVED'; -export type TaskDifficulty = 1 | 2 | 3 | 4 | 5; +import { ProgrammingLanguage } from '@/entities/programmingLanguage/@x/task'; -export interface TaskListItem { - id: string; - name: string; - slug: string; - difficulty: TaskDifficulty; - supportedLanguagesIds: number[]; - status: TaskStatus; - mainCategory: string; - canSolve: boolean; -} +export type TaskStatus = 'solved' | 'attempted' | 'not_started'; +export type TaskDifficulty = 1 | 2 | 3 | 4 | 5; export interface TestCase { input: Record; @@ -22,8 +13,9 @@ export interface TestCase { export interface TaskStructure { languageId: number; - solutionTemplate: string; + solutionStub: string; testFixture: string; + isActive: boolean; } export interface Task { @@ -31,15 +23,39 @@ export interface Task { name: string; slug: string; description: string; + status: TaskStatus; difficulty: TaskDifficulty; - langIds: number[]; - categoryId: number; + supportedLanguages: ProgrammingLanguage[]; + mainCategory: TaskCategoryCode; constraints: string[]; testCases: TestCase[]; taskStructures: TaskStructure[]; solutionSignature: string; + timeLimit: number; + memoryLimit: number; + canSolve: boolean; +} + +export type TaskCategoryCode = + | 'algorithms' + | 'data-structures' + | 'databases' + | 'strings' + | 'arrays' + | 'dynamic-programming'; + +export interface TaskCategory { + id: number; + name: string; + code: TaskCategoryCode; + description: string; + parentCode: string | null; + childrenCodes: TaskCategoryCode[]; + isActive: boolean; } +export type GetTaskCategoriesResponse = TaskCategory[]; + export interface GetTasksListParams { page?: number; limit?: number; @@ -55,10 +71,33 @@ export interface GetTasksListParams { sortOrder?: SortOrder; } -export type GetTasksListResponse = Response; +export type GetTasksListResponse = Response; export type GetTaskByIdResponse = Task; +export type GetTasksProfileSolutionsParamRequest = { + taskId: string; + profileId: string; +}; + +export type TaskSolution = { + id: string; + profileId: string; + taskId: string; + status: TaskStatus; + attemptsCount: number; + lastAttemptAt: string; + solvedAt: string; + bestExecutionTime: number; + bestMemoryUsage: number; + solutionCode: string; + solutionLanguage: ProgrammingLanguage; + createdAt: string; + updatedAt: string; +}; + +export type GetTasksProfileSolutionsResponse = TaskSolution[]; + export interface ExecuteCodeRequest { taskId: string; sourceCode: string; @@ -66,8 +105,10 @@ export interface ExecuteCodeRequest { profileId?: string; } +export type OverallStatus = 'ERROR' | 'SUCCESS'; + export interface ExecuteCodeResponse { - overall_status: string; + overall_status: OverallStatus; passed_tests: number; total_tests: number; success_rate: number; @@ -89,3 +130,16 @@ export interface ExecuteCodeResponse { language_id: number; executed_at: string; } + +export type CreateOrEditTaskFormValues = Omit< + Task, + | 'slug' + | 'supportedLanguages' + | 'status' + | 'solutionSignature' + | 'mainCategory' + | 'testCases' + | 'canSolve' +> & { + categoryCode: TaskCategoryCode; +}; diff --git a/src/entities/task/ui/TaskCard/TaskCard.tsx b/src/entities/task/ui/TaskCard/TaskCard.tsx index 2cba7bf13..2b3f5aa46 100644 --- a/src/entities/task/ui/TaskCard/TaskCard.tsx +++ b/src/entities/task/ui/TaskCard/TaskCard.tsx @@ -5,18 +5,22 @@ import { Card } from '@/shared/ui/Card'; import { Flex } from '@/shared/ui/Flex'; import { Text } from '@/shared/ui/Text'; -import { TaskListItem } from '../../model/types/task'; +import { ProgrammingLanguageList } from '@/entities/programmingLanguage/@x/task'; + +import { Task } from '../../model/types/task'; +import { TaskCategoryChip } from '../TaskCategoryChip/TaskCategoryChip'; import { TaskDifficultyChip } from '../TaskDifficultyChip/TaskDifficultyChip'; +import { TaskStatusChip } from '../TaskStatusChip/TaskStatusChip'; import styles from './TaskCard.module.css'; type TaskCardProps = { - task: TaskListItem; + task: Task; className?: string; }; export const TaskCard = ({ task, className }: TaskCardProps) => { - const { id, name, difficulty } = task; + const { id, name, difficulty, mainCategory, status, supportedLanguages } = task; const taskPath = generatePath(ROUTES.liveCoding.tasks.detail.page, { taskId: id }); @@ -31,7 +35,10 @@ export const TaskCard = ({ task, className }: TaskCardProps) => { + + + diff --git a/src/entities/task/ui/TaskCategoryChip/TaskCategoryChip.tsx b/src/entities/task/ui/TaskCategoryChip/TaskCategoryChip.tsx new file mode 100644 index 000000000..678b007eb --- /dev/null +++ b/src/entities/task/ui/TaskCategoryChip/TaskCategoryChip.tsx @@ -0,0 +1,25 @@ +import { useTranslation } from 'react-i18next'; + +import { i18Namespace } from '@/shared/config'; +import { StatusChip } from '@/shared/ui/StatusChip'; + +import { taskCategories } from '../../model/constants/task'; +import { TaskCategoryCode } from '../../model/types/task'; + +interface TaskCategoryChipProps { + category: TaskCategoryCode; +} + +export const TaskCategoryChip = ({ category }: TaskCategoryChipProps) => { + const { t } = useTranslation(i18Namespace.task); + + return ( + + ); +}; diff --git a/src/entities/task/ui/TaskCategorySelect/TaskCategorySelect.tsx b/src/entities/task/ui/TaskCategorySelect/TaskCategorySelect.tsx new file mode 100644 index 000000000..d766b66b0 --- /dev/null +++ b/src/entities/task/ui/TaskCategorySelect/TaskCategorySelect.tsx @@ -0,0 +1,127 @@ +import { ComponentProps, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { i18Namespace, Marketplace, Tasks } from '@/shared/config'; +import { Dropdown, Option } from '@/shared/ui/Dropdown'; +import { SelectWithChips } from '@/shared/ui/SelectWithChips'; + +import { useGetTaskCategoriesQuery } from '../../api/taskApi'; +import { taskCategories } from '../../model/constants/task'; + +type TaskCategorySelectProps = Omit< + ComponentProps, + 'options' | 'type' | 'value' | 'onChange' | 'children' +> & { + value: string | string[]; + onChange: (value: string[] | string) => void; + hasMultiple?: boolean; + disabled?: boolean; +}; + +type TaskCategoryType = { + id: string; + title: string; +}; + +export const TaskCategorySelect = ({ + onChange, + value, + hasMultiple, + disabled, +}: TaskCategorySelectProps) => { + const { t } = useTranslation(i18Namespace.task); + + const { data } = useGetTaskCategoriesQuery(); + + const categories = data?.map((category) => ({ + id: category.code, + title: t(taskCategories[category.code]), + })); + + const [selectedCategories, setSelectedCategories] = useState( + Array.isArray(value) ? value : value !== undefined ? [value] : [], + ); + + const handleChangeCategory = (newValue: string | undefined) => { + if (disabled || !newValue) return; + const strValue = newValue; + + if (hasMultiple) { + const updates = [...selectedCategories, strValue]; + setSelectedCategories(updates); + onChange(updates); + } else { + setSelectedCategories([strValue]); + onChange(strValue); + } + }; + + const handleDeleteCategory = (id: string) => () => { + if (disabled) return; + const updates = selectedCategories.filter((categoryId) => categoryId !== id); + setSelectedCategories(updates); + onChange(updates); + }; + + const options = useMemo(() => { + if (hasMultiple) { + return (categories || []) + .map((category) => ({ + label: category.title, + value: category.id, + limit: 100, + })) + .filter((category) => !selectedCategories?.includes(category.value)); + } else { + return (categories || []).map((category) => ({ + label: category.title, + value: category.id, + limit: 100, + })); + } + }, [selectedCategories, categories]); + + const categoriesDictionary = useMemo(() => { + const emptyCategory: TaskCategoryType = { + id: '0', + title: t(Tasks.SELECT_CHOOSE), + }; + return (categories || []).reduce( + (acc, category) => { + acc[category.id] = category; + return acc; + }, + { '0': emptyCategory } as Record, + ); + }, [categories]); + + if (!hasMultiple) { + return ( + <> + handleChangeCategory(String(val))} + > + {options.map((option) => ( + + + ); + } + + return ( + + ); +}; diff --git a/src/entities/task/ui/TaskConstraintsField/TaskConstraintsField.tsx b/src/entities/task/ui/TaskConstraintsField/TaskConstraintsField.tsx new file mode 100644 index 000000000..59c656366 --- /dev/null +++ b/src/entities/task/ui/TaskConstraintsField/TaskConstraintsField.tsx @@ -0,0 +1,51 @@ +import { useEffect } from 'react'; +import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import { i18Namespace, Tasks } from '@/shared/config'; +import { Button } from '@/shared/ui/Button'; +import { Flex } from '@/shared/ui/Flex'; +import { Icon } from '@/shared/ui/Icon'; +import { IconButton } from '@/shared/ui/IconButton'; +import { Input } from '@/shared/ui/Input'; + +interface TaskConstraintsFieldProps { + hasError?: boolean; +} + +export const TaskConstraintsField = ({ hasError }: TaskConstraintsFieldProps) => { + const { t } = useTranslation(i18Namespace.task); + const { control } = useFormContext(); + const { + fields: constraints, + append, + remove, + } = useFieldArray({ + control, + name: 'constraints', + }); + + useEffect(() => { + if (constraints.length === 0) { + append(''); + } + }, [constraints.length, append]); + + return ( + + {constraints.map((constraint, index) => ( + + } + /> + {constraints.length > 1 && ( + remove(index)} />} /> + )} + + ))} + + + ); +}; diff --git a/src/entities/task/ui/TaskDescription/TaskDescription.module.css b/src/entities/task/ui/TaskDescription/TaskDescription.module.css index 2f19bc418..ec19057f9 100644 --- a/src/entities/task/ui/TaskDescription/TaskDescription.module.css +++ b/src/entities/task/ui/TaskDescription/TaskDescription.module.css @@ -1,7 +1,3 @@ -.wrapper { - padding: 0 16px 16px; -} - .constraints { margin-top: 24px; } diff --git a/src/entities/task/ui/TaskDescription/TaskDescription.tsx b/src/entities/task/ui/TaskDescription/TaskDescription.tsx index 218bfb5c2..66a9113f0 100644 --- a/src/entities/task/ui/TaskDescription/TaskDescription.tsx +++ b/src/entities/task/ui/TaskDescription/TaskDescription.tsx @@ -1,10 +1,16 @@ import { useTranslation } from 'react-i18next'; -import { i18Namespace, Task as TaskTranslations } from '@/shared/config'; +import { i18Namespace, Tasks } from '@/shared/config'; +import { Flex } from '@/shared/ui/Flex'; import { Text } from '@/shared/ui/Text'; import { TextHtml } from '@/shared/ui/TextHtml'; +import { ProgrammingLanguageList } from '@/entities/programmingLanguage/@x/task'; +import { TaskCategoryChip } from '@/entities/task/ui/TaskCategoryChip/TaskCategoryChip'; + import type { Task } from '../../model/types/task'; +import { TaskDifficultyChip } from '../TaskDifficultyChip/TaskDifficultyChip'; +import { TaskStatusChip } from '../TaskStatusChip/TaskStatusChip'; import styles from './TaskDescription.module.css'; @@ -15,14 +21,22 @@ type TaskDescriptionProps = { export const TaskDescription = ({ task }: TaskDescriptionProps) => { const { t } = useTranslation(i18Namespace.task); return ( -
    - {task.name} - + + + {task.name} + + + + + + + + {task.constraints?.length > 0 && (
    - {t(TaskTranslations.DESCRIPTION_CONSTRAINTS_TITLE)} + {t(Tasks.DESCRIPTION_CONSTRAINTS_TITLE)}
      @@ -34,6 +48,6 @@ export const TaskDescription = ({ task }: TaskDescriptionProps) => {
    )} -
    + ); }; diff --git a/src/entities/task/ui/TaskForm/TaskForm.module.css b/src/entities/task/ui/TaskForm/TaskForm.module.css new file mode 100644 index 000000000..dd0dc20ee --- /dev/null +++ b/src/entities/task/ui/TaskForm/TaskForm.module.css @@ -0,0 +1,56 @@ +.difficulty { + position: relative; + top: -15px; + width: 360px; +} + + + + +.text-wrapper { + width: 100%; + max-width: 246px; +} + +.main-title { + margin-bottom: 28px; +} + + +.textarea { + display: block; + margin-top: 20px; + width: 100%; + height: 200px; + min-height: 80px; + resize: vertical; + border-radius: 16px; +} + +.skills-input { + align-items: center; + height: 48px; +} + +.skills-select { + align-items: flex-start; +} + +.input-form { + width: 100%; + min-width: 100px; + max-width: 408px; +} + +.select { + width: 100%; + max-width: 706px; +} + +.select > span { + display: inline-block; + margin-bottom: 8px; + color: var(--text-color-light); + font-weight: var(--font-weight-400); + font-size: var(--font-size-p-s); +} \ No newline at end of file diff --git a/src/entities/task/ui/TaskForm/TaskForm.tsx b/src/entities/task/ui/TaskForm/TaskForm.tsx new file mode 100644 index 000000000..84717ca9a --- /dev/null +++ b/src/entities/task/ui/TaskForm/TaskForm.tsx @@ -0,0 +1,97 @@ +import { useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import { i18Namespace, Tasks } from '@/shared/config'; +import { Flex } from '@/shared/ui/Flex'; +import { FormControl } from '@/shared/ui/FormControl'; +import { FormField } from '@/shared/ui/FormField'; +import { Input } from '@/shared/ui/Input'; +import { Range } from '@/shared/ui/Range'; +import { Text } from '@/shared/ui/Text'; +import { TextEditor } from '@/shared/ui/TextEditor'; + +import { CreateOrEditTaskFormValues } from '../../model/types/task'; +import { TaskCategorySelect } from '../TaskCategorySelect/TaskCategorySelect'; +import { TaskConstraintsField } from '../TaskConstraintsField/TaskConstraintsField'; +import { TaskStructuresField } from '../TaskStructuresField/TaskStructuresField'; + +import styles from './TaskForm.module.css'; + +interface TaskFormProps { + isEdit?: boolean; +} + +export const TaskForm = ({ isEdit }: TaskFormProps) => { + const { t } = useTranslation(i18Namespace.task); + + const { control } = useFormContext(); + + return ( + <> + + {isEdit ? t(Tasks.EDIT_PAGE_TITLE) : t(Tasks.CREATE_PAGE_TITLE)} + + + + + {(register, hasError) => } + + + + + {(field) => ( + + )} + + + + + {({ onChange, value }) => } + + + + + {(field) => } + + + + + {(register, hasError) => } + + + + + {(register, hasError) => } + + + + + {(_, hasError) => } + + + + + {() => } + + + + + ); +}; diff --git a/src/entities/task/ui/TaskSolutionInfo/TaskSolutionInfo.module.css b/src/entities/task/ui/TaskSolutionInfo/TaskSolutionInfo.module.css new file mode 100644 index 000000000..1c957cead --- /dev/null +++ b/src/entities/task/ui/TaskSolutionInfo/TaskSolutionInfo.module.css @@ -0,0 +1,7 @@ +.button { + color: var(--color-black-500); +} + +.code { + width: 100%; +} \ No newline at end of file diff --git a/src/entities/task/ui/TaskSolutionInfo/TaskSolutionInfo.tsx b/src/entities/task/ui/TaskSolutionInfo/TaskSolutionInfo.tsx new file mode 100644 index 000000000..44fbff66b --- /dev/null +++ b/src/entities/task/ui/TaskSolutionInfo/TaskSolutionInfo.tsx @@ -0,0 +1,41 @@ +import { useTranslation } from 'react-i18next'; + +import { i18Namespace, Tasks } from '@/shared/config'; +import { Button } from '@/shared/ui/Button'; +import { Flex } from '@/shared/ui/Flex'; +import { Icon } from '@/shared/ui/Icon'; +import { Text } from '@/shared/ui/Text'; +import { TextHtml } from '@/shared/ui/TextHtml'; + +import { TaskSolution } from '../../model/types/task'; + +import styles from './TaskSolutionInfo.module.css'; + +interface TaskSolutionInfoProps { + solution: TaskSolution; + setSelectedSolution: (solution: TaskSolution | null) => void; +} + +export const TaskSolutionInfo = ({ solution, setSelectedSolution }: TaskSolutionInfoProps) => { + const { t } = useTranslation(i18Namespace.task); + + return ( + + + + {t( + solution.status === 'solved' ? Tasks.TABLE_STATUS_SOLVED : Tasks.TABLE_STATUS_NOT_SOLVED, + )} + + ${solution.solutionCode}`} /> + + ); +}; diff --git a/src/entities/task/ui/TaskSolutions/TaskSolutions.tsx b/src/entities/task/ui/TaskSolutions/TaskSolutions.tsx index f3e34f9a5..8679d7f11 100644 --- a/src/entities/task/ui/TaskSolutions/TaskSolutions.tsx +++ b/src/entities/task/ui/TaskSolutions/TaskSolutions.tsx @@ -1,32 +1,91 @@ +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; -import { i18Namespace, Task as TaskTranslations } from '@/shared/config'; +import { i18Namespace, Tasks } from '@/shared/config'; +import { useAppSelector } from '@/shared/libs'; import { Stub } from '@/shared/ui/Stub'; +import { Table } from '@/shared/ui/Table'; +import { Text } from '@/shared/ui/Text'; -type TaskSolutionsProps = { - solutions?: string[]; -}; +import { getProfileId } from '@/entities/profile/@x/task'; + +import { useGetTasksProfileSolutionsQuery } from '../../api/taskApi'; +import { TaskSolution } from '../../model/types/task'; +import { TaskSolutionInfo } from '../TaskSolutionInfo/TaskSolutionInfo'; -export const TaskSolutions = ({ solutions }: TaskSolutionsProps) => { +export const TaskSolutions = () => { + const { taskId = '' } = useParams>(); const { t } = useTranslation(i18Namespace.task); + const profileId = useAppSelector(getProfileId); + const [selectedSolution, setSelectedSolution] = useState(null); + + const { data: solutions } = useGetTasksProfileSolutionsQuery({ profileId, taskId }); if (!solutions || solutions.length === 0) { return ( ); } - return ( -
    - {solutions.map((solution, index) => ( -
    -					{solution}
    -				
    - ))} -
    + const renderTableColumnWidths = () => { + const columnWidths = { + title: 'auto', + description: 'auto', + type: '15%', + specialization: '20%', + }; + + return Object.values(columnWidths)?.map((width, idx) => ); + }; + + const renderTableHeader = () => { + const columns = { + status: t(Tasks.TABLE_STATUS), + language: t(Tasks.TABLE_LANGUAGE), + time: t(Tasks.TABLE_TIME), + memory: t(Tasks.TABLE_MEMORY), + }; + + return Object.entries(columns)?.map(([k, v]) => {v}); + }; + + const renderTableBody = (solution: TaskSolution) => { + const columns = { + status: ( + + {t( + solution.status === 'solved' + ? Tasks.TABLE_STATUS_SOLVED + : Tasks.TABLE_STATUS_NOT_SOLVED, + )} + + ), + language: solution.solutionLanguage.name, + time: t(Tasks.TIME_LIMIT_VALUE, { count: solution.bestExecutionTime || 0 }), + memory: t(Tasks.MEMORY_LIMIT_VALUE, { count: solution.bestMemoryUsage || 0 }), + }; + + return Object.entries(columns)?.map(([k, v]) => {v}); + }; + + const onRowClick = (solution: TaskSolution) => { + setSelectedSolution(solution); + }; + + return selectedSolution ? ( + + ) : ( + ); }; diff --git a/src/entities/task/ui/TaskStatusChip/TaskStatusChip.tsx b/src/entities/task/ui/TaskStatusChip/TaskStatusChip.tsx new file mode 100644 index 000000000..dcb0e766a --- /dev/null +++ b/src/entities/task/ui/TaskStatusChip/TaskStatusChip.tsx @@ -0,0 +1,32 @@ +import { useTranslation } from 'react-i18next'; + +import { i18Namespace, Tasks } from '@/shared/config'; +import { StatusChip, StatusChipItem, StatusChipSize } from '@/shared/ui/StatusChip'; + +import type { TaskStatus } from '../../model/types/task'; + +interface TaskStatusChipProps { + status: TaskStatus; + size?: StatusChipSize; +} + +export const TaskStatusChip = ({ status, size = 'small' }: TaskStatusChipProps) => { + const { t } = useTranslation(i18Namespace.task); + + const statuses: Record = { + attempted: { + text: t(Tasks.STATUS_ATTEMPTED), + variant: 'yellow', + }, + not_started: { + text: t(Tasks.STATUS_NOT_STARTED), + variant: 'purple', + }, + solved: { + text: t(Tasks.STATUS_SOLVED), + variant: 'green', + }, + }; + + return ; +}; diff --git a/src/entities/task/ui/TaskStructuresField/TaskStructuresField.module.css b/src/entities/task/ui/TaskStructuresField/TaskStructuresField.module.css new file mode 100644 index 000000000..507d09ec2 --- /dev/null +++ b/src/entities/task/ui/TaskStructuresField/TaskStructuresField.module.css @@ -0,0 +1,19 @@ +.editor { + padding: 16px; + width: 100%; + height: 200px; + border: 1px solid var(--color-black-50); + border-radius: 20px; + background-color: var(--color-white-900); +} + +.delete { + position: absolute; + top: 8px; + right: 8px; +} + +.add { + margin-left: auto; + width: 300px; +} \ No newline at end of file diff --git a/src/entities/task/ui/TaskStructuresField/TaskStructuresField.tsx b/src/entities/task/ui/TaskStructuresField/TaskStructuresField.tsx new file mode 100644 index 000000000..707b9fa54 --- /dev/null +++ b/src/entities/task/ui/TaskStructuresField/TaskStructuresField.tsx @@ -0,0 +1,133 @@ +import MonacoEditor from '@monaco-editor/react'; +import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import { i18Namespace, Tasks } from '@/shared/config'; +import { Button } from '@/shared/ui/Button'; +import { Card } from '@/shared/ui/Card'; +import { Flex } from '@/shared/ui/Flex'; +import { FormField } from '@/shared/ui/FormField'; +import { Icon } from '@/shared/ui/Icon'; +import { IconButton } from '@/shared/ui/IconButton'; +import { Switch } from '@/shared/ui/Switch'; + +import { + ProgrammingLanguageCode, + ProgrammingLanguageSelect, + useGetLanguagesQuery, +} from '@/entities/programmingLanguage/@x/task'; +import { CreateOrEditTaskFormValues } from '@/entities/task'; + +import styles from './TaskStructuresField.module.css'; + +type LanguageOptions = Record; + +export const TaskStructuresField = () => { + const { t } = useTranslation(i18Namespace.task); + const { control, watch } = useFormContext(); + const { + fields: taskStructures, + append, + remove, + } = useFieldArray({ + control, + name: 'taskStructures', + }); + + const { data } = useGetLanguagesQuery(); + + const taskStructuresValues = watch('taskStructures'); + + const languages = + data?.reduce((result, language) => { + result[language.id] = language.monacoLangId; + return result; + }, {} as LanguageOptions) || {}; + + const selectedLanguages = taskStructuresValues.map(({ languageId }) => Number(languageId)); + + return ( + + {taskStructures.map((structure, index) => ( + + + + } + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + {taskStructures.length > 1 && ( + remove(index)} />} + /> + )} + + + ))} + + + ); +}; diff --git a/src/features/task/createTask/api/createTaskApi.ts b/src/features/task/createTask/api/createTaskApi.ts new file mode 100644 index 000000000..b81137851 --- /dev/null +++ b/src/features/task/createTask/api/createTaskApi.ts @@ -0,0 +1,33 @@ +import { ApiTags, baseApi, ExtraArgument, i18n, ROUTES, Translation } from '@/shared/config'; +import { route } from '@/shared/libs'; +import { toast } from '@/shared/ui/Toast'; + +import { createTaskApiUrls } from '../model/constants/createTaskConstants'; +import { CreateTaskBodyRequest, CreateTaskResponse } from '../model/types/taskCreateTypes'; + +export const createTaskApi = baseApi.injectEndpoints({ + endpoints: (build) => ({ + createTask: build.mutation({ + query: (task) => ({ + url: createTaskApiUrls.createTask, + method: 'POST', + body: task, + }), + async onQueryStarted(_, { queryFulfilled, extra }) { + try { + const result = await queryFulfilled; + const typedExtra = extra as ExtraArgument; + typedExtra.navigate(route(ROUTES.admin.tasks.details.page, result.data.id)); + toast.success(i18n.t(Translation.TOAST_TASKS_CREATE_SUCCESS)); + } catch (error) { + toast.error(i18n.t(Translation.TOAST_TASKS_CREATE_FAILED)); + // eslint-disable-next-line no-console + console.error(error); + } + }, + invalidatesTags: [ApiTags.TASKS], + }), + }), +}); + +export const { useCreateTaskMutation } = createTaskApi; diff --git a/src/features/task/createTask/index.ts b/src/features/task/createTask/index.ts new file mode 100644 index 000000000..f1031fc63 --- /dev/null +++ b/src/features/task/createTask/index.ts @@ -0,0 +1 @@ +export { TaskCreateForm } from './ui/TaskCreateForm/TaskCreateForm'; diff --git a/src/features/task/createTask/lib/validation/taskCreateSchema.ts b/src/features/task/createTask/lib/validation/taskCreateSchema.ts new file mode 100644 index 000000000..499b129d9 --- /dev/null +++ b/src/features/task/createTask/lib/validation/taskCreateSchema.ts @@ -0,0 +1,44 @@ +import * as yup from 'yup'; + +import { i18n, Translation } from '@/shared/config'; + +import { TaskCategoryCode, TaskDifficulty, TaskStructure } from '@/entities/task'; + +import { CreateTaskFormValues } from '../../model/types/taskCreateTypes'; + +export const taskCreateSchema: yup.ObjectSchema = yup.object().shape({ + name: yup.string().required(i18n.t(Translation.VALIDATION_REQUIRED)), + description: yup.string().required(i18n.t(Translation.VALIDATION_REQUIRED)), + difficulty: yup + .number() + .transform((value) => (Number.isNaN(value) ? null : value)) + .required(i18n.t(Translation.VALIDATION_REQUIRED)), + categoryCode: yup.string().required(i18n.t(Translation.VALIDATION_REQUIRED)), + memoryLimit: yup.number().required(i18n.t(Translation.VALIDATION_REQUIRED)), + timeLimit: yup.number().required(i18n.t(Translation.VALIDATION_REQUIRED)), + constraints: yup + .array() + .of(yup.string().required()) + .required(i18n.t(Translation.VALIDATION_REQUIRED)), + // testCases: yup + // .array() + // .of( + // yup.object().shape({ + // input: yup.object().required(i18n.t(Translation.VALIDATION_REQUIRED)), + // expected_output: yup.mixed().required(i18n.t(Translation.VALIDATION_REQUIRED)), + // is_hidden: yup.boolean().required(i18n.t(Translation.VALIDATION_REQUIRED)), + // }), + // ) + // .required(i18n.t(Translation.VALIDATION_REQUIRED)), + taskStructures: yup + .array() + .of( + yup.object().shape({ + languageId: yup.number().required(i18n.t(Translation.VALIDATION_REQUIRED)), + solutionStub: yup.string().required(i18n.t(Translation.VALIDATION_REQUIRED)), + testFixture: yup.string().required(i18n.t(Translation.VALIDATION_REQUIRED)), + isActive: yup.boolean().required(i18n.t(Translation.VALIDATION_REQUIRED)), + }), + ) + .required(), +}); diff --git a/src/features/task/createTask/model/constants/createTaskConstants.ts b/src/features/task/createTask/model/constants/createTaskConstants.ts new file mode 100644 index 000000000..fb21a04fd --- /dev/null +++ b/src/features/task/createTask/model/constants/createTaskConstants.ts @@ -0,0 +1,5 @@ +import { API_VERSION } from '@/shared/config'; + +export const createTaskApiUrls = { + createTask: `${API_VERSION.V1}/live-coding/tasks`, +}; diff --git a/src/features/task/createTask/model/types/taskCreateTypes.ts b/src/features/task/createTask/model/types/taskCreateTypes.ts new file mode 100644 index 000000000..74d99418a --- /dev/null +++ b/src/features/task/createTask/model/types/taskCreateTypes.ts @@ -0,0 +1,6 @@ +import { CreateOrEditTaskFormValues, Task } from '@/entities/task'; + +export type CreateTaskFormValues = Omit; + +export type CreateTaskBodyRequest = CreateTaskFormValues; +export type CreateTaskResponse = Task; diff --git a/src/features/task/createTask/ui/TaskCreateForm/TaskCreateForm.module.css b/src/features/task/createTask/ui/TaskCreateForm/TaskCreateForm.module.css new file mode 100644 index 000000000..bc2c6233a --- /dev/null +++ b/src/features/task/createTask/ui/TaskCreateForm/TaskCreateForm.module.css @@ -0,0 +1,3 @@ +.content { + flex: 1; +} \ No newline at end of file diff --git a/src/features/task/createTask/ui/TaskCreateForm/TaskCreateForm.tsx b/src/features/task/createTask/ui/TaskCreateForm/TaskCreateForm.tsx new file mode 100644 index 000000000..b2a05708a --- /dev/null +++ b/src/features/task/createTask/ui/TaskCreateForm/TaskCreateForm.tsx @@ -0,0 +1,43 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { Card } from '@/shared/ui/Card'; +import { Flex } from '@/shared/ui/Flex'; +import { LeavingPageBlocker } from '@/shared/ui/LeavingPageBlocker'; + +import { TaskForm } from '@/entities/task'; + +import { taskCreateSchema } from '../../lib/validation/taskCreateSchema'; +import { CreateTaskFormValues } from '../../model/types/taskCreateTypes'; +import { TaskCreateFormHeader } from '../TaskCreateFormHeader/TaskCreateFormHeader'; + +import styles from './TaskCreateForm.module.css'; + +export const TaskCreateForm = () => { + const taskMethods = useForm({ + resolver: yupResolver(taskCreateSchema), + mode: 'onTouched', + defaultValues: { + constraints: [''], + taskStructures: [{ languageId: 0, solutionStub: '', testFixture: '', isActive: true }], + difficulty: 3, + }, + }); + + const { isDirty, isSubmitting, isSubmitted } = taskMethods.formState; + + return ( + <> + + + + + + + + + + + + ); +}; diff --git a/src/features/task/createTask/ui/TaskCreateFormHeader/TaskCreateFormHeader.tsx b/src/features/task/createTask/ui/TaskCreateFormHeader/TaskCreateFormHeader.tsx new file mode 100644 index 000000000..0a89f8614 --- /dev/null +++ b/src/features/task/createTask/ui/TaskCreateFormHeader/TaskCreateFormHeader.tsx @@ -0,0 +1,29 @@ +import { useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import { i18Namespace, Translation } from '@/shared/config'; +import { BackButton } from '@/shared/ui/BackButton'; +import { Button } from '@/shared/ui/Button'; +import { Flex } from '@/shared/ui/Flex'; + +import { useCreateTaskMutation } from '../../api/createTaskApi'; +import { CreateTaskFormValues } from '../../model/types/taskCreateTypes'; + +export const TaskCreateFormHeader = () => { + const [createTaskMutation, { isLoading }] = useCreateTaskMutation(); + const { handleSubmit } = useFormContext(); + const { t } = useTranslation(i18Namespace.translation); + + const onCreateTask = async (data: CreateTaskFormValues) => { + await createTaskMutation(data); + }; + + return ( + + + + + ); +}; diff --git a/src/features/task/deleteTask/api/deleteTaskApi.ts b/src/features/task/deleteTask/api/deleteTaskApi.ts new file mode 100644 index 000000000..aac823beb --- /dev/null +++ b/src/features/task/deleteTask/api/deleteTaskApi.ts @@ -0,0 +1,31 @@ +import { i18n, Translation, ApiTags, baseApi, ROUTES, ExtraArgument } from '@/shared/config'; +import { route } from '@/shared/libs'; +import { toast } from '@/shared/ui/Toast'; + +import { deleteTaskApiUrls } from '../model/constants/deleteTaskConstants'; + +const deleteTaskApi = baseApi.injectEndpoints({ + endpoints: (build) => ({ + deleteTask: build.mutation({ + query: (taskId) => ({ + url: route(deleteTaskApiUrls.deleteTask, taskId), + method: 'DELETE', + }), + invalidatesTags: [ApiTags.TASKS, ApiTags.TASK_DETAIL], + async onQueryStarted(_, { queryFulfilled, extra }) { + try { + await queryFulfilled; + const typedExtra = extra as ExtraArgument; + toast.success(i18n.t(Translation.TOAST_TASKS_DELETE_SINGLE_SUCCESS)); + typedExtra.navigate(ROUTES.admin.tasks.page); + } catch (error) { + toast.error(i18n.t(Translation.TOAST_TASKS_DELETE_SINGLE_FAILED)); + // eslint-disable-next-line no-console + console.error(error); + } + }, + }), + }), +}); + +export const { useDeleteTaskMutation } = deleteTaskApi; diff --git a/src/features/task/deleteTask/index.ts b/src/features/task/deleteTask/index.ts new file mode 100644 index 000000000..48d632ba5 --- /dev/null +++ b/src/features/task/deleteTask/index.ts @@ -0,0 +1,2 @@ +export { DeleteTaskButton } from './ui/DeleteTaskButton/DeleteTaskButton'; +export { DeleteTaskButtonSkeleton } from './ui/DeleteTaskButton/DeleteTaskButton.skeleton'; diff --git a/src/features/task/deleteTask/model/constants/deleteTaskConstants.ts b/src/features/task/deleteTask/model/constants/deleteTaskConstants.ts new file mode 100644 index 000000000..1a13d443b --- /dev/null +++ b/src/features/task/deleteTask/model/constants/deleteTaskConstants.ts @@ -0,0 +1,5 @@ +import { API_VERSION } from '@/shared/config'; + +export const deleteTaskApiUrls = { + deleteTask: `${API_VERSION.V1}/live-coding/tasks/:taskId`, +}; diff --git a/src/features/task/deleteTask/ui/DeleteTaskButton/DeleteTaskButton.module.css b/src/features/task/deleteTask/ui/DeleteTaskButton/DeleteTaskButton.module.css new file mode 100644 index 000000000..02f357880 --- /dev/null +++ b/src/features/task/deleteTask/ui/DeleteTaskButton/DeleteTaskButton.module.css @@ -0,0 +1,5 @@ +.detail-button { + justify-content: flex-start; + padding: 6px 10px; + border-radius: 12px; +} \ No newline at end of file diff --git a/src/features/task/deleteTask/ui/DeleteTaskButton/DeleteTaskButton.skeleton.tsx b/src/features/task/deleteTask/ui/DeleteTaskButton/DeleteTaskButton.skeleton.tsx new file mode 100644 index 000000000..285de0317 --- /dev/null +++ b/src/features/task/deleteTask/ui/DeleteTaskButton/DeleteTaskButton.skeleton.tsx @@ -0,0 +1,16 @@ +import { ButtonSkeleton } from '@/shared/ui/Button'; + +import { DeleteTaskButtonProps } from './DeleteTaskButton'; + +export const DeleteTaskButtonSkeleton = ({ + isDetailPage = false, +}: Partial) => { + return ( + + ); +}; diff --git a/src/features/task/deleteTask/ui/DeleteTaskButton/DeleteTaskButton.tsx b/src/features/task/deleteTask/ui/DeleteTaskButton/DeleteTaskButton.tsx new file mode 100644 index 000000000..ad22b71e2 --- /dev/null +++ b/src/features/task/deleteTask/ui/DeleteTaskButton/DeleteTaskButton.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { i18Namespace, Translation } from '@/shared/config'; +import { BlockerDialog } from '@/shared/ui/BlockerDialogModal'; +import { Button } from '@/shared/ui/Button'; +import { Icon } from '@/shared/ui/Icon'; + +import { useDeleteTaskMutation } from '../../api/deleteTaskApi'; + +export interface DeleteTaskButtonProps { + taskId: string; + isDetailPage?: boolean; +} + +export const DeleteTaskButton = ({ taskId, isDetailPage = false }: DeleteTaskButtonProps) => { + const [deleteTaskMutation] = useDeleteTaskMutation(); + + const { t } = useTranslation(i18Namespace.translation); + const [isDeleteModalOpen, setIsModalOpen] = useState(false); + + const handleOpenModal = () => { + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + }; + + const onDeleteTask = async () => { + try { + await deleteTaskMutation(taskId); + handleCloseModal(); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + }; + + return ( + <> + + {isDeleteModalOpen && ( + + )} + + ); +}; diff --git a/src/features/task/editTask/api/editTaskApi.ts b/src/features/task/editTask/api/editTaskApi.ts new file mode 100644 index 000000000..aa7691b5b --- /dev/null +++ b/src/features/task/editTask/api/editTaskApi.ts @@ -0,0 +1,33 @@ +import { ApiTags, baseApi, i18n, Translation, ROUTES, ExtraArgument } from '@/shared/config'; +import { route } from '@/shared/libs'; +import { toast } from '@/shared/ui/Toast'; + +import { editTaskApiUrls } from '../model/constants/editTaskConstants'; +import { EditTaskBodyRequest, EditTaskResponse } from '../model/types/taskEditTypes'; + +export const editTaskApi = baseApi.injectEndpoints({ + endpoints: (build) => ({ + editTask: build.mutation({ + query: (task) => ({ + url: route(editTaskApiUrls.editTask, task.id), + method: 'PUT', + body: task, + }), + async onQueryStarted(_, { queryFulfilled, extra }) { + try { + const result = await queryFulfilled; + const typedExtra = extra as ExtraArgument; + typedExtra.navigate(route(ROUTES.admin.tasks.details.page, result.data.id)); + toast.success(i18n.t(Translation.TOAST_TASKS_EDIT_SUCCESS)); + } catch (error) { + toast.error(i18n.t(Translation.TOAST_TASKS_EDIT_FAILED)); + // eslint-disable-next-line no-console + console.error(error); + } + }, + invalidatesTags: [ApiTags.TASKS, ApiTags.TASK_DETAIL], + }), + }), +}); + +export const { useEditTaskMutation } = editTaskApi; diff --git a/src/features/task/editTask/index.ts b/src/features/task/editTask/index.ts new file mode 100644 index 000000000..12e5be79f --- /dev/null +++ b/src/features/task/editTask/index.ts @@ -0,0 +1,2 @@ +export { TaskEditForm } from './ui/TaskEditForm/TaskEditForm'; +export { TaskEditButton } from './ui/TaskEditButton/TaskEditButton'; diff --git a/src/features/task/editTask/lib/validation/taskEditSchema.ts b/src/features/task/editTask/lib/validation/taskEditSchema.ts new file mode 100644 index 000000000..18adc217d --- /dev/null +++ b/src/features/task/editTask/lib/validation/taskEditSchema.ts @@ -0,0 +1,45 @@ +import * as yup from 'yup'; + +import { i18n, Translation } from '@/shared/config'; + +import { TaskCategoryCode, TaskDifficulty, TaskStructure } from '@/entities/task'; + +import { EditTaskFormValues } from '../../model/types/taskEditTypes'; + +export const taskEditSchema: yup.ObjectSchema = yup.object().shape({ + id: yup.string().required(i18n.t(Translation.VALIDATION_REQUIRED)), + name: yup.string().required(i18n.t(Translation.VALIDATION_REQUIRED)), + description: yup.string().required(i18n.t(Translation.VALIDATION_REQUIRED)), + difficulty: yup + .number() + .transform((value) => (Number.isNaN(value) ? null : value)) + .required(i18n.t(Translation.VALIDATION_REQUIRED)), + categoryCode: yup.string().required(i18n.t(Translation.VALIDATION_REQUIRED)), + memoryLimit: yup.number().required(i18n.t(Translation.VALIDATION_REQUIRED)), + timeLimit: yup.number().required(i18n.t(Translation.VALIDATION_REQUIRED)), + constraints: yup + .array() + .of(yup.string().required()) + .required(i18n.t(Translation.VALIDATION_REQUIRED)), + // testCases: yup + // .array() + // .of( + // yup.object().shape({ + // input: yup.object().required(i18n.t(Translation.VALIDATION_REQUIRED)), + // expected_output: yup.mixed().required(i18n.t(Translation.VALIDATION_REQUIRED)), + // is_hidden: yup.boolean().required(i18n.t(Translation.VALIDATION_REQUIRED)), + // }), + // ) + // .required(i18n.t(Translation.VALIDATION_REQUIRED)), + taskStructures: yup + .array() + .of( + yup.object().shape({ + languageId: yup.number().required(i18n.t(Translation.VALIDATION_REQUIRED)), + solutionStub: yup.string().required(i18n.t(Translation.VALIDATION_REQUIRED)), + testFixture: yup.string().required(i18n.t(Translation.VALIDATION_REQUIRED)), + isActive: yup.boolean().required(i18n.t(Translation.VALIDATION_REQUIRED)), + }), + ) + .required(), +}); diff --git a/src/features/task/editTask/model/constants/editTaskConstants.ts b/src/features/task/editTask/model/constants/editTaskConstants.ts new file mode 100644 index 000000000..ac32e90e9 --- /dev/null +++ b/src/features/task/editTask/model/constants/editTaskConstants.ts @@ -0,0 +1,5 @@ +import { API_VERSION } from '@/shared/config'; + +export const editTaskApiUrls = { + editTask: `${API_VERSION.V1}/live-coding/tasks/:taskId`, +}; diff --git a/src/features/task/editTask/model/types/taskEditTypes.ts b/src/features/task/editTask/model/types/taskEditTypes.ts new file mode 100644 index 000000000..98a99bb1a --- /dev/null +++ b/src/features/task/editTask/model/types/taskEditTypes.ts @@ -0,0 +1,6 @@ +import { CreateOrEditTaskFormValues, Task } from '@/entities/task'; + +export type EditTaskFormValues = CreateOrEditTaskFormValues; + +export type EditTaskBodyRequest = EditTaskFormValues; +export type EditTaskResponse = Task; diff --git a/src/features/task/editTask/ui/TaskEditButton/TaskEditButton.tsx b/src/features/task/editTask/ui/TaskEditButton/TaskEditButton.tsx new file mode 100644 index 000000000..20707f0b8 --- /dev/null +++ b/src/features/task/editTask/ui/TaskEditButton/TaskEditButton.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from 'react-i18next'; +import { NavLink } from 'react-router-dom'; + +import { Translation, ROUTES } from '@/shared/config'; +import { route } from '@/shared/libs'; +import { Button } from '@/shared/ui/Button'; + +interface TaskEditButtonProps { + taskId: string; +} + +export const TaskEditButton = ({ taskId }: TaskEditButtonProps) => { + const { t } = useTranslation(); + + return ( + + + + ); +}; diff --git a/src/features/task/editTask/ui/TaskEditForm/TaskEditForm.module.css b/src/features/task/editTask/ui/TaskEditForm/TaskEditForm.module.css new file mode 100644 index 000000000..bc2c6233a --- /dev/null +++ b/src/features/task/editTask/ui/TaskEditForm/TaskEditForm.module.css @@ -0,0 +1,3 @@ +.content { + flex: 1; +} \ No newline at end of file diff --git a/src/features/task/editTask/ui/TaskEditForm/TaskEditForm.tsx b/src/features/task/editTask/ui/TaskEditForm/TaskEditForm.tsx new file mode 100644 index 000000000..d3c84dad3 --- /dev/null +++ b/src/features/task/editTask/ui/TaskEditForm/TaskEditForm.tsx @@ -0,0 +1,70 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { Card } from '@/shared/ui/Card'; +import { Flex } from '@/shared/ui/Flex'; +import { LeavingPageBlocker } from '@/shared/ui/LeavingPageBlocker'; + +import { Task, TaskForm } from '@/entities/task'; + +import { useEditTaskMutation } from '../../api/editTaskApi'; +import { taskEditSchema } from '../../lib/validation/taskEditSchema'; +import { EditTaskFormValues } from '../../model/types/taskEditTypes'; +import { TaskEditFormHeader } from '../TaskEditFormHeader/TaskEditFormHeader'; + +import styles from './TaskEditForm.module.css'; + +interface TaskEditFormProps { + task: Task; +} + +export const TaskEditForm = ({ task }: TaskEditFormProps) => { + const { + name, + description, + taskStructures, + difficulty, + constraints, + mainCategory, + id, + timeLimit, + memoryLimit, + } = task; + + const methods = useForm({ + resolver: yupResolver(taskEditSchema), + mode: 'onTouched', + defaultValues: { + name, + description, + taskStructures, + difficulty, + constraints, + categoryCode: mainCategory, + memoryLimit, + timeLimit, + id, + }, + }); + + const { isDirty, isSubmitted, isSubmitting } = methods.formState; + + const [editTopicMutation] = useEditTaskMutation(); + + const onEditTopic = async (data: EditTaskFormValues) => { + await editTopicMutation(data); + }; + + return ( + + + + + + + + + + + ); +}; diff --git a/src/features/task/editTask/ui/TaskEditFormHeader/TaskEditFormHeader.module.css b/src/features/task/editTask/ui/TaskEditFormHeader/TaskEditFormHeader.module.css new file mode 100644 index 000000000..779da0584 --- /dev/null +++ b/src/features/task/editTask/ui/TaskEditFormHeader/TaskEditFormHeader.module.css @@ -0,0 +1,4 @@ +.btn { + width: 170px; + height: 48px; +} \ No newline at end of file diff --git a/src/features/task/editTask/ui/TaskEditFormHeader/TaskEditFormHeader.tsx b/src/features/task/editTask/ui/TaskEditFormHeader/TaskEditFormHeader.tsx new file mode 100644 index 000000000..ff1ec6176 --- /dev/null +++ b/src/features/task/editTask/ui/TaskEditFormHeader/TaskEditFormHeader.tsx @@ -0,0 +1,46 @@ +import { useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import { i18Namespace, Translation } from '@/shared/config'; +import { BackHeader } from '@/shared/ui/BackHeader'; +import { Button } from '@/shared/ui/Button'; +import { VariantType } from '@/shared/ui/IconButton'; + +import { CreateOrEditTaskFormValues } from '@/entities/task'; + +import styles from './TaskEditFormHeader.module.css'; + +interface TaskEditFormHeaderProps { + onSubmit: (formData: T) => Promise; + className?: string; + btnVariant?: VariantType; +} + +export const TaskEditFormHeader = ({ + onSubmit, + className, + btnVariant = 'secondary', +}: TaskEditFormHeaderProps) => { + const { t } = useTranslation(i18Namespace.translation); + + const { handleSubmit, reset, formState } = useFormContext(); + + const onResetFormValues = () => { + reset(); + }; + + return ( + + + + + ); +}; diff --git a/src/features/task/filterTasks/ui/TaskDifficultyFilter/TaskDifficultyFilter.tsx b/src/features/task/filterTasks/ui/TaskDifficultyFilter/TaskDifficultyFilter.tsx index 2f5043426..85a00c481 100644 --- a/src/features/task/filterTasks/ui/TaskDifficultyFilter/TaskDifficultyFilter.tsx +++ b/src/features/task/filterTasks/ui/TaskDifficultyFilter/TaskDifficultyFilter.tsx @@ -1,7 +1,7 @@ import classnames from 'classnames'; import { useTranslation } from 'react-i18next'; -import { i18Namespace, Task } from '@/shared/config'; +import { i18Namespace, Tasks } from '@/shared/config'; import { BaseFilterSection } from '@/shared/ui/BaseFilterSection'; import type { TaskDifficulty } from '@/entities/task'; @@ -45,7 +45,7 @@ export const TaskDifficultyFilter = ({ return ( ( diff --git a/src/features/task/filterTasks/ui/TaskFilters/TasksFilters.tsx b/src/features/task/filterTasks/ui/TaskFilters/TasksFilters.tsx index 6992f79b2..b488feef4 100644 --- a/src/features/task/filterTasks/ui/TaskFilters/TasksFilters.tsx +++ b/src/features/task/filterTasks/ui/TaskFilters/TasksFilters.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next'; -import { i18Namespace, Task } from '@/shared/config'; +import { i18Namespace, Tasks } from '@/shared/config'; import { Flex } from '@/shared/ui/Flex'; import { SearchInput } from '@/shared/ui/SearchInput'; @@ -28,7 +28,7 @@ export const TasksFilters = ({ return ( diff --git a/src/features/task/filterTasks/ui/TaskLanguagesFilter/TaskLanguagesFilter.tsx b/src/features/task/filterTasks/ui/TaskLanguagesFilter/TaskLanguagesFilter.tsx index 68a7b84e4..12fc032a8 100644 --- a/src/features/task/filterTasks/ui/TaskLanguagesFilter/TaskLanguagesFilter.tsx +++ b/src/features/task/filterTasks/ui/TaskLanguagesFilter/TaskLanguagesFilter.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next'; -import { i18Namespace, Task } from '@/shared/config'; +import { i18Namespace, Tasks } from '@/shared/config'; import { BaseFilterSection } from '@/shared/ui/BaseFilterSection'; import { useGetLanguagesQuery } from '@/entities/programmingLanguage'; @@ -38,7 +38,7 @@ export const TaskLanguagesFilter = ({ return ( { } = useGetQuestionsListQuery({ skills: filters.skills, page: filters.page, - specialization: filters.specialization, + specializationId: filters.specialization, title: filters.title, complexity: filters.complexity, rate: filters.rate, diff --git a/src/pages/admin/task/taskCreate/index.ts b/src/pages/admin/task/taskCreate/index.ts new file mode 100644 index 000000000..9eb7570c3 --- /dev/null +++ b/src/pages/admin/task/taskCreate/index.ts @@ -0,0 +1 @@ +export { TaskCreatePage } from './ui/TaskCreatePage/TaskCreatePage.lazy'; diff --git a/src/pages/admin/task/taskCreate/ui/TaskCreatePage/TaskCreatePage.lazy.tsx b/src/pages/admin/task/taskCreate/ui/TaskCreatePage/TaskCreatePage.lazy.tsx new file mode 100644 index 000000000..6b65560c5 --- /dev/null +++ b/src/pages/admin/task/taskCreate/ui/TaskCreatePage/TaskCreatePage.lazy.tsx @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const TaskCreatePage = lazy(() => import('./TaskCreatePage')); diff --git a/src/pages/admin/task/taskCreate/ui/TaskCreatePage/TaskCreatePage.tsx b/src/pages/admin/task/taskCreate/ui/TaskCreatePage/TaskCreatePage.tsx new file mode 100644 index 000000000..2658b8f8d --- /dev/null +++ b/src/pages/admin/task/taskCreate/ui/TaskCreatePage/TaskCreatePage.tsx @@ -0,0 +1,15 @@ +import { TaskCreateForm } from '@/features/task/createTask'; + +import { PageWrapper } from '@/widgets/PageWrapper'; + +const TaskCreatePage = () => { + const content = ; + + return ( + + {({ content }) => content} + + ); +}; + +export default TaskCreatePage; diff --git a/src/pages/admin/task/taskDetail/index.ts b/src/pages/admin/task/taskDetail/index.ts new file mode 100644 index 000000000..c8b7abce7 --- /dev/null +++ b/src/pages/admin/task/taskDetail/index.ts @@ -0,0 +1 @@ +export { TaskPage } from './ui/TaskPage/TaskPage.lazy'; diff --git a/src/pages/admin/task/taskDetail/ui/TaskAdditionalInfo/TaskAdditionalInfo.module.css b/src/pages/admin/task/taskDetail/ui/TaskAdditionalInfo/TaskAdditionalInfo.module.css new file mode 100644 index 000000000..4558a0d9d --- /dev/null +++ b/src/pages/admin/task/taskDetail/ui/TaskAdditionalInfo/TaskAdditionalInfo.module.css @@ -0,0 +1,3 @@ +.additional { + width: 360px; +} \ No newline at end of file diff --git a/src/pages/admin/task/taskDetail/ui/TaskAdditionalInfo/TaskAdditionalInfo.tsx b/src/pages/admin/task/taskDetail/ui/TaskAdditionalInfo/TaskAdditionalInfo.tsx new file mode 100644 index 000000000..bb1bc2253 --- /dev/null +++ b/src/pages/admin/task/taskDetail/ui/TaskAdditionalInfo/TaskAdditionalInfo.tsx @@ -0,0 +1,73 @@ +import classnames from 'classnames'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import { i18Namespace, Tasks } from '@/shared/config'; +import { Card } from '@/shared/ui/Card'; +import { Chip } from '@/shared/ui/Chip'; +import { Flex } from '@/shared/ui/Flex'; +import { Text } from '@/shared/ui/Text'; + +import { ProgrammingLanguage, ProgrammingLanguageChipList } from '@/entities/programmingLanguage'; +import { + TaskCategoryCode, + TaskDifficulty, + TaskDifficultyChip, + taskCategories, +} from '@/entities/task'; + +import styles from './TaskAdditionalInfo.module.css'; + +export interface TaskAdditionalInfoProps { + languages: ProgrammingLanguage[]; + difficulty: TaskDifficulty; + category: TaskCategoryCode; + className?: string; + route: string; +} + +export const TaskAdditionalInfo = ({ + languages, + difficulty, + category, + className = '', + route, +}: TaskAdditionalInfoProps) => { + const navigate = useNavigate(); + + const { t } = useTranslation(i18Namespace.task); + const onMoveToTasksWithLanguages = (languageId: number) => { + navigate(`${route}?page=1&langIds=` + encodeURIComponent(languageId)); + }; + + return ( + + + + + + {t(Tasks.LANGUAGES_TITLE)} + + + + + + + {t(Tasks.DIFFICULTY_TITLE)} + + + + + + {t(Tasks.CATEGORY_TITLE)} + + + + + + + ); +}; diff --git a/src/pages/admin/task/taskDetail/ui/TaskAdditionalInfoDrawer/TaskAdditionalInfoDrawer.module.css b/src/pages/admin/task/taskDetail/ui/TaskAdditionalInfoDrawer/TaskAdditionalInfoDrawer.module.css new file mode 100644 index 000000000..5a60ee28c --- /dev/null +++ b/src/pages/admin/task/taskDetail/ui/TaskAdditionalInfoDrawer/TaskAdditionalInfoDrawer.module.css @@ -0,0 +1,14 @@ +.icon-button { + border: 1px solid var(--color-black-200); +} + +.drawer { + top: 0; + min-width: 328px; + height: auto; + overflow-x: hidden; +} + +.drawer-mobile { + padding-bottom: var(--header-height); +} \ No newline at end of file diff --git a/src/pages/admin/task/taskDetail/ui/TaskAdditionalInfoDrawer/TaskAdditionalInfoDrawer.tsx b/src/pages/admin/task/taskDetail/ui/TaskAdditionalInfoDrawer/TaskAdditionalInfoDrawer.tsx new file mode 100644 index 000000000..13db05de5 --- /dev/null +++ b/src/pages/admin/task/taskDetail/ui/TaskAdditionalInfoDrawer/TaskAdditionalInfoDrawer.tsx @@ -0,0 +1,59 @@ +import classNames from 'classnames'; + +import SlidersHorizontalIcon from '@/shared/assets/icons/slidersHorizontal.svg'; +import { ROUTES } from '@/shared/config'; +import { useCurrentProject, useScreenSize, useModal } from '@/shared/libs'; +import { Drawer } from '@/shared/ui/Drawer'; +import { IconButton } from '@/shared/ui/IconButton'; + +import { ProgrammingLanguage } from '@/entities/programmingLanguage'; +import { TaskCategoryCode, TaskDifficulty } from '@/entities/task'; + +import { TaskAdditionalInfo } from '../TaskAdditionalInfo/TaskAdditionalInfo'; + +import styles from './TaskAdditionalInfoDrawer.module.css'; + +interface TaskAdditionalInfoDrawerProps { + languages: ProgrammingLanguage[]; + difficulty: TaskDifficulty; + category: TaskCategoryCode; +} + +export const TaskAdditionalInfoDrawer = ({ + languages, + difficulty, + category, +}: TaskAdditionalInfoDrawerProps) => { + const { isMobileS } = useScreenSize(); + const { isOpen, onToggle, onClose } = useModal(); + const project = useCurrentProject(); + + return ( + <> + } + size="medium" + variant="tertiary" + onClick={onToggle} + /> + + + + + ); +}; diff --git a/src/pages/admin/task/taskDetail/ui/TaskBody/TaskBody.module.css b/src/pages/admin/task/taskDetail/ui/TaskBody/TaskBody.module.css new file mode 100644 index 000000000..2f23abc84 --- /dev/null +++ b/src/pages/admin/task/taskDetail/ui/TaskBody/TaskBody.module.css @@ -0,0 +1,3 @@ +.description { + width: 100%; +} \ No newline at end of file diff --git a/src/pages/admin/task/taskDetail/ui/TaskBody/TaskBody.tsx b/src/pages/admin/task/taskDetail/ui/TaskBody/TaskBody.tsx new file mode 100644 index 000000000..c13caaf7c --- /dev/null +++ b/src/pages/admin/task/taskDetail/ui/TaskBody/TaskBody.tsx @@ -0,0 +1,27 @@ +import { useTranslation } from 'react-i18next'; + +import { i18Namespace, Tasks } from '@/shared/config'; +import { Flex } from '@/shared/ui/Flex'; +import { Text } from '@/shared/ui/Text'; +import { TextHtml } from '@/shared/ui/TextHtml'; + +import styles from './TaskBody.module.css'; + +type TaskBodyProps = { + description: string; +}; + +export const TaskBody = ({ description }: TaskBodyProps) => { + const { t } = useTranslation(i18Namespace.task); + + return ( + + + + {t(Tasks.DESCRIPTION_TITLE)} + + + + + ); +}; diff --git a/src/pages/admin/task/taskDetail/ui/TaskHeader/TaskHeader.tsx b/src/pages/admin/task/taskDetail/ui/TaskHeader/TaskHeader.tsx new file mode 100644 index 000000000..1e4b4c39f --- /dev/null +++ b/src/pages/admin/task/taskDetail/ui/TaskHeader/TaskHeader.tsx @@ -0,0 +1,36 @@ +import { useScreenSize } from '@/shared/libs'; +import { Flex } from '@/shared/ui/Flex'; +import { Text } from '@/shared/ui/Text'; + +import { ProgrammingLanguage } from '@/entities/programmingLanguage'; +import { TaskCategoryCode, TaskDifficulty } from '@/entities/task'; + +import { TaskAdditionalInfoDrawer } from '../TaskAdditionalInfoDrawer/TaskAdditionalInfoDrawer'; + +interface TaskHeaderProps { + name: string; + languages: ProgrammingLanguage[]; + difficulty: TaskDifficulty; + category: TaskCategoryCode; +} + +export const TaskHeader = ({ name, languages, difficulty, category }: TaskHeaderProps) => { + const { isMobile, isTablet } = useScreenSize(); + + return ( + + + + {name} + + {(isMobile || isTablet) && ( + + )} + + + ); +}; diff --git a/src/pages/tasks/task/ui/TaskPage.lazy.tsx b/src/pages/admin/task/taskDetail/ui/TaskPage/TaskPage.lazy.tsx similarity index 100% rename from src/pages/tasks/task/ui/TaskPage.lazy.tsx rename to src/pages/admin/task/taskDetail/ui/TaskPage/TaskPage.lazy.tsx diff --git a/src/pages/admin/task/taskDetail/ui/TaskPage/TaskPage.tsx b/src/pages/admin/task/taskDetail/ui/TaskPage/TaskPage.tsx new file mode 100644 index 000000000..457048df4 --- /dev/null +++ b/src/pages/admin/task/taskDetail/ui/TaskPage/TaskPage.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; + +import { i18Namespace, Tasks } from '@/shared/config'; + +import { useGetTaskByIdQuery } from '@/entities/task'; + +import { PageWrapper, PageWrapperStubs } from '@/widgets/PageWrapper'; + +import { TaskPageContent } from '../TaskPageContent/TaskPageContent'; + +const TaskPage = () => { + const { t } = useTranslation(i18Namespace.task); + const { taskId = '' } = useParams<{ taskId: string }>(); + const { data: task, refetch, isLoading, isError } = useGetTaskByIdQuery(taskId); + + const stubs: PageWrapperStubs = { + error: { + onClick: refetch, + }, + empty: { + title: t(Tasks.STUB_EMPTY_TASK_TITLE), + subtitle: t(Tasks.STUB_EMPTY_TASK_SUBTITLE), + buttonText: t(Tasks.STUB_EMPTY_TASK_SUBMIT), + onClick: refetch, + }, + }; + + const hasTask = !!task && Object.keys(task).length > 0; + + return ( + : null} + > + {({ content }) => content} + + ); +}; + +export default TaskPage; diff --git a/src/pages/admin/task/taskDetail/ui/TaskPageContent/TaskPageContent.module.css b/src/pages/admin/task/taskDetail/ui/TaskPageContent/TaskPageContent.module.css new file mode 100644 index 000000000..4567d905f --- /dev/null +++ b/src/pages/admin/task/taskDetail/ui/TaskPageContent/TaskPageContent.module.css @@ -0,0 +1,3 @@ +.main { + flex: 1; +} \ No newline at end of file diff --git a/src/pages/admin/task/taskDetail/ui/TaskPageContent/TaskPageContent.tsx b/src/pages/admin/task/taskDetail/ui/TaskPageContent/TaskPageContent.tsx new file mode 100644 index 000000000..24969cf63 --- /dev/null +++ b/src/pages/admin/task/taskDetail/ui/TaskPageContent/TaskPageContent.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { ROUTES } from '@/shared/config'; +import { useScreenSize } from '@/shared/libs'; +import { BackHeader } from '@/shared/ui/BackHeader'; +import { Card } from '@/shared/ui/Card'; +import { Flex } from '@/shared/ui/Flex'; + +import { Task } from '@/entities/task'; + +import { DeleteTaskButton } from '@/features/task/deleteTask'; +import { TaskEditButton } from '@/features/task/editTask'; + +import { TaskAdditionalInfo } from '../TaskAdditionalInfo/TaskAdditionalInfo'; +import { TaskBody } from '../TaskBody/TaskBody'; +import { TaskHeader } from '../TaskHeader/TaskHeader'; + +import styles from './TaskPageContent.module.css'; + +interface TaskPageContentProps { + task: Task; +} + +export const TaskPageContent = ({ task }: TaskPageContentProps) => { + const { isMobile, isTablet } = useScreenSize(); + + return ( + <> + + + + + + + + + + + + + {!isMobile && !isTablet && ( + + )} + + + ); +}; diff --git a/src/pages/admin/task/taskEdit/index.ts b/src/pages/admin/task/taskEdit/index.ts new file mode 100644 index 000000000..b8f7ca8e7 --- /dev/null +++ b/src/pages/admin/task/taskEdit/index.ts @@ -0,0 +1 @@ +export { TaskEditPage } from './ui/TaskEditPage/TaskEditPage.lazy'; diff --git a/src/pages/admin/task/taskEdit/ui/TaskEditPage/TaskEditPage.lazy.tsx b/src/pages/admin/task/taskEdit/ui/TaskEditPage/TaskEditPage.lazy.tsx new file mode 100644 index 000000000..d21ba1cbf --- /dev/null +++ b/src/pages/admin/task/taskEdit/ui/TaskEditPage/TaskEditPage.lazy.tsx @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const TaskEditPage = lazy(() => import('./TaskEditPage')); diff --git a/src/pages/admin/task/taskEdit/ui/TaskEditPage/TaskEditPage.tsx b/src/pages/admin/task/taskEdit/ui/TaskEditPage/TaskEditPage.tsx new file mode 100644 index 000000000..63ccbc275 --- /dev/null +++ b/src/pages/admin/task/taskEdit/ui/TaskEditPage/TaskEditPage.tsx @@ -0,0 +1,38 @@ +import { useParams } from 'react-router-dom'; + +import { useGetTaskByIdQuery } from '@/entities/task'; + +import { TaskEditForm } from '@/features/task/editTask'; + +import { PageWrapper, PageWrapperStubs } from '@/widgets/PageWrapper'; + +const TaskEditPage = () => { + const { taskId = '' } = useParams<{ taskId: string }>(); + + const { data: task, isLoading, isError, refetch } = useGetTaskByIdQuery(taskId); + + const hasTask = task && Object.keys(task).length > 0; + + const content = hasTask ? : null; + + const stubs: PageWrapperStubs = { + error: { + onClick: refetch, + }, + }; + + return ( + + {({ content }) => content} + + ); +}; + +export default TaskEditPage; diff --git a/src/pages/admin/task/tasks/ui/TasksTable/TasksTable.tsx b/src/pages/admin/task/tasks/ui/TasksTable/TasksTable.tsx index 53fc68c7c..89b7b84fe 100644 --- a/src/pages/admin/task/tasks/ui/TasksTable/TasksTable.tsx +++ b/src/pages/admin/task/tasks/ui/TasksTable/TasksTable.tsx @@ -1,27 +1,34 @@ import { useTranslation } from 'react-i18next'; +import { Link, useNavigate } from 'react-router-dom'; -import { i18Namespace, Task } from '@/shared/config'; -import { type SelectedAdminEntities } from '@/shared/libs'; +import { i18Namespace, ROUTES, Tasks, Translation } from '@/shared/config'; +import { route, type SelectedAdminEntities } from '@/shared/libs'; +import { Flex } from '@/shared/ui/Flex'; +import { Icon } from '@/shared/ui/Icon'; +import { IconButton } from '@/shared/ui/IconButton'; +import { Popover, PopoverMenuItem } from '@/shared/ui/Popover'; import { Table } from '@/shared/ui/Table'; import { Text } from '@/shared/ui/Text'; -import type { TaskListItem } from '@/entities/task'; +import { Task } from '@/entities/task'; + +import { DeleteTaskButton } from '@/features/task/deleteTask'; interface TasksTableProps { - tasks: TaskListItem[]; + tasks: Task[]; selectedTasks?: SelectedAdminEntities; onSelectTasks?: (ids: SelectedAdminEntities) => void; } -export const TasksTable = (props: TasksTableProps) => { - const { tasks, selectedTasks, onSelectTasks } = props; +export const TasksTable = ({ tasks, selectedTasks, onSelectTasks }: TasksTableProps) => { + const navigate = useNavigate(); + const { t } = useTranslation([i18Namespace.task, i18Namespace.translation]); const renderTableColumnWidths = () => { const columnWidths = { title: 'auto', difficulty: '15%', - status: '15%', }; return Object.values(columnWidths)?.map((width, idx) => ); @@ -29,26 +36,71 @@ export const TasksTable = (props: TasksTableProps) => { const renderTableHeader = () => { const columns = { - title: t(Task.TABLE_TASK_TITLE), - difficulty: t(Task.TABLE_DIFFICULTY_TITLE), - status: t(Task.TABLE_STATUS_TITLE), + title: t(Tasks.TABLE_TASK), + difficulty: t(Tasks.TABLE_DIFFICULTY), }; return Object.entries(columns)?.map(([k, v]) => ); }; - const renderTableBody = (task: TaskListItem) => { + const renderTableBody = (task: Task) => { const columns = { - title: task.name, + title: ( + + {task.name} + + ), difficulty: task.difficulty, - status: task.status, }; return Object.entries(columns)?.map(([k, v]) => { - return ; + return ; }); }; + const renderActions = (task: Task) => { + const menuItems: PopoverMenuItem[] = [ + { + icon: , + title: t(Translation.SHOW, { ns: i18Namespace.translation }), + onClick: () => { + navigate(route(ROUTES.admin.tasks.details.route, task.id)); + }, + }, + { + icon: , + title: t(Translation.EDIT, { ns: i18Namespace.translation }), + onClick: () => { + navigate(route(ROUTES.admin.tasks.edit.route, task.id)); + }, + tooltip: { + color: 'red', + text: t(Translation.TOOLTIP_COLLECTION_DISABLED_INFO, { ns: i18Namespace.translation }), + }, + }, + { + renderComponent: () => , + }, + ]; + + return ( + + + {({ onToggle }) => ( + } + size="medium" + variant="tertiary" + onClick={onToggle} + /> + )} + + + ); + }; + return (
    {v}{k === 'title' ? {v} : v}{v}
    { selectedItems={selectedTasks} onSelectItems={onSelectTasks} renderTableColumnWidths={renderTableColumnWidths} + renderActions={renderActions} hasCopyButton /> ); diff --git a/src/pages/admin/task/tasks/ui/TasksTablePage/TasksTablePage.tsx b/src/pages/admin/task/tasks/ui/TasksTablePage/TasksTablePage.tsx index d7e2d2992..6d964b8be 100644 --- a/src/pages/admin/task/tasks/ui/TasksTablePage/TasksTablePage.tsx +++ b/src/pages/admin/task/tasks/ui/TasksTablePage/TasksTablePage.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next'; -import { i18Namespace, Task } from '@/shared/config'; +import { i18Namespace, Tasks } from '@/shared/config'; import { type SelectedAdminEntities, useAppDispatch, useAppSelector } from '@/shared/libs'; import { Card } from '@/shared/ui/Card'; import { Flex } from '@/shared/ui/Flex'; @@ -58,9 +58,9 @@ const TasksTablePage = () => { const stubs: PageWrapperStubs = { empty: { - title: t(Task.STUB_EMPTY_TASKS_ADMIN_TITLE), - subtitle: t(Task.STUB_EMPTY_TASKS_ADMIN_SUBTITLE), - buttonText: t(Task.STUB_EMPTY_TASKS_ADMIN_SUBMIT), + title: t(Tasks.STUB_EMPTY_TASKS_ADMIN_TITLE), + subtitle: t(Tasks.STUB_EMPTY_TASKS_ADMIN_SUBTITLE), + buttonText: t(Tasks.STUB_EMPTY_TASKS_ADMIN_SUBMIT), }, error: { onClick: refetchAllTasks, diff --git a/src/pages/interview/interview/ui/InterviewPage/InterviewPage.tsx b/src/pages/interview/interview/ui/InterviewPage/InterviewPage.tsx index 47932bb53..fcc48b079 100644 --- a/src/pages/interview/interview/ui/InterviewPage/InterviewPage.tsx +++ b/src/pages/interview/interview/ui/InterviewPage/InterviewPage.tsx @@ -32,7 +32,7 @@ const InterviewPage = () => { const { isLoading: isQuestionsListLoading } = useGetQuestionsListQuery({ random: true, limit: 3, - specialization: specializationId, + specializationId, }); const { isLoading: isActiveQuizLoading } = useGetActiveQuizQuery( diff --git a/src/pages/tasks/task/index.ts b/src/pages/tasks/task/index.ts index 65ab7ed44..c8b7abce7 100644 --- a/src/pages/tasks/task/index.ts +++ b/src/pages/tasks/task/index.ts @@ -1 +1 @@ -export { TaskPage } from './ui/TaskPage.lazy'; +export { TaskPage } from './ui/TaskPage/TaskPage.lazy'; diff --git a/src/pages/tasks/task/ui/TaskPage.tsx b/src/pages/tasks/task/ui/TaskPage.tsx deleted file mode 100644 index e5057d818..000000000 --- a/src/pages/tasks/task/ui/TaskPage.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; -import { Group, Panel, Separator } from 'react-resizable-panels'; -import { useParams } from 'react-router-dom'; - -import { i18Namespace, Task as TaskTranslations } from '@/shared/config'; - -import { getProfileId } from '@/entities/profile'; -import { - ExecuteCodeResponse, - LANGUAGE_IDS, - useExecuteCodeMutation, - useGetTaskByIdQuery, - useTestCodeMutation, -} from '@/entities/task'; - -import { PageWrapper, PageWrapperStubs } from '@/widgets/PageWrapper'; -import { TaskTabs } from '@/widgets/task/TaskTabs'; -import { TaskWorkspace } from '@/widgets/task/TaskWorkspace'; - -import styles from './TaskPage.module.css'; - -const TaskPage = () => { - const { t } = useTranslation(i18Namespace.task); - const { taskId } = useParams<{ taskId: string }>(); - const profileId = useSelector(getProfileId); - const { data, isLoading, isError, refetch } = useGetTaskByIdQuery(taskId || '', { - skip: !taskId, - }); - const [executeCode, { isLoading: isExecuting }] = useExecuteCodeMutation(); - const [testCode, { isLoading: isTesting }] = useTestCodeMutation(); - - const [code, setCode] = useState(''); - const [selectedLanguageId, setSelectedLanguageId] = useState(LANGUAGE_IDS.JAVASCRIPT); - const [output, setOutput] = useState(null); - - useEffect(() => { - if (data) { - const jsStructure = data.taskStructures.find( - (structure) => structure.languageId === LANGUAGE_IDS.JAVASCRIPT, - ); - if (jsStructure) { - setCode(jsStructure.solutionTemplate); - } else { - setCode('// JavaScript is not supported for this task'); - } - } - }, [data]); - - const handleReset = useCallback(() => { - if (data) { - const jsStructure = data.taskStructures.find( - (structure) => structure.languageId === selectedLanguageId, - ); - if (jsStructure) { - setCode(jsStructure.solutionTemplate); - } - } - }, [data, selectedLanguageId]); - - const handleRunCode = async () => { - if (!taskId) return; - - try { - const response = await executeCode({ - taskId, - languageId: selectedLanguageId, - sourceCode: code, - profileId, - }).unwrap(); - - setOutput(response); - } catch (error) { - // eslint-disable-next-line no-console - console.error('Failed to execute code:', error); - } - }; - - const handleTestCode = async () => { - if (!taskId) return; - - try { - const response = await testCode({ - taskId, - languageId: selectedLanguageId, - sourceCode: code, - profileId, - }).unwrap(); - - setOutput(response); - } catch (error) { - // eslint-disable-next-line no-console - console.error('Failed to test code:', error); - } - }; - - const supportedLanguages = [{ id: LANGUAGE_IDS.JAVASCRIPT, name: 'JavaScript' }]; - - const stubs: PageWrapperStubs = { - empty: { - title: t(TaskTranslations.STUB_EMPTY_TASK_PUBLIC_TITLE), - subtitle: t(TaskTranslations.STUB_EMPTY_TASK_PUBLIC_SUBTITLE), - buttonText: t(TaskTranslations.STUB_EMPTY_TASK_PUBLIC_SUBMIT), - onClick: () => refetch(), - }, - error: { - onClick: () => refetch(), - }, - }; - - return ( - - - - - - - - - - ) - } - > - {({ content }) => content} - - ); -}; - -export default TaskPage; diff --git a/src/pages/tasks/task/ui/TaskPage/TaskPage.lazy.tsx b/src/pages/tasks/task/ui/TaskPage/TaskPage.lazy.tsx new file mode 100644 index 000000000..a38757d30 --- /dev/null +++ b/src/pages/tasks/task/ui/TaskPage/TaskPage.lazy.tsx @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const TaskPage = lazy(() => import('./TaskPage')); diff --git a/src/pages/tasks/task/ui/TaskPage/TaskPage.tsx b/src/pages/tasks/task/ui/TaskPage/TaskPage.tsx new file mode 100644 index 000000000..c42c66555 --- /dev/null +++ b/src/pages/tasks/task/ui/TaskPage/TaskPage.tsx @@ -0,0 +1,49 @@ +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; + +import { i18Namespace, Tasks } from '@/shared/config'; + +import { useGetTaskByIdQuery } from '@/entities/task'; + +import { PageWrapper, PageWrapperStubs } from '@/widgets/PageWrapper'; + +import { TaskPageContent } from '../TaskPageContent/TaskPageContent'; + +const TaskPage = () => { + const { t } = useTranslation(i18Namespace.task); + const { taskId = '' } = useParams<{ taskId: string }>(); + const { + data: task, + isLoading, + isError, + refetch, + } = useGetTaskByIdQuery(taskId, { + skip: !taskId, + }); + + const stubs: PageWrapperStubs = { + empty: { + title: t(Tasks.STUB_EMPTY_TASK_TITLE), + subtitle: t(Tasks.STUB_EMPTY_TASK_SUBTITLE), + buttonText: t(Tasks.STUB_EMPTY_TASK_SUBMIT), + onClick: () => refetch(), + }, + error: { + onClick: () => refetch(), + }, + }; + + return ( + 0} + stubs={stubs} + content={task && } + > + {({ content }) => content} + + ); +}; + +export default TaskPage; diff --git a/src/pages/tasks/task/ui/TaskPage.module.css b/src/pages/tasks/task/ui/TaskPageContent/TaskPageContent.module.css similarity index 94% rename from src/pages/tasks/task/ui/TaskPage.module.css rename to src/pages/tasks/task/ui/TaskPageContent/TaskPageContent.module.css index 6a92f48b1..d292155f1 100644 --- a/src/pages/tasks/task/ui/TaskPage.module.css +++ b/src/pages/tasks/task/ui/TaskPageContent/TaskPageContent.module.css @@ -1,6 +1,6 @@ .page { width: 100%; - height: calc(100vh - var(--header-height) - var(--header-height-sm)) !important; + height: calc(100vh - var(--header-height) - var(--header-height-sm) - 48px) !important; border-radius: 16px; background-color: var(--background-block); } diff --git a/src/pages/tasks/task/ui/TaskPageContent/TaskPageContent.tsx b/src/pages/tasks/task/ui/TaskPageContent/TaskPageContent.tsx new file mode 100644 index 000000000..eb074e250 --- /dev/null +++ b/src/pages/tasks/task/ui/TaskPageContent/TaskPageContent.tsx @@ -0,0 +1,111 @@ +import { useCallback, useMemo, useState } from 'react'; +import { Group, Panel, Separator } from 'react-resizable-panels'; + +import { useAppSelector } from '@/shared/libs'; + +import { getProfileId } from '@/entities/profile'; +import { ProgrammingLanguage } from '@/entities/programmingLanguage'; +import { + ExecuteCodeResponse, + Task, + useExecuteCodeMutation, + useTestCodeMutation, +} from '@/entities/task'; + +import { TaskTabs } from '@/widgets/task/TaskTabs'; +import { TaskWorkspace } from '@/widgets/task/TaskWorkspace'; + +import styles from './TaskPageContent.module.css'; + +interface TaskPageContentProps { + task: Task; +} + +export const TaskPageContent = ({ task }: TaskPageContentProps) => { + const profileId = useAppSelector(getProfileId); + + const [executeCode, { isLoading: isExecuting }] = useExecuteCodeMutation(); + const [testCode, { isLoading: isTesting }] = useTestCodeMutation(); + + const [code, setCode] = useState(task.taskStructures[0].solutionStub); + const [output, setOutput] = useState(null); + const [selectedLanguage, setSelectedLanguage] = useState( + task.supportedLanguages[0], + ); + + const taskStructure = useMemo(() => { + return task.taskStructures.find(({ languageId }) => languageId === selectedLanguage.id); + }, [selectedLanguage, task]); + + const onChangeSelectedLanguage = (languageId: number) => { + const selectedLanguage = + task.supportedLanguages.find(({ id }) => id === languageId) || task.supportedLanguages[0]; + setSelectedLanguage(selectedLanguage); + setCode( + ( + task.taskStructures.find((taskStructure) => taskStructure.languageId === languageId) || + task.taskStructures[0] + ).solutionStub, + ); + }; + + const handleExecuteCode = async () => { + try { + const response = await executeCode({ + taskId: task.id, + languageId: selectedLanguage.id, + sourceCode: code, + profileId, + }).unwrap(); + + setOutput(response); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to execute code:', error); + } + }; + + const handleRunCode = async () => { + try { + const response = await testCode({ + taskId: task.id, + languageId: selectedLanguage.id, + sourceCode: code, + profileId, + }).unwrap(); + + setOutput(response); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to test code:', error); + } + }; + + const handleReset = useCallback(() => { + setCode(taskStructure?.solutionStub || ''); + }, [taskStructure]); + + return ( + + + + + + + + + + ); +}; diff --git a/src/pages/tasks/tasks/index.ts b/src/pages/tasks/tasks/index.ts index 09ff1dd8e..fd6b97307 100644 --- a/src/pages/tasks/tasks/index.ts +++ b/src/pages/tasks/tasks/index.ts @@ -1 +1 @@ -export { TasksPage } from './ui/TasksPage.lazy'; +export { TasksPage } from './ui/TasksPage/TasksPage.lazy'; diff --git a/src/pages/tasks/tasks/ui/TasksPage.lazy.tsx b/src/pages/tasks/tasks/ui/TasksPage/TasksPage.lazy.tsx similarity index 100% rename from src/pages/tasks/tasks/ui/TasksPage.lazy.tsx rename to src/pages/tasks/tasks/ui/TasksPage/TasksPage.lazy.tsx diff --git a/src/pages/tasks/tasks/ui/TasksPage.module.css b/src/pages/tasks/tasks/ui/TasksPage/TasksPage.module.css similarity index 100% rename from src/pages/tasks/tasks/ui/TasksPage.module.css rename to src/pages/tasks/tasks/ui/TasksPage/TasksPage.module.css diff --git a/src/pages/tasks/tasks/ui/TasksPage.tsx b/src/pages/tasks/tasks/ui/TasksPage/TasksPage.tsx similarity index 90% rename from src/pages/tasks/tasks/ui/TasksPage.tsx rename to src/pages/tasks/tasks/ui/TasksPage/TasksPage.tsx index 1e0349365..cf74b43b2 100644 --- a/src/pages/tasks/tasks/ui/TasksPage.tsx +++ b/src/pages/tasks/tasks/ui/TasksPage/TasksPage.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next'; -import { i18Namespace, Task } from '@/shared/config'; +import { i18Namespace, Tasks } from '@/shared/config'; import { useScreenSize } from '@/shared/libs'; import { Card } from '@/shared/ui/Card'; import { FiltersDrawer } from '@/shared/ui/FiltersDrawer'; @@ -57,11 +57,11 @@ const TasksPage = () => { const stubs: PageWrapperStubs = { empty: { - title: t(Task.STUB_EMPTY_TASKS_PUBLIC_TITLE), - subtitle: t(Task.STUB_EMPTY_TASKS_PUBLIC_SUBTITLE), + title: t(Tasks.STUB_EMPTY_TASKS_PUBLIC_TITLE), + subtitle: t(Tasks.STUB_EMPTY_TASKS_PUBLIC_SUBTITLE), }, 'filter-empty': { - subtitle: t(Task.STUB_EMPTY_TASKS_PUBLIC_FILTERS_SUBTITLE), + subtitle: t(Tasks.STUB_EMPTY_TASKS_PUBLIC_FILTERS_SUBTITLE), onClick: onResetFilters, }, error: { @@ -91,7 +91,7 @@ const TasksPage = () => {
    - {t(Task.TITLE_SHORT)} + {t(Tasks.TITLE_SHORT)} {(isMobile || isTablet) && {renderFilters()}}
    diff --git a/src/pages/wiki/question/questions/ui/QuestionsPage/QuestionsPage.tsx b/src/pages/wiki/question/questions/ui/QuestionsPage/QuestionsPage.tsx index 27bd6ee82..57f3220cd 100644 --- a/src/pages/wiki/question/questions/ui/QuestionsPage/QuestionsPage.tsx +++ b/src/pages/wiki/question/questions/ui/QuestionsPage/QuestionsPage.tsx @@ -57,7 +57,7 @@ const QuestionsPage = () => { { ...getParams, profileId, - specialization: specializationId, + specializationId, areFavorites: status === 'favorite' ? true : undefined, }, { diff --git a/src/shared/config/i18n/i18nNamespaces.ts b/src/shared/config/i18n/i18nNamespaces.ts index 5281d699f..d09283211 100644 --- a/src/shared/config/i18n/i18nNamespaces.ts +++ b/src/shared/config/i18n/i18nNamespaces.ts @@ -28,4 +28,5 @@ export const i18Namespace = { media: 'media', topic: 'topic', task: 'task', + programmingLanguage: 'programmingLanguage', }; diff --git a/src/shared/config/i18n/i18nTranslations.ts b/src/shared/config/i18n/i18nTranslations.ts index 2699465e3..53599f2c9 100644 --- a/src/shared/config/i18n/i18nTranslations.ts +++ b/src/shared/config/i18n/i18nTranslations.ts @@ -171,6 +171,12 @@ export enum Translation { TOAST_QUESTION_CREATE_FAILED = 'toast.questions.create.failed', TOAST_QUESTION_EDIT_SUCCESS = 'toast.questions.edit.success', TOAST_QUESTION_EDIT_FAILED = 'toast.questions.edit.failed', + TOAST_TASKS_CREATE_SUCCESS = 'toast.tasks.create.success', + TOAST_TASKS_CREATE_FAILED = 'toast.tasks.create.failed', + TOAST_TASKS_EDIT_SUCCESS = 'toast.tasks.edit.success', + TOAST_TASKS_EDIT_FAILED = 'toast.tasks.edit.failed', + TOAST_TASKS_DELETE_SINGLE_SUCCESS = 'toast.tasks.delete.single.success', + TOAST_TASKS_DELETE_SINGLE_FAILED = 'toast.tasks.delete.single.failed', TOAST_COLLECTION_DELETE_SUCCESS = 'toast.collections.delete.single.success', TOAST_COLLECTION_DELETE_FAILED = 'toast.collections.delete.single.failed', TOOLTIP_COLLECTION_DISABLED_INFO = 'tooltip.collections.disabled.info', @@ -1334,29 +1340,85 @@ export enum Topics { ADDITIONAL_INFO_UPDATED_AT = 'additional.info.updated.at', } -export enum Task { +export enum ProgrammingLanguages { + SELECT_CHOOSE = 'select.choose', + SELECT_EMPTY = 'select.empty', + SELECT_SELECTED = 'select.selected', +} + +export enum Tasks { TITLE_SHORT = 'title.short', + NAME_TITLE = 'name.title', + NAME_SUBTITLE = 'name.subtitle', + MEMORY_LIMIT_TITLE = 'memory.limit.title', + MEMORY_LIMIT_SUBTITLE = 'memory.limit.subtitle', + MEMORY_LIMIT_VALUE = 'memory.limit.value', + TIME_LIMIT_TITLE = 'time.limit.title', + TIME_LIMIT_SUBTITLE = 'time.limit.subtitle', + TIME_LIMIT_VALUE = 'time.limit.value', + CONSTRAINTS_TITLE = 'constraints.title', + CONSTRAINTS_SUBTITLE = 'constraints.subtitle', + CONSTRAINTS_ADD_BUTTON = 'constraints.add.button', + TEST_CASES_TITLE = 'test.cases.title', + TEST_CASES_SUBTITLE = 'test.cases.subtitle', + TEST_CASES_INPUT = 'test.cases.input', + TEST_CASES_EXPECTED_OUTPUT = 'test.cases.expected.output', + TEST_CASES_HIDDEN = 'test.cases.hidden', + TEST_CASES_ADD_BUTTON = 'test.cases.add.button', + TASK_STRUCTURES_TITLE = 'task.structures.title', + TASK_STRUCTURES_SUBTITLE = 'task.structures.subtitle', + TASK_STRUCTURES_LANGUAGE = 'task.structures.language', + TASK_STRUCTURES_SOLUTION_STUB = 'task.structures.solution.stub', + TASK_STRUCTURES_TEXT_FIXTURE = 'task.structures.text.fixture', + TASK_STRUCTURES_ACTIVE = 'task.structures.active', + TASK_STRUCTURES_ADD_BUTTON = 'task.structures.add.button', + EDIT_PAGE_TITLE = 'edit.page.title', + CREATE_PAGE_TITLE = 'create.page.title', SEARCH_PLACEHOLDER = 'search.placeholder', + SELECT_CHOOSE = 'select.choose', + SELECT_EMPTY = 'select.empty', + SELECT_SELECTED = 'select.selected', DIFFICULTY_TITLE = 'difficulty.title', + DIFFICULTY_SUBTITLE = 'difficulty.subtitle', DIFFICULTY_TITLE_SHORT = 'difficulty.title.short', LANGUAGES_TITLE = 'languages.title', - TABLE_TASK_TITLE = 'table.task.title', - TABLE_DIFFICULTY_TITLE = 'table.difficulty.title', - TABLE_STATUS_TITLE = 'table.status.title', + TABLE_TASK = 'table.task', + TABLE_DIFFICULTY = 'table.difficulty', + TABLE_STATUS = 'table.status', + TABLE_STATUS_SOLVED = 'table.status.solved', + TABLE_STATUS_NOT_SOLVED = 'table.status.not.solved', + TABLE_LANGUAGE = 'table.language', + TABLE_MEMORY = 'table.memory', + TABLE_TIME = 'table.time', STUB_EMPTY_TASKS_PUBLIC_TITLE = 'stub.empty.tasks.public.title', STUB_EMPTY_TASKS_PUBLIC_SUBTITLE = 'stub.empty.tasks.public.subtitle', STUB_EMPTY_TASKS_PUBLIC_FILTERS_SUBTITLE = 'stub.empty.tasks.public.filters.subtitle', STUB_EMPTY_TASKS_ADMIN_TITLE = 'stub.empty.tasks.admin.title', STUB_EMPTY_TASKS_ADMIN_SUBTITLE = 'stub.empty.tasks.admin.subtitle', STUB_EMPTY_TASKS_ADMIN_SUBMIT = 'stub.empty.tasks.admin.submit', - STUB_EMPTY_TASK_PUBLIC_TITLE = 'stub.empty.task.public.title', - STUB_EMPTY_TASK_PUBLIC_SUBTITLE = 'stub.empty.task.public.subtitle', - STUB_EMPTY_TASK_PUBLIC_SUBMIT = 'stub.empty.task.public.submit', + STUB_EMPTY_TASK_TITLE = 'stub.empty.task.title', + STUB_EMPTY_TASK_SUBTITLE = 'stub.empty.task.subtitle', + STUB_EMPTY_TASK_SUBMIT = 'stub.empty.task.submit', DESCRIPTION_TAB_TITLE = 'description.tab.title', DESCRIPTION_CONSTRAINTS_TITLE = 'description.constraints.title', DESCRIPTION_TAB_SUBTITLE = 'description.tab.subtitle', + STATUS_ATTEMPTED = 'status.attempted', + STATUS_NOT_STARTED = 'status.not.started', + STATUS_SOLVED = 'status.solved', + DESCRIPTION_TITLE = 'description.title', + DESCRIPTION_SUBTITLE = 'description.subtitle', + CATEGORY_TITLE = 'category.title', + CATEGORY_SUBTITLE = 'category.subtitle', + CATEGORY_PLACEHOLDER = 'category.placeholder', + CATEGORY_DATA_STRUCTURES = 'category.data.structures', + CATEGORY_ALGORITHMS = 'category.algorithms', + CATEGORY_ARRAYS = 'category.arrays', + CATEGORY_DATABASES = 'category.databases', + CATEGORY_STRINGS = 'category.strings', + CATEGORY_DYNAMIC_PROGRAMMING = 'category.dynamic.programming', SOLUTIONS_TAB_TITLE = 'solutions.tab.title', SOLUTIONS_TAB_SUBTITLE = 'solutions.tab.subtitle', + SOLUTIONS_BACK_BUTTON = 'solutions.back.button', EDITOR_ACTIONS_RUN = 'editor.actions.run', EDITOR_ACTIONS_SUBMIT = 'editor.actions.submit', OUTPUT_RESULT_TAB_TITLE = 'output.result.tab.title', diff --git a/src/shared/config/query/apiTags.ts b/src/shared/config/query/apiTags.ts index 85e134efa..e08298391 100644 --- a/src/shared/config/query/apiTags.ts +++ b/src/shared/config/query/apiTags.ts @@ -38,6 +38,8 @@ export enum ApiTags { TOPICS = 'topics', TASKS = 'tasks', TASK_DETAIL = 'task_detail', + TASK_SOLUTIONS = 'task_solutions', + TASK_CATEGORIES = 'task_categories', USERS_RATING = 'users_rating', TOPIC_DETAIL = 'topic_detail', PROGRAMMING_LANGUAGE = 'programming_language', diff --git a/src/shared/config/router/routes.ts b/src/shared/config/router/routes.ts index a7fbc2966..fecbbe002 100644 --- a/src/shared/config/router/routes.ts +++ b/src/shared/config/router/routes.ts @@ -142,6 +142,18 @@ export const ROUTES = { tasks: { route: 'tasks', page: '/admin/tasks', + create: { + route: 'create', + page: '/admin/tasks/create', + }, + edit: { + route: ':taskId/edit', + page: '/admin/tasks/:taskId/edit', + }, + details: { + route: ':taskId', + page: '/admin/tasks/:taskId', + }, }, }, auth: { diff --git a/src/shared/libs/jest/renderComponent/renderComponent.tsx b/src/shared/libs/jest/renderComponent/renderComponent.tsx index 07dfc7a05..2ba2b952a 100644 --- a/src/shared/libs/jest/renderComponent/renderComponent.tsx +++ b/src/shared/libs/jest/renderComponent/renderComponent.tsx @@ -25,7 +25,7 @@ interface RenderComponentOptions { */ export const renderComponent = (component: ReactNode, options: RenderComponentOptions = {}) => { const { route = '/', initialState = {}, reducers = {} } = options; - console.log(2222, component); + return render( diff --git a/src/shared/ui/Dropdown/Dropdown/Dropdown.tsx b/src/shared/ui/Dropdown/Dropdown/Dropdown.tsx index 3dce06929..8de221c71 100644 --- a/src/shared/ui/Dropdown/Dropdown/Dropdown.tsx +++ b/src/shared/ui/Dropdown/Dropdown/Dropdown.tsx @@ -12,10 +12,8 @@ import { Select } from '../Select/Select'; import styles from './Dropdown.module.css'; -export interface DropdownProps extends Omit< - React.HTMLProps, - 'prefix' | 'size' | 'onSelect' | 'value' -> { +export interface DropdownProps + extends Omit, 'prefix' | 'size' | 'onSelect' | 'value'> { prefix?: React.ReactNode; suffix?: React.ReactNode; size?: DropdownSize; diff --git a/src/shared/ui/FormField/FormField.tsx b/src/shared/ui/FormField/FormField.tsx index 09b706fc7..146919f59 100644 --- a/src/shared/ui/FormField/FormField.tsx +++ b/src/shared/ui/FormField/FormField.tsx @@ -7,25 +7,35 @@ import { Text } from '@/shared/ui/Text'; import styles from './FormField.module.css'; export interface FormFieldProps { - description: string; + description?: string; label: string; children: ReactNode; isLimitWidth?: boolean; + direction?: 'row' | 'column'; } -export const FormField = ({ description, label, children, isLimitWidth }: FormFieldProps) => { +export const FormField = ({ + description, + label, + children, + isLimitWidth, + direction = 'row', +}: FormFieldProps) => { return ( {label} - - {description} - + {description && ( + + {description} + + )} {children} diff --git a/src/shared/ui/IconButton/types.ts b/src/shared/ui/IconButton/types.ts index 3aec782b5..7e3200bc2 100644 --- a/src/shared/ui/IconButton/types.ts +++ b/src/shared/ui/IconButton/types.ts @@ -18,5 +18,5 @@ export interface ButtonProps extends React.ComponentPropsWithRef<'button'> { size?: IconButtonSize; icon: React.ReactNode; destructive?: boolean; - 'aria-label': string; + 'aria-label'?: string; } diff --git a/src/shared/ui/StatusChip/StatusChip.module.css b/src/shared/ui/StatusChip/StatusChip.module.css index e017f3bac..6510d43f5 100644 --- a/src/shared/ui/StatusChip/StatusChip.module.css +++ b/src/shared/ui/StatusChip/StatusChip.module.css @@ -1,8 +1,6 @@ .wrapper { padding: 2px 8px; width: fit-content; - height: 20px; - border-radius: 30px; } .variant-yellow { @@ -23,4 +21,14 @@ .variant-purple { background-color: var(--color-purple-100); color: var(--color-purple-800); +} + +.size-small { + height: 20px; + border-radius: 30px; +} + +.size-medium { + height: 32px; + border-radius: 8px; } \ No newline at end of file diff --git a/src/shared/ui/StatusChip/StatusChip.tsx b/src/shared/ui/StatusChip/StatusChip.tsx index d0482d631..583add5d9 100644 --- a/src/shared/ui/StatusChip/StatusChip.tsx +++ b/src/shared/ui/StatusChip/StatusChip.tsx @@ -7,6 +7,7 @@ import { Text } from '@/shared/ui/Text'; import styles from './StatusChip.module.css'; export type StatusChipVariant = 'green' | 'yellow' | 'red' | 'purple'; +export type StatusChipSize = 'small' | 'medium'; export interface StatusChipItem { text: string; @@ -14,9 +15,10 @@ export interface StatusChipItem { } export interface StatusChipProps { status: StatusChipItem; + size?: StatusChipSize; } -export const StatusChip = ({ status }: StatusChipProps) => { +export const StatusChip = ({ status, size = 'small' }: StatusChipProps) => { const { variant, text } = status; return ( @@ -24,7 +26,7 @@ export const StatusChip = ({ status }: StatusChipProps) => { justify="center" align="center" dataTestId={statusChipTestIds.statusChip} - className={classNames(styles.wrapper, styles[`variant-${variant}`])} + className={classNames(styles.wrapper, styles[`variant-${variant}`], styles[`size-${size}`])} > { - /** - * Array of elements displayed in the table - */ items: T[]; - /** - * Render function for displaying the table header - */ renderTableHeader: () => ReactNode; - /** - * Render function for displaying the table body - */ renderTableBody: (item: T, index?: number) => ReactNode; - /** - * Render function for displaying the table actions in the last column - */ renderActions?: (item: T) => ReactNode; selectedItems?: SelectedEntities; onSelectItems?: (ids: SelectedEntities) => void; - /** - * Render function for defining column widths in the table. - */ renderTableColumnWidths?: () => ReactNode; - /** - * Shows a copy button - */ hasCopyButton?: boolean; + onRowClick?: (item: T) => void; } -/** - * Component that is used to display data in a tabular structure. - * - * @param items - Array of elements displayed in the table. - * @param renderTableHeader - Render function for displaying the table header. - * @param renderTableBody - Render function for displaying the table body. - * @param renderActions - Render function for displaying the table actions in the last column. - * @param selectedItems - Array of currently selected entities. - * @param onSelectItems - Callback function triggered when selection changes. - */ export const Table = >({ items, renderTableHeader, @@ -54,6 +28,7 @@ export const Table = >( onSelectItems, renderTableColumnWidths, hasCopyButton, + onRowClick, }: TableProps) => { const hasActions = !!renderActions; @@ -122,7 +97,12 @@ export const Table = >(
    {items.map((item, index) => ( - + onRowClick?.(item)} + > {selectedItems && (
    { +interface AdditionalInfoProps + extends Pick< + Collection, + 'specializations' | 'isFree' | 'company' | 'questionsCount' | 'createdBy' | 'keywords' + > { showAuthor?: boolean; className?: string; media?: Media | undefined; diff --git a/src/widgets/Collection/ui/CollectionHeader/CollectionHeader.tsx b/src/widgets/Collection/ui/CollectionHeader/CollectionHeader.tsx index cd9c2b54f..4eca4e37c 100644 --- a/src/widgets/Collection/ui/CollectionHeader/CollectionHeader.tsx +++ b/src/widgets/Collection/ui/CollectionHeader/CollectionHeader.tsx @@ -11,10 +11,8 @@ import { Collection } from '@/entities/collection'; import styles from './CollectionHeader.module.css'; -interface CollectionHeaderProps extends Pick< - Collection, - 'title' | 'description' | 'imageSrc' | 'company' -> { +interface CollectionHeaderProps + extends Pick { renderDrawer: () => ReactNode; } diff --git a/src/widgets/analytics/PopularSkillsWidget/ui/PopularSkillsWidget/PopularSkillsWidget.tsx b/src/widgets/analytics/PopularSkillsWidget/ui/PopularSkillsWidget/PopularSkillsWidget.tsx index adab4f37d..19f58ee6b 100644 --- a/src/widgets/analytics/PopularSkillsWidget/ui/PopularSkillsWidget/PopularSkillsWidget.tsx +++ b/src/widgets/analytics/PopularSkillsWidget/ui/PopularSkillsWidget/PopularSkillsWidget.tsx @@ -36,14 +36,16 @@ const PopularSkillsWidget = () => { isActionPositionBottom > - {data?.data.slice(0, 3).map((item) => ( - - ))} + {data?.data + .slice(0, 3) + .map((item) => ( + + ))} ); diff --git a/src/widgets/question/QuestionsList/ui/PreviewQuestionsList/PreviewQuestionsList.tsx b/src/widgets/question/QuestionsList/ui/PreviewQuestionsList/PreviewQuestionsList.tsx index 47112cbf4..977604b01 100644 --- a/src/widgets/question/QuestionsList/ui/PreviewQuestionsList/PreviewQuestionsList.tsx +++ b/src/widgets/question/QuestionsList/ui/PreviewQuestionsList/PreviewQuestionsList.tsx @@ -24,7 +24,7 @@ export const PreviewQuestionsList = ({ className }: PreviewQuestionsListProps) = const { data: response, isSuccess } = useGetQuestionsListQuery({ random: true, limit: 3, - specialization: specializationId, + specializationId, }); const questions = response?.data ?? []; diff --git a/src/widgets/task/TaskTabs/model/types/types.ts b/src/widgets/task/TaskTabs/model/types/types.ts index 6af712e63..e1964a488 100644 --- a/src/widgets/task/TaskTabs/model/types/types.ts +++ b/src/widgets/task/TaskTabs/model/types/types.ts @@ -4,5 +4,4 @@ export type TaskTabId = 'description' | 'solutions'; export type TaskTabsProps = { task: Task; - solutions?: string[]; }; diff --git a/src/widgets/task/TaskTabs/ui/TaskTabs.module.css b/src/widgets/task/TaskTabs/ui/TaskTabs.module.css index f3eb3a4dc..c4136d4d1 100644 --- a/src/widgets/task/TaskTabs/ui/TaskTabs.module.css +++ b/src/widgets/task/TaskTabs/ui/TaskTabs.module.css @@ -1,6 +1,7 @@ .wrapper { display: flex; flex-direction: column; + padding: 16px; height: 100%; } diff --git a/src/widgets/task/TaskTabs/ui/TaskTabs.tsx b/src/widgets/task/TaskTabs/ui/TaskTabs.tsx index 2a8b0b332..2b5475a6c 100644 --- a/src/widgets/task/TaskTabs/ui/TaskTabs.tsx +++ b/src/widgets/task/TaskTabs/ui/TaskTabs.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { i18Namespace, Task as TaskTranslations } from '@/shared/config'; +import { i18Namespace, Tasks } from '@/shared/config'; import { Card } from '@/shared/ui/Card'; import { Tabs, Tab } from '@/shared/ui/Tabs'; @@ -12,23 +12,23 @@ import type { TaskTabId, TaskTabsProps } from '../model/types/types'; import styles from './TaskTabs.module.css'; -export const TaskTabs = ({ task, solutions }: TaskTabsProps) => { +export const TaskTabs = ({ task }: TaskTabsProps) => { const { t } = useTranslation(i18Namespace.task); const tabs: Tab[] = useMemo( () => [ { id: 'description', - label: t(TaskTranslations.DESCRIPTION_TAB_TITLE), + label: t(Tasks.DESCRIPTION_TAB_TITLE), Component: () => , }, { id: 'solutions', - label: t(TaskTranslations.SOLUTIONS_TAB_TITLE), - Component: () => , + label: t(Tasks.SOLUTIONS_TAB_TITLE), + Component: () => , }, ], - [task, solutions], + [task], ); const { activeTab, setActiveTab } = useTaskTabsQuery(tabs); @@ -38,8 +38,14 @@ export const TaskTabs = ({ task, solutions }: TaskTabsProps) => { } return ( - - + +
    {activeTab.Component()}
    ); diff --git a/src/widgets/task/TaskWorkspace/index.ts b/src/widgets/task/TaskWorkspace/index.ts index e781e634e..d9319cf3e 100644 --- a/src/widgets/task/TaskWorkspace/index.ts +++ b/src/widgets/task/TaskWorkspace/index.ts @@ -1,2 +1 @@ export { TaskWorkspace } from './ui/TaskWorkspace'; -export type { TaskWorkspaceProps } from './model/types/types'; diff --git a/src/widgets/task/TaskWorkspace/model/types/types.ts b/src/widgets/task/TaskWorkspace/model/types/types.ts index b36047ae8..5fc7e0644 100644 --- a/src/widgets/task/TaskWorkspace/model/types/types.ts +++ b/src/widgets/task/TaskWorkspace/model/types/types.ts @@ -1,19 +1,5 @@ import type { ExecuteCodeResponse } from '@/entities/task'; -export interface TaskWorkspaceProps { - code: string; - languageId: number; - supportedLanguages: Array<{ id: number; name: string }>; - isExecuting: boolean; - isTesting: boolean; - output: ExecuteCodeResponse | null; - onCodeChange: (code: string) => void; - onLanguageChange: (languageId: number) => void; - onReset: () => void; - onRun: () => void; - onSubmit: () => void; -} - export type OutputTabId = 'result' | 'tests'; export type TaskOutputProps = { diff --git a/src/widgets/task/TaskWorkspace/ui/TaskEditor/TaskEditor.module.css b/src/widgets/task/TaskWorkspace/ui/TaskEditor/TaskEditor.module.css index 0aac7a036..38d2f6b4a 100644 --- a/src/widgets/task/TaskWorkspace/ui/TaskEditor/TaskEditor.module.css +++ b/src/widgets/task/TaskWorkspace/ui/TaskEditor/TaskEditor.module.css @@ -1,41 +1,14 @@ .wrapper { - display: flex; - flex-direction: column; - gap: 16px; - padding: 16px; height: 100%; } .header { - display: flex; - justify-content: space-between; - align-items: center; + z-index: 1; } -.actions { - display: flex; - align-items: center; - gap: 12px; -} - -.select { - padding: 8px 12px; - outline: none; - border: 1px solid var(--color-gray-300); - border-radius: 8px; - background-color: var(--color-white); - color: var(--color-black-700); - font-size: var(--font-size-p-s); - transition: border-color 0.2s; - cursor: pointer; -} - -.select:hover { - border-color: var(--color-purple-500); -} - -.select:focus { - border-color: var(--color-purple-700); +.block { + height: 100%; + overflow: hidden; } .editor { diff --git a/src/widgets/task/TaskWorkspace/ui/TaskEditor/TaskEditor.tsx b/src/widgets/task/TaskWorkspace/ui/TaskEditor/TaskEditor.tsx index 9f0fbbf3f..a0455af28 100644 --- a/src/widgets/task/TaskWorkspace/ui/TaskEditor/TaskEditor.tsx +++ b/src/widgets/task/TaskWorkspace/ui/TaskEditor/TaskEditor.tsx @@ -1,16 +1,21 @@ import MonacoEditor from '@monaco-editor/react'; import { useTranslation } from 'react-i18next'; -import { i18Namespace, Task as TaskTranslations } from '@/shared/config'; +import { i18Namespace, Tasks } from '@/shared/config'; import { Button } from '@/shared/ui/Button'; +import { Card } from '@/shared/ui/Card'; +import { Flex } from '@/shared/ui/Flex'; import { Icon } from '@/shared/ui/Icon'; +import { IconButton } from '@/shared/ui/IconButton'; + +import { ProgrammingLanguage, ProgrammingLanguageSelect } from '@/entities/programmingLanguage'; import styles from './TaskEditor.module.css'; type TaskEditorProps = { code: string; languageId: number; - supportedLanguages: { id: number; name: string }[]; + supportedLanguages: ProgrammingLanguage[]; isExecuting: boolean; isTesting: boolean; onCodeChange: (code: string) => void; @@ -36,46 +41,46 @@ export const TaskEditor = ({ const currentLanguage = supportedLanguages.find((lang) => lang.id === languageId); return ( -
    -
    -
    - - -
    -
    - - + + + + + { + onLanguageChange(Number(value)); + }} + supportedLanguages={supportedLanguages} + /> + } onClick={onReset} /> + + + + + + + + + +
    + onCodeChange(value || '')} + theme="vs-light" + options={{ + minimap: { enabled: false }, + scrollBeyondLastLine: false, + fontSize: 14, + }} + />
    -
    -
    - onCodeChange(value || '')} - theme="vs-light" - options={{ - minimap: { enabled: false }, - scrollBeyondLastLine: false, - fontSize: 14, - }} - /> -
    -
    + + ); }; diff --git a/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutput.tsx b/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutput.tsx index a3f5459de..7048ccd5a 100644 --- a/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutput.tsx +++ b/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutput.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { i18Namespace, Task as TaskTranslations } from '@/shared/config'; +import { i18Namespace, Tasks } from '@/shared/config'; import { Tab, Tabs } from '@/shared/ui/Tabs'; import { useTaskOutputQuery } from '../../model/hooks/useTaskOutputQuery'; @@ -9,7 +9,7 @@ import type { TaskOutputProps, OutputTabId } from '../../model/types/types'; import styles from './TaskOutput.module.css'; import { TaskOutputResult } from './TaskOutputResult/TaskOutputResult'; -import { TaskOutputTests } from './TaskOutputTests/TaskOutputTests'; +// import { TaskOutputTests } from './TaskOutputTests/TaskOutputTests'; export const TaskOutput = ({ result }: TaskOutputProps) => { const { t } = useTranslation(i18Namespace.task); @@ -18,14 +18,14 @@ export const TaskOutput = ({ result }: TaskOutputProps) => { () => [ { id: 'result', - label: t(TaskTranslations.OUTPUT_RESULT_TAB_TITLE), + label: t(Tasks.OUTPUT_RESULT_TAB_TITLE), Component: () => , }, - { - id: 'tests', - label: t(TaskTranslations.OUTPUT_TESTS_TAB_TITLE), - Component: () => , - }, + // { + // id: 'tests', + // label: t(Tasks.OUTPUT_TESTS_TAB_TITLE), + // Component: () => , + // }, ], [result, t], ); @@ -38,7 +38,13 @@ export const TaskOutput = ({ result }: TaskOutputProps) => { return (
    - +
    {activeTab.Component()}
    ); diff --git a/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutputResult/TaskOutputResult.module.css b/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutputResult/TaskOutputResult.module.css index 4f6256ce5..efd188bb4 100644 --- a/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutputResult/TaskOutputResult.module.css +++ b/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutputResult/TaskOutputResult.module.css @@ -1,9 +1,3 @@ -.wrapper { - display: flex; - flex-direction: column; - gap: 16px; -} - .error, .output { display: flex; @@ -15,7 +9,7 @@ padding: 12px; overflow-x: auto; border-radius: 8px; - background-color: var(--color-gray-100); + background-color: var(--color-black-400); color: var(--color-black-700); font-size: var(--font-size-p-s); font-family: var(--font-family-code); @@ -23,10 +17,7 @@ } .stats { - display: flex; - flex-direction: column; - gap: 8px; padding: 16px; border-radius: 8px; - background-color: var(--color-gray-50); + background-color: var(--color-black-100); } diff --git a/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutputResult/TaskOutputResult.tsx b/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutputResult/TaskOutputResult.tsx index acd9964bb..db520cb96 100644 --- a/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutputResult/TaskOutputResult.tsx +++ b/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutputResult/TaskOutputResult.tsx @@ -1,6 +1,7 @@ import { useTranslation } from 'react-i18next'; -import { i18Namespace, Task as TaskTranslations } from '@/shared/config'; +import { i18Namespace, Tasks } from '@/shared/config'; +import { Flex } from '@/shared/ui/Flex'; import { Stub } from '@/shared/ui/Stub'; import { Text } from '@/shared/ui/Text'; @@ -19,47 +20,55 @@ export const TaskOutputResult = ({ result }: TaskOutputResultProps) => { return ( ); } return ( -
    - {result.compilation_error && ( -
    - - {t(TaskTranslations.OUTPUT_RESULT_COMPILATION_ERROR)}: - -
    {result.compilation_error}
    -
    - )} + + {/*{result.compilation_error && (*/} + {/*
    */} + {/* */} + {/* {t(Tasks.OUTPUT_RESULT_COMPILATION_ERROR)}:*/} + {/* */} + {/*
    {result.compilation_error}
    */} + {/*
    */} + {/*)}*/} - {result.runtime_output && ( -
    - {t(TaskTranslations.OUTPUT_RESULT_RUNTIME_OUTPUT)}: -
    {result.runtime_output}
    -
    - )} - -
    - - {t(TaskTranslations.OUTPUT_RESULT_TESTS_PASSED)}: {result.passed_tests}/ - {result.total_tests} - - - {t(TaskTranslations.OUTPUT_RESULT_SUCCESS_RATE)}: {result.success_rate.toFixed(2)}% - + {/*{result.runtime_output && (*/} + {/*
    */} + {/* {t(Tasks.OUTPUT_RESULT_RUNTIME_OUTPUT)}:*/} + {/*
    {result.runtime_output}
    */} + {/*
    */} + {/*)}*/} + + {t( + result.overall_status === 'SUCCESS' + ? Tasks.TABLE_STATUS_SOLVED + : Tasks.TABLE_STATUS_NOT_SOLVED, + )} + + + {/**/} + {/* {t(Tasks.OUTPUT_RESULT_TESTS_PASSED)}: {result.passed_tests}/{result.total_tests}*/} + {/**/} + {/**/} + {/* {t(Tasks.OUTPUT_RESULT_SUCCESS_RATE)}: {result.success_rate.toFixed(2)}%*/} + {/**/} - {t(TaskTranslations.OUTPUT_RESULT_EXECUTION_TIME)}:{' '} - {result.total_execution_time.toFixed(2)} мс + {t(Tasks.OUTPUT_RESULT_EXECUTION_TIME)}:{' '} + {t(Tasks.TIME_LIMIT_VALUE, { count: result.total_execution_time || 0 })} - {t(TaskTranslations.OUTPUT_RESULT_MEMORY_USAGE)}: {result.average_memory_usage.toFixed(2)}{' '} - KB + {t(Tasks.OUTPUT_RESULT_MEMORY_USAGE)}:{' '} + {t(Tasks.MEMORY_LIMIT_VALUE, { count: result.average_memory_usage || 0 })} -
    -
    + + ); }; diff --git a/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutputTests/TaskOutputTests.tsx b/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutputTests/TaskOutputTests.tsx index 9ee2b539f..755777b2c 100644 --- a/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutputTests/TaskOutputTests.tsx +++ b/src/widgets/task/TaskWorkspace/ui/TaskOutput/TaskOutputTests/TaskOutputTests.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { i18Namespace, Task as TaskTranslations } from '@/shared/config'; +import { i18Namespace, Tasks } from '@/shared/config'; import { Tab, Tabs } from '@/shared/ui/Tabs'; import { Text } from '@/shared/ui/Text'; @@ -23,7 +23,7 @@ export const TaskOutputTests = ({ result }: TaskOutputTestsProps) => { return result.test_cases.map((testCase, index) => ({ id: index, - label: t(TaskTranslations.OUTPUT_TESTS_TEST_CASE_TITLE, { index: index + 1 }), + label: t(Tasks.OUTPUT_TESTS_TEST_CASE_TITLE, { index: index + 1 }), Component: () => (
    @@ -31,28 +31,26 @@ export const TaskOutputTests = ({ result }: TaskOutputTestsProps) => { variant="body3-strong" color={testCase.status === 'PASSED' ? 'green-500' : 'red-500'} > - {t(TaskTranslations.OUTPUT_TESTS_TEST_CASE_STATUS)}:{' '} + {t(Tasks.OUTPUT_TESTS_TEST_CASE_STATUS)}:{' '} {testCase.status === 'PASSED' - ? t(TaskTranslations.OUTPUT_TESTS_TEST_CASE_PASSED) - : t(TaskTranslations.OUTPUT_TESTS_TEST_CASE_FAILED)} + ? t(Tasks.OUTPUT_TESTS_TEST_CASE_PASSED) + : t(Tasks.OUTPUT_TESTS_TEST_CASE_FAILED)} {testCase.is_hidden && ( - {t(TaskTranslations.OUTPUT_TESTS_TEST_CASE_HIDDEN)} + {t(Tasks.OUTPUT_TESTS_TEST_CASE_HIDDEN)} )}
    {!testCase.is_hidden && ( <>
    - - {t(TaskTranslations.OUTPUT_TESTS_TEST_CASE_INPUT)}: - + {t(Tasks.OUTPUT_TESTS_TEST_CASE_INPUT)}:
    {JSON.stringify(testCase.input, null, 2)}
    - {t(TaskTranslations.OUTPUT_TESTS_TEST_CASE_EXPECTED_OUTPUT)}: + {t(Tasks.OUTPUT_TESTS_TEST_CASE_EXPECTED_OUTPUT)}:
     									{JSON.stringify(testCase.expected_output, null, 2)}
    @@ -60,9 +58,7 @@ export const TaskOutputTests = ({ result }: TaskOutputTestsProps) => {
     							
    - - {t(TaskTranslations.OUTPUT_TESTS_TEST_CASE_ACTUAL_OUTPUT)}: - + {t(Tasks.OUTPUT_TESTS_TEST_CASE_ACTUAL_OUTPUT)}:
    {testCase.actual_output}
    @@ -71,7 +67,7 @@ export const TaskOutputTests = ({ result }: TaskOutputTestsProps) => { {testCase.error_message && (
    - {t(TaskTranslations.OUTPUT_TESTS_TEST_CASE_ERROR_MESSAGE)}: + {t(Tasks.OUTPUT_TESTS_TEST_CASE_ERROR_MESSAGE)}:
    {testCase.error_message}
    @@ -79,12 +75,11 @@ export const TaskOutputTests = ({ result }: TaskOutputTestsProps) => {
    - {t(TaskTranslations.OUTPUT_TESTS_TEST_CASE_EXECUTION_TIME)}:{' '} - {testCase.execution_time.toFixed(2)} мс + {t(Tasks.OUTPUT_TESTS_TEST_CASE_EXECUTION_TIME)}: {testCase.execution_time.toFixed(2)}{' '} + мс - {t(TaskTranslations.OUTPUT_TESTS_TEST_CASE_MEMORY_USAGE)}:{' '} - {testCase.memory_usage.toFixed(2)} KB + {t(Tasks.OUTPUT_TESTS_TEST_CASE_MEMORY_USAGE)}: {testCase.memory_usage.toFixed(2)} KB
    @@ -99,7 +94,7 @@ export const TaskOutputTests = ({ result }: TaskOutputTestsProps) => { if (testTabs.length === 0 || !activeTestTab) { return (
    - {t(TaskTranslations.OUTPUT_TESTS_TEST_CASE_EMPTY)} + {t(Tasks.OUTPUT_TESTS_TEST_CASE_EMPTY)}
    ); } diff --git a/src/widgets/task/TaskWorkspace/ui/TaskWorkspace.module.css b/src/widgets/task/TaskWorkspace/ui/TaskWorkspace.module.css index 8a6fb68e0..31c9cf606 100644 --- a/src/widgets/task/TaskWorkspace/ui/TaskWorkspace.module.css +++ b/src/widgets/task/TaskWorkspace/ui/TaskWorkspace.module.css @@ -12,6 +12,7 @@ .editor-card { display: flex; flex-direction: column; + padding: 16px; height: 100%; overflow: hidden; } @@ -24,6 +25,7 @@ .output-card { display: flex; flex-direction: column; + padding: 16px; height: 100%; overflow: hidden; } diff --git a/src/widgets/task/TaskWorkspace/ui/TaskWorkspace.tsx b/src/widgets/task/TaskWorkspace/ui/TaskWorkspace.tsx index 38a5c668d..950c894ee 100644 --- a/src/widgets/task/TaskWorkspace/ui/TaskWorkspace.tsx +++ b/src/widgets/task/TaskWorkspace/ui/TaskWorkspace.tsx @@ -4,12 +4,27 @@ import type { PanelImperativeHandle } from 'react-resizable-panels'; import { Card } from '@/shared/ui/Card'; -import type { TaskWorkspaceProps } from '../model/types/types'; +import { ProgrammingLanguage } from '@/entities/programmingLanguage'; +import type { ExecuteCodeResponse } from '@/entities/task'; import { TaskEditor } from './TaskEditor/TaskEditor'; import { TaskOutput } from './TaskOutput/TaskOutput'; import styles from './TaskWorkspace.module.css'; +interface TaskWorkspaceProps { + code: string; + languageId: number; + supportedLanguages: ProgrammingLanguage[]; + isExecuting: boolean; + isTesting: boolean; + output: ExecuteCodeResponse | null; + onCodeChange: (code: string) => void; + onLanguageChange: (languageId: number) => void; + onReset: () => void; + onRun: () => void; + onSubmit: () => void; +} + export const TaskWorkspace = ({ code, languageId, @@ -37,32 +52,31 @@ export const TaskWorkspace = ({ return ( - - - - + + - + diff --git a/src/widgets/task/TasksList/ui/TasksList.tsx b/src/widgets/task/TasksList/ui/TasksList.tsx index aa88ac1da..b6cd06a49 100644 --- a/src/widgets/task/TasksList/ui/TasksList.tsx +++ b/src/widgets/task/TasksList/ui/TasksList.tsx @@ -1,10 +1,10 @@ import { Flex } from '@/shared/ui/Flex'; -import type { TaskListItem } from '@/entities/task'; +import type { Task } from '@/entities/task'; import { TaskCard } from '@/entities/task'; interface TasksListProps { - tasks: TaskListItem[]; + tasks: Task[]; } export const TasksList = ({ tasks }: TasksListProps) => {