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:
2024-03-01 12:49:55 +03:00
parent 0574d4e8d0
commit 25511f37d1
51 changed files with 3033 additions and 7778 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -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
View 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
View 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'));

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

View File

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

View File

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

View File

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