diff --git a/package-lock.json b/package-lock.json index 82c172c..364ee9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.2.0", "license": "MIT", "dependencies": { + "@chakra-ui/icons": "^2.1.1", "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", @@ -24,6 +25,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-helmet": "^6.1.0", + "react-hook-form": "^7.51.2", "react-redux": "^9.1.0", "react-router-dom": "^6.22.1", "redux": "^5.0.1", @@ -2213,6 +2215,18 @@ "react": ">=18" } }, + "node_modules/@chakra-ui/icons": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/icons/-/icons-2.1.1.tgz", + "integrity": "sha512-3p30hdo4LlRZTT5CwoAJq3G9fHI0wDc0pBaMHj4SUn0yomO+RcDRlzhdXqdr5cVnzax44sqXJVnf3oQG0eI+4g==", + "dependencies": { + "@chakra-ui/icon": "3.2.0" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, "node_modules/@chakra-ui/image": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@chakra-ui/image/-/image-2.1.0.tgz", @@ -8546,6 +8560,21 @@ "react": ">=16.3.0" } }, + "node_modules/react-hook-form": { + "version": "7.51.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.2.tgz", + "integrity": "sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-i18next": { "version": "11.8.5", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.8.5.tgz", diff --git a/package.json b/package.json index fc3daf7..4d87616 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@ijl/cli": "^5.0.3" }, "dependencies": { + "@chakra-ui/icons": "^2.1.1", "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", @@ -35,6 +36,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-helmet": "^6.1.0", + "react-hook-form": "^7.51.2", "react-redux": "^9.1.0", "react-router-dom": "^6.22.1", "redux": "^5.0.1", diff --git a/src/app.tsx b/src/app.tsx index 40c795c..ebe2ca9 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -4,6 +4,7 @@ import { Global, css } from '@emotion/react' import { BrowserRouter } from 'react-router-dom'; import ruLocale from 'dayjs/locale/ru'; import dayjs from 'dayjs'; +import { ChakraProvider } from '@chakra-ui/react' import { Dashboard } from './dashboard'; @@ -11,72 +12,74 @@ dayjs.locale('ru', ruLocale); const App = ({ store }) => { return( - - - - Журнал - - + + + + Журнал + + - - + @font-face { + font-family: 'KiyosunaSans'; + src: url('${__webpack_public_path__ + '/remote-assets/KiyosunaSans/KiyosunaSans-B.otf'}'); + font-weight: normal; + font-style: normal; + } + @font-face { + font-family: 'KiyosunaSans'; + src: url('${__webpack_public_path__ + '/remote-assets/KiyosunaSans/KiyosunaSans-L.otf'}'); + font-weight: lighter; + font-style: normal; + } + @font-face { + font-family: 'RFKrabuler'; + src: url('${__webpack_public_path__ + '/remote-assets/RF-Krabuler/WEB/RFKrabuler-Regular.eot'}'); + src: + url('${__webpack_public_path__ + '/remote-assets/RF-Krabuler/WEB/RFKrabuler-Regular.woff2'}') format('woff2'), + url('${__webpack_public_path__ + '/remote-assets/RF-Krabuler/WEB/RFKrabuler-Regular.woff'}') format('woff'), + url('${__webpack_public_path__ + '/remote-assets/RF-Krabuler/TTF/RF-Krabuler-Regular.ttf'}') format('truetype'); + font-weight: normal; + font-style: normal; + } + `} + /> + + + ) } diff --git a/src/dashboard.tsx b/src/dashboard.tsx index e5631d8..1927709 100644 --- a/src/dashboard.tsx +++ b/src/dashboard.tsx @@ -1,34 +1,82 @@ -import React, { useEffect, Suspense } from "react"; -import { Routes, Route, useNavigate } from "react-router-dom"; -import { Provider } from "react-redux"; +import React, { useEffect, Suspense } from 'react' +import { Routes, Route, useNavigate } from 'react-router-dom' +import { Provider } from 'react-redux' import { CourseListPage, LessonDetailsPage, LessonListPage, - UserPage + UserPage, } from './pages' -import { getNavigationsValue } from "@ijl/cli"; +import { getNavigationsValue } from '@ijl/cli' +import { Box, Container, Spinner, VStack } from '@chakra-ui/react' const Redirect = ({ path }) => { - const navigate = useNavigate(); + const navigate = useNavigate() useEffect(() => { - navigate(path); - }, []); + navigate(path) + }, []) - return null; -}; + return null +} -const Wrapper = ({ children }) => {children} +const Wrapper = ({ children }) => ( + + + + + + + } + > + {children} + +) export const Dashboard = ({ store }) => ( - } /> - } /> - } /> - } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> -); +) diff --git a/src/pages/course-list.tsx b/src/pages/course-list.tsx index ad94b89..6b0b1fa 100644 --- a/src/pages/course-list.tsx +++ b/src/pages/course-list.tsx @@ -1,8 +1,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' import dayjs from 'dayjs' -import { Link } from 'react-router-dom' -import { getConfigValue, getNavigationsValue } from '@ijl/cli' - +import { Link as ConnectedLink } from 'react-router-dom' +import { getNavigationsValue } from '@ijl/cli' import { Box, CardHeader, @@ -12,155 +11,299 @@ import { Stack, StackDivider, Button, - UnorderedList, + Card, Heading, Tooltip, + Spinner, + Container, + VStack, + Link, + Input, + CloseButton, + FormControl, + FormLabel, + FormHelperText, + Center, + FormErrorMessage, + useToast, } from '@chakra-ui/react' +import { useForm, Controller } from 'react-hook-form' -import { - ArrowImg, - IconButton, - InputElement, - InputLabel, - InputWrapper, - StartWrapper, - Papper, - ErrorSpan, - Cross, - AddButton, - MainWrapper, - StyledCard, -} from './style' +import { ErrorSpan, MainWrapper } from './style' -import { linkOpen, moreDetails } from '../assets' - -import arrow from '../assets/36-arrow-right.svg' -import { keycloak } from '../__data__/kc' import { useAppSelector } from '../__data__/store' import { api } from '../__data__/api/api' import { isTeacher } from '../utils/user' +import { AddIcon, ArrowDownIcon, ArrowUpIcon, LinkIcon } from '@chakra-ui/icons' + +interface NewCourseForm { + startDt: string + name: string +} const CoursesList = () => { + const toast = useToast() const user = useAppSelector((s) => s.user) - const { data, isLoading, error } = api.useCoursesListQuery() - const [createCourse, crcQuery] = api.useCreateUpdateCourseMutation() - const [value, setValue] = useState('') + const { data, isLoading } = api.useCoursesListQuery() + const [createUpdateCourse, crucQuery] = api.useCreateUpdateCourseMutation() const [showForm, setShowForm] = useState(false) - const [showDetails, setShowDetails] = useState(false) + const [courseDetailsOpenedId, setCourseDetailsOpenedId] = useState< + string | null + >(null) + const toastRef = useRef(null) - const handleChange = useCallback( - (event) => { - setValue(event.target.value.toUpperCase()) + const { + control, + handleSubmit, + reset, + formState: { errors }, + getValues, + } = useForm({ + defaultValues: { + startDt: '', + name: '', }, - [setValue], - ) - const handleSubmit = useCallback( - (event) => { - event.preventDefault() - createCourse({ name: value }) - }, - [value], - ) + }) + + const onSubmit = ({ startDt, name }) => { + toastRef.current = toast({ + title: 'Отправляем', + status: 'loading', + duration: 9000, + }) + createUpdateCourse({ name, startDt }) + } useEffect(() => { - if (crcQuery.isSuccess) { - setValue('') + if (crucQuery.isSuccess) { + const values = getValues() + if (toastRef.current) { + toast.update(toastRef.current, { + title: 'Курс создан.', + description: `Курс ${values.name} успешно создан`, + status: 'success', + duration: 9000, + isClosable: true, + }) + } + reset() } - }, [crcQuery.isSuccess]) + }, [crucQuery.isSuccess]) + + if (isLoading) { + return ( + +
+ +
+
+ ) + } return ( - - - {isTeacher(user) && ( - <> - {showForm ? ( - - setShowForm(false)}> - X - -
- - - Название новой лекции: - - + {isTeacher(user) && ( + + {showForm ? ( + + + + Создание курса + + setShowForm(false)} /> + + + + + ( + + Дата начала + + {errors.startDt ? ( + + {errors.startDt?.message} + + ) : ( + + Укажите дату начала курса + + )} + + )} /> - - - - - {crcQuery?.error && ( - {(crcQuery?.error as any).error} + ( + + Название новой лекции: + + {errors.name && ( + + {errors.name.message} + + )} + + )} + /> + + + + + + + {crucQuery?.error && ( + {(crucQuery?.error as any).error} )} -
+ + + ) : ( + + + + )} + + )} + + {data?.body?.map((c) => ( + + courseDetailsOpenedId === c._id + ? setCourseDetailsOpenedId(null) + : setCourseDetailsOpenedId(c._id) + } + /> + ))} + + + ) +} + +const CourseCard = ({ course, isOpened, openDetails }) => { + const [getLessonList, lessonList] = api.useLazyLessonListQuery() + useEffect(() => { + if (isOpened) { + getLessonList(course._id, true) + } + }, [isOpened]) + const user = useAppSelector((s) => s.user) + + return ( + + + + {course.name} + + + {isOpened && ( + + } spacing="8px"> + + {`Дата начала курса - ${dayjs(course.startDt).format('DD MMMM YYYYг.')}`} + + + Количество занятий - {course.lessons.length} + + {lessonList.isFetching ? ( + ) : ( - setShowForm(true)}>Добавить - )} - - )} - - {data?.body?.map((course) => ( - - - - {course.name} + <> + + Список занятий: - - {showDetails && ( - - } spacing="8px"> - - {`Дата начала курса - ${dayjs(course.startDt).format('DD MMMM YYYYг.')}`} - - - Количество занятий - {course.lessons.length} - - - - )} - - - - - - - - - - - - ))} - -
-
+ {lesson.name} + + ))} + + + )} + + + )} + + + + + + + + + + + ) } diff --git a/src/pages/lesson-details.tsx b/src/pages/lesson-details.tsx index 34c5d35..c88f250 100644 --- a/src/pages/lesson-details.tsx +++ b/src/pages/lesson-details.tsx @@ -84,18 +84,18 @@ const LessonDetail = () => { - + Журнал - + - Курс - + @@ -103,8 +103,10 @@ const LessonDetail = () => { -

Тема занятия - {accessCode?.body?.lesson?.name}

- +

+ Тема занятия - {accessCode?.body?.lesson?.name} +

+ {dayjs(accessCode?.body?.lesson?.date).format('DD MMMM YYYYг.')}{' '} Отмечено - {accessCode?.body?.lesson?.students?.length}{' '} {AllStudents.isSuccess ? `/ ${AllStudents?.data?.body?.length}` : ''}{' '} diff --git a/src/pages/lesson-list.tsx b/src/pages/lesson-list.tsx index f6e8562..a14eaab 100644 --- a/src/pages/lesson-list.tsx +++ b/src/pages/lesson-list.tsx @@ -63,7 +63,7 @@ const LessonList = () => { - Журнал + Журнал diff --git a/stubs/api/index.js b/stubs/api/index.js index 40f2b46..0e91787 100644 --- a/stubs/api/index.js +++ b/stubs/api/index.js @@ -2,6 +2,13 @@ const router = require('express').Router() const fs = require('node:fs') const path = require('node:path') +const timer = + (time = 1000) => + (_req, _res, next) => + setTimeout(next, time) + +router.use(timer()) + router.get('/course/list', (req, res) => { res.send(require('../mocks/courses/list/success.json')) }) @@ -23,7 +30,9 @@ router.post('/lesson', (req, res) => { }) router.post('/lesson/access-code', (req, res) => { - const answer = fs.readFileSync(path.resolve(__dirname, '../mocks/lessons/access-code/create/success.json')) + const answer = fs.readFileSync( + path.resolve(__dirname, '../mocks/lessons/access-code/create/success.json'), + ) // res.send(require('../mocks/lessons/access-code/create/success.json')) res.send(answer) }) diff --git a/stubs/mocks/courses/list/success.json b/stubs/mocks/courses/list/success.json index e6a6fa3..3f761e7 100644 --- a/stubs/mocks/courses/list/success.json +++ b/stubs/mocks/courses/list/success.json @@ -23,6 +23,29 @@ "startDt": "2024-03-02T15:37:05.907Z", "created": "2024-03-02T15:37:05.908Z", "__v": 2 + }, + { + "_id": "65e73c21ced789d2f679128z", + "name": "KFU-24-2", + "teachers": [], + "lessons": [ + "65e2e5fbec37fec650f28489", + "65e301c4ec37fec650f2aafe", + "65e78bebced789d2f6791315", + "65e78c0fced789d2f679131b" + ], + "creator": { + "sub": "f62905b1-e223-40ca-910f-c8d84c6137c1", + "email_verified": true, + "name": "Александр Примаков", + "preferred_username": "primakov", + "given_name": "Александр", + "family_name": "Примаков", + "email": "primakovpro@gmail.com" + }, + "startDt": "2024-03-02T15:37:05.907Z", + "created": "2024-03-02T15:37:05.908Z", + "__v": 2 } ] }