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

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