Обновлены стили компонента UserCard для улучшения визуального восприятия и добавлены анимации при наведении. Реализована поддержка отображения недавно присутствующих студентов с помощью анимации. Обновлен компонент LessonDetail для отслеживания новых студентов и их анимации при появлении. Улучшены стили списков студентов для лучшей адаптивности и пользовательского опыта.

This commit is contained in:
2025-03-23 21:45:16 +03:00
parent 570ae4b171
commit 3d383f2e25
6 changed files with 497 additions and 131 deletions
+98 -25
View File
@@ -4,6 +4,7 @@ import dayjs from 'dayjs'
import QRCode from 'qrcode'
import { sha256 } from 'js-sha256'
import { getConfigValue, getNavigationValue } from '@brojs/cli'
import { motion, AnimatePresence } from 'framer-motion'
import {
Box,
Breadcrumb,
@@ -13,6 +14,7 @@ import {
VStack,
Heading,
Stack,
useColorMode,
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
@@ -42,6 +44,10 @@ const LessonDetail = () => {
const canvRef = useRef(null)
const user = useAppSelector((s) => s.user)
const { t } = useTranslation()
const { colorMode } = useColorMode()
// Создаем ref для отслеживания ранее присутствовавших студентов
const prevPresentStudentsRef = useRef(new Set<string>())
const {
isFetching,
@@ -64,6 +70,20 @@ const LessonDetail = () => {
[accessCode, lessonId],
)
// Эффект для обнаружения и обновления новых присутствующих студентов
useEffect(() => {
if (accessCode?.body) {
const currentPresent = new Set(accessCode.body.lesson.students.map(s => s.sub))
// Очищаем флаги предыдущего состояния после задержки
const timeoutId = setTimeout(() => {
prevPresentStudentsRef.current = currentPresent
}, 3000)
return () => clearTimeout(timeoutId)
}
}, [accessCode])
useEffect(() => {
if (manualAddRqst.isSuccess) {
refetch()
@@ -118,13 +138,17 @@ const LessonDetail = () => {
}, [isFetching, isSuccess, userUrl])
const studentsArr = useMemo(() => {
let allStudents: (User & { present?: boolean })[] = [
let allStudents: (User & { present?: boolean; recentlyPresent?: boolean })[] = [
...(AllStudents.data?.body || []),
].map((st) => ({ ...st, present: false }))
].map((st) => ({ ...st, present: false, recentlyPresent: false }))
let presentStudents: (User & { present?: boolean })[] = [
...(accessCode?.body.lesson.students || []),
]
// Находим новых студентов по сравнению с предыдущим состоянием
const currentPresent = new Set(presentStudents.map(s => s.sub))
const newlyPresent = [...currentPresent].filter(id => !prevPresentStudentsRef.current.has(id))
while (presentStudents.length) {
const student = presentStudents.pop()
@@ -132,13 +156,18 @@ const LessonDetail = () => {
if (present) {
present.present = true
present.recentlyPresent = newlyPresent.includes(student.sub)
} else {
allStudents.push({ ...student, present: true })
allStudents.push({
...student,
present: true,
recentlyPresent: newlyPresent.includes(student.sub)
})
}
}
return allStudents.sort((a, b) => (a.present ? -1 : 1))
}, [accessCode?.body, AllStudents.data])
}, [accessCode?.body, AllStudents.data, prevPresentStudentsRef.current])
return (
<>
@@ -170,32 +199,76 @@ const LessonDetail = () => {
{t('journal.pl.lesson.topicTitle')}
</Heading>
<Box as="span">{accessCode?.body?.lesson?.name}</Box>
<Box as="span">
{dayjs(accessCode?.body?.lesson?.date).format(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.people')}
</Box>
</VStack>
<Stack spacing="8" direction={{ base: "column", md: "row" }}>
<Box flexShrink={0} alignSelf="center">
<Stack spacing="8" direction={{ base: "column", md: "row" }} mt={6}>
<Box
flexShrink={0}
alignSelf="flex-start"
p={4}
borderRadius="xl"
bg={colorMode === "light" ? "gray.50" : "gray.700"}
boxShadow="md"
><Box as="span">
{dayjs(accessCode?.body?.lesson?.date).format(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.people')}
</Box>
<a href={userUrl}>
<QRCanvas ref={canvRef} />
</a>
</Box>
<StudentList>
{isTeacher(user) && studentsArr.map((student) => (
<UserCard
wrapperAS="li"
key={student.sub}
student={student}
present={student.present}
onAddUser={(user: User) => manualAdd({ lessonId, user })}
/>
))}
</StudentList>
<Box
flex={1}
p={4}
borderRadius="xl"
bg={colorMode === "light" ? "gray.50" : "gray.700"}
boxShadow="md"
>
<StudentList>
{isTeacher(user) && (
<AnimatePresence initial={false}>
{studentsArr.map((student) => (
<motion.li
key={student.sub}
layout
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: 1,
scale: 1,
// Добавляем подсветку для недавно отметившихся студентов
boxShadow: student.recentlyPresent
? ['0 0 0 0 rgba(66, 153, 225, 0)', '0 0 20px 5px rgba(66, 153, 225, 0.7)', '0 0 0 0 rgba(66, 153, 225, 0)']
: '0 0 0 0 rgba(0, 0, 0, 0)'
}}
exit={{ opacity: 0, scale: 0.8 }}
transition={{
type: "spring",
stiffness: 300,
damping: 30,
layout: { duration: 0.4 },
boxShadow: {
repeat: student.recentlyPresent ? 3 : 0,
duration: 1.5
}
}}
>
<UserCard
wrapperAS="div"
student={student}
present={student.present}
recentlyPresent={student.recentlyPresent}
onAddUser={(user: User) => manualAdd({ lessonId, user })}
/>
</motion.li>
))}
</AnimatePresence>
)}
</StudentList>
</Box>
</Stack>
</Container>
</>