From ca6727d9bf28f3f147313e7e5e79b0b8d5a2b269 Mon Sep 17 00:00:00 2001 From: andrei Date: Sat, 29 Nov 2025 10:36:34 +0500 Subject: [PATCH] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=B4?= =?UTF-8?q?=D0=B0=D1=87=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/app/application/courses_repository.py | 2 +- api/app/application/task_files_repository.py | 38 ++++ api/app/application/tasks_repository.py | 45 +++++ api/app/controllers/tasks_router.py | 151 ++++++++++++++ api/app/domain/entities/task_files.py | 11 + api/app/domain/entities/tasks.py | 30 +++ api/app/domain/models/tasks.py | 2 +- api/app/infrastructure/task_files_service.py | 118 +++++++++++ api/app/infrastructure/tasks_service.py | 113 +++++++++++ api/app/main.py | 2 + web/src/Api/lessonsApi.js | 7 + web/src/Api/tasksApi.js | 104 ++++++++++ .../CreateLessonModalForm.jsx | 3 +- .../useCreateLessonModalForm.js | 2 +- .../CreateTaskModalForm.jsx | 86 ++++++++ .../useCreateTaskModalForm.js | 178 +++++++++++++++++ .../UpdateLessonModalForm.jsx | 1 - .../useUpdateLessonModalForm.js | 2 +- .../CourseDetailPage/CourseDetailPage.jsx | 189 +++++++++++------- .../CourseDetailPage/useCourseDetailPage.js | 41 +++- web/src/Redux/Slices/tasksSlice.js | 32 +++ web/src/Redux/store.js | 10 +- 22 files changed, 1088 insertions(+), 79 deletions(-) create mode 100644 api/app/application/task_files_repository.py create mode 100644 api/app/application/tasks_repository.py create mode 100644 api/app/controllers/tasks_router.py create mode 100644 api/app/domain/entities/task_files.py create mode 100644 api/app/domain/entities/tasks.py create mode 100644 api/app/infrastructure/task_files_service.py create mode 100644 api/app/infrastructure/tasks_service.py create mode 100644 web/src/Api/tasksApi.js create mode 100644 web/src/Components/Pages/CourseDetailPage/Components/CreateTaskModalForm/CreateTaskModalForm.jsx create mode 100644 web/src/Components/Pages/CourseDetailPage/Components/CreateTaskModalForm/useCreateTaskModalForm.js create mode 100644 web/src/Redux/Slices/tasksSlice.js diff --git a/api/app/application/courses_repository.py b/api/app/application/courses_repository.py index 72436c9..69c9483 100644 --- a/api/app/application/courses_repository.py +++ b/api/app/application/courses_repository.py @@ -43,7 +43,7 @@ class CoursesRepository: selectinload(Course.enrollments) ) .join(Course.enrollments) - .filter(Enrollment.user_id == user_id) + .filter(Enrollment.student_id == user_id) ) result = await self.db.execute(query) return result.scalars().all() diff --git a/api/app/application/task_files_repository.py b/api/app/application/task_files_repository.py new file mode 100644 index 0000000..9028818 --- /dev/null +++ b/api/app/application/task_files_repository.py @@ -0,0 +1,38 @@ +from typing import Optional + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.domain.models import TaskFile + + +class TaskFilesRepository: + def __init__(self, db: AsyncSession): + self.db = db + + async def get_by_id(self, file_id: int) -> Optional[TaskFile]: + query = ( + select(TaskFile) + .filter_by(id=file_id) + ) + result = await self.db.execute(query) + return result.scalars().first() + + async def get_by_task_id(self, task_id: int) -> Optional[TaskFile]: + query = ( + select(TaskFile) + .filter_by(task_id=task_id) + ) + result = await self.db.execute(query) + return result.scalars().all() + + async def create(self, task_file: TaskFile) -> TaskFile: + self.db.add(task_file) + await self.db.commit() + await self.db.refresh(task_file) + return task_file + + async def delete(self, task_file: TaskFile) -> TaskFile: + await self.db.delete(task_file) + await self.db.commit() + return task_file diff --git a/api/app/application/tasks_repository.py b/api/app/application/tasks_repository.py new file mode 100644 index 0000000..caf8698 --- /dev/null +++ b/api/app/application/tasks_repository.py @@ -0,0 +1,45 @@ +from typing import List, Optional + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.domain.models import Task + + +class TasksRepository: + def __init__(self, db: AsyncSession) -> None: + self.db = db + + async def get_all_by_course(self, course_id: int) -> List[Task]: + query = ( + select(Task) + .filter_by(course_id=course_id) + .order_by(Task.number) + ) + result = await self.db.execute(query) + return result.scalars().all() + + async def get_by_id(self, task_id: int) -> Optional[Task]: + query = ( + select(Task) + .filter_by(id=task_id) + ) + result = await self.db.execute(query) + return result.scalars().first() + + async def create(self, task: Task) -> Task: + self.db.add(task) + await self.db.commit() + await self.db.refresh(task) + return task + + async def update(self, task: Task) -> Task: + await self.db.merge(task) + await self.db.commit() + await self.db.refresh(task) + return task + + async def delete(self, task: Task) -> Task: + await self.db.delete(task) + await self.db.commit() + return task diff --git a/api/app/controllers/tasks_router.py b/api/app/controllers/tasks_router.py new file mode 100644 index 0000000..8a69320 --- /dev/null +++ b/api/app/controllers/tasks_router.py @@ -0,0 +1,151 @@ +from typing import List, Optional + +from fastapi import APIRouter, Depends, status, File, UploadFile +from sqlalchemy.ext.asyncio import AsyncSession +from starlette.responses import FileResponse + +from app.database.session import get_db +from app.domain.entities.task_files import ReadTaskFile +from app.domain.entities.tasks import TaskRead, TaskCreate +from app.domain.models import User +from app.infrastructure.dependencies import require_auth_user, require_teacher +from app.infrastructure.task_files_service import TaskFilesService +from app.infrastructure.tasks_service import TasksService + +tasks_router = APIRouter() + + +@tasks_router.get( + '/course/{course_id}/', + response_model=Optional[List[TaskRead]], + summary='Get all tasks by course', + description='Get all tasks by course', +) +async def get_course_tasks( + course_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_auth_user), +): + tasks_service = TasksService(db) + return await tasks_service.get_all_by_course(course_id) + + +@tasks_router.get( + '/{task_id}/', + response_model=Optional[TaskRead], + summary='Get task by task ID', + description='Get task by task ID', +) +async def get_task( + task_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_auth_user), +): + tasks_service = TasksService(db) + return await tasks_service.get_by_id(task_id) + + +@tasks_router.post( + '/{course_id}/', + response_model=Optional[TaskRead], + summary='Create a new task', + description='Create a new task', +) +async def create_task( + course_id: int, + task_data: TaskCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_teacher), +): + tasks_service = TasksService(db) + return await tasks_service.create(task_data, current_user, course_id) + + +@tasks_router.put( + '/{task_id}/', + response_model=Optional[TaskRead], + summary='Update a task', + description='Update a task', +) +async def update_task( + task_id: int, + task_data: TaskCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_teacher), +): + tasks_service = TasksService(db) + return await tasks_service.update(task_id, task_data, current_user) + + +@tasks_router.delete( + '/{task_id}/', + response_model=Optional[TaskRead], + summary='Delete a task', + description='Delete a task', +) +async def delete_task( + task_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_teacher), +): + tasks_service = TasksService(db) + return await tasks_service.delete(task_id, current_user) + + +@tasks_router.get( + '/files/{task_id}/', + response_model=Optional[List[ReadTaskFile]], + summary='Get a files list by task ID', + description='Get a files list by task ID', +) +async def get_files( + task_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_auth_user), +): + task_files_service = TaskFilesService(db) + return await task_files_service.get_files_list_by_task(task_id) + + +@tasks_router.get( + '/file/{file_id}/', + response_class=FileResponse, +) +async def get_file( + file_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_auth_user), +): + task_files_service = TaskFilesService(db) + return await task_files_service.get_file_by_id(file_id) + + +@tasks_router.post( + '/files/{task_id}/upload/', + response_model=ReadTaskFile, + summary='Upload a file', + description='Upload a file', +) +async def upload_file( + task_id: int, + file: UploadFile = File(...), + db: AsyncSession = Depends(get_db), + user: User = Depends(require_teacher), +): + task_files_service = TaskFilesService(db) + return await task_files_service.upload_file(task_id, file) + + +@tasks_router.delete( + '/files/{file_id}/', + response_model=Optional[ReadTaskFile], + summary='Delete a file', + description='Delete a file', +) +async def delete_file( + file_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_teacher), +): + task_files_service = TaskFilesService(db) + return await task_files_service.delete_file(file_id) diff --git a/api/app/domain/entities/task_files.py b/api/app/domain/entities/task_files.py new file mode 100644 index 0000000..5102024 --- /dev/null +++ b/api/app/domain/entities/task_files.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + + +class ReadTaskFile(BaseModel): + id: int + filename: str + file_path: str + task_id: int + + class Config: + from_attributes = True diff --git a/api/app/domain/entities/tasks.py b/api/app/domain/entities/tasks.py new file mode 100644 index 0000000..64da1f5 --- /dev/null +++ b/api/app/domain/entities/tasks.py @@ -0,0 +1,30 @@ +from typing import Optional +from pydantic import BaseModel, Field + +from app.domain.entities.users import UserRead + + +class TaskBase(BaseModel): + title: str = Field(..., max_length=250) + description: Optional[str] = None + text: Optional[str] = None + number: int = Field(..., ge=1) + + +class TaskCreate(TaskBase): + pass + + +class TaskUpdate(TaskBase): + pass + + +class TaskRead(TaskBase): + id: int + course_id: int + creator_id: int + + creator: UserRead + + class Config: + from_attributes = True diff --git a/api/app/domain/models/tasks.py b/api/app/domain/models/tasks.py index 7f1446d..cefca2c 100644 --- a/api/app/domain/models/tasks.py +++ b/api/app/domain/models/tasks.py @@ -18,6 +18,6 @@ class Task(RootTable): creator_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False) course: Mapped['Course'] = relationship('Course', back_populates='tasks') - creator: Mapped['User'] = relationship('User', back_populates='created_tasks') + creator: Mapped['User'] = relationship('User', back_populates='created_tasks', lazy='joined') files: Mapped[List['TaskFile']] = relationship('TaskFile', back_populates='task') diff --git a/api/app/infrastructure/task_files_service.py b/api/app/infrastructure/task_files_service.py new file mode 100644 index 0000000..a8d2466 --- /dev/null +++ b/api/app/infrastructure/task_files_service.py @@ -0,0 +1,118 @@ +import os +import uuid +from typing import List + +import aiofiles +from fastapi import UploadFile, HTTPException +from starlette.responses import FileResponse +from sqlalchemy.ext.asyncio import AsyncSession +from werkzeug.utils import secure_filename + +from app.application.task_files_repository import TaskFilesRepository +from app.application.tasks_repository import TasksRepository +from app.domain.entities.task_files import ReadTaskFile +from app.domain.models import TaskFile + + +class TaskFilesService: + def __init__(self, db: AsyncSession): + self.task_files_repository = TaskFilesRepository(db) + self.tasks_repository = TasksRepository(db) + + async def get_file_by_id(self, file_id: int) -> FileResponse: + task_file = await self.task_files_repository.get_by_id(file_id) + + if not task_file: + raise HTTPException(404, "Файл с таким ID не найден") + + return FileResponse( + task_file.file_path, + media_type=self.get_media_type(task_file.filename), + filename=os.path.basename(task_file.filename), + ) + + async def get_files_list_by_task(self, task_id: int) -> List[ReadTaskFile]: + task = await self.tasks_repository.get_by_id(task_id) + + if task is None: + raise HTTPException(404, "Лекционный материал не найден") + + task_files = await self.task_files_repository.get_by_task_id(task_id) + + response = [] + for task_file in task_files: + response.append( + ReadTaskFile.model_validate( + task_file + ) + ) + + return response + + async def upload_file(self, task_id: int, file: UploadFile) -> ReadTaskFile: + task = await self.tasks_repository.get_by_id(task_id) + + if task is None: + raise HTTPException(404, "Лекционный материал не найден") + + file_path = await self.save_file(file, f'uploads/tasks/{task.id}') + + task_file_model = TaskFile( + filename=file.filename, + file_path=file_path, + task_id=task.id, + ) + + task_file_model = await self.task_files_repository.create(task_file_model) + + return ReadTaskFile.model_validate(task_file_model) + + async def delete_file(self, file_id: int) -> ReadTaskFile: + task_file = await self.task_files_repository.get_by_id(file_id) + + if task_file is None: + raise HTTPException(404, "Файл не найден") + + if not os.path.exists(task_file.file_path): + raise HTTPException(404, "Файл не найден на диске") + + if os.path.exists(task_file.file_path): + os.remove(task_file.file_path) + + task_file = await self.task_files_repository.delete(task_file) + + return ReadTaskFile.model_validate(task_file) + + async def save_file(self, file: UploadFile, upload_dir: str = 'uploads/tasks') -> str: + os.makedirs(upload_dir, exist_ok=True) + filename = self.generate_filename(file) + file_path = os.path.join(upload_dir, filename) + + async with aiofiles.open(file_path, 'wb') as out_file: + content = await file.read() + await out_file.write(content) + return file_path + + @staticmethod + def generate_filename(file: UploadFile) -> str: + return secure_filename(f"{uuid.uuid4()}_{file.filename}") + + @staticmethod + def get_media_type(filename: str) -> str: + extension = filename.split('.')[-1].lower() + if extension in ['jpeg', 'jpg', 'png']: + return f"image/{extension}" + if extension == 'pdf': + return "application/pdf" + if extension in ['zip']: + return "application/zip" + if extension in ['doc', 'docx']: + return "application/msword" + if extension in ['xls', 'xlsx']: + return "application/vnd.ms-excel" + if extension in ['ppt', 'pptx']: + return "application/vnd.ms-powerpoint" + if extension in ['txt']: + return "text/plain" + + return "application/octet-stream" diff --git a/api/app/infrastructure/tasks_service.py b/api/app/infrastructure/tasks_service.py new file mode 100644 index 0000000..160071c --- /dev/null +++ b/api/app/infrastructure/tasks_service.py @@ -0,0 +1,113 @@ +import os +from typing import List, Optional + +from fastapi import HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.application.courses_repository import CoursesRepository +from app.application.task_files_repository import TaskFilesRepository +from app.application.tasks_repository import TasksRepository +from app.domain.entities.tasks import TaskCreate, TaskUpdate +from app.domain.entities.tasks import TaskRead +from app.domain.models import User, Task +from app.settings import Settings + + +class TasksService: + def __init__(self, db: AsyncSession): + self.tasks_repository = TasksRepository(db) + self.courses_repository = CoursesRepository(db) + self.task_files_repository = TaskFilesRepository(db) + self.settings = Settings() + + async def get_all_by_course(self, course_id: int) -> List[TaskRead]: + tasks = await self.tasks_repository.get_all_by_course(course_id) + response = [] + + for task in tasks: + response.append(TaskRead.model_validate(task)) + + return response + + async def get_by_id(self, task_id: int) -> TaskRead: + task = await self.tasks_repository.get_by_id(task_id) + if not task: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Задание не найдено" + ) + return TaskRead.model_validate(task) + + async def create(self, task_data: TaskCreate, creator: User, course_id) -> TaskRead: + course_model = await self.courses_repository.get_by_id(course_id) + if not course_model: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Курс не найден" + ) + + task = Task( + title=task_data.title, + description=task_data.description, + text=task_data.text, + number=task_data.number, + course_id=course_id, + creator_id=creator.id + ) + + created_task = await self.tasks_repository.create(task) + return TaskRead.model_validate(created_task) + + async def update(self, task_id: int, task_data: TaskUpdate, current_user: User) -> Optional[TaskRead]: + task = await self.tasks_repository.get_by_id(task_id) + if not task: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Задание не найдено" + ) + + is_admin = current_user.role.title == self.settings.root_role_name + if task.creator_id != current_user.id and not is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Доступ запрещён" + ) + + update_dict = task_data.dict(exclude_unset=True) + for key, value in update_dict.items(): + setattr(task, key, value) + + updated_task = await self.tasks_repository.update(task) + return TaskRead.model_validate(updated_task) + + async def delete(self, task_id: int, current_user: User) -> None: + task = await self.tasks_repository.get_by_id(task_id) + if not task: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Задание не найдено" + ) + + is_admin = current_user.role.title == self.settings.root_role_name + if task.creator_id != current_user.id and not is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Доступ запрещён" + ) + + task_files = await self.task_files_repository.get_by_task_id(task_id) + for file in task_files: + task_file = await self.task_files_repository.get_by_id(file.id) + + if task_file is None: + raise HTTPException(404, "Файл не найден") + + if not os.path.exists(task_file.file_path): + raise HTTPException(404, "Файл не найден на диске") + + if os.path.exists(task_file.file_path): + os.remove(task_file.file_path) + + await self.task_files_repository.delete(task_file) + + await self.tasks_repository.delete(task) diff --git a/api/app/main.py b/api/app/main.py index fc997a2..b7ff192 100644 --- a/api/app/main.py +++ b/api/app/main.py @@ -7,6 +7,7 @@ from app.controllers.lessons_router import lessons_router from app.controllers.register_router import register_router from app.controllers.roles_router import roles_router from app.controllers.statuses_router import statuses_router +from app.controllers.tasks_router import tasks_router from app.controllers.users_router import users_router from app.settings import Settings @@ -29,6 +30,7 @@ def start_app(): api_app.include_router(register_router, prefix=f'{settings.prefix}/register', tags=['register']) api_app.include_router(roles_router, prefix=f'{settings.prefix}/roles', tags=['roles']) api_app.include_router(statuses_router, prefix=f'{settings.prefix}/statuses', tags=['statuses']) + api_app.include_router(tasks_router, prefix=f'{settings.prefix}/tasks', tags=['tasks']) api_app.include_router(users_router, prefix=f'{settings.prefix}/users', tags=['users']) return api_app diff --git a/web/src/Api/lessonsApi.js b/web/src/Api/lessonsApi.js index da8d82f..b7daeb8 100644 --- a/web/src/Api/lessonsApi.js +++ b/web/src/Api/lessonsApi.js @@ -13,6 +13,13 @@ export const lessonsApi = createApi({ method: "GET", }), providesTags: ["lesson"], + transformResponse: (response) => { + return response.map(lesson => ({ + ...lesson, + contentType: "lesson", + __typename: "Lesson" + })); + }, }), getLessonById: builder.query({ query: (lessonId) => ({ diff --git a/web/src/Api/tasksApi.js b/web/src/Api/tasksApi.js new file mode 100644 index 0000000..2080683 --- /dev/null +++ b/web/src/Api/tasksApi.js @@ -0,0 +1,104 @@ +import {createApi} from "@reduxjs/toolkit/query/react"; +import {baseQueryWithAuth} from "./baseQuery.js"; + + +export const tasksApi = createApi({ + reducerPath: "tasksApi", + baseQuery: baseQueryWithAuth, + tagTypes: ["task"], + endpoints: (builder) => ({ + getTasksByCourseId: builder.query({ + query: (courseId) => ({ + url: `/tasks/course/${courseId}/`, + method: "GET", + }), + providesTags: ["task"], + transformResponse: (response) => { + return response.map(task => ({ + ...task, + contentType: "task", + __typename: "Task" + })); + }, + }), + getTaskById: builder.query({ + query: (taskId) => ({ + url: `/tasks/${taskId}/`, + method: "GET", + }), + providesTags: ["task"], + }), + createTask: builder.mutation({ + query: ({courseId, taskData}) => ({ + url: `/tasks/${courseId}/`, + method: "POST", + body: taskData, + }), + invalidatesTags: ["task"], + }), + updateTask: builder.mutation({ + query: ({taskId, taskData}) => ({ + url: `/tasks/${taskId}/`, + method: "PUT", + body: taskData, + }), + invalidatesTags: ["task"], + }), + deleteTask: builder.mutation({ + query: (taskId) => ({ + url: `/tasks/${taskId}/`, + method: "DELETE", + }), + invalidatesTags: ["task"], + }), + getTaskFilesList: builder.query({ + query: (taskId) => ({ + url: `/tasks/files/${taskId}/`, + method: "GET", + }), + providesTags: ["task"], + }), + getDownloadFile: builder.query({ + query: (fileId) => ({ + url: `/tasks/file/${fileId}/`, + method: "GET", + }), + providesTags: ["task"], + }), + uploadFile: builder.mutation({ + query: ({task_id, fileData}) => { + if (!(fileData instanceof File)) { + throw new Error('Invalid file object'); + } + const formData = new FormData(); + formData.append('file', fileData); + return { + url: `/tasks/files/${task_id}/upload/`, + method: 'POST', + formData: true, + body: formData, + }; + }, + invalidatesTags: ["task"], + }), + deleteFile: builder.mutation({ + query: (fileId) => ({ + url: `/tasks/files/${fileId}/`, + method: "DELETE", + }), + invalidatesTags: ["task"], + }), + }), +}); + +export const { + useGetTasksByCourseIdQuery, + useGetTaskByIdQuery, + useCreateTaskMutation, + useUpdateTaskMutation, + useDeleteTaskMutation, + useGetTaskFilesListQuery, + useGetDownloadFileQuery, + useUploadFileMutation, + useDeleteFileMutation, +} = tasksApi; diff --git a/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/CreateLessonModalForm.jsx b/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/CreateLessonModalForm.jsx index 9c41b85..52fa09a 100644 --- a/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/CreateLessonModalForm.jsx +++ b/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/CreateLessonModalForm.jsx @@ -46,7 +46,7 @@ const CreateLessonModalForm = ({courseId}) => { label="Название лекции" rules={[{required: true, message: "Введите название лекции"}]} > - + @@ -74,7 +74,6 @@ const CreateLessonModalForm = ({courseId}) => { return false; }} onRemove={(file) => handleRemoveFile(file)} - accept=".pdf,.doc,.docx,.jpg,.jpeg,.png" multiple > diff --git a/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/useCreateLessonModalForm.js b/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/useCreateLessonModalForm.js index 58859ab..942bf6e 100644 --- a/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/useCreateLessonModalForm.js +++ b/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/useCreateLessonModalForm.js @@ -118,7 +118,7 @@ const useCreateLessonModalForm = ({courseId}) => { askBeforePasteFromWord: false, defaultActionOnPaste: "insert_clear_html", spellcheck: true, - placeholder: "Введите результаты приёма...", + placeholder: "Заполните содержимое лекционного материала", showCharsCounter: true, showWordsCounter: true, showXPathInStatusbar: false, diff --git a/web/src/Components/Pages/CourseDetailPage/Components/CreateTaskModalForm/CreateTaskModalForm.jsx b/web/src/Components/Pages/CourseDetailPage/Components/CreateTaskModalForm/CreateTaskModalForm.jsx new file mode 100644 index 0000000..114b6a4 --- /dev/null +++ b/web/src/Components/Pages/CourseDetailPage/Components/CreateTaskModalForm/CreateTaskModalForm.jsx @@ -0,0 +1,86 @@ +import useCreateTaskModalForm from "./useCreateTaskModalForm.js"; +import {Button, Form, Input, InputNumber, Modal, Upload} from "antd"; +import JoditEditor from "jodit-react"; +import {UploadOutlined} from "@ant-design/icons"; + +const {TextArea} = Input; + +const CreateTaskModalForm = ({courseId}) => { + const { + isModalOpen, + handleCancel, + handleOk, + form, + joditConfig, + editorRef, + isLoading, + handleAddFile, + handleRemoveFile, + draftFiles, + } = useCreateTaskModalForm({courseId}); + + return ( + + Отмена + , + , + ]} + destroyOnHidden + > +
+ + + + + +