From 44a7ac2bfd8a3040f05fef4288d89973bf34ff65 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Tue, 4 Nov 2025 10:25:12 +0300 Subject: [PATCH] Enhance localization support by integrating i18next for translations across various components and pages. Update UI elements to utilize translated strings for improved user experience in both English and Russian. Additionally, refactor the Toaster component to support a context-based approach for toast notifications. --- locales/en.json | 191 +++++++++++++++++++- locales/ru.json | 190 +++++++++++++++++++- src/app.tsx | 12 +- src/components/ConfirmDialog.tsx | 15 +- src/components/ErrorAlert.tsx | 9 +- src/components/Layout.tsx | 44 ++++- src/components/LoadingSpinner.tsx | 7 +- src/components/StatusBadge.tsx | 20 +-- src/components/ui/toaster.tsx | 202 +++++++++++++++++++++- src/pages/chains/ChainFormPage.tsx | 66 ++++--- src/pages/chains/ChainsListPage.tsx | 50 +++--- src/pages/dashboard/DashboardPage.tsx | 42 ++--- src/pages/index.ts | 4 - src/pages/main/index.ts | 2 - src/pages/main/main.tsx | 11 -- src/pages/submissions/SubmissionsPage.tsx | 68 ++++---- src/pages/tasks/TaskFormPage.tsx | 76 ++++---- src/pages/tasks/TasksListPage.tsx | 56 +++--- src/pages/users/UsersPage.tsx | 120 ++++++------- 19 files changed, 892 insertions(+), 293 deletions(-) delete mode 100644 src/pages/index.ts delete mode 100644 src/pages/main/index.ts delete mode 100644 src/pages/main/main.tsx diff --git a/locales/en.json b/locales/en.json index 0487407..31cd851 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,3 +1,192 @@ { - "challenge.title": "Challenge" + "challenge.title": "Challenge", + "challenge.admin.common.success": "Success", + "challenge.admin.common.error": "Error", + "challenge.admin.common.cancel": "Cancel", + "challenge.admin.common.loading.tasks": "Loading tasks...", + "challenge.admin.common.not.found": "Nothing found", + "challenge.admin.common.validation.error": "Validation error", + "challenge.admin.tasks.updated": "Task updated", + "challenge.admin.tasks.created": "Task created", + "challenge.admin.tasks.validation.fill.required.fields": "Fill in required fields", + "challenge.admin.tasks.save.error": "Failed to save task", + "challenge.admin.tasks.loading": "Loading task...", + "challenge.admin.tasks.load.error": "Failed to load task", + "challenge.admin.tasks.edit.title": "Edit task", + "challenge.admin.tasks.create.title": "Create task", + "challenge.admin.tasks.field.title": "Task title", + "challenge.admin.tasks.field.title.placeholder": "Enter task title", + "challenge.admin.tasks.field.title.helper": "Maximum 255 characters", + "challenge.admin.tasks.field.description": "Description (Markdown)", + "challenge.admin.tasks.field.description.placeholder": "# Task title\n\nTask description in Markdown format...", + "challenge.admin.tasks.field.description.helper": "Use Markdown to format text", + "challenge.admin.tasks.tab.editor": "Editor", + "challenge.admin.tasks.tab.preview": "Preview", + "challenge.admin.tasks.preview.empty": "Preview will appear here...", + "challenge.admin.tasks.field.hidden.instructions": "🔒 Hidden instructions for LLM", + "challenge.admin.tasks.field.hidden.instructions.description": "These instructions will be passed to the LLM when checking student solutions. Students will not see them.", + "challenge.admin.tasks.field.hidden.instructions.placeholder": "Example: Check that algorithm complexity is O(n log n). Code should handle edge cases...", + "challenge.admin.tasks.field.hidden.instructions.helper": "Optional. Use for fine-tuning LLM verification.", + "challenge.admin.tasks.meta.created": "Created:", + "challenge.admin.tasks.meta.author": "Author:", + "challenge.admin.tasks.meta.updated": "Updated:", + "challenge.admin.tasks.button.save": "Save changes", + "challenge.admin.tasks.button.create": "Create task", + "challenge.admin.tasks.list.title": "Tasks", + "challenge.admin.tasks.list.create.button": "+ Create Task", + "challenge.admin.tasks.list.search.placeholder": "Search by name...", + "challenge.admin.tasks.list.empty.title": "No tasks", + "challenge.admin.tasks.list.empty.description": "Create your first task to get started", + "challenge.admin.tasks.list.empty.action": "Create task", + "challenge.admin.tasks.list.search.empty": "Nothing found for \"{query}\"", + "challenge.admin.tasks.list.table.title": "Title", + "challenge.admin.tasks.list.table.creator": "Creator", + "challenge.admin.tasks.list.table.created": "Created date", + "challenge.admin.tasks.list.table.hidden.instructions": "Hidden instructions", + "challenge.admin.tasks.list.table.actions": "Actions", + "challenge.admin.tasks.list.badge.has.instructions": "🔒 Yes", + "challenge.admin.tasks.list.button.edit": "Edit", + "challenge.admin.tasks.list.button.delete": "Delete", + "challenge.admin.tasks.deleted": "Task deleted", + "challenge.admin.tasks.delete.error": "Failed to delete task", + "challenge.admin.tasks.list.loading": "Loading tasks...", + "challenge.admin.tasks.list.load.error": "Failed to load tasks list", + "challenge.admin.tasks.delete.confirm.title": "Delete task", + "challenge.admin.tasks.delete.confirm.message": "Are you sure you want to delete task \"{title}\"? This action cannot be undone.", + "challenge.admin.tasks.delete.confirm.button": "Delete", + "challenge.admin.chains.updated": "Chain updated", + "challenge.admin.chains.created": "Chain created", + "challenge.admin.chains.validation.enter.name": "Enter chain name", + "challenge.admin.chains.validation.add.task": "Add at least one task", + "challenge.admin.chains.save.error": "Failed to save chain", + "challenge.admin.chains.loading": "Loading chain...", + "challenge.admin.chains.load.error": "Failed to load chain", + "challenge.admin.chains.tasks.load.error": "Failed to load task list", + "challenge.admin.chains.edit.title": "Edit chain", + "challenge.admin.chains.create.title": "Create chain", + "challenge.admin.chains.field.name": "Chain name", + "challenge.admin.chains.field.name.placeholder": "Enter chain name", + "challenge.admin.chains.selected.tasks": "Tasks in chain", + "challenge.admin.chains.selected.tasks.empty": "Add tasks from the list below", + "challenge.admin.chains.available.tasks": "Available tasks", + "challenge.admin.chains.search.placeholder": "Search tasks...", + "challenge.admin.chains.all.tasks.added": "All tasks already added", + "challenge.admin.chains.button.add": "+ Add", + "challenge.admin.chains.button.save": "Save changes", + "challenge.admin.chains.button.create": "Create chain", + "challenge.admin.chains.list.title": "Task Chains", + "challenge.admin.chains.list.create.button": "+ Create Chain", + "challenge.admin.chains.list.search.placeholder": "Search by name...", + "challenge.admin.chains.list.empty.title": "No chains", + "challenge.admin.chains.list.empty.description": "Create your first task chain", + "challenge.admin.chains.list.empty.action": "Create chain", + "challenge.admin.chains.list.search.empty": "Nothing found for \"{query}\"", + "challenge.admin.chains.list.table.name": "Name", + "challenge.admin.chains.list.table.tasks.count": "Number of tasks", + "challenge.admin.chains.list.table.created": "Created date", + "challenge.admin.chains.list.table.actions": "Actions", + "challenge.admin.chains.list.badge.tasks": "tasks", + "challenge.admin.chains.list.button.edit": "Edit", + "challenge.admin.chains.list.button.delete": "Delete", + "challenge.admin.chains.deleted": "Chain deleted", + "challenge.admin.chains.delete.error": "Failed to delete chain", + "challenge.admin.chains.list.loading": "Loading chains...", + "challenge.admin.chains.list.load.error": "Failed to load chains list", + "challenge.admin.chains.delete.confirm.title": "Delete chain", + "challenge.admin.chains.delete.confirm.message": "Are you sure you want to delete chain \"{name}\"? This action cannot be undone.", + "challenge.admin.chains.delete.confirm.button": "Delete", + "challenge.admin.dashboard.title": "Dashboard", + "challenge.admin.dashboard.loading": "Loading statistics...", + "challenge.admin.dashboard.load.error": "Failed to load system statistics", + "challenge.admin.dashboard.stats.users": "Total users", + "challenge.admin.dashboard.stats.tasks": "Total tasks", + "challenge.admin.dashboard.stats.chains": "Total chains", + "challenge.admin.dashboard.stats.submissions": "Total checks", + "challenge.admin.dashboard.submissions.title": "Check statistics", + "challenge.admin.dashboard.submissions.accepted": "Accepted", + "challenge.admin.dashboard.submissions.rejected": "Rejected", + "challenge.admin.dashboard.submissions.pending": "Pending", + "challenge.admin.dashboard.submissions.in.progress": "In progress", + "challenge.admin.dashboard.queue.title": "Queue status", + "challenge.admin.dashboard.queue.processing": "Processing", + "challenge.admin.dashboard.queue.waiting": "Waiting in queue", + "challenge.admin.dashboard.queue.total": "Total in queue", + "challenge.admin.dashboard.queue.utilization": "Queue utilization:", + "challenge.admin.dashboard.check.time.title": "Average check time", + "challenge.admin.dashboard.check.time.value": "{{time}} sec", + "challenge.admin.dashboard.check.time.description": "Time from solution submission to result", + "challenge.admin.users.title": "Users", + "challenge.admin.users.loading": "Loading users...", + "challenge.admin.users.load.error": "Failed to load users list", + "challenge.admin.users.search.placeholder": "Search by nickname...", + "challenge.admin.users.empty.title": "No users", + "challenge.admin.users.empty.description": "Users will appear after registration", + "challenge.admin.users.search.empty": "Nothing found for \"{query}\"", + "challenge.admin.users.table.nickname": "Nickname", + "challenge.admin.users.table.id": "ID", + "challenge.admin.users.table.registered": "Registration date", + "challenge.admin.users.table.actions": "Actions", + "challenge.admin.users.button.stats": "Statistics", + "challenge.admin.users.stats.title": "User statistics", + "challenge.admin.users.stats.loading": "Loading statistics...", + "challenge.admin.users.stats.no.data": "No data", + "challenge.admin.users.stats.completed": "Completed", + "challenge.admin.users.stats.total.submissions": "Total attempts", + "challenge.admin.users.stats.in.progress": "In progress", + "challenge.admin.users.stats.needs.revision": "Needs revision", + "challenge.admin.users.stats.chains.progress": "Chain progress", + "challenge.admin.users.stats.tasks": "Tasks", + "challenge.admin.users.stats.status.completed": "Completed", + "challenge.admin.users.stats.status.needs_revision": "Revision", + "challenge.admin.users.stats.status.in_progress": "In progress", + "challenge.admin.users.stats.status.not_started": "Not started", + "challenge.admin.users.stats.attempts": "Attempts:", + "challenge.admin.users.stats.avg.check.time": "Average check time", + "challenge.admin.users.stats.close": "Close", + "challenge.admin.submissions.title": "Solution attempts", + "challenge.admin.submissions.loading": "Loading attempts...", + "challenge.admin.submissions.load.error": "Failed to load attempts list", + "challenge.admin.submissions.search.placeholder": "Search by user or task...", + "challenge.admin.submissions.filter.status": "Status", + "challenge.admin.submissions.status.all": "All statuses", + "challenge.admin.submissions.status.accepted": "Accepted", + "challenge.admin.submissions.status.needs_revision": "Needs revision", + "challenge.admin.submissions.status.in_progress": "Checking", + "challenge.admin.submissions.status.pending": "Pending", + "challenge.admin.submissions.empty.title": "No attempts", + "challenge.admin.submissions.empty.description": "Attempts will appear after solution submissions", + "challenge.admin.submissions.search.empty.title": "Nothing found", + "challenge.admin.submissions.search.empty.description": "Try changing filters", + "challenge.admin.submissions.table.user": "User", + "challenge.admin.submissions.table.task": "Task", + "challenge.admin.submissions.table.status": "Status", + "challenge.admin.submissions.table.attempt": "Attempt", + "challenge.admin.submissions.table.submitted": "Submitted date", + "challenge.admin.submissions.table.check.time": "Check time", + "challenge.admin.submissions.table.actions": "Actions", + "challenge.admin.submissions.button.details": "Details", + "challenge.admin.submissions.check.time": "{{time}} sec", + "challenge.admin.submissions.details.title": "Attempt details", + "challenge.admin.submissions.details.user": "User", + "challenge.admin.submissions.details.status": "Status", + "challenge.admin.submissions.details.submitted": "Submitted:", + "challenge.admin.submissions.details.checked": "Checked:", + "challenge.admin.submissions.details.check.time": "Check time:", + "challenge.admin.submissions.details.task": "Task:", + "challenge.admin.submissions.details.solution": "User solution:", + "challenge.admin.submissions.details.feedback": "LLM feedback:", + "challenge.admin.submissions.details.close": "Close", + "challenge.admin.layout.title": "Challenge Admin", + "challenge.admin.layout.nav.dashboard": "Dashboard", + "challenge.admin.layout.nav.tasks": "Tasks", + "challenge.admin.layout.nav.chains": "Chains", + "challenge.admin.layout.nav.users": "Users", + "challenge.admin.layout.nav.submissions": "Attempts", + "challenge.admin.layout.button.player": "Open Player", + "challenge.admin.layout.button.logout": "Logout", + "challenge.admin.common.loading.default": "Loading...", + "challenge.admin.common.error.default": "An error occurred while loading data", + "challenge.admin.common.retry": "Try again", + "challenge.admin.common.confirm": "Confirm", + "challenge.admin.common.close": "Close" } \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json index 0487407..b2cbe4e 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1,3 +1,191 @@ { - "challenge.title": "Challenge" + "challenge.admin.common.success": "Успешно", + "challenge.admin.common.error": "Ошибка", + "challenge.admin.common.cancel": "Отмена", + "challenge.admin.common.loading.tasks": "Загрузка заданий...", + "challenge.admin.common.not.found": "Ничего не найдено", + "challenge.admin.common.validation.error": "Ошибка валидации", + "challenge.admin.tasks.updated": "Задание обновлено", + "challenge.admin.tasks.created": "Задание создано", + "challenge.admin.tasks.validation.fill.required.fields": "Заполните обязательные поля", + "challenge.admin.tasks.save.error": "Не удалось сохранить задание", + "challenge.admin.tasks.loading": "Загрузка задания...", + "challenge.admin.tasks.load.error": "Не удалось загрузить задание", + "challenge.admin.tasks.edit.title": "Редактировать задание", + "challenge.admin.tasks.create.title": "Создать задание", + "challenge.admin.tasks.field.title": "Название задания", + "challenge.admin.tasks.field.title.placeholder": "Введите название задания", + "challenge.admin.tasks.field.title.helper": "Максимум 255 символов", + "challenge.admin.tasks.field.description": "Описание (Markdown)", + "challenge.admin.tasks.field.description.placeholder": "# Заголовок задания\n\nОписание задания в формате Markdown...", + "challenge.admin.tasks.field.description.helper": "Используйте Markdown для форматирования текста", + "challenge.admin.tasks.tab.editor": "Редактор", + "challenge.admin.tasks.tab.preview": "Превью", + "challenge.admin.tasks.preview.empty": "Предпросмотр появится здесь...", + "challenge.admin.tasks.field.hidden.instructions": "🔒 Скрытые инструкции для LLM", + "challenge.admin.tasks.field.hidden.instructions.description": "Эти инструкции будут переданы LLM при проверке решений студентов. Студенты их не увидят.", + "challenge.admin.tasks.field.hidden.instructions.placeholder": "Например: Проверь, что сложность алгоритма O(n log n). Код должен обрабатывать edge cases...", + "challenge.admin.tasks.field.hidden.instructions.helper": "Опционально. Используйте для тонкой настройки проверки LLM.", + "challenge.admin.tasks.meta.created": "Создано:", + "challenge.admin.tasks.meta.author": "Автор:", + "challenge.admin.tasks.meta.updated": "Обновлено:", + "challenge.admin.tasks.button.save": "Сохранить изменения", + "challenge.admin.tasks.button.create": "Создать задание", + "challenge.admin.tasks.list.title": "Задания", + "challenge.admin.tasks.list.create.button": "+ Создать задание", + "challenge.admin.tasks.list.search.placeholder": "Поиск по названию...", + "challenge.admin.tasks.list.empty.title": "Нет заданий", + "challenge.admin.tasks.list.empty.description": "Создайте первое задание для начала работы", + "challenge.admin.tasks.list.empty.action": "Создать задание", + "challenge.admin.tasks.list.search.empty": "По запросу \"{query}\" ничего не найдено", + "challenge.admin.tasks.list.table.title": "Название", + "challenge.admin.tasks.list.table.creator": "Создатель", + "challenge.admin.tasks.list.table.created": "Дата создания", + "challenge.admin.tasks.list.table.hidden.instructions": "Скрытые инструкции", + "challenge.admin.tasks.list.table.actions": "Действия", + "challenge.admin.tasks.list.badge.has.instructions": "🔒 Есть", + "challenge.admin.tasks.list.button.edit": "Редактировать", + "challenge.admin.tasks.list.button.delete": "Удалить", + "challenge.admin.tasks.deleted": "Задание удалено", + "challenge.admin.tasks.delete.error": "Не удалось удалить задание", + "challenge.admin.tasks.list.loading": "Загрузка заданий...", + "challenge.admin.tasks.list.load.error": "Не удалось загрузить список заданий", + "challenge.admin.tasks.delete.confirm.title": "Удалить задание", + "challenge.admin.tasks.delete.confirm.message": "Вы уверены, что хотите удалить задание \"{title}\"? Это действие нельзя отменить.", + "challenge.admin.tasks.delete.confirm.button": "Удалить", + "challenge.admin.chains.updated": "Цепочка обновлена", + "challenge.admin.chains.created": "Цепочка создана", + "challenge.admin.chains.validation.enter.name": "Введите название цепочки", + "challenge.admin.chains.validation.add.task": "Добавьте хотя бы одно задание", + "challenge.admin.chains.save.error": "Не удалось сохранить цепочку", + "challenge.admin.chains.loading": "Загрузка цепочки...", + "challenge.admin.chains.load.error": "Не удалось загрузить цепочку", + "challenge.admin.chains.tasks.load.error": "Не удалось загрузить список заданий", + "challenge.admin.chains.edit.title": "Редактировать цепочку", + "challenge.admin.chains.create.title": "Создать цепочку", + "challenge.admin.chains.field.name": "Название цепочки", + "challenge.admin.chains.field.name.placeholder": "Введите название цепочки", + "challenge.admin.chains.selected.tasks": "Задания в цепочке", + "challenge.admin.chains.selected.tasks.empty": "Добавьте задания из списка ниже", + "challenge.admin.chains.available.tasks": "Доступные задания", + "challenge.admin.chains.search.placeholder": "Поиск заданий...", + "challenge.admin.chains.all.tasks.added": "Все задания уже добавлены", + "challenge.admin.chains.button.add": "+ Добавить", + "challenge.admin.chains.button.save": "Сохранить изменения", + "challenge.admin.chains.button.create": "Создать цепочку", + "challenge.admin.chains.list.title": "Цепочки заданий", + "challenge.admin.chains.list.create.button": "+ Создать цепочку", + "challenge.admin.chains.list.search.placeholder": "Поиск по названию...", + "challenge.admin.chains.list.empty.title": "Нет цепочек", + "challenge.admin.chains.list.empty.description": "Создайте первую цепочку заданий", + "challenge.admin.chains.list.empty.action": "Создать цепочку", + "challenge.admin.chains.list.search.empty": "По запросу \"{query}\" ничего не найдено", + "challenge.admin.chains.list.table.name": "Название", + "challenge.admin.chains.list.table.tasks.count": "Количество заданий", + "challenge.admin.chains.list.table.created": "Дата создания", + "challenge.admin.chains.list.table.actions": "Действия", + "challenge.admin.chains.list.badge.tasks": "заданий", + "challenge.admin.chains.list.button.edit": "Редактировать", + "challenge.admin.chains.list.button.delete": "Удалить", + "challenge.admin.chains.deleted": "Цепочка удалена", + "challenge.admin.chains.delete.error": "Не удалось удалить цепочку", + "challenge.admin.chains.list.loading": "Загрузка цепочек...", + "challenge.admin.chains.list.load.error": "Не удалось загрузить список цепочек", + "challenge.admin.chains.delete.confirm.title": "Удалить цепочку", + "challenge.admin.chains.delete.confirm.message": "Вы уверены, что хотите удалить цепочку \"{name}\"? Это действие нельзя отменить.", + "challenge.admin.chains.delete.confirm.button": "Удалить", + "challenge.admin.dashboard.title": "Dashboard", + "challenge.admin.dashboard.loading": "Загрузка статистики...", + "challenge.admin.dashboard.load.error": "Не удалось загрузить статистику системы", + "challenge.admin.dashboard.stats.users": "Всего пользователей", + "challenge.admin.dashboard.stats.tasks": "Всего заданий", + "challenge.admin.dashboard.stats.chains": "Всего цепочек", + "challenge.admin.dashboard.stats.submissions": "Всего проверок", + "challenge.admin.dashboard.submissions.title": "Статистика проверок", + "challenge.admin.dashboard.submissions.accepted": "Принято", + "challenge.admin.dashboard.submissions.rejected": "Отклонено", + "challenge.admin.dashboard.submissions.pending": "Ожидают", + "challenge.admin.dashboard.submissions.in.progress": "В процессе", + "challenge.admin.dashboard.queue.title": "Статус очереди", + "challenge.admin.dashboard.queue.processing": "В обработке", + "challenge.admin.dashboard.queue.waiting": "Ожидают в очереди", + "challenge.admin.dashboard.queue.total": "Всего в очереди", + "challenge.admin.dashboard.queue.utilization": "Загруженность очереди:", + "challenge.admin.dashboard.check.time.title": "Среднее время проверки", + "challenge.admin.dashboard.check.time.value": "{{time}} сек", + "challenge.admin.dashboard.check.time.description": "Время от отправки решения до получения результата", + "challenge.admin.users.title": "Пользователи", + "challenge.admin.users.loading": "Загрузка пользователей...", + "challenge.admin.users.load.error": "Не удалось загрузить список пользователей", + "challenge.admin.users.search.placeholder": "Поиск по nickname...", + "challenge.admin.users.empty.title": "Нет пользователей", + "challenge.admin.users.empty.description": "Пользователи появятся после регистрации", + "challenge.admin.users.search.empty": "По запросу \"{query}\" ничего не найдено", + "challenge.admin.users.table.nickname": "Nickname", + "challenge.admin.users.table.id": "ID", + "challenge.admin.users.table.registered": "Дата регистрации", + "challenge.admin.users.table.actions": "Действия", + "challenge.admin.users.button.stats": "Статистика", + "challenge.admin.users.stats.title": "Статистика пользователя", + "challenge.admin.users.stats.loading": "Загрузка статистики...", + "challenge.admin.users.stats.no.data": "Нет данных", + "challenge.admin.users.stats.completed": "Выполнено", + "challenge.admin.users.stats.total.submissions": "Всего попыток", + "challenge.admin.users.stats.in.progress": "В процессе", + "challenge.admin.users.stats.needs.revision": "Требует доработки", + "challenge.admin.users.stats.chains.progress": "Прогресс по цепочкам", + "challenge.admin.users.stats.tasks": "Задания", + "challenge.admin.users.stats.status.completed": "Завершено", + "challenge.admin.users.stats.status.needs_revision": "Доработка", + "challenge.admin.users.stats.status.in_progress": "В процессе", + "challenge.admin.users.stats.status.not_started": "Не начато", + "challenge.admin.users.stats.attempts": "Попыток:", + "challenge.admin.users.stats.avg.check.time": "Среднее время проверки", + "challenge.admin.users.stats.close": "Закрыть", + "challenge.admin.submissions.title": "Попытки решений", + "challenge.admin.submissions.loading": "Загрузка попыток...", + "challenge.admin.submissions.load.error": "Не удалось загрузить список попыток", + "challenge.admin.submissions.search.placeholder": "Поиск по пользователю или заданию...", + "challenge.admin.submissions.filter.status": "Статус", + "challenge.admin.submissions.status.all": "Все статусы", + "challenge.admin.submissions.status.accepted": "Принято", + "challenge.admin.submissions.status.needs_revision": "Доработка", + "challenge.admin.submissions.status.in_progress": "Проверяется", + "challenge.admin.submissions.status.pending": "Ожидает", + "challenge.admin.submissions.empty.title": "Нет попыток", + "challenge.admin.submissions.empty.description": "Попытки появятся после отправки решений", + "challenge.admin.submissions.search.empty.title": "Ничего не найдено", + "challenge.admin.submissions.search.empty.description": "Попробуйте изменить фильтры", + "challenge.admin.submissions.table.user": "Пользователь", + "challenge.admin.submissions.table.task": "Задание", + "challenge.admin.submissions.table.status": "Статус", + "challenge.admin.submissions.table.attempt": "Попытка", + "challenge.admin.submissions.table.submitted": "Дата отправки", + "challenge.admin.submissions.table.check.time": "Время проверки", + "challenge.admin.submissions.table.actions": "Действия", + "challenge.admin.submissions.button.details": "Детали", + "challenge.admin.submissions.check.time": "{{time}} сек", + "challenge.admin.submissions.details.title": "Детали попытки", + "challenge.admin.submissions.details.user": "Пользователь", + "challenge.admin.submissions.details.status": "Статус", + "challenge.admin.submissions.details.submitted": "Отправлено:", + "challenge.admin.submissions.details.checked": "Проверено:", + "challenge.admin.submissions.details.check.time": "Время проверки:", + "challenge.admin.submissions.details.task": "Задание:", + "challenge.admin.submissions.details.solution": "Решение пользователя:", + "challenge.admin.submissions.details.feedback": "Обратная связь от LLM:", + "challenge.admin.submissions.details.close": "Закрыть", + "challenge.admin.layout.title": "Challenge Admin", + "challenge.admin.layout.nav.dashboard": "Dashboard", + "challenge.admin.layout.nav.tasks": "Задания", + "challenge.admin.layout.nav.chains": "Цепочки", + "challenge.admin.layout.nav.users": "Пользователи", + "challenge.admin.layout.nav.submissions": "Попытки", + "challenge.admin.layout.button.player": "Открыть проигрыватель", + "challenge.admin.layout.button.logout": "Выйти", + "challenge.admin.common.loading.default": "Загрузка...", + "challenge.admin.common.error.default": "Произошла ошибка при загрузке данных", + "challenge.admin.common.retry": "Попробовать снова", + "challenge.admin.common.confirm": "Подтвердить", + "challenge.admin.common.close": "Закрыть" } \ No newline at end of file diff --git a/src/app.tsx b/src/app.tsx index 7480e5d..c6daa2c 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -3,7 +3,7 @@ import { BrowserRouter } from 'react-router-dom' import { Dashboard } from './dashboard' import { Provider } from './theme' import { Provider as ReduxProvider } from 'react-redux' -import { Toaster } from './components/ui/toaster' +import { ToasterProvider, Toaster } from './components/ui/toaster' import type { PropsWithChildren } from 'react' const App = ({ store }: PropsWithChildren<{ store?: any }>) => { @@ -14,10 +14,12 @@ const App = ({ store }: PropsWithChildren<{ store?: any }>) => { return ( - - - - + + + + + + ) diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx index b6ae72d..e1310ba 100644 --- a/src/components/ConfirmDialog.tsx +++ b/src/components/ConfirmDialog.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { DialogRoot, DialogContent, @@ -27,10 +28,14 @@ export const ConfirmDialog: React.FC = ({ onConfirm, title, message, - confirmLabel = 'Подтвердить', - cancelLabel = 'Отмена', + confirmLabel, + cancelLabel, isLoading = false, }) => { + const { t } = useTranslation() + + const confirm = confirmLabel || t('challenge.admin.common.confirm') + const cancel = cancelLabel || t('challenge.admin.common.cancel') return ( !e.open && onClose()}> @@ -43,15 +48,15 @@ export const ConfirmDialog: React.FC = ({ diff --git a/src/components/ErrorAlert.tsx b/src/components/ErrorAlert.tsx index c5f0d1f..e57fdf5 100644 --- a/src/components/ErrorAlert.tsx +++ b/src/components/ErrorAlert.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { Box, Text, Button } from '@chakra-ui/react' interface ErrorAlertProps { @@ -7,9 +8,11 @@ interface ErrorAlertProps { } export const ErrorAlert: React.FC = ({ - message = 'Произошла ошибка при загрузке данных', + message, onRetry, }) => { + const { t } = useTranslation() + return ( = ({ textAlign="center" > - {message} + {message || t('challenge.admin.common.error.default')} {onRetry && ( )} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 4f29600..7645e14 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { Link, useLocation, useNavigate } from 'react-router-dom' import { Box, Container, Flex, HStack, VStack, Button, Text } from '@chakra-ui/react' import { useAppSelector } from '../__data__/store' @@ -10,6 +11,7 @@ interface LayoutProps { } export const Layout: React.FC = ({ children }) => { + const { t, i18n } = useTranslation() const location = useLocation() const navigate = useNavigate() const user = useAppSelector((state) => state.user) @@ -22,16 +24,20 @@ export const Layout: React.FC = ({ children }) => { navigate(URLs.challengePlayer) } + const handleChangeLanguage = (lang: string) => { + i18n.changeLanguage(lang) + } + const isActive = (path: string) => { return location.pathname === path } const navItems = [ - { label: 'Dashboard', path: URLs.dashboard }, - { label: 'Задания', path: URLs.tasks }, - { label: 'Цепочки', path: URLs.chains }, - { label: 'Пользователи', path: URLs.users }, - { label: 'Попытки', path: URLs.submissions }, + { label: t('challenge.admin.layout.nav.dashboard'), path: URLs.dashboard }, + { label: t('challenge.admin.layout.nav.tasks'), path: URLs.tasks }, + { label: t('challenge.admin.layout.nav.chains'), path: URLs.chains }, + { label: t('challenge.admin.layout.nav.users'), path: URLs.users }, + { label: t('challenge.admin.layout.nav.submissions'), path: URLs.submissions }, ] return ( @@ -41,16 +47,38 @@ export const Layout: React.FC = ({ children }) => { - Challenge Admin + {t('challenge.admin.layout.title')} + {/* Language Switcher */} + + + + + {user && ( @@ -64,7 +92,7 @@ export const Layout: React.FC = ({ children }) => { variant="ghost" onClick={handleLogout} > - Выйти + {t('challenge.admin.layout.button.logout')} )} diff --git a/src/components/LoadingSpinner.tsx b/src/components/LoadingSpinner.tsx index 4aebbbb..d8f5dd1 100644 --- a/src/components/LoadingSpinner.tsx +++ b/src/components/LoadingSpinner.tsx @@ -1,16 +1,19 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { Flex, Spinner, Text, VStack } from '@chakra-ui/react' interface LoadingSpinnerProps { message?: string } -export const LoadingSpinner: React.FC = ({ message = 'Загрузка...' }) => { +export const LoadingSpinner: React.FC = ({ message }) => { + const { t } = useTranslation() + return ( - {message} + {message || t('challenge.admin.common.loading.default')} ) diff --git a/src/components/StatusBadge.tsx b/src/components/StatusBadge.tsx index eb476ca..d0597bc 100644 --- a/src/components/StatusBadge.tsx +++ b/src/components/StatusBadge.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { Badge } from '@chakra-ui/react' import type { SubmissionStatus } from '../types/challenge' @@ -7,6 +8,8 @@ interface StatusBadgeProps { } export const StatusBadge: React.FC = ({ status }) => { + const { t } = useTranslation() + const getColorPalette = () => { switch (status) { case 'accepted': @@ -22,24 +25,9 @@ export const StatusBadge: React.FC = ({ status }) => { } } - const getLabel = () => { - switch (status) { - case 'accepted': - return 'Принято' - case 'needs_revision': - return 'Доработка' - case 'in_progress': - return 'Проверяется' - case 'pending': - return 'Ожидает' - default: - return status - } - } - return ( - {getLabel()} + {t(`challenge.admin.submissions.status.${status}`)} ) } diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx index 31ab7cd..b992ce8 100644 --- a/src/components/ui/toaster.tsx +++ b/src/components/ui/toaster.tsx @@ -1,12 +1,198 @@ -import React from 'react' -import { createToaster, Toaster as ChakraToaster } from '@chakra-ui/react' +import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react' +import { createPortal } from 'react-dom' +import { Box, Text, HStack, IconButton } from '@chakra-ui/react' -export const toaster = createToaster({ - placement: 'top-end', - duration: 3000, -}) +type ToastType = 'success' | 'error' | 'info' | 'warning' -export const Toaster = () => { - return +interface Toast { + id: string + title: string + description?: string + type: ToastType + duration?: number } +interface ToasterContextValue { + create: (toast: Omit) => void +} + +const ToasterContext = createContext(null) + +export const useToaster = () => { + const context = useContext(ToasterContext) + if (!context) { + throw new Error('useToaster must be used within ToasterProvider') + } + return context +} + +const getToastColor = (type: ToastType) => { + switch (type) { + case 'success': + return { bg: 'green.500', color: 'white' } + case 'error': + return { bg: 'red.500', color: 'white' } + case 'warning': + return { bg: 'orange.500', color: 'white' } + case 'info': + return { bg: 'blue.500', color: 'white' } + } +} + +const getToastIcon = (type: ToastType) => { + switch (type) { + case 'success': + return '✓' + case 'error': + return '✕' + case 'warning': + return '⚠' + case 'info': + return 'ℹ' + } +} + +export const ToasterProvider = ({ children }: { children: ReactNode }) => { + const [toasts, setToasts] = useState([]) + + const create = useCallback((toast: Omit) => { + const id = Math.random().toString(36).substring(2, 9) + const newToast = { ...toast, id } + + setToasts((prev) => [...prev, newToast]) + + const duration = toast.duration ?? 3000 + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== id)) + }, duration) + }, []) + + const remove = useCallback((id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)) + }, []) + + return ( + + {children} + {typeof window !== 'undefined' && + createPortal( + + {toasts.map((toast) => { + const colors = getToastColor(toast.type) + return ( + + + + + {getToastIcon(toast.type)} + + + + {toast.title} + + {toast.description && ( + + {toast.description} + + )} + + + remove(toast.id)} + _hover={{ bg: 'whiteAlpha.300' }} + > + ✕ + + + + ) + })} + , + document.body + )} + + ) +} + +// Singleton instance для совместимости со старым API +class ToasterSingleton { + private static instance: ToasterSingleton + private createFn: ((toast: Omit) => void) | null = null + + static getInstance(): ToasterSingleton { + if (!ToasterSingleton.instance) { + ToasterSingleton.instance = new ToasterSingleton() + } + return ToasterSingleton.instance + } + + setCreateFn(fn: (toast: Omit) => void) { + this.createFn = fn + } + + create(toast: Omit) { + if (this.createFn) { + this.createFn(toast) + } else { + console.error('Toaster not initialized') + } + } +} + +export const toaster = ToasterSingleton.getInstance() + +// Компонент для инициализации singleton +export const ToasterInitializer = () => { + const { create } = useToaster() + + React.useEffect(() => { + toaster.setCreateFn(create) + }, [create]) + + return null +} + +// Старый компонент Toaster для совместимости +export const Toaster = () => { + return ( + <> + + + + ) +} diff --git a/src/pages/chains/ChainFormPage.tsx b/src/pages/chains/ChainFormPage.tsx index e7cc4ba..e487569 100644 --- a/src/pages/chains/ChainFormPage.tsx +++ b/src/pages/chains/ChainFormPage.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react' import { useNavigate, useParams } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import { Box, Heading, @@ -29,6 +30,7 @@ export const ChainFormPage: React.FC = () => { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const isEdit = !!id + const { t } = useTranslation() const { data: chain, isLoading: isLoadingChain, error: loadError } = useGetChainQuery(id!, { skip: !id, @@ -53,8 +55,8 @@ export const ChainFormPage: React.FC = () => { if (!name.trim()) { toaster.create({ - title: 'Ошибка валидации', - description: 'Введите название цепочки', + title: t('challenge.admin.common.validation.error'), + description: t('challenge.admin.chains.validation.enter.name'), type: 'error', }) return @@ -62,8 +64,8 @@ export const ChainFormPage: React.FC = () => { if (selectedTasks.length === 0) { toaster.create({ - title: 'Ошибка валидации', - description: 'Добавьте хотя бы одно задание', + title: t('challenge.admin.common.validation.error'), + description: t('challenge.admin.chains.validation.add.task'), type: 'error', }) return @@ -81,8 +83,8 @@ export const ChainFormPage: React.FC = () => { }, }).unwrap() toaster.create({ - title: 'Успешно', - description: 'Цепочка обновлена', + title: t('challenge.admin.common.success'), + description: t('challenge.admin.chains.updated'), type: 'success', }) } else { @@ -91,16 +93,24 @@ export const ChainFormPage: React.FC = () => { tasks: taskIds, }).unwrap() toaster.create({ - title: 'Успешно', - description: 'Цепочка создана', + title: t('challenge.admin.common.success'), + description: t('challenge.admin.chains.created'), type: 'success', }) } navigate(URLs.chains) - } catch (err: any) { + } catch (err: unknown) { + const errorMessage = + (err && typeof err === 'object' && 'data' in err && + err.data && typeof err.data === 'object' && 'error' in err.data && + err.data.error && typeof err.data.error === 'object' && 'message' in err.data.error && + typeof err.data.error.message === 'string') + ? err.data.error.message + : t('challenge.admin.chains.save.error') + toaster.create({ - title: 'Ошибка', - description: err?.data?.error?.message || 'Не удалось сохранить цепочку', + title: t('challenge.admin.common.error'), + description: errorMessage, type: 'error', }) } @@ -131,19 +141,19 @@ export const ChainFormPage: React.FC = () => { } if (isEdit && isLoadingChain) { - return + return } if (isEdit && loadError) { - return + return } if (isLoadingTasks) { - return + return } if (!allTasks) { - return + return } const isLoading = isCreating || isUpdating @@ -156,7 +166,7 @@ export const ChainFormPage: React.FC = () => { return ( - {isEdit ? 'Редактировать цепочку' : 'Создать цепочку'} + {isEdit ? t('challenge.admin.chains.edit.title') : t('challenge.admin.chains.create.title')} { {/* Name */} - Название цепочки + {t('challenge.admin.chains.field.name')} setName(e.target.value)} - placeholder="Введите название цепочки" + placeholder={t('challenge.admin.chains.field.name.placeholder')} maxLength={255} disabled={isLoading} /> @@ -184,7 +194,7 @@ export const ChainFormPage: React.FC = () => { {/* Selected Tasks */} - Задания в цепочке ({selectedTasks.length}) + {t('challenge.admin.chains.selected.tasks')} ({selectedTasks.length}) {selectedTasks.length === 0 ? ( { borderRadius="md" textAlign="center" > - Добавьте задания из списка ниже + {t('challenge.admin.chains.selected.tasks.empty')} ) : ( @@ -255,10 +265,10 @@ export const ChainFormPage: React.FC = () => { {/* Available Tasks */} - Доступные задания + {t('challenge.admin.chains.available.tasks')} setSearchQuery(e.target.value)} mb={3} @@ -273,8 +283,8 @@ export const ChainFormPage: React.FC = () => { > {allTasks.length === selectedTasks.length - ? 'Все задания уже добавлены' - : 'Ничего не найдено'} + ? t('challenge.admin.chains.all.tasks.added') + : t('challenge.admin.common.not.found')} ) : ( @@ -295,7 +305,7 @@ export const ChainFormPage: React.FC = () => { > {task.title} ))} @@ -306,10 +316,10 @@ export const ChainFormPage: React.FC = () => { {/* Actions */} - diff --git a/src/pages/chains/ChainsListPage.tsx b/src/pages/chains/ChainsListPage.tsx index f8ee337..fd6ee41 100644 --- a/src/pages/chains/ChainsListPage.tsx +++ b/src/pages/chains/ChainsListPage.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react' import { useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import { Box, Heading, @@ -22,6 +23,7 @@ import { toaster } from '../../components/ui/toaster' export const ChainsListPage: React.FC = () => { const navigate = useNavigate() + const { t } = useTranslation() const { data: chains, isLoading, error, refetch } = useGetChainsQuery() const [deleteChain, { isLoading: isDeleting }] = useDeleteChainMutation() @@ -34,26 +36,26 @@ export const ChainsListPage: React.FC = () => { try { await deleteChain(chainToDelete.id).unwrap() toaster.create({ - title: 'Успешно', - description: 'Цепочка удалена', + title: t('challenge.admin.common.success'), + description: t('challenge.admin.chains.deleted'), type: 'success', }) setChainToDelete(null) } catch (err) { toaster.create({ - title: 'Ошибка', - description: 'Не удалось удалить цепочку', + title: t('challenge.admin.common.error'), + description: t('challenge.admin.chains.delete.error'), type: 'error', }) } } if (isLoading) { - return + return } if (error || !chains) { - return + return } const filteredChains = chains.filter((chain) => @@ -71,16 +73,16 @@ export const ChainsListPage: React.FC = () => { return ( - Цепочки заданий + {t('challenge.admin.chains.list.title')} {chains.length > 0 && ( setSearchQuery(e.target.value)} maxW="400px" @@ -90,25 +92,25 @@ export const ChainsListPage: React.FC = () => { {filteredChains.length === 0 && chains.length === 0 ? ( navigate(URLs.chainNew)} /> ) : filteredChains.length === 0 ? ( ) : ( - Название - Количество заданий - Дата создания - Действия + {t('challenge.admin.chains.list.table.name')} + {t('challenge.admin.chains.list.table.tasks.count')} + {t('challenge.admin.chains.list.table.created')} + {t('challenge.admin.chains.list.table.actions')} @@ -117,7 +119,7 @@ export const ChainsListPage: React.FC = () => { {chain.name} - {chain.tasks.length} заданий + {chain.tasks.length} {t('challenge.admin.chains.list.badge.tasks')} @@ -132,7 +134,7 @@ export const ChainsListPage: React.FC = () => { variant="ghost" onClick={() => navigate(URLs.chainEdit(chain.id))} > - Редактировать + {t('challenge.admin.chains.list.button.edit')} @@ -155,9 +157,9 @@ export const ChainsListPage: React.FC = () => { isOpen={!!chainToDelete} onClose={() => setChainToDelete(null)} onConfirm={handleDeleteChain} - title="Удалить цепочку" - message={`Вы уверены, что хотите удалить цепочку "${chainToDelete?.name}"? Это действие нельзя отменить.`} - confirmLabel="Удалить" + title={t('challenge.admin.chains.delete.confirm.title')} + message={t('challenge.admin.chains.delete.confirm.message', { name: chainToDelete?.name })} + confirmLabel={t('challenge.admin.chains.delete.confirm.button')} isLoading={isDeleting} /> diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index 0c3711c..649178b 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { Box, Heading, Grid, Text, VStack, HStack, Badge, Progress } from '@chakra-ui/react' import { useGetSystemStatsQuery } from '../../__data__/api/api' import { StatCard } from '../../components/StatCard' @@ -6,16 +7,17 @@ import { LoadingSpinner } from '../../components/LoadingSpinner' import { ErrorAlert } from '../../components/ErrorAlert' export const DashboardPage: React.FC = () => { + const { t } = useTranslation() const { data: stats, isLoading, error, refetch } = useGetSystemStatsQuery(undefined, { pollingInterval: 10000, // Обновление каждые 10 секунд }) if (isLoading) { - return + return } if (error || !stats) { - return + return } const acceptanceRate = stats.submissions.total > 0 @@ -32,25 +34,25 @@ export const DashboardPage: React.FC = () => { return ( - Dashboard + {t('challenge.admin.dashboard.title')} {/* Main Stats */} - - - - + + + + {/* Submissions Stats */} - Статистика проверок + {t('challenge.admin.dashboard.submissions.title')} - Принято + {t('challenge.admin.dashboard.submissions.accepted')} @@ -62,7 +64,7 @@ export const DashboardPage: React.FC = () => { - Отклонено + {t('challenge.admin.dashboard.submissions.rejected')} @@ -74,7 +76,7 @@ export const DashboardPage: React.FC = () => { - Ожидают + {t('challenge.admin.dashboard.submissions.pending')} {stats.submissions.pending} @@ -83,7 +85,7 @@ export const DashboardPage: React.FC = () => { - В процессе + {t('challenge.admin.dashboard.submissions.in.progress')} {stats.submissions.inProgress} @@ -95,13 +97,13 @@ export const DashboardPage: React.FC = () => { {/* Queue Stats */} - Статус очереди + {t('challenge.admin.dashboard.queue.title')} - В обработке + {t('challenge.admin.dashboard.queue.processing')} @@ -115,7 +117,7 @@ export const DashboardPage: React.FC = () => { - Ожидают в очереди + {t('challenge.admin.dashboard.queue.waiting')} {stats.queue.waiting} @@ -124,7 +126,7 @@ export const DashboardPage: React.FC = () => { - Всего в очереди + {t('challenge.admin.dashboard.queue.total')} {stats.queue.queueLength} @@ -134,7 +136,7 @@ export const DashboardPage: React.FC = () => { - Загруженность очереди: {queueUtilization}% + {t('challenge.admin.dashboard.queue.utilization')} {queueUtilization}% @@ -147,13 +149,13 @@ export const DashboardPage: React.FC = () => { {/* Average Check Time */} - Среднее время проверки + {t('challenge.admin.dashboard.check.time.title')} - {(stats.averageCheckTimeMs / 1000).toFixed(2)} сек + {t('challenge.admin.dashboard.check.time.value', { time: (stats.averageCheckTimeMs / 1000).toFixed(2) })} - Время от отправки решения до получения результата + {t('challenge.admin.dashboard.check.time.description')} diff --git a/src/pages/index.ts b/src/pages/index.ts deleted file mode 100644 index 22fff66..0000000 --- a/src/pages/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { lazy } from 'react' - -export const MainPage = lazy(() => import(/* webpackChunkName: 'main' */'./main')) - diff --git a/src/pages/main/index.ts b/src/pages/main/index.ts deleted file mode 100644 index 52a5afb..0000000 --- a/src/pages/main/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { MainPage as default } from './main' - diff --git a/src/pages/main/main.tsx b/src/pages/main/main.tsx deleted file mode 100644 index d9bb34e..0000000 --- a/src/pages/main/main.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' - -export const MainPage = () => { - return ( -
-

Главная страница проекта challenge-admin-pl

-

Это базовая страница с React Router

-
- ) -} - diff --git a/src/pages/submissions/SubmissionsPage.tsx b/src/pages/submissions/SubmissionsPage.tsx index e7d6ffe..d26910e 100644 --- a/src/pages/submissions/SubmissionsPage.tsx +++ b/src/pages/submissions/SubmissionsPage.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' import { Box, Heading, @@ -27,6 +28,7 @@ import { StatusBadge } from '../../components/StatusBadge' import type { ChallengeSubmission, SubmissionStatus, ChallengeTask, ChallengeUser } from '../../types/challenge' export const SubmissionsPage: React.FC = () => { + const { t } = useTranslation() const { data: submissions, isLoading, error, refetch } = useGetAllSubmissionsQuery() const [searchQuery, setSearchQuery] = useState('') @@ -34,11 +36,11 @@ export const SubmissionsPage: React.FC = () => { const [selectedSubmission, setSelectedSubmission] = useState(null) if (isLoading) { - return + return } if (error || !submissions) { - return + return } const filteredSubmissions = submissions.filter((submission) => { @@ -69,28 +71,28 @@ export const SubmissionsPage: React.FC = () => { const submitted = new Date(submission.submittedAt).getTime() const checked = new Date(submission.checkedAt).getTime() const diff = Math.round((checked - submitted) / 1000) - return `${diff} сек` + return t('challenge.admin.submissions.check.time', { time: diff }) } const statusOptions = createListCollection({ items: [ - { label: 'Все статусы', value: 'all' }, - { label: 'Принято', value: 'accepted' }, - { label: 'Доработка', value: 'needs_revision' }, - { label: 'Проверяется', value: 'in_progress' }, - { label: 'Ожидает', value: 'pending' }, + { label: t('challenge.admin.submissions.status.all'), value: 'all' }, + { label: t('challenge.admin.submissions.status.accepted'), value: 'accepted' }, + { label: t('challenge.admin.submissions.status.needs.revision'), value: 'needs_revision' }, + { label: t('challenge.admin.submissions.status.in.progress'), value: 'in_progress' }, + { label: t('challenge.admin.submissions.status.pending'), value: 'pending' }, ], }) return ( - Попытки решений + {t('challenge.admin.submissions.title')} {/* Filters */} {submissions.length > 0 && ( setSearchQuery(e.target.value)} maxW="400px" @@ -102,7 +104,7 @@ export const SubmissionsPage: React.FC = () => { maxW="200px" > - + {statusOptions.items.map((option) => ( @@ -116,21 +118,21 @@ export const SubmissionsPage: React.FC = () => { )} {filteredSubmissions.length === 0 && submissions.length === 0 ? ( - + ) : filteredSubmissions.length === 0 ? ( - + ) : ( - Пользователь - Задание - Статус - Попытка - Дата отправки - Время проверки - Действия + {t('challenge.admin.submissions.table.user')} + {t('challenge.admin.submissions.table.task')} + {t('challenge.admin.submissions.table.status')} + {t('challenge.admin.submissions.table.attempt')} + {t('challenge.admin.submissions.table.submitted')} + {t('challenge.admin.submissions.table.check.time')} + {t('challenge.admin.submissions.table.actions')} @@ -167,7 +169,7 @@ export const SubmissionsPage: React.FC = () => { colorPalette="teal" onClick={() => setSelectedSubmission(submission)} > - Детали + {t('challenge.admin.submissions.button.details')} @@ -199,6 +201,8 @@ const SubmissionDetailsModal: React.FC = ({ isOpen, onClose, }) => { + const { t } = useTranslation() + if (!submission) return null const user = submission.user as ChallengeUser @@ -215,7 +219,7 @@ const SubmissionDetailsModal: React.FC = ({ }) } - const getCheckTime = () => { + const getCheckTimeValue = () => { if (!submission.checkedAt) return null const submitted = new Date(submission.submittedAt).getTime() const checked = new Date(submission.checkedAt).getTime() @@ -226,7 +230,7 @@ const SubmissionDetailsModal: React.FC = ({ !e.open && onClose()} size="xl"> - Детали попытки #{submission.attemptNumber} + {t('challenge.admin.submissions.details.title')} #{submission.attemptNumber} @@ -235,13 +239,13 @@ const SubmissionDetailsModal: React.FC = ({ - Пользователь + {t('challenge.admin.submissions.details.user')} {user.nickname} - Статус + {t('challenge.admin.submissions.details.status')} @@ -249,15 +253,15 @@ const SubmissionDetailsModal: React.FC = ({ - Отправлено: {formatDate(submission.submittedAt)} + {t('challenge.admin.submissions.details.submitted')} {formatDate(submission.submittedAt)} {submission.checkedAt && ( <> - Проверено: {formatDate(submission.checkedAt)} + {t('challenge.admin.submissions.details.checked')} {formatDate(submission.checkedAt)} - Время проверки: {getCheckTime()} сек + {t('challenge.admin.submissions.details.check.time')} {t('challenge.admin.submissions.check.time', { time: getCheckTimeValue() })} )} @@ -267,7 +271,7 @@ const SubmissionDetailsModal: React.FC = ({ {/* Task */} - Задание: {task.title} + {t('challenge.admin.submissions.details.task')} {task.title} = ({ {/* Solution */} - Решение пользователя: + {t('challenge.admin.submissions.details.solution')} = ({ {submission.feedback && ( - Обратная связь от LLM: + {t('challenge.admin.submissions.details.feedback')} = ({ diff --git a/src/pages/tasks/TaskFormPage.tsx b/src/pages/tasks/TaskFormPage.tsx index c8c3b7e..7884239 100644 --- a/src/pages/tasks/TaskFormPage.tsx +++ b/src/pages/tasks/TaskFormPage.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react' import { useNavigate, useParams } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import { Box, Heading, @@ -9,8 +10,6 @@ import { VStack, HStack, Text, - Flex, - Stack, Field, Tabs, } from '@chakra-ui/react' @@ -29,7 +28,7 @@ export const TaskFormPage: React.FC = () => { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const isEdit = !!id - + const { t } = useTranslation() const { data: task, isLoading: isLoadingTask, error: loadError } = useGetTaskQuery(id!, { skip: !id, }) @@ -54,8 +53,8 @@ export const TaskFormPage: React.FC = () => { if (!title.trim() || !description.trim()) { toaster.create({ - title: 'Ошибка валидации', - description: 'Заполните обязательные поля', + title: t('challenge.admin.common.validation.error'), + description: t('challenge.admin.tasks.validation.fill.required.fields'), type: 'error', }) return @@ -72,8 +71,8 @@ export const TaskFormPage: React.FC = () => { }, }).unwrap() toaster.create({ - title: 'Успешно', - description: 'Задание обновлено', + title: t('challenge.admin.common.success'), + description: t('challenge.admin.tasks.updated'), type: 'success', }) } else { @@ -83,34 +82,42 @@ export const TaskFormPage: React.FC = () => { hiddenInstructions: hiddenInstructions.trim() || undefined, }).unwrap() toaster.create({ - title: 'Успешно', - description: 'Задание создано', + title: t('challenge.admin.common.success'), + description: t('challenge.admin.tasks.created'), type: 'success', }) } navigate(URLs.tasks) - } catch (err: any) { + } catch (err: unknown) { + const errorMessage = + (err && typeof err === 'object' && 'data' in err && + err.data && typeof err.data === 'object' && 'error' in err.data && + err.data.error && typeof err.data.error === 'object' && 'message' in err.data.error && + typeof err.data.error.message === 'string') + ? err.data.error.message + : t('challenge.admin.tasks.save.error') + toaster.create({ - title: 'Ошибка', - description: err?.data?.error?.message || 'Не удалось сохранить задание', + title: t('challenge.admin.common.error'), + description: errorMessage, type: 'error', }) } } if (isEdit && isLoadingTask) { - return + return } if (isEdit && loadError) { - return + return } const isLoading = isCreating || isUpdating return ( - {isEdit ? 'Редактировать задание' : 'Создать задание'} + {isEdit ? t('challenge.admin.tasks.edit.title') : t('challenge.admin.tasks.create.title')} { {/* Title */} - Название задания + {t('challenge.admin.tasks.field.title')} setTitle(e.target.value)} - placeholder="Введите название задания" + placeholder={t('challenge.admin.tasks.field.title.placeholder')} maxLength={255} disabled={isLoading} /> - Максимум 255 символов + {t('challenge.admin.tasks.field.title.helper')} {/* Description with Markdown */} - Описание (Markdown) + {t('challenge.admin.tasks.field.description')} setShowDescPreview(e.value === 'preview')} > - Редактор - Превью + {t('challenge.admin.tasks.tab.editor')} + {t('challenge.admin.tasks.tab.preview')}