fix typo
polling interval from config qrcode as link courses list page journal.pl as name fix package-lock lesson missed students merge students list reset version 1.0.0 styled page of visits rename students fake students, delete comments Обновил Readme Squashed commit of the following: commit ebc511e36b84c077f7bc029540ead1e3c58c49ab Author: Alexei <adu864222@gmail.com> Date: Tue Mar 19 00:23:30 2024 +1000 fake students, delete comments commit 018582d16c581663103e5fded99cc10fca400532 Author: Alexei <adu864222@gmail.com> Date: Mon Mar 18 23:13:01 2024 +1000 rename students commit e53922d7fd89cf371c90e167c6486b36b0789036 Author: Alexei <adu864222@gmail.com> Date: Sun Mar 17 00:45:11 2024 +1000 styled page of visits Обновил Readme Squashed commit of the following: commit ebc511e36b84c077f7bc029540ead1e3c58c49ab Author: Alexei <adu864222@gmail.com> Date: Tue Mar 19 00:23:30 2024 +1000 fake students, delete comments commit 018582d16c581663103e5fded99cc10fca400532 Author: Alexei <adu864222@gmail.com> Date: Mon Mar 18 23:13:01 2024 +1000 rename students commit e53922d7fd89cf371c90e167c6486b36b0789036 Author: Alexei <adu864222@gmail.com> Date: Sun Mar 17 00:45:11 2024 +1000 styled page of visits JRL-51 breadcrumbs + fonts 1.1.0 1.2.0 correct width h1 on page of visits students styled card of course
This commit is contained in:
@@ -2,7 +2,7 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
import { getConfigValue } from "@ijl/cli";
|
||||
|
||||
import { keycloak } from "../kc";
|
||||
import { AccessCode, BaseResponse, Lesson, UserData } from "../model";
|
||||
import { AccessCode, BaseResponse, Course, Lesson, User, UserData } from "../model";
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: "auth",
|
||||
@@ -11,9 +11,7 @@ export const api = createApi({
|
||||
fetchFn: async (input: RequestInfo | URL, init?: RequestInit | undefined) => {
|
||||
const response = await fetch(input, init);
|
||||
|
||||
if (response.status === 403) {
|
||||
keycloak.login();
|
||||
}
|
||||
if (response.status === 403) keycloak.login()
|
||||
|
||||
return response;
|
||||
},
|
||||
@@ -24,23 +22,47 @@ export const api = createApi({
|
||||
headers.set('Authorization', `Bearer ${keycloak.token}`)
|
||||
}
|
||||
}),
|
||||
tagTypes: ['Lesson'],
|
||||
tagTypes: ['LessonList', 'CourseList'],
|
||||
endpoints: (builder) => ({
|
||||
lessonList: builder.query<BaseResponse<Lesson[]>, void>({
|
||||
query: () => '/lesson/list',
|
||||
providesTags: ['Lesson']
|
||||
coursesList: builder.query<BaseResponse<Course[]>, void>({
|
||||
query: () => '/course/list',
|
||||
providesTags: ['CourseList']
|
||||
}),
|
||||
createLesson: builder.mutation<BaseResponse<Lesson>, Pick<Lesson, 'name'>>({
|
||||
query: ({ name }) => ({
|
||||
createUpdateCourse: builder.mutation<BaseResponse<Course>, Partial<Course> & Pick<Course, 'name'>>({
|
||||
query: (course) => ({
|
||||
url: '/course',
|
||||
method: 'POST',
|
||||
body: course,
|
||||
}),
|
||||
invalidatesTags: ['CourseList']
|
||||
}),
|
||||
courseAllStudents: builder.query<BaseResponse<User[]>, string>({
|
||||
query: (courseId) => `/course/students/${courseId}`
|
||||
}),
|
||||
manualAddStudent: builder.mutation<BaseResponse<void>, { lessonId: string, user: User }>({
|
||||
query: ({lessonId, user}) => ({
|
||||
url: `/lesson/add-student/${lessonId}`,
|
||||
method: 'POST',
|
||||
body: user
|
||||
})
|
||||
}),
|
||||
|
||||
lessonList: builder.query<BaseResponse<Lesson[]>, string>({
|
||||
query: (courseId) => `/lesson/list/${courseId}`,
|
||||
providesTags: ['LessonList']
|
||||
}),
|
||||
createLesson: builder.mutation<BaseResponse<Lesson>, Pick<Lesson, 'name'> & { courseId: string }>({
|
||||
query: ({ name, courseId }) => ({
|
||||
url: '/lesson',
|
||||
method: 'POST',
|
||||
body: {name },
|
||||
body: { name, courseId },
|
||||
}),
|
||||
invalidatesTags: ['Lesson']
|
||||
invalidatesTags: ['LessonList']
|
||||
}),
|
||||
lessonById: builder.query<BaseResponse<Lesson>, string>({
|
||||
query: (lessonId: string) => `/lesson/${lessonId}`
|
||||
}),
|
||||
|
||||
createAccessCode: builder.query<BaseResponse<AccessCode>, { lessonId: string }>({
|
||||
query: ({ lessonId }) => ({
|
||||
url: '/lesson/access-code',
|
||||
|
||||
@@ -52,7 +52,7 @@ export type BaseResponse<Data> = {
|
||||
export interface Lesson {
|
||||
_id: string;
|
||||
name: string;
|
||||
students: Student[];
|
||||
students: User[];
|
||||
date: string;
|
||||
created: string;
|
||||
}
|
||||
@@ -62,10 +62,9 @@ export interface AccessCode {
|
||||
lesson: Lesson;
|
||||
_id: string;
|
||||
created: string;
|
||||
__v: number;
|
||||
}
|
||||
|
||||
export interface Student {
|
||||
export interface User {
|
||||
sub: string;
|
||||
email_verified: boolean;
|
||||
gravatar: string;
|
||||
@@ -75,4 +74,14 @@ export interface Student {
|
||||
given_name: string;
|
||||
family_name: string;
|
||||
email: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Course {
|
||||
_id: string;
|
||||
name: string;
|
||||
teachers: User[];
|
||||
lessons: Lesson[];
|
||||
creator: User;
|
||||
startDt: string;
|
||||
created: string;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { TypedUseSelectorHook, useSelector } from 'react-redux';
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import { TypedUseSelectorHook, useSelector } from 'react-redux'
|
||||
|
||||
import { api } from './api/api';
|
||||
import { userSlice } from './slices/user';
|
||||
import { api } from './api/api'
|
||||
import { userSlice } from './slices/user'
|
||||
|
||||
export const createStore= (preloadedState = {}) => configureStore({
|
||||
preloadedState,
|
||||
export const createStore = (preloadedState = {}) =>
|
||||
configureStore({
|
||||
preloadedState,
|
||||
reducer: {
|
||||
[api.reducerPath]: api.reducer,
|
||||
[userSlice.name]: userSlice.reducer
|
||||
[api.reducerPath]: api.reducer,
|
||||
[userSlice.name]: userSlice.reducer,
|
||||
},
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware().concat(api.middleware),
|
||||
});
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
immutableCheck: false,
|
||||
serializableCheck: false,
|
||||
}).concat(api.middleware),
|
||||
})
|
||||
|
||||
export type Store = ReturnType<ReturnType<typeof createStore>['getState']>;
|
||||
export type Store = ReturnType<ReturnType<typeof createStore>['getState']>
|
||||
|
||||
export const useAppSelector: TypedUseSelectorHook<Store> = useSelector;
|
||||
export const useAppSelector: TypedUseSelectorHook<Store> = useSelector
|
||||
|
||||
29
src/app.tsx
29
src/app.tsx
@@ -14,6 +14,7 @@ const App = ({ store }) => {
|
||||
<BrowserRouter>
|
||||
<Helmet>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no" />
|
||||
<title>Журнал</title>
|
||||
</Helmet>
|
||||
<Global
|
||||
styles={css`
|
||||
@@ -42,8 +43,8 @@ const App = ({ store }) => {
|
||||
rgba(255, 255, 255, 0) 65%
|
||||
);
|
||||
height: 100%;
|
||||
/* font-family: "SB Sans Screen"; */
|
||||
font-family: "SBSansScreenRegular";
|
||||
font-family: KiyosunaSans, Montserrat, RFKrabuler, sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
#app {
|
||||
height: 100%;
|
||||
@@ -51,14 +52,24 @@ const App = ({ store }) => {
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'SBSansScreenRegular';
|
||||
src: url('${__webpack_public_path__ + 'remote-assets/SBSansScreen.eot'}');
|
||||
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/SBSansScreen.eot?#iefix'}') format('embedded-opentype'),
|
||||
url('${__webpack_public_path__ + '/remote-assets/SBSansScreen.woff2'}') format('woff2'),
|
||||
url('${__webpack_public_path__ + '/remote-assets/SBSansScreen.woff'}') format('woff'),
|
||||
url('${__webpack_public_path__ + '/remote-assets/SBSansScreen.ttf'}') format('truetype'),
|
||||
url('${__webpack_public_path__ + '/remote-assets/SBSansScreen.svg#SBSansScreen-Regular'}') format('svg');
|
||||
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;
|
||||
}
|
||||
|
||||
20
src/assets/details-more.svg
Normal file
20
src/assets/details-more.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2 8C2 7.44772 2.44772 7 3 7H21C21.5523 7 22 7.44772 22 8C22 8.55228 21.5523 9 21 9H3C2.44772 9 2 8.55228 2 8Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M2 12C2 11.4477 2.44772 11 3 11H21C21.5523 11 22 11.4477 22 12C22 12.5523 21.5523 13 21 13H3C2.44772 13 2 12.5523 2 12Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M3 15C2.44772 15 2 15.4477 2 16C2 16.5523 2.44772 17 3 17H15C15.5523 17 16 16.5523 16 16C16 15.4477 15.5523 15 15 15H3Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 599 B |
3
src/assets/index.ts
Normal file
3
src/assets/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as linkOpen } from './link-open.svg'
|
||||
export { default as qrCode } from './qr-code.svg'
|
||||
export { default as moreDetails } from './details-more.svg'
|
||||
1
src/assets/link-open.svg
Normal file
1
src/assets/link-open.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="m17 2h5v5"/><path d="m21 13v6c0 1.1046-.8954 2-2 2h-14c-1.10457 0-2-.8954-2-2v-14c0-1.10457.89543-2 2-2h6"/><path d="m13 11 8.5-8.5"/></g></svg>
|
||||
|
After Width: | Height: | Size: 329 B |
1
src/assets/qr-code.svg
Normal file
1
src/assets/qr-code.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="m100.00244 36h-44a20.02229 20.02229 0 0 0 -20 20v44a20.02229 20.02229 0 0 0 20 20h44a20.02229 20.02229 0 0 0 20-20v-44a20.02229 20.02229 0 0 0 -20-20zm-4 60h-36v-36h36z"/><path d="m100.00244 136h-44a20.02229 20.02229 0 0 0 -20 20v44a20.02229 20.02229 0 0 0 20 20h44a20.02229 20.02229 0 0 0 20-20v-44a20.02229 20.02229 0 0 0 -20-20zm-4 60h-36v-36h36z"/><path d="m200.00244 36h-44a20.02229 20.02229 0 0 0 -20 20v44a20.02229 20.02229 0 0 0 20 20h44a20.02229 20.02229 0 0 0 20-20v-44a20.02229 20.02229 0 0 0 -20-20zm-4 60h-36v-36h36z"/><path d="m148.00244 184a12.0006 12.0006 0 0 0 12-12v-24a12 12 0 0 0 -24 0v24a12.0006 12.0006 0 0 0 12 12z"/><path d="m208.00244 152h-12v-4a12 12 0 0 0 -24 0v48h-24a12 12 0 1 0 0 24h36a12.0006 12.0006 0 0 0 12-12v-32h12a12 12 0 0 0 0-24z"/></svg>
|
||||
|
After Width: | Height: | Size: 848 B |
@@ -1,10 +1,14 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, Suspense } from "react";
|
||||
import { Routes, Route, useNavigate } from "react-router-dom";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
import { MainPage } from "./pages/main";
|
||||
import { Lesson } from "./pages/Lesson";
|
||||
import { UserPage } from "./pages/UserPage";
|
||||
import {
|
||||
CourseListPage,
|
||||
LessonDetailsPage,
|
||||
LessonListPage,
|
||||
UserPage
|
||||
} from './pages'
|
||||
import { getNavigationsValue } from "@ijl/cli";
|
||||
|
||||
const Redirect = ({ path }) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -16,13 +20,15 @@ const Redirect = ({ path }) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }) => <Suspense fallback="...">{children}</Suspense>
|
||||
|
||||
export const Dashboard = ({ store }) => (
|
||||
<Provider store={store}>
|
||||
<Routes>
|
||||
<Route path="/journal" element={<Redirect path="/journal/main" />} />
|
||||
<Route path="/journal/main" element={<MainPage />} />
|
||||
<Route path="/journal/u/:lessonId/:accessId" element={<UserPage />} />
|
||||
<Route path="/journal/l/:lessonId" element={<Lesson />} />
|
||||
<Route path={getNavigationsValue('journal.main')} element={<Wrapper><CourseListPage /></Wrapper>} />
|
||||
<Route path={`${getNavigationsValue('journal.main')}/lessons-list/:courseId`} element={<Wrapper><LessonListPage /></Wrapper>} />
|
||||
<Route path={`${getNavigationsValue('journal.main')}/u/:lessonId/:accessId`} element={<Wrapper><UserPage /></Wrapper>} />
|
||||
<Route path={`${getNavigationsValue('journal.main')}/lesson/:courseId/:lessonId`} element={<Wrapper><LessonDetailsPage /></Wrapper>} />
|
||||
</Routes>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { getConfigValue } from '@ijl/cli'
|
||||
|
||||
import {
|
||||
ArrowImg,
|
||||
IconButton,
|
||||
InputElement,
|
||||
InputLabel,
|
||||
InputWrapper,
|
||||
StartWrapper,
|
||||
LessonItem,
|
||||
Lessonname,
|
||||
Papper,
|
||||
ErrorSpan,
|
||||
Cross,
|
||||
AddButton,
|
||||
} from './style'
|
||||
|
||||
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'
|
||||
|
||||
export const Journal = () => {
|
||||
const user = useAppSelector((s) => s.user)
|
||||
const { data, isLoading, error } = api.useLessonListQuery()
|
||||
const [createLesson, crLQuery] = api.useCreateLessonMutation()
|
||||
const [value, setValue] = useState('')
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event) => {
|
||||
setValue(event.target.value.toUpperCase())
|
||||
},
|
||||
[setValue],
|
||||
)
|
||||
const handleSubmit = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault()
|
||||
createLesson({ name: value })
|
||||
},
|
||||
[value],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (crLQuery.isSuccess) {
|
||||
setValue('')
|
||||
}
|
||||
}, [crLQuery.isSuccess])
|
||||
|
||||
return (
|
||||
<StartWrapper>
|
||||
{isTeacher(user) && (
|
||||
<>
|
||||
{showForm ? (
|
||||
<Papper>
|
||||
<Cross role="button" onClick={() => setShowForm(false)}>X</Cross>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<InputWrapper>
|
||||
<InputLabel htmlFor="input">
|
||||
Название новой лекции:
|
||||
</InputLabel>
|
||||
<InputElement
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
id="input"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<IconButton type="submit">
|
||||
<ArrowImg src={arrow} />
|
||||
</IconButton>
|
||||
</InputWrapper>
|
||||
{crLQuery.error && (
|
||||
<ErrorSpan>{crLQuery.error.error}</ErrorSpan>
|
||||
)}
|
||||
</form>
|
||||
</Papper>
|
||||
) : (
|
||||
<AddButton onClick={() => setShowForm(true)}>Добавить</AddButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<ul style={{ paddingLeft: 0 }}>
|
||||
{data?.body?.map((lesson) => (
|
||||
<LessonItem key={lesson._id}>
|
||||
<Link
|
||||
to={isTeacher(user) ? `/journal/l/${lesson._id}` : ''}
|
||||
style={{ display: 'flex' }}
|
||||
>
|
||||
<Lessonname>{lesson.name}</Lessonname>
|
||||
<span>{dayjs(lesson.date).format('DD MMMM YYYYг.')}</span>
|
||||
<span style={{ marginLeft: 'auto' }}>
|
||||
Участников - {lesson.students.length}
|
||||
</span>
|
||||
</Link>
|
||||
</LessonItem>
|
||||
))}
|
||||
</ul>
|
||||
</StartWrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import React, { useEffect, useState, useRef } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import dayjs from 'dayjs'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
import {
|
||||
MainWrapper,
|
||||
StartWrapper,
|
||||
QRCanvas,
|
||||
LessonItem,
|
||||
Lessonname,
|
||||
} from './style'
|
||||
import { api } from '../__data__/api/api'
|
||||
|
||||
export const Lesson = () => {
|
||||
const { lessonId } = useParams()
|
||||
const canvRef = useRef(null)
|
||||
const [lesson, setLesson] = useState(null)
|
||||
const { isFetching, isLoading, data: accessCode, error, isSuccess } =
|
||||
api.useCreateAccessCodeQuery(
|
||||
{ lessonId },
|
||||
{
|
||||
pollingInterval: 3000,
|
||||
skipPollingIfUnfocused: true,
|
||||
},
|
||||
)
|
||||
useEffect(() => {
|
||||
if (!isFetching && isSuccess) {
|
||||
console.log(`${location.origin}/journal/u/${lessonId}/${accessCode.body._id}`)
|
||||
QRCode.toCanvas(
|
||||
canvRef.current,
|
||||
`${location.origin}/journal/u/${lessonId}/${accessCode.body._id}`,
|
||||
{ width: 600 },
|
||||
function (error) {
|
||||
if (error) console.error(error)
|
||||
console.log('success!')
|
||||
},
|
||||
)
|
||||
}
|
||||
}, [isFetching, isSuccess])
|
||||
|
||||
return (
|
||||
<MainWrapper>
|
||||
<StartWrapper>
|
||||
<h1>Тема занятия - {accessCode?.body?.lesson?.name}</h1>
|
||||
<span>{dayjs(accessCode?.body?.lesson?.date).format('DD MMMM YYYYг.')} Отмечено - {accessCode?.body?.lesson?.students?.length} человек</span>
|
||||
|
||||
<QRCanvas ref={canvRef} />
|
||||
|
||||
<ul style={{ paddingLeft: 0 }}>
|
||||
{accessCode?.body?.lesson?.students?.map((student, index) => (
|
||||
<LessonItem key={index}>
|
||||
<Lessonname>{student.name || student.preferred_username}</Lessonname>
|
||||
</LessonItem>
|
||||
))}
|
||||
</ul>
|
||||
</StartWrapper>
|
||||
</MainWrapper>
|
||||
)
|
||||
}
|
||||
167
src/pages/course-list.tsx
Normal file
167
src/pages/course-list.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
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 {
|
||||
Box,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
ButtonGroup,
|
||||
Stack,
|
||||
StackDivider,
|
||||
Button,
|
||||
UnorderedList,
|
||||
Heading,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
import {
|
||||
ArrowImg,
|
||||
IconButton,
|
||||
InputElement,
|
||||
InputLabel,
|
||||
InputWrapper,
|
||||
StartWrapper,
|
||||
Papper,
|
||||
ErrorSpan,
|
||||
Cross,
|
||||
AddButton,
|
||||
MainWrapper,
|
||||
StyledCard,
|
||||
} 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'
|
||||
|
||||
const CoursesList = () => {
|
||||
const user = useAppSelector((s) => s.user)
|
||||
const { data, isLoading, error } = api.useCoursesListQuery()
|
||||
const [createCourse, crcQuery] = api.useCreateUpdateCourseMutation()
|
||||
const [value, setValue] = useState('')
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event) => {
|
||||
setValue(event.target.value.toUpperCase())
|
||||
},
|
||||
[setValue],
|
||||
)
|
||||
const handleSubmit = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault()
|
||||
createCourse({ name: value })
|
||||
},
|
||||
[value],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (crcQuery.isSuccess) {
|
||||
setValue('')
|
||||
}
|
||||
}, [crcQuery.isSuccess])
|
||||
|
||||
return (
|
||||
<MainWrapper>
|
||||
<StartWrapper>
|
||||
{isTeacher(user) && (
|
||||
<>
|
||||
{showForm ? (
|
||||
<Papper>
|
||||
<Cross role="button" onClick={() => setShowForm(false)}>
|
||||
X
|
||||
</Cross>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<InputWrapper>
|
||||
<InputLabel htmlFor="input">
|
||||
Название новой лекции:
|
||||
</InputLabel>
|
||||
<InputElement
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
id="input"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<IconButton type="submit">
|
||||
<ArrowImg src={arrow} />
|
||||
</IconButton>
|
||||
</InputWrapper>
|
||||
{crcQuery?.error && (
|
||||
<ErrorSpan>{(crcQuery?.error as any).error}</ErrorSpan>
|
||||
)}
|
||||
</form>
|
||||
</Papper>
|
||||
) : (
|
||||
<AddButton onClick={() => setShowForm(true)}>Добавить</AddButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<UnorderedList spacing={3}>
|
||||
{data?.body?.map((course) => (
|
||||
<StyledCard key={course._id} align="left">
|
||||
<CardHeader>
|
||||
<Heading as="h2" mt="0">
|
||||
{course.name}
|
||||
</Heading>
|
||||
</CardHeader>
|
||||
{showDetails && (
|
||||
<CardBody mt="16px">
|
||||
<Stack divider={<StackDivider />} spacing="8px">
|
||||
<Box as="span" textAlign="left">
|
||||
{`Дата начала курса - ${dayjs(course.startDt).format('DD MMMM YYYYг.')}`}
|
||||
</Box>
|
||||
<Box as="span" textAlign="left">
|
||||
Количество занятий - {course.lessons.length}
|
||||
</Box>
|
||||
</Stack>
|
||||
</CardBody>
|
||||
)}
|
||||
<CardFooter>
|
||||
<ButtonGroup spacing="12" mt="16px">
|
||||
<Tooltip
|
||||
label="На страницу с лекциями"
|
||||
fontSize="12px"
|
||||
top="16px"
|
||||
>
|
||||
<Button variant="ghost" border="none" bg="#ffffff">
|
||||
<Link
|
||||
to={`${getNavigationsValue('journal.main')}/lessons-list/${course._id}`}
|
||||
>
|
||||
<img src={linkOpen} alt="Перейти к лекциям" />
|
||||
</Link>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label="Детали" fontSize="12px" top="16px">
|
||||
<Button
|
||||
variant="ghost"
|
||||
border="none"
|
||||
bg="#ffffff"
|
||||
onClick={() => {
|
||||
showDetails
|
||||
? setShowDetails(null)
|
||||
: setShowDetails(true)
|
||||
}}
|
||||
cursor="pointer"
|
||||
>
|
||||
<img src={moreDetails} alt="Просмотреть детали" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</CardFooter>
|
||||
</StyledCard>
|
||||
))}
|
||||
</UnorderedList>
|
||||
</StartWrapper>
|
||||
</MainWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default CoursesList
|
||||
8
src/pages/index.ts
Normal file
8
src/pages/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { lazy } from 'react'
|
||||
|
||||
export const CourseListPage = lazy(/* chank-name="course-details" */() => import('./course-list'));
|
||||
|
||||
export const LessonDetailsPage = lazy(/* chank-name="course-details" */() => import('./lesson-details'));
|
||||
export const LessonListPage = lazy(/* chank-name="course-details" */() => import('./lesson-list'));
|
||||
|
||||
export const UserPage = lazy(/* chank-name="course-details" */() => import('./user-page'));
|
||||
139
src/pages/lesson-details.tsx
Normal file
139
src/pages/lesson-details.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React, { useEffect, useState, useRef, useMemo } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import dayjs from 'dayjs'
|
||||
import QRCode from 'qrcode'
|
||||
import { getConfigValue, getNavigationsValue } from '@ijl/cli'
|
||||
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'
|
||||
|
||||
import {
|
||||
MainWrapper,
|
||||
StartWrapper,
|
||||
QRCanvas,
|
||||
LessonItem,
|
||||
Lessonname,
|
||||
AddMissedButton,
|
||||
Wrapper,
|
||||
UnorderList,
|
||||
} from './style'
|
||||
import { api } from '../__data__/api/api'
|
||||
import { User } from '../__data__/model'
|
||||
|
||||
const LessonDetail = () => {
|
||||
const { lessonId, courseId } = useParams()
|
||||
const canvRef = useRef(null)
|
||||
const [lesson, setLesson] = useState(null)
|
||||
const {
|
||||
isFetching,
|
||||
isLoading,
|
||||
data: accessCode,
|
||||
error,
|
||||
isSuccess,
|
||||
} = api.useCreateAccessCodeQuery(
|
||||
{ lessonId },
|
||||
{
|
||||
pollingInterval:
|
||||
Number(getConfigValue('journal.polling-interval')) || 3000,
|
||||
skipPollingIfUnfocused: true,
|
||||
},
|
||||
)
|
||||
const AllStudents = api.useCourseAllStudentsQuery(courseId)
|
||||
const [manualAdd, manualAddRqst] = api.useManualAddStudentMutation()
|
||||
const userUrl = useMemo(
|
||||
() => `${location.origin}/journal/u/${lessonId}/${accessCode?.body?._id}`,
|
||||
[accessCode, lessonId],
|
||||
)
|
||||
useEffect(() => {
|
||||
if (!isFetching && isSuccess) {
|
||||
QRCode.toCanvas(
|
||||
canvRef.current,
|
||||
userUrl,
|
||||
{ width: 600 },
|
||||
function (error) {
|
||||
if (error) console.error(error)
|
||||
console.log('success!')
|
||||
},
|
||||
)
|
||||
}
|
||||
}, [isFetching, isSuccess])
|
||||
|
||||
const studentsArr = useMemo(() => {
|
||||
let allStudents: (User & { present?: boolean })[] = [
|
||||
...(AllStudents.data?.body || []),
|
||||
].map((st) => ({ ...st, present: false }))
|
||||
let presentStudents: (User & { present?: boolean })[] = [
|
||||
...(accessCode?.body.lesson.students || []),
|
||||
]
|
||||
|
||||
while (allStudents.length && presentStudents.length) {
|
||||
const student = presentStudents.pop()
|
||||
|
||||
const present = allStudents.find((st) => st.sub === student.sub)
|
||||
|
||||
if (present) {
|
||||
present.present = true
|
||||
} else {
|
||||
allStudents.push({ ...student, present: true })
|
||||
}
|
||||
}
|
||||
|
||||
return allStudents
|
||||
}, [accessCode?.body, AllStudents.data])
|
||||
|
||||
return (
|
||||
<MainWrapper>
|
||||
<StartWrapper>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbItem>
|
||||
<Link as={BreadcrumbLink} to={getNavigationsValue('journal.main')}>
|
||||
Журнал
|
||||
</Link>
|
||||
</BreadcrumbItem>
|
||||
|
||||
<BreadcrumbItem>
|
||||
<Link
|
||||
as={BreadcrumbLink}
|
||||
to={`${getNavigationsValue('journal.main')}/lessons-list/${courseId}`}
|
||||
>
|
||||
Курс
|
||||
</Link>
|
||||
</BreadcrumbItem>
|
||||
|
||||
<BreadcrumbItem isCurrentPage>
|
||||
<BreadcrumbLink href="#">Лекция</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
|
||||
<h1 style={{ width: '70%' }} >Тема занятия - {accessCode?.body?.lesson?.name}</h1>
|
||||
<span style={{ display: 'flex' }}>
|
||||
{dayjs(accessCode?.body?.lesson?.date).format('DD MMMM YYYYг.')}{' '}
|
||||
Отмечено - {accessCode?.body?.lesson?.students?.length}{' '}
|
||||
{AllStudents.isSuccess ? `/ ${AllStudents?.data?.body?.length}` : ''}{' '}
|
||||
человек
|
||||
</span>
|
||||
<Wrapper>
|
||||
<a href={userUrl}>
|
||||
<QRCanvas ref={canvRef} />
|
||||
</a>
|
||||
<UnorderList>
|
||||
{studentsArr.map((student) => (
|
||||
<LessonItem key={student.sub} warn={!student.present}>
|
||||
<Lessonname>
|
||||
{student.name || student.preferred_username}{' '}
|
||||
{!student.present && (
|
||||
<AddMissedButton
|
||||
onClick={() => manualAdd({ lessonId, user: student })}
|
||||
>
|
||||
add
|
||||
</AddMissedButton>
|
||||
)}
|
||||
</Lessonname>
|
||||
</LessonItem>
|
||||
))}
|
||||
</UnorderList>
|
||||
</Wrapper>
|
||||
</StartWrapper>
|
||||
</MainWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default LessonDetail
|
||||
132
src/pages/lesson-list.tsx
Normal file
132
src/pages/lesson-list.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { getNavigationsValue } from '@ijl/cli'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
import {
|
||||
ArrowImg,
|
||||
IconButton,
|
||||
InputElement,
|
||||
InputLabel,
|
||||
InputWrapper,
|
||||
StartWrapper,
|
||||
LessonItem,
|
||||
Lessonname,
|
||||
Papper,
|
||||
ErrorSpan,
|
||||
Cross,
|
||||
AddButton,
|
||||
MainWrapper,
|
||||
} from './style'
|
||||
|
||||
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'
|
||||
|
||||
const LessonList = () => {
|
||||
const { courseId } = useParams()
|
||||
const user = useAppSelector((s) => s.user)
|
||||
const { data, isLoading, error } = api.useLessonListQuery(courseId)
|
||||
const [createLesson, crLQuery] = api.useCreateLessonMutation()
|
||||
const [value, setValue] = useState('')
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event) => {
|
||||
setValue(event.target.value.toUpperCase())
|
||||
},
|
||||
[setValue],
|
||||
)
|
||||
const handleSubmit = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault()
|
||||
createLesson({ name: value, courseId })
|
||||
},
|
||||
[value],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (crLQuery.isSuccess) {
|
||||
setValue('')
|
||||
}
|
||||
}, [crLQuery.isSuccess])
|
||||
|
||||
return (
|
||||
<MainWrapper>
|
||||
<StartWrapper>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbItem>
|
||||
<Link as={BreadcrumbLink} to={getNavigationsValue('journal.main')}>Журнал</Link>
|
||||
</BreadcrumbItem>
|
||||
|
||||
<BreadcrumbItem isCurrentPage>
|
||||
<BreadcrumbLink href="#">Курс</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
|
||||
{isTeacher(user) && (
|
||||
<>
|
||||
{showForm ? (
|
||||
<Papper>
|
||||
<Cross role="button" onClick={() => setShowForm(false)}>
|
||||
X
|
||||
</Cross>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<InputWrapper>
|
||||
<InputLabel htmlFor="input">
|
||||
Название новой лекции:
|
||||
</InputLabel>
|
||||
<InputElement
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
id="input"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<IconButton type="submit">
|
||||
<ArrowImg src={arrow} />
|
||||
</IconButton>
|
||||
</InputWrapper>
|
||||
{crLQuery.error && (
|
||||
<ErrorSpan>{(crLQuery.error as any).error}</ErrorSpan>
|
||||
)}
|
||||
</form>
|
||||
</Papper>
|
||||
) : (
|
||||
<AddButton onClick={() => setShowForm(true)}>Добавить</AddButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<ul style={{ paddingLeft: 0 }}>
|
||||
{data?.body?.map((lesson) => (
|
||||
<LessonItem key={lesson._id}>
|
||||
<Link
|
||||
to={
|
||||
isTeacher(user)
|
||||
? `${getNavigationsValue('journal.main')}/lesson/${courseId}/${lesson._id}`
|
||||
: ''
|
||||
}
|
||||
style={{ display: 'flex' }}
|
||||
>
|
||||
<Lessonname>{lesson.name}</Lessonname>
|
||||
<span>{dayjs(lesson.date).format('DD MMMM YYYYг.')}</span>
|
||||
<span style={{ marginLeft: 'auto' }}>
|
||||
Участников - {lesson.students.length}
|
||||
</span>
|
||||
</Link>
|
||||
</LessonItem>
|
||||
))}
|
||||
</ul>
|
||||
</StartWrapper>
|
||||
</MainWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default LessonList
|
||||
@@ -1,16 +0,0 @@
|
||||
import React, {useState, useCallback, useRef, useEffect} from 'react';
|
||||
|
||||
import arrow from '../assets/36-arrow-right.svg';
|
||||
|
||||
import {
|
||||
MainWrapper,
|
||||
} from './style';
|
||||
import { Journal } from './Journal';
|
||||
|
||||
export const MainPage = () => {
|
||||
return (
|
||||
<MainWrapper>
|
||||
<Journal />
|
||||
</MainWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,13 @@
|
||||
import styled from '@emotion/styled'
|
||||
import { css, keyframes } from '@emotion/react'
|
||||
import {
|
||||
Card
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
|
||||
export const MainWrapper = styled.main`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* align-items: center; */
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
@@ -34,6 +37,13 @@ export const InputElement = styled.input`
|
||||
box-shadow: inset 7px 8px 20px 8px #4990db12;
|
||||
`
|
||||
|
||||
export const StyledCard = styled(Card)`
|
||||
box-shadow: 2px 2px 6px #0000005c;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
min-width: 400px;
|
||||
`
|
||||
|
||||
export const ArrowImg = styled.img`
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
@@ -49,7 +59,7 @@ export const IconButton = styled.button`
|
||||
|
||||
const reveal = keyframes`
|
||||
0% {
|
||||
transform: scale(0.1, 0.1);
|
||||
transform: scale(0.85, 0.85);
|
||||
}
|
||||
|
||||
100% {
|
||||
@@ -58,21 +68,31 @@ const reveal = keyframes`
|
||||
`
|
||||
|
||||
export const StartWrapper = styled.div`
|
||||
animation: ${reveal} 1s ease forwards;
|
||||
/* box-shadow: 0 -2px 5px rgba(255,255,255,0.05), 0 2px 5px rgba(255,255,255,0.1); */
|
||||
width: 650px;
|
||||
animation: ${reveal} 0.4s ease forwards;
|
||||
height: calc(100vh - 300px);
|
||||
/* margin: 60px auto; */
|
||||
position: relative;
|
||||
`
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
width: auto;
|
||||
`
|
||||
|
||||
export const UnorderList = styled.ul`
|
||||
padding-left: 0px;
|
||||
height: 600px;
|
||||
overflow: auto;
|
||||
padding-right: 20px;
|
||||
`
|
||||
|
||||
export const Svg = styled.svg`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform-origin: 50% 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
/* stroke-dasharray: 600; */
|
||||
`
|
||||
|
||||
export const Papper = styled.div`
|
||||
@@ -83,18 +103,41 @@ export const Papper = styled.div`
|
||||
box-shadow: 2px 2px 6px #0000005c;
|
||||
`
|
||||
|
||||
export const LessonItem = styled.li`
|
||||
export const LessonItem = styled.li<{ warn?: boolean }>`
|
||||
list-style: none;
|
||||
background-color: #ffffff;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 2px 2px 6px #0000005c;
|
||||
margin-bottom: 12px;
|
||||
transition: all 0.5;
|
||||
|
||||
${(props) =>
|
||||
props.warn
|
||||
? css`
|
||||
background-color: #fde3c5;
|
||||
color: #919191;
|
||||
box-shadow: inset 3px 2px 7px #c9b5a9;
|
||||
`
|
||||
: ''}
|
||||
`
|
||||
|
||||
export const AddMissedButton = styled.button`
|
||||
float: right;
|
||||
border: none;
|
||||
background-color: #00000000;
|
||||
opacity: 0.1;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
export const Lessonname = styled.span`
|
||||
display: inline-box;
|
||||
margin-right: 12px;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
export const QRCanvas = styled.canvas`
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { api } from '../__data__/api/api'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export const UserPage = () => {
|
||||
const UserPage = () => {
|
||||
const { lessonId, accessId } = useParams()
|
||||
const acc = api.useGetAccessQuery({ accessCode: accessId })
|
||||
|
||||
@@ -31,7 +31,7 @@ export const UserPage = () => {
|
||||
<ErrorSpan>
|
||||
{(acc as any).error?.data?.body?.errorMessage ===
|
||||
'Code is expired' ? (
|
||||
'Не удалось активировать код доступа. Попробуйте отсканировать кодеще раз'
|
||||
'Не удалось активировать код доступа. Попробуйте отсканировать код ещё раз'
|
||||
) : (
|
||||
<pre>{JSON.stringify(acc.error, null, 4)}</pre>
|
||||
)}
|
||||
@@ -54,3 +54,5 @@ export const UserPage = () => {
|
||||
</MainWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserPage
|
||||
Reference in New Issue
Block a user