diff --git a/api/app/application/lessons_repository.py b/api/app/application/lessons_repository.py new file mode 100644 index 0000000..5a87bb2 --- /dev/null +++ b/api/app/application/lessons_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 Lesson + + +class LessonsRepository: + def __init__(self, db: AsyncSession) -> None: + self.db = db + + async def get_all_by_course(self, course_id: int) -> List[Lesson]: + query = ( + select(Lesson) + .filter_by(course_id=course_id) + .order_by(Lesson.number) + ) + result = await self.db.execute(query) + return result.scalars().all() + + async def get_by_id(self, lesson_id: int) -> Optional[Lesson]: + query = ( + select(Lesson) + .filter_by(id=lesson_id) + ) + result = await self.db.execute(query) + return result.scalars().first() + + async def create(self, lesson: Lesson) -> Lesson: + self.db.add(lesson) + await self.db.commit() + await self.db.refresh(lesson) + return lesson + + async def update(self, lesson: Lesson) -> Lesson: + await self.db.merge(lesson) + await self.db.commit() + await self.db.refresh(lesson) + return lesson + + async def delete(self, lesson: Lesson) -> Lesson: + await self.db.delete(lesson) + await self.db.commit() + return lesson diff --git a/api/app/controllers/courses_router.py b/api/app/controllers/courses_router.py index 3cc3ed6..4a9c016 100644 --- a/api/app/controllers/courses_router.py +++ b/api/app/controllers/courses_router.py @@ -30,6 +30,21 @@ async def get_all_courses( return await courses_service.get_all() +@courses_router.get( + '/{course_id}/', + response_model=Optional[CourseRead], + summary='Return a specific course', + description='Return a specific course', +) +async def get_course( + course_id: int, + db: AsyncSession = Depends(get_db), + user: User = Depends(require_auth_user), +): + courses_service = CoursesService(db) + return await courses_service.get_by_id(course_id) + + @courses_router.post( '/', response_model=Optional[CourseCreated], diff --git a/api/app/controllers/lessons_router.py b/api/app/controllers/lessons_router.py new file mode 100644 index 0000000..882a0e4 --- /dev/null +++ b/api/app/controllers/lessons_router.py @@ -0,0 +1,90 @@ +from typing import List, Optional + +from fastapi import APIRouter, Depends, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.database.session import get_db +from app.domain.entities.lessons import LessonCreate, LessonUpdate, LessonRead +from app.domain.models import User +from app.infrastructure.dependencies import require_auth_user, require_teacher, require_admin +from app.infrastructure.lessons_service import LessonsService + +lessons_router = APIRouter() + + +@lessons_router.get( + '/course/{course_id}/', + response_model=Optional[List[LessonRead]], + summary='Get all lessons by course', + description='Get all lessons by course', +) +async def get_course_lessons( + course_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_auth_user), +): + lessons_service = LessonsService(db) + return await lessons_service.get_all_by_course(course_id) + + +@lessons_router.get( + '/{lesson_id}/', + response_model=Optional[LessonRead], + summary='Get lesson by lesson ID', + description='Get lesson by lesson ID', +) +async def get_lesson( + lesson_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_auth_user), +): + lessons_service = LessonsService(db) + return await lessons_service.get_by_id(lesson_id) + + +@lessons_router.post( + '/{course_id}/', + response_model=Optional[LessonRead], + summary='Create a new lesson', + description='Create a new lesson', +) +async def create_lesson( + course_id: int, + lesson_data: LessonCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_teacher), +): + lessons_service = LessonsService(db) + return await lessons_service.create(lesson_data, current_user, course_id) + + +@lessons_router.put( + '/{lesson_id}/', + response_model=Optional[LessonRead], + summary='Update a lesson', + description='Update a lesson', +) +async def update_lesson( + lesson_id: int, + lesson_data: LessonUpdate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_teacher), +): + lessons_service = LessonsService(db) + return await lessons_service.update(lesson_id, lesson_data, current_user) + + +@lessons_router.delete( + '/{lesson_id}', + response_model=Optional[LessonRead], + summary='Delete a lesson', + description='Delete a lesson', +) +async def delete_lesson( + lesson_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_teacher), +): + lessons_service = LessonsService(db) + await lessons_service.delete(lesson_id, current_user) + return None diff --git a/api/app/domain/entities/lessons.py b/api/app/domain/entities/lessons.py new file mode 100644 index 0000000..df77e43 --- /dev/null +++ b/api/app/domain/entities/lessons.py @@ -0,0 +1,29 @@ +from typing import Optional +from pydantic import BaseModel, Field + + +class LessonBase(BaseModel): + title: str = Field(..., max_length=250) + description: Optional[str] = None + text: Optional[str] = None + number: int = Field(..., ge=1) + + +class LessonCreate(LessonBase): + course_id: int + + +class LessonUpdate(BaseModel): + title: Optional[str] = Field(None, max_length=250) + description: Optional[str] = None + text: Optional[str] = None + number: Optional[int] = Field(None, ge=1) + + +class LessonRead(LessonBase): + id: int + course_id: int + creator_id: int + + class Config: + from_attributes = True diff --git a/api/app/infrastructure/courses_service.py b/api/app/infrastructure/courses_service.py index 798b686..f9cd492 100644 --- a/api/app/infrastructure/courses_service.py +++ b/api/app/infrastructure/courses_service.py @@ -22,6 +22,13 @@ class CoursesService: return response + async def get_by_id(self, course_id: int) -> Optional[CourseRead]: + course = await self.courses_repository.get_by_id(course_id) + if course is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Курс с таким Id не найден') + + return CourseRead.model_validate(course) + async def create(self, course: CourseCreate) -> CourseCreated: # ← возвращаем CourseCreated course_model = Course( title=course.title, diff --git a/api/app/infrastructure/dependencies.py b/api/app/infrastructure/dependencies.py index 9d02695..601f9fe 100644 --- a/api/app/infrastructure/dependencies.py +++ b/api/app/infrastructure/dependencies.py @@ -48,6 +48,7 @@ def require_admin(user: User = Depends(require_auth_user)): def require_teacher(user: User = Depends(require_auth_user)): + print(user.role.title, user.role.title not in [UserRoles.TEACHER, Settings().root_role_name], [UserRoles.TEACHER, Settings().root_role_name]) if user.role.title not in [UserRoles.TEACHER, Settings().root_role_name]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Ошибка доступа') diff --git a/api/app/infrastructure/lessons_service.py b/api/app/infrastructure/lessons_service.py new file mode 100644 index 0000000..8f610cc --- /dev/null +++ b/api/app/infrastructure/lessons_service.py @@ -0,0 +1,94 @@ +from fastapi import HTTPException, status +from typing import List, Optional + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.application.courses_repository import CoursesRepository +from app.application.lessons_repository import LessonsRepository +from app.domain.entities.lessons import LessonCreate, LessonUpdate, LessonRead +from app.domain.models import Lesson, User +from app.settings import Settings + + +class LessonsService: + def __init__(self, db: AsyncSession): + self.lessons_repository = LessonsRepository(db) + self.courses_repository = CoursesRepository(db) + self.settings = Settings() + + async def get_all_by_course(self, course_id: int) -> List[LessonRead]: + lessons = await self.lessons_repository.get_all_by_course(course_id) + response = [] + + for lesson in lessons: + response.append(LessonRead.model_validate(lesson)) + + return response + + async def get_by_id(self, lesson_id: int) -> LessonRead: + lesson = await self.lessons_repository.get_by_id(lesson_id) + if not lesson: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Урок не найден" + ) + return LessonRead.model_validate(lesson) + + async def create(self, lesson_data: LessonCreate, creator: User, course_id) -> LessonRead: + 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="Курс не найден" + ) + + lesson = Lesson( + title=lesson_data.title, + description=lesson_data.description, + text=lesson_data.text, + number=lesson_data.number, + course_id=course_id, + creator_id=creator.id + ) + + created_lesson = await self.lessons_repository.create(lesson) + return LessonRead.model_validate(created_lesson) + + async def update(self, lesson_id: int, lesson_data: LessonUpdate, current_user: User) -> Optional[LessonRead]: + lesson = await self.lessons_repository.get_by_id(lesson_id) + if not lesson: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Урок не найден" + ) + + is_admin = current_user.role.title == self.settings.root_role_name + if lesson.creator_id != current_user.id and not is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Доступ запрещён" + ) + + update_dict = lesson_data.dict(exclude_unset=True) + for key, value in update_dict.items(): + setattr(lesson, key, value) + + updated_lesson = await self.lessons_repository.update(lesson) + return LessonRead.model_validate(updated_lesson) + + async def delete(self, lesson_id: int, current_user: User) -> None: + lesson = await self.lessons_repository.get_by_id(lesson_id) + if not lesson: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Урок не найден" + ) + + is_admin = current_user.role.title == self.settings.root_role_name + if lesson.creator_id != current_user.id and not is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Доступ запрещён" + ) + + await self.lessons_repository.delete(lesson) diff --git a/api/app/main.py b/api/app/main.py index 1b238a9..fc997a2 100644 --- a/api/app/main.py +++ b/api/app/main.py @@ -3,6 +3,7 @@ from starlette.middleware.cors import CORSMiddleware from app.controllers.auth_router import auth_router from app.controllers.courses_router import courses_router +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 @@ -24,6 +25,7 @@ def start_app(): api_app.include_router(auth_router, prefix=f'{settings.prefix}/auth', tags=['auth']) api_app.include_router(courses_router, prefix=f'{settings.prefix}/courses', tags=['courses']) + api_app.include_router(lessons_router, prefix=f'{settings.prefix}/lessons', tags=['lessons']) 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']) diff --git a/web/package-lock.json b/web/package-lock.json index 6d5f6a3..dcd517d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,6 +12,7 @@ "@reduxjs/toolkit": "^2.11.0", "antd": "^6.0.0", "dayjs": "^1.11.19", + "jodit-react": "^5.2.38", "react": "^19.2.0", "react-dom": "^19.2.0", "react-redux": "^9.2.0", @@ -3128,6 +3129,25 @@ "dev": true, "license": "ISC" }, + "node_modules/jodit": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/jodit/-/jodit-4.7.9.tgz", + "integrity": "sha512-dPlewnu2+vVXELh9BEJTNQoSBL3Cf2E0fNh30yjSEgUtJHYSUYToDjMDEqr0T/L9iNpqPTODy2b4pzyGpPRUog==", + "license": "MIT" + }, + "node_modules/jodit-react": { + "version": "5.2.38", + "resolved": "https://registry.npmjs.org/jodit-react/-/jodit-react-5.2.38.tgz", + "integrity": "sha512-k98vjch0JWX13Dlf7tWv3wPo6dgO9bpi0NUBC6YrpGLYtRJx8d5Ttz5TLWuydEReedWWyNKobSGIGYQDihP8Vw==", + "license": "MIT", + "dependencies": { + "jodit": "^4.7.9" + }, + "peerDependencies": { + "react": "~0.14 || ^15 || ^16 || ^17 || ^18 || ^19", + "react-dom": "~0.14 || ^15 || ^16 || ^17 || ^18 || ^19" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/web/package.json b/web/package.json index abadb48..66962ca 100644 --- a/web/package.json +++ b/web/package.json @@ -14,6 +14,7 @@ "@reduxjs/toolkit": "^2.11.0", "antd": "^6.0.0", "dayjs": "^1.11.19", + "jodit-react": "^5.2.38", "react": "^19.2.0", "react-dom": "^19.2.0", "react-redux": "^9.2.0", diff --git a/web/src/Api/coursesApi.js b/web/src/Api/coursesApi.js index 566f4a8..e296f9b 100644 --- a/web/src/Api/coursesApi.js +++ b/web/src/Api/coursesApi.js @@ -14,6 +14,13 @@ export const coursesApi = createApi({ }), providesTags: ['course'], }), + getCourseById: builder.query({ + query: (courseId) => ({ + url: `/courses/${courseId}/`, + method: "GET", + }), + providesTags: ['course'], + }), createCourse: builder.mutation({ query: (data) => ({ url: "/courses/", @@ -65,6 +72,7 @@ export const coursesApi = createApi({ export const { useGetAllCoursesQuery, + useGetCourseByIdQuery, useCreateCourseMutation, useUpdateCourseMutation, useGetCourseTeachersQuery, diff --git a/web/src/Api/lessonsApi.js b/web/src/Api/lessonsApi.js new file mode 100644 index 0000000..6cd47d4 --- /dev/null +++ b/web/src/Api/lessonsApi.js @@ -0,0 +1,56 @@ +import {createApi} from "@reduxjs/toolkit/query/react"; +import {baseQueryWithAuth} from "./baseQuery.js"; + + +export const lessonsApi = createApi({ + reducerPath: "lessonsApi", + baseQuery: baseQueryWithAuth, + tagTypes: ["lesson"], + endpoints: (builder) => ({ + getLessonsByCourseId: builder.query({ + query: (courseId) => ({ + url: `/lessons/course/${courseId}/`, + method: "GET", + }), + providesTags: ["lesson"], + }), + getLessonById: builder.query({ + query: (lessonId) => ({ + url: `/lessons/${lessonId}/`, + method: "GET", + }), + providesTags: ["lesson"], + }), + createLesson: builder.mutation({ + query: ({courseId, lessonData}) => ({ + url: `/lessons/${courseId}/`, + method: "POST", + body: lessonData, + }), + invalidatesTags: ["lesson"], + }), + updateLesson: builder.mutation({ + query: ({lessonId, lessonData}) => ({ + url: `/lessons/${lessonId}/`, + method: "PUT", + body: lessonData, + }), + invalidatesTags: ["lesson"], + }), + deleteLesson: builder.mutation({ + query: (lessonId) => ({ + url: `/lessons/${lessonId}/`, + method: "DELETE", + }), + invalidatesTags: ["lesson"], + }), + }), +}); + +export const { + useGetLessonsByCourseIdQuery, + useGetLessonByIdQuery, + useCreateLessonMutation, + useUpdateLessonMutation, + useDeleteLessonMutation, +} = lessonsApi; diff --git a/web/src/App/AppRouter.jsx b/web/src/App/AppRouter.jsx index e4ca10f..3ef53e9 100644 --- a/web/src/App/AppRouter.jsx +++ b/web/src/App/AppRouter.jsx @@ -2,10 +2,11 @@ import {Routes, Route, Navigate} from "react-router-dom"; import PrivateRoute from "./PrivateRoute.jsx"; import AdminRoute from "./AdminRoute.jsx"; import LoginPage from "../Components/Pages/LoginPage/LoginPage.jsx"; -import CoursesPage from "../Components/Pages/Courses/CoursesPage.jsx"; +import CoursesPage from "../Components/Pages/CoursesPage/CoursesPage.jsx"; import MainLayout from "../Components/Layouts/MainLayout.jsx"; import ProfilePage from "../Components/Pages/ProfilePage/ProfilePage.jsx"; import AdminPage from "../Components/Pages/AdminPage/AdminPage.jsx"; +import CourseDetailPage from "../Components/Pages/CourseDetailPage/CourseDetailPage.jsx"; const AppRouter = () => ( @@ -16,6 +17,7 @@ const AppRouter = () => ( }> }/> }/> + } /> }/> diff --git a/web/src/Components/Layouts/MainLayout.jsx b/web/src/Components/Layouts/MainLayout.jsx index d6ea6d8..96dff20 100644 --- a/web/src/Components/Layouts/MainLayout.jsx +++ b/web/src/Components/Layouts/MainLayout.jsx @@ -1,6 +1,6 @@ import useMainLayout from "./useMainLayout.js"; import {Layout, Menu} from "antd"; -import CoursesPage from "../Pages/Courses/CoursesPage.jsx"; +import CoursesPage from "../Pages/CoursesPage/CoursesPage.jsx"; import LoadingIndicator from "../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import {Outlet} from "react-router-dom"; import {BookOutlined, ControlOutlined, LogoutOutlined, UserOutlined} from "@ant-design/icons"; diff --git a/web/src/Components/Pages/AdminPage/useAdminPage.js b/web/src/Components/Pages/AdminPage/useAdminPage.js index c84335c..c38d3fc 100644 --- a/web/src/Components/Pages/AdminPage/useAdminPage.js +++ b/web/src/Components/Pages/AdminPage/useAdminPage.js @@ -1,6 +1,6 @@ import {useGetAllUsersQuery, useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js"; import {useGetAllRolesQuery} from "../../../Api/rolesApi.js"; -import {useMemo, useState} from "react"; +import {useEffect, useMemo, useState} from "react"; import {useDispatch} from "react-redux"; import {setOpenModalCreateUser, setSelectedUserToUpdate} from "../../../Redux/Slices/usersSlice.js"; @@ -18,6 +18,10 @@ const useAdminPage = () => { setSearchString, ] = useState(""); + useEffect(() => { + window.document.title = "Система обучения lectio - Панель администратора"; + }, []); + const { data: usersData = [], isLoading: usersIsLoading, diff --git a/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/CreateLessonModalForm.jsx b/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/CreateLessonModalForm.jsx new file mode 100644 index 0000000..d844707 --- /dev/null +++ b/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/CreateLessonModalForm.jsx @@ -0,0 +1,77 @@ +import useCreateLessonModalForm from "./useCreateLessonModalForm.js"; +import {Button, Form, Input, InputNumber, Modal, Upload} from "antd"; +import JoditEditor from "jodit-react"; +import {UploadOutlined} from "@ant-design/icons"; + + +const CreateLessonModalForm = () => { + const { + isModalOpen, + handleCancel, + form, + joditConfig, + editorRef, + } = useCreateLessonModalForm(); + + return ( + +
+ + + + + + + +
+ +
+
+ + + + + + + + + + + +
+
+ ) +}; + +export default CreateLessonModalForm; \ No newline at end of file diff --git a/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/useCreateLessonModalForm.js b/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/useCreateLessonModalForm.js new file mode 100644 index 0000000..1e9e37b --- /dev/null +++ b/web/src/Components/Pages/CourseDetailPage/Components/CreateLessonModalForm/useCreateLessonModalForm.js @@ -0,0 +1,95 @@ +import {useDispatch, useSelector} from "react-redux"; +import {setOpenModalCreateLesson} from "../../../../../Redux/Slices/lessonsSlice.js"; +import {Form, notification} from "antd"; +import {useMemo, useRef} from "react"; + + +const useCreateLessonModalForm = () => { + const dispatch = useDispatch(); + const { + openModalCreateLesson + } = useSelector((state) => state.lessons); + const [form] = Form.useForm(); + + const isModalOpen = openModalCreateLesson; + const editorRef = useRef(null); + + const handleCancel = () => { + dispatch(setOpenModalCreateLesson(false)); + }; + + const joditConfig = useMemo( + () => ({ + readonly: false, + height: 150, + toolbarAdaptive: false, + buttons: [ + "bold", "italic", "underline", "strikethrough", "|", + "superscript", "subscript", "|", + "ul", "ol", "outdent", "indent", "|", + "font", "fontsize", "brush", "paragraph", "|", + "align", "hr", "|", + "table", "link", "image", "video", "symbols", "|", + "undo", "redo", "cut", "copy", "paste", "selectall", "eraser", "|", + "find", "source", "fullsize", "print", "preview", + ], + autofocus: false, + preserveSelection: true, + askBeforePasteHTML: false, + askBeforePasteFromWord: false, + defaultActionOnPaste: "insert_clear_html", + spellcheck: true, + placeholder: "Введите результаты приёма...", + showCharsCounter: true, + showWordsCounter: true, + showXPathInStatusbar: false, + toolbarSticky: true, + toolbarButtonSize: "middle", + cleanHTML: { + removeEmptyElements: true, + replaceNBSP: false, + }, + hotkeys: { + "ctrl + shift + f": "find", + "ctrl + b": "bold", + "ctrl + i": "italic", + "ctrl + u": "underline", + }, + image: { + editSrc: true, + editTitle: true, + editAlt: true, + openOnDblClick: false, + }, + video: { + allowedSources: ["youtube", "vimeo"], + }, + uploader: { + insertImageAsBase64URI: true, + }, + paste: { + insertAsBase64: true, + mimeTypes: ["image/png", "image/jpeg", "image/gif"], + maxFileSize: 5 * 1024 * 1024, + error: () => { + notification.error({ + title: "Ошибка вставки", + description: "Файл слишком большой или неподдерживаемый формат.", + placement: "topRight", + }); + }, + }, + }), + [] + ); + + return { + isModalOpen, + handleCancel, + form, + joditConfig, + editorRef, + } +}; + +export default useCreateLessonModalForm; \ No newline at end of file diff --git a/web/src/Components/Pages/CourseDetailPage/CourseDetailPage.jsx b/web/src/Components/Pages/CourseDetailPage/CourseDetailPage.jsx new file mode 100644 index 0000000..2c04bf1 --- /dev/null +++ b/web/src/Components/Pages/CourseDetailPage/CourseDetailPage.jsx @@ -0,0 +1,73 @@ +import useCourseDetailPage from "./useCourseDetailPage.js"; +import {Button, Col, FloatButton, Result, Row, Tooltip, Typography} from "antd"; +import {ArrowLeftOutlined, BookOutlined, FormOutlined, PlusOutlined} from "@ant-design/icons"; +import {useNavigate, useParams} from "react-router-dom"; +import {ROLES} from "../../../Core/constants.js"; +import CONFIG from "../../../Core/сonfig.js"; +import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; +import CreateLessonModalForm from "./Components/CreateLessonModalForm/CreateLessonModalForm.jsx"; + + +const {Title} = Typography; + +const CourseDetailPage = () => { + const navigate = useNavigate(); + const {courseId} = useParams(); + const { + userData, + courseData, + isLoading, + isError, + handleCreateLesson, + } = useCourseDetailPage(courseId); + + if (isLoading) { + return ; + } + + if (isError) { + return ; + } + + return ( +
+ + + + + <BookOutlined/> Курс - {courseData.title} + + + + + + {[CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData.role.title) && ( + } + tooltip="Добавить элемент курса" + > + } + tooltip="Лекционный материал" + onClick={handleCreateLesson} + /> + } + tooltip="Задание" + /> + + )} +
+ ) +}; + +export default CourseDetailPage; \ No newline at end of file diff --git a/web/src/Components/Pages/CourseDetailPage/useCourseDetailPage.js b/web/src/Components/Pages/CourseDetailPage/useCourseDetailPage.js new file mode 100644 index 0000000..e5f5caa --- /dev/null +++ b/web/src/Components/Pages/CourseDetailPage/useCourseDetailPage.js @@ -0,0 +1,44 @@ +import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js"; +import {useGetCourseByIdQuery} from "../../../Api/coursesApi.js"; +import {useEffect} from "react"; +import {useDispatch} from "react-redux"; +import {setOpenModalCreateLesson} from "../../../Redux/Slices/lessonsSlice.js"; + + +const useCourseDetailPage = (courseId) => { + const dispatch = useDispatch(); + + const { + data: userData, + isLoading: isUserLoading, + isError: isUserError, + } = useGetAuthenticatedUserDataQuery(undefined, { + pollingInterval: 60000, + }); + + const { + data: courseData, + isLoading: isCourseLoading, + isError: isCourseError, + } = useGetCourseByIdQuery(courseId, { + pollingInterval: 60000, + }); + + useEffect(() => { + window.document.title = `Система обучения lectio - Курс: ${courseData?.title}`; + }, [courseData]); + + const handleCreateLesson = () => { + dispatch(setOpenModalCreateLesson(true)) + }; + + return { + userData, + courseData, + isLoading: isUserLoading || isCourseLoading, + isError: isUserError || isCourseError, + handleCreateLesson, + } +}; + +export default useCourseDetailPage; \ No newline at end of file diff --git a/web/src/Components/Pages/Courses/Components/CreateCourseModalForm/CreateCourseModalForm.jsx b/web/src/Components/Pages/CoursesPage/Components/CreateCourseModalForm/CreateCourseModalForm.jsx similarity index 100% rename from web/src/Components/Pages/Courses/Components/CreateCourseModalForm/CreateCourseModalForm.jsx rename to web/src/Components/Pages/CoursesPage/Components/CreateCourseModalForm/CreateCourseModalForm.jsx diff --git a/web/src/Components/Pages/Courses/Components/CreateCourseModalForm/useCreateCourseModalForm.js b/web/src/Components/Pages/CoursesPage/Components/CreateCourseModalForm/useCreateCourseModalForm.js similarity index 100% rename from web/src/Components/Pages/Courses/Components/CreateCourseModalForm/useCreateCourseModalForm.js rename to web/src/Components/Pages/CoursesPage/Components/CreateCourseModalForm/useCreateCourseModalForm.js diff --git a/web/src/Components/Pages/Courses/Components/UpdateCourseModalForm/UpdateCourseModalForm.jsx b/web/src/Components/Pages/CoursesPage/Components/UpdateCourseModalForm/UpdateCourseModalForm.jsx similarity index 100% rename from web/src/Components/Pages/Courses/Components/UpdateCourseModalForm/UpdateCourseModalForm.jsx rename to web/src/Components/Pages/CoursesPage/Components/UpdateCourseModalForm/UpdateCourseModalForm.jsx diff --git a/web/src/Components/Pages/Courses/Components/UpdateCourseModalForm/useUpdateCourseModalForm.js b/web/src/Components/Pages/CoursesPage/Components/UpdateCourseModalForm/useUpdateCourseModalForm.js similarity index 97% rename from web/src/Components/Pages/Courses/Components/UpdateCourseModalForm/useUpdateCourseModalForm.js rename to web/src/Components/Pages/CoursesPage/Components/UpdateCourseModalForm/useUpdateCourseModalForm.js index 080a051..b70e375 100644 --- a/web/src/Components/Pages/Courses/Components/UpdateCourseModalForm/useUpdateCourseModalForm.js +++ b/web/src/Components/Pages/CoursesPage/Components/UpdateCourseModalForm/useUpdateCourseModalForm.js @@ -88,14 +88,14 @@ const useUpdateCourseModalForm = () => { }).unwrap(); notification.success({ - message: "Успех", + title: "Успех", description: "Курс успешно обновлён!", }); handleCancel(); } catch (error) { notification.error({ - message: "Ошибка", + title: "Ошибка", description: error?.data?.detail || "Не удалось обновить курс", }); } diff --git a/web/src/Components/Pages/Courses/CoursesPage.jsx b/web/src/Components/Pages/CoursesPage/CoursesPage.jsx similarity index 97% rename from web/src/Components/Pages/Courses/CoursesPage.jsx rename to web/src/Components/Pages/CoursesPage/CoursesPage.jsx index 594c579..2af5982 100644 --- a/web/src/Components/Pages/Courses/CoursesPage.jsx +++ b/web/src/Components/Pages/CoursesPage/CoursesPage.jsx @@ -19,10 +19,12 @@ import useCoursesPage from "./useCoursesPage.js"; import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import CreateCourseModalForm from "./Components/CreateCourseModalForm/CreateCourseModalForm.jsx"; import UpdateCourseModalForm from "./Components/UpdateCourseModalForm/UpdateCourseModalForm.jsx"; +import {useNavigate} from "react-router-dom"; const {Title, Text} = Typography; const CoursesPage = () => { + const navigate = useNavigate(); const { courses, isLoading, @@ -69,6 +71,8 @@ const CoursesPage = () => { {courses.map((course) => ( navigate(`/courses/${course.id}`)} + hoverable cover={
{ @@ -25,6 +26,10 @@ const useCoursesPage = () => { dispatch(setSelectedCourseToUpdate(course)); }; + useEffect(() => { + window.document.title = "Система обучения lectio - Курсы"; + }, []); + return { courses, isLoading: isCoursesLoading || isUserLoading || isLoading, diff --git a/web/src/Components/Pages/LoginPage/useLoginPage.js b/web/src/Components/Pages/LoginPage/useLoginPage.js index 1549f6e..71914a5 100644 --- a/web/src/Components/Pages/LoginPage/useLoginPage.js +++ b/web/src/Components/Pages/LoginPage/useLoginPage.js @@ -22,7 +22,7 @@ const useLoginPage = () => { hasRedirected.current = true; navigate("/"); } - document.title = "Аутентификация"; + document.title = "Система обучения lectio - Аутентификация"; }, [user, userData, isLoading, navigate]); const onFinish = async (loginData) => { diff --git a/web/src/Components/Pages/ProfilePage/useProfilePage.js b/web/src/Components/Pages/ProfilePage/useProfilePage.js index 9fe83fa..c13c514 100644 --- a/web/src/Components/Pages/ProfilePage/useProfilePage.js +++ b/web/src/Components/Pages/ProfilePage/useProfilePage.js @@ -13,7 +13,7 @@ const useProfilePage = () => { const [passwordForm] = Form.useForm(); useEffect(() => { - window.document.title = "Профиль"; + window.document.title = "Система обучения lectio - Профиль"; }, []); const { diff --git a/web/src/Redux/Slices/lessonsSlice.js b/web/src/Redux/Slices/lessonsSlice.js new file mode 100644 index 0000000..1faab7c --- /dev/null +++ b/web/src/Redux/Slices/lessonsSlice.js @@ -0,0 +1,24 @@ +import {createSlice} from "@reduxjs/toolkit"; + + +const initialState = { + selectedLessonToUpdate: null, + openModalCreateLesson: false, +}; + +const lessonSlice = createSlice({ + name: "lessons", + initialState, + reducers: { + setSelectedLessonToUpdate(state, action) { + state.selectedLessonToUpdate = action.payload; + }, + setOpenModalCreateLesson(state, action) { + state.openModalCreateLesson = action.payload; + }, + }, +}); + +export const {setSelectedLessonToUpdate, setOpenModalCreateLesson} = lessonSlice.actions; + +export default lessonSlice.reducer; \ No newline at end of file diff --git a/web/src/Redux/store.js b/web/src/Redux/store.js index 068eec4..d8a21ad 100644 --- a/web/src/Redux/store.js +++ b/web/src/Redux/store.js @@ -2,11 +2,13 @@ import {configureStore} from "@reduxjs/toolkit"; import authReducer from "./Slices/authSlice.js"; import usersReducer from "./Slices/usersSlice.js"; import coursesReducer from "./Slices/coursesSlice.js"; +import lessonReducer from "./Slices/lessonsSlice.js"; import {authApi} from "../Api/authApi.js"; import {usersApi} from "../Api/usersApi.js"; import {rolesApi} from "../Api/rolesApi.js"; import {statusesApi} from "../Api/statusesApi.js"; import {coursesApi} from "../Api/coursesApi.js"; +import {lessonsApi} from "../Api/lessonsApi.js"; export const store = configureStore({ reducer: { @@ -21,7 +23,10 @@ export const store = configureStore({ [statusesApi.reducerPath]: statusesApi.reducer, courses: coursesReducer, - [coursesApi.reducerPath]: coursesApi.reducer + [coursesApi.reducerPath]: coursesApi.reducer, + + lessons: lessonReducer, + [lessonsApi.reducerPath]: lessonsApi.reducer }, middleware: (getDefaultMiddleware) => ( getDefaultMiddleware().concat( @@ -29,7 +34,8 @@ export const store = configureStore({ usersApi.middleware, rolesApi.middleware, statusesApi.middleware, - coursesApi.middleware + coursesApi.middleware, + lessonsApi.middleware ) ), });