Добавлено состояние для отслеживания пульсации в компоненте LessonDetail при изменении количества студентов. Реализована функция для определения цвета на основе посещаемости. Обновлен компонент LessonList для отображения статистики посещаемости с использованием новых стилей и анимаций.
This commit is contained in:
parent
5885124630
commit
e50fb4fd82
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useMemo } from 'react'
|
||||
import React, { useEffect, useRef, useMemo, useState } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import QRCode from 'qrcode'
|
||||
import { sha256 } from 'js-sha256'
|
||||
@ -49,6 +49,11 @@ const LessonDetail = () => {
|
||||
// Создаем ref для отслеживания ранее присутствовавших студентов
|
||||
const prevPresentStudentsRef = useRef(new Set<string>())
|
||||
|
||||
// Добавляем состояние для отслеживания пульсации
|
||||
const [isPulsing, setIsPulsing] = useState(false)
|
||||
// Отслеживаем предыдущее количество студентов
|
||||
const prevStudentCountRef = useRef(0)
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
data: accessCode,
|
||||
@ -75,6 +80,20 @@ const LessonDetail = () => {
|
||||
if (accessCode?.body) {
|
||||
const currentPresent = new Set(accessCode.body.lesson.students.map(s => s.sub))
|
||||
|
||||
// Проверяем, изменилось ли количество студентов
|
||||
const currentCount = accessCode.body.lesson.students.length;
|
||||
if (prevStudentCountRef.current !== currentCount && prevStudentCountRef.current > 0) {
|
||||
// Запускаем эффект пульсации
|
||||
setIsPulsing(true);
|
||||
// Сбрасываем эффект через 1.5 секунды
|
||||
setTimeout(() => {
|
||||
setIsPulsing(false);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// Обновляем предыдущее количество
|
||||
prevStudentCountRef.current = currentCount;
|
||||
|
||||
// Очищаем флаги предыдущего состояния после задержки
|
||||
const timeoutId = setTimeout(() => {
|
||||
prevPresentStudentsRef.current = currentPresent
|
||||
@ -169,6 +188,17 @@ const LessonDetail = () => {
|
||||
return allStudents.sort((a, b) => (a.present ? -1 : 1))
|
||||
}, [accessCode?.body, AllStudents.data, prevPresentStudentsRef.current])
|
||||
|
||||
// Функция для определения цвета на основе посещаемости
|
||||
const getAttendanceColor = (attendance: number, total: number) => {
|
||||
const percentage = total > 0 ? (attendance / total) * 100 : 0
|
||||
|
||||
if (percentage > 80) return { bg: 'green.100', color: 'green.800', dark: { bg: 'green.800', color: 'green.100' } }
|
||||
if (percentage > 60) return { bg: 'teal.100', color: 'teal.800', dark: { bg: 'teal.800', color: 'teal.100' } }
|
||||
if (percentage > 40) return { bg: 'yellow.100', color: 'yellow.800', dark: { bg: 'yellow.800', color: 'yellow.100' } }
|
||||
if (percentage > 20) return { bg: 'orange.100', color: 'orange.800', dark: { bg: 'orange.800', color: 'orange.100' } }
|
||||
return { bg: 'red.100', color: 'red.800', dark: { bg: 'red.800', color: 'red.100' } }
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BreadcrumbsWrapper>
|
||||
@ -211,10 +241,49 @@ const LessonDetail = () => {
|
||||
boxShadow="md"
|
||||
><Box pb={3}>
|
||||
{formatDate(accessCode?.body?.lesson?.date, t('journal.pl.lesson.dateFormat'))}{' '}
|
||||
{t('journal.pl.common.marked')} - {accessCode?.body?.lesson?.students?.length}{' '}
|
||||
{AllStudents.isSuccess
|
||||
? `/ ${AllStudents?.data?.body?.length}`
|
||||
: ''}{' '}
|
||||
{t('journal.pl.common.marked')} -
|
||||
{AllStudents.isSuccess && (
|
||||
<Box
|
||||
as="span"
|
||||
px={2}
|
||||
py={1}
|
||||
ml={2}
|
||||
borderRadius="md"
|
||||
fontWeight="bold"
|
||||
bg={getAttendanceColor(
|
||||
accessCode?.body?.lesson?.students?.length || 0,
|
||||
AllStudents?.data?.body?.length || 1
|
||||
).bg}
|
||||
color={getAttendanceColor(
|
||||
accessCode?.body?.lesson?.students?.length || 0,
|
||||
AllStudents?.data?.body?.length || 1
|
||||
).color}
|
||||
_dark={{
|
||||
bg: getAttendanceColor(
|
||||
accessCode?.body?.lesson?.students?.length || 0,
|
||||
AllStudents?.data?.body?.length || 1
|
||||
).dark.bg,
|
||||
color: getAttendanceColor(
|
||||
accessCode?.body?.lesson?.students?.length || 0,
|
||||
AllStudents?.data?.body?.length || 1
|
||||
).dark.color
|
||||
}}
|
||||
position="relative"
|
||||
animation={isPulsing ? "pulse 1.5s ease-in-out" : "none"}
|
||||
sx={{
|
||||
'@keyframes pulse': {
|
||||
'0%': { transform: 'scale(1)' },
|
||||
'50%': { transform: 'scale(1.15)', boxShadow: '0 0 10px rgba(66, 153, 225, 0.7)' },
|
||||
'100%': { transform: 'scale(1)' }
|
||||
}
|
||||
}}
|
||||
>
|
||||
{accessCode?.body?.lesson?.students?.length} / {AllStudents?.data?.body?.length}
|
||||
</Box>
|
||||
)}
|
||||
{!AllStudents.isSuccess && (
|
||||
<span> {accessCode?.body?.lesson?.students?.length}</span>
|
||||
)}{' '}
|
||||
{t('journal.pl.common.people')}
|
||||
</Box>
|
||||
<a href={userUrl}>
|
||||
|
@ -26,8 +26,14 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogOverlay,
|
||||
useBreakpointValue,
|
||||
Flex,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
useColorMode,
|
||||
} from '@chakra-ui/react'
|
||||
import { AddIcon } from '@chakra-ui/icons'
|
||||
import { AddIcon, EditIcon } from '@chakra-ui/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useAppSelector } from '../../__data__/store'
|
||||
@ -35,6 +41,7 @@ import { api } from '../../__data__/api/api'
|
||||
import { isTeacher } from '../../utils/user'
|
||||
import { Lesson } from '../../__data__/model'
|
||||
import { XlSpinner } from '../../components/xl-spinner'
|
||||
import { qrCode } from '../../assets'
|
||||
|
||||
import { LessonForm } from './components/lessons-form'
|
||||
import { Bar } from './components/bar'
|
||||
@ -58,6 +65,7 @@ const LessonList = () => {
|
||||
error: errorGenerateLessons,
|
||||
isSuccess: isSuccessGenerateLessons
|
||||
}, ] = api.useGenerateLessonsMutation()
|
||||
const { colorMode } = useColorMode()
|
||||
|
||||
const [createLesson, crLQuery] = api.useCreateLessonMutation()
|
||||
const [deleteLesson, deletingRqst] = api.useDeleteLessonMutation()
|
||||
@ -76,6 +84,23 @@ const LessonList = () => {
|
||||
[data, data?.body],
|
||||
)
|
||||
|
||||
// Найдем максимальное количество студентов среди всех уроков
|
||||
const maxStudents = useMemo(() => {
|
||||
if (!sorted || sorted.length === 0) return 1
|
||||
const max = Math.max(...sorted.map(lesson => lesson.students?.length || 0))
|
||||
return max > 0 ? max : 1 // Избегаем деления на ноль
|
||||
}, [sorted])
|
||||
|
||||
// Функция для определения цвета на основе посещаемости
|
||||
const getAttendanceColor = (attendance: number) => {
|
||||
const percentage = (attendance / maxStudents) * 100
|
||||
if (percentage > 80) return { bg: 'green.100', color: 'green.800', dark: { bg: 'green.800', color: 'green.100' } }
|
||||
if (percentage > 60) return { bg: 'teal.100', color: 'teal.800', dark: { bg: 'teal.800', color: 'teal.100' } }
|
||||
if (percentage > 40) return { bg: 'yellow.100', color: 'yellow.800', dark: { bg: 'yellow.800', color: 'yellow.100' } }
|
||||
if (percentage > 20) return { bg: 'orange.100', color: 'orange.800', dark: { bg: 'orange.800', color: 'orange.100' } }
|
||||
return { bg: 'red.100', color: 'red.800', dark: { bg: 'red.800', color: 'red.100' } }
|
||||
}
|
||||
|
||||
const lessonCalc = useMemo(() => {
|
||||
if (!isSuccess) {
|
||||
return []
|
||||
@ -379,38 +404,157 @@ const LessonList = () => {
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<TableContainer whiteSpace="wrap" pb={13}>
|
||||
<Table variant="striped" colorScheme="cyan">
|
||||
<Thead>
|
||||
<Tr>
|
||||
{isTeacher(user) && (
|
||||
<Th align="center" width={1}>
|
||||
{t('journal.pl.lesson.link')}
|
||||
</Th>
|
||||
)}
|
||||
<Th textAlign="center" width={1}>
|
||||
{groupByDate ? t('journal.pl.lesson.time') : t('journal.pl.common.date')}
|
||||
</Th>
|
||||
<Th width="100%">{t('journal.pl.common.name')}</Th>
|
||||
{isTeacher(user) && <Th>{t('journal.pl.lesson.action')}</Th>}
|
||||
<Th isNumeric>{t('journal.pl.common.marked')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{lessonCalc?.map(({ data: lessons, date }) => (
|
||||
<LessonItems
|
||||
courseId={courseId}
|
||||
date={date}
|
||||
isTeacher={isTeacher(user)}
|
||||
lessons={lessons}
|
||||
setlessonToDelete={setlessonToDelete}
|
||||
setEditLesson={handleEditLesson}
|
||||
key={date}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Box pb={13}>
|
||||
{lessonCalc?.map(({ data: lessons, date }) => (
|
||||
<Box key={date} mb={6}>
|
||||
{date && (
|
||||
<Box
|
||||
p={3}
|
||||
mb={4}
|
||||
bg="cyan.50"
|
||||
borderRadius="md"
|
||||
_dark={{ bg: "cyan.900" }}
|
||||
boxShadow="sm"
|
||||
>
|
||||
<Text fontWeight="bold" fontSize="lg">
|
||||
{formatDate(date, 'DD MMMM YYYY')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
{lessons.map((lesson, index) => (
|
||||
<Box
|
||||
key={lesson.id}
|
||||
borderRadius="lg"
|
||||
boxShadow="md"
|
||||
bg="white"
|
||||
_dark={{ bg: "gray.700" }}
|
||||
transition="all 0.3s"
|
||||
_hover={{
|
||||
transform: "translateX(5px)",
|
||||
boxShadow: "lg"
|
||||
}}
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
mb={4}
|
||||
animation={`slideIn 0.6s ease-out ${index * 0.15}s both`}
|
||||
sx={{
|
||||
'@keyframes slideIn': {
|
||||
'0%': {
|
||||
opacity: 0,
|
||||
transform: 'translateX(-30px)'
|
||||
},
|
||||
'100%': {
|
||||
opacity: 1,
|
||||
transform: 'translateX(0)'
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Flex direction={{ base: "column", sm: "row" }}>
|
||||
{/* QR код и ссылка - левая часть карточки */}
|
||||
{isTeacher(user) && (
|
||||
<Link
|
||||
to={`${getNavigationValue('journal.main')}/lesson/${courseId}/${lesson.id}`}
|
||||
>
|
||||
<Box
|
||||
p={4}
|
||||
bg="cyan.500"
|
||||
_dark={{ bg: "cyan.600" }}
|
||||
color="white"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
transition="all 0.2s"
|
||||
_hover={{ bg: "cyan.600", _dark: { bg: "cyan.700" } }}
|
||||
height="100%"
|
||||
minW="150px"
|
||||
>
|
||||
<Box
|
||||
mr={0}
|
||||
bg="white"
|
||||
borderRadius="md"
|
||||
p={2}
|
||||
display="flex"
|
||||
>
|
||||
<img width={32} src={qrCode} alt="QR код" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Содержимое карточки */}
|
||||
<Box p={5} w="100%" display="flex" flexDirection="column" justifyContent="space-between">
|
||||
<Flex mb={3} justify="space-between" align="center">
|
||||
{/* Название урока */}
|
||||
<Text fontWeight="bold" fontSize="xl" lineHeight="1.4" flex="1">
|
||||
{lesson.name}
|
||||
</Text>
|
||||
|
||||
<Text fontSize="sm" color="gray.500" _dark={{ color: "gray.300" }} ml={3} whiteSpace="nowrap">
|
||||
{formatDate(lesson.date, groupByDate ? 'HH:mm' : 'HH:mm DD.MM.YY')}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{/* Нижняя часть с метками и действиями */}
|
||||
<Flex justifyContent="space-between" alignItems="center" mt={1}>
|
||||
<Flex align="center">
|
||||
<Text fontSize="sm" mr={2}>
|
||||
{t('journal.pl.common.marked')}:
|
||||
</Text>
|
||||
<Text
|
||||
px={2}
|
||||
py={1}
|
||||
bg={getAttendanceColor(lesson.students.length).bg}
|
||||
color={getAttendanceColor(lesson.students.length).color}
|
||||
_dark={{
|
||||
bg: getAttendanceColor(lesson.students.length).dark.bg,
|
||||
color: getAttendanceColor(lesson.students.length).dark.color
|
||||
}}
|
||||
borderRadius="md"
|
||||
fontWeight="bold"
|
||||
fontSize="sm"
|
||||
>
|
||||
{lesson.students.length}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{isTeacher(user) && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
size="sm"
|
||||
colorScheme="cyan"
|
||||
variant="ghost"
|
||||
rightIcon={<EditIcon />}
|
||||
>
|
||||
{t('journal.pl.edit')}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
onClick={() => handleEditLesson(lesson)}
|
||||
icon={<EditIcon />}
|
||||
>
|
||||
{t('journal.pl.edit')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => setlessonToDelete(lesson)}
|
||||
color="red.500"
|
||||
>
|
||||
{t('journal.pl.delete')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
|
Loading…
x
Reference in New Issue
Block a user