заготовка под создание лекции

This commit is contained in:
Андрей Дувакин 2025-11-28 22:22:26 +05:00
parent 9fe0d1e3e9
commit 87a14f0eb1
29 changed files with 711 additions and 9 deletions

View File

@ -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

View File

@ -30,6 +30,21 @@ async def get_all_courses(
return await courses_service.get_all() 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( @courses_router.post(
'/', '/',
response_model=Optional[CourseCreated], response_model=Optional[CourseCreated],

View File

@ -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

View File

@ -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

View File

@ -22,6 +22,13 @@ class CoursesService:
return response 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 async def create(self, course: CourseCreate) -> CourseCreated: # ← возвращаем CourseCreated
course_model = Course( course_model = Course(
title=course.title, title=course.title,

View File

@ -48,6 +48,7 @@ def require_admin(user: User = Depends(require_auth_user)):
def require_teacher(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]: if user.role.title not in [UserRoles.TEACHER, Settings().root_role_name]:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Ошибка доступа') raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Ошибка доступа')

View File

@ -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)

View File

@ -3,6 +3,7 @@ from starlette.middleware.cors import CORSMiddleware
from app.controllers.auth_router import auth_router from app.controllers.auth_router import auth_router
from app.controllers.courses_router import courses_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.register_router import register_router
from app.controllers.roles_router import roles_router from app.controllers.roles_router import roles_router
from app.controllers.statuses_router import statuses_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(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(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(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(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(statuses_router, prefix=f'{settings.prefix}/statuses', tags=['statuses'])

20
web/package-lock.json generated
View File

@ -12,6 +12,7 @@
"@reduxjs/toolkit": "^2.11.0", "@reduxjs/toolkit": "^2.11.0",
"antd": "^6.0.0", "antd": "^6.0.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"jodit-react": "^5.2.38",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
@ -3128,6 +3129,25 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@ -14,6 +14,7 @@
"@reduxjs/toolkit": "^2.11.0", "@reduxjs/toolkit": "^2.11.0",
"antd": "^6.0.0", "antd": "^6.0.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"jodit-react": "^5.2.38",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",

View File

@ -14,6 +14,13 @@ export const coursesApi = createApi({
}), }),
providesTags: ['course'], providesTags: ['course'],
}), }),
getCourseById: builder.query({
query: (courseId) => ({
url: `/courses/${courseId}/`,
method: "GET",
}),
providesTags: ['course'],
}),
createCourse: builder.mutation({ createCourse: builder.mutation({
query: (data) => ({ query: (data) => ({
url: "/courses/", url: "/courses/",
@ -65,6 +72,7 @@ export const coursesApi = createApi({
export const { export const {
useGetAllCoursesQuery, useGetAllCoursesQuery,
useGetCourseByIdQuery,
useCreateCourseMutation, useCreateCourseMutation,
useUpdateCourseMutation, useUpdateCourseMutation,
useGetCourseTeachersQuery, useGetCourseTeachersQuery,

56
web/src/Api/lessonsApi.js Normal file
View File

@ -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;

View File

@ -2,10 +2,11 @@ import {Routes, Route, Navigate} from "react-router-dom";
import PrivateRoute from "./PrivateRoute.jsx"; import PrivateRoute from "./PrivateRoute.jsx";
import AdminRoute from "./AdminRoute.jsx"; import AdminRoute from "./AdminRoute.jsx";
import LoginPage from "../Components/Pages/LoginPage/LoginPage.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 MainLayout from "../Components/Layouts/MainLayout.jsx";
import ProfilePage from "../Components/Pages/ProfilePage/ProfilePage.jsx"; import ProfilePage from "../Components/Pages/ProfilePage/ProfilePage.jsx";
import AdminPage from "../Components/Pages/AdminPage/AdminPage.jsx"; import AdminPage from "../Components/Pages/AdminPage/AdminPage.jsx";
import CourseDetailPage from "../Components/Pages/CourseDetailPage/CourseDetailPage.jsx";
const AppRouter = () => ( const AppRouter = () => (
@ -16,6 +17,7 @@ const AppRouter = () => (
<Route element={<MainLayout/>}> <Route element={<MainLayout/>}>
<Route path={"/courses"} element={<CoursesPage/>}/> <Route path={"/courses"} element={<CoursesPage/>}/>
<Route path={"/profile"} element={<ProfilePage/>}/> <Route path={"/profile"} element={<ProfilePage/>}/>
<Route path="/courses/:courseId" element={<CourseDetailPage />} />
<Route path={"*"} element={<Navigate to={"/courses"}/>}/> <Route path={"*"} element={<Navigate to={"/courses"}/>}/>
</Route> </Route>
</Route> </Route>

View File

@ -1,6 +1,6 @@
import useMainLayout from "./useMainLayout.js"; import useMainLayout from "./useMainLayout.js";
import {Layout, Menu} from "antd"; 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 LoadingIndicator from "../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import {Outlet} from "react-router-dom"; import {Outlet} from "react-router-dom";
import {BookOutlined, ControlOutlined, LogoutOutlined, UserOutlined} from "@ant-design/icons"; import {BookOutlined, ControlOutlined, LogoutOutlined, UserOutlined} from "@ant-design/icons";

View File

@ -1,6 +1,6 @@
import {useGetAllUsersQuery, useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js"; import {useGetAllUsersQuery, useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
import {useGetAllRolesQuery} from "../../../Api/rolesApi.js"; import {useGetAllRolesQuery} from "../../../Api/rolesApi.js";
import {useMemo, useState} from "react"; import {useEffect, useMemo, useState} from "react";
import {useDispatch} from "react-redux"; import {useDispatch} from "react-redux";
import {setOpenModalCreateUser, setSelectedUserToUpdate} from "../../../Redux/Slices/usersSlice.js"; import {setOpenModalCreateUser, setSelectedUserToUpdate} from "../../../Redux/Slices/usersSlice.js";
@ -18,6 +18,10 @@ const useAdminPage = () => {
setSearchString, setSearchString,
] = useState(""); ] = useState("");
useEffect(() => {
window.document.title = "Система обучения lectio - Панель администратора";
}, []);
const { const {
data: usersData = [], data: usersData = [],
isLoading: usersIsLoading, isLoading: usersIsLoading,

View File

@ -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 (
<Modal
open={isModalOpen}
onCancel={handleCancel}
footer={null}
title={"Создание лекционного материала"}
style={{
minWidth: "70%",
minHeight: "80%",
}}
>
<Form
name={"lesson"}
form={form}
layout={"vertical"}
>
<Form.Item
name="title"
label="Название"
rules={[{required: true, message: "Пожалуйста, введите название"}]}
>
<Input/>
</Form.Item>
<Form.Item
name="description"
label="Описание"
>
<Input/>
</Form.Item>
<Form.Item
name="text"
label="Текстовый материал"
>
<div className="jodit-container">
<JoditEditor
ref={editorRef}
config={joditConfig}
/>
</div>
</Form.Item>
<Form.Item
name="number"
label="Порядковый номер отображения"
>
<InputNumber min={1} defaultValue={1}/>
</Form.Item>
<Form.Item name="files" label="Прикрепить файлы">
<Upload
multiple
>
<Button icon={<UploadOutlined/>}>Выбрать файлы</Button>
</Upload>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">Сохранить</Button>
</Form.Item>
</Form>
</Modal>
)
};
export default CreateLessonModalForm;

View File

@ -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;

View File

@ -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 <LoadingIndicator/>;
}
if (isError) {
return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке курса"/>;
}
return (
<div style={{minHeight: "100vh"}}>
<Row justify="space-between" align="middle" style={{marginBottom: 24}}>
<Col>
<Button
icon={<ArrowLeftOutlined/>}
onClick={() => navigate(-1)}
style={{marginBottom: 16}}
>
Назад
</Button>
<Title level={2} style={{margin: 0}}>
<BookOutlined/> Курс - {courseData.title}
</Title>
</Col>
</Row>
<CreateLessonModalForm/>
{[CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData.role.title) && (
<FloatButton.Group
placement={"left"}
trigger="hover"
type="primary"
icon={<PlusOutlined/>}
tooltip="Добавить элемент курса"
>
<FloatButton
icon={<PlusOutlined/>}
tooltip="Лекционный материал"
onClick={handleCreateLesson}
/>
<FloatButton
icon={<FormOutlined/>}
tooltip="Задание"
/>
</FloatButton.Group>
)}
</div>
)
};
export default CourseDetailPage;

View File

@ -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;

View File

@ -88,14 +88,14 @@ const useUpdateCourseModalForm = () => {
}).unwrap(); }).unwrap();
notification.success({ notification.success({
message: "Успех", title: "Успех",
description: "Курс успешно обновлён!", description: "Курс успешно обновлён!",
}); });
handleCancel(); handleCancel();
} catch (error) { } catch (error) {
notification.error({ notification.error({
message: "Ошибка", title: "Ошибка",
description: error?.data?.detail || "Не удалось обновить курс", description: error?.data?.detail || "Не удалось обновить курс",
}); });
} }

View File

@ -19,10 +19,12 @@ import useCoursesPage from "./useCoursesPage.js";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import CreateCourseModalForm from "./Components/CreateCourseModalForm/CreateCourseModalForm.jsx"; import CreateCourseModalForm from "./Components/CreateCourseModalForm/CreateCourseModalForm.jsx";
import UpdateCourseModalForm from "./Components/UpdateCourseModalForm/UpdateCourseModalForm.jsx"; import UpdateCourseModalForm from "./Components/UpdateCourseModalForm/UpdateCourseModalForm.jsx";
import {useNavigate} from "react-router-dom";
const {Title, Text} = Typography; const {Title, Text} = Typography;
const CoursesPage = () => { const CoursesPage = () => {
const navigate = useNavigate();
const { const {
courses, courses,
isLoading, isLoading,
@ -69,6 +71,8 @@ const CoursesPage = () => {
{courses.map((course) => ( {courses.map((course) => (
<Col xs={24} sm={12} lg={8} xl={6} key={course.id}> <Col xs={24} sm={12} lg={8} xl={6} key={course.id}>
<Card <Card
onClick={() => navigate(`/courses/${course.id}`)}
hoverable hoverable
cover={ cover={
<div <div

View File

@ -4,6 +4,7 @@ import CONFIG from "../../../Core/сonfig.js";
import {ROLES} from "../../../Core/constants.js"; import {ROLES} from "../../../Core/constants.js";
import {useDispatch} from "react-redux"; import {useDispatch} from "react-redux";
import {setOpenCreateCourseModal, setSelectedCourseToUpdate} from "../../../Redux/Slices/coursesSlice.js"; import {setOpenCreateCourseModal, setSelectedCourseToUpdate} from "../../../Redux/Slices/coursesSlice.js";
import {useEffect} from "react";
const useCoursesPage = () => { const useCoursesPage = () => {
@ -25,6 +26,10 @@ const useCoursesPage = () => {
dispatch(setSelectedCourseToUpdate(course)); dispatch(setSelectedCourseToUpdate(course));
}; };
useEffect(() => {
window.document.title = "Система обучения lectio - Курсы";
}, []);
return { return {
courses, courses,
isLoading: isCoursesLoading || isUserLoading || isLoading, isLoading: isCoursesLoading || isUserLoading || isLoading,

View File

@ -22,7 +22,7 @@ const useLoginPage = () => {
hasRedirected.current = true; hasRedirected.current = true;
navigate("/"); navigate("/");
} }
document.title = "Аутентификация"; document.title = "Система обучения lectio - Аутентификация";
}, [user, userData, isLoading, navigate]); }, [user, userData, isLoading, navigate]);
const onFinish = async (loginData) => { const onFinish = async (loginData) => {

View File

@ -13,7 +13,7 @@ const useProfilePage = () => {
const [passwordForm] = Form.useForm(); const [passwordForm] = Form.useForm();
useEffect(() => { useEffect(() => {
window.document.title = "Профиль"; window.document.title = "Система обучения lectio - Профиль";
}, []); }, []);
const { const {

View File

@ -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;

View File

@ -2,11 +2,13 @@ import {configureStore} from "@reduxjs/toolkit";
import authReducer from "./Slices/authSlice.js"; import authReducer from "./Slices/authSlice.js";
import usersReducer from "./Slices/usersSlice.js"; import usersReducer from "./Slices/usersSlice.js";
import coursesReducer from "./Slices/coursesSlice.js"; import coursesReducer from "./Slices/coursesSlice.js";
import lessonReducer from "./Slices/lessonsSlice.js";
import {authApi} from "../Api/authApi.js"; import {authApi} from "../Api/authApi.js";
import {usersApi} from "../Api/usersApi.js"; import {usersApi} from "../Api/usersApi.js";
import {rolesApi} from "../Api/rolesApi.js"; import {rolesApi} from "../Api/rolesApi.js";
import {statusesApi} from "../Api/statusesApi.js"; import {statusesApi} from "../Api/statusesApi.js";
import {coursesApi} from "../Api/coursesApi.js"; import {coursesApi} from "../Api/coursesApi.js";
import {lessonsApi} from "../Api/lessonsApi.js";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
@ -21,7 +23,10 @@ export const store = configureStore({
[statusesApi.reducerPath]: statusesApi.reducer, [statusesApi.reducerPath]: statusesApi.reducer,
courses: coursesReducer, courses: coursesReducer,
[coursesApi.reducerPath]: coursesApi.reducer [coursesApi.reducerPath]: coursesApi.reducer,
lessons: lessonReducer,
[lessonsApi.reducerPath]: lessonsApi.reducer
}, },
middleware: (getDefaultMiddleware) => ( middleware: (getDefaultMiddleware) => (
getDefaultMiddleware().concat( getDefaultMiddleware().concat(
@ -29,7 +34,8 @@ export const store = configureStore({
usersApi.middleware, usersApi.middleware,
rolesApi.middleware, rolesApi.middleware,
statusesApi.middleware, statusesApi.middleware,
coursesApi.middleware coursesApi.middleware,
lessonsApi.middleware
) )
), ),
}); });