232 lines
7.3 KiB
TypeScript
232 lines
7.3 KiB
TypeScript
import React, { useEffect, useState } from 'react'
|
||
import { useParams } from 'react-router-dom'
|
||
import { useTranslation } from 'react-i18next'
|
||
import { motion, AnimatePresence } from 'framer-motion'
|
||
|
||
import { api } from '../__data__/api/api'
|
||
import { formatDate } from '../utils/dayjs-config'
|
||
import {
|
||
Alert,
|
||
AlertIcon,
|
||
Box,
|
||
Center,
|
||
Container,
|
||
Spinner,
|
||
Text,
|
||
Heading,
|
||
Badge,
|
||
Flex,
|
||
useColorMode,
|
||
} from '@chakra-ui/react'
|
||
import { UserCard } from '../components/user-card'
|
||
import { StudentListView } from './style'
|
||
|
||
const UserPage = () => {
|
||
const { lessonId, accessId } = useParams()
|
||
const { t } = useTranslation()
|
||
const { colorMode } = useColorMode()
|
||
const acc = api.useGetAccessQuery({ accessCode: accessId })
|
||
const [animatedStudents, setAnimatedStudents] = useState([])
|
||
|
||
const ls = api.useLessonByIdQuery(lessonId, {
|
||
pollingInterval: 1000,
|
||
skipPollingIfUnfocused: true,
|
||
})
|
||
|
||
// Эффект для поэтапного появления карточек студентов
|
||
useEffect(() => {
|
||
if (ls.data?.body?.students?.length) {
|
||
// Сначала очищаем список
|
||
setAnimatedStudents([])
|
||
|
||
// Затем постепенно добавляем студентов для красивой анимации
|
||
const students = [...ls.data.body.students]
|
||
const addStudentWithDelay = (index) => {
|
||
if (index < students.length) {
|
||
setAnimatedStudents(prev => [...prev, {...students[index], isNew: true}])
|
||
|
||
// Для следующего студента
|
||
setTimeout(() => {
|
||
addStudentWithDelay(index + 1)
|
||
}, 100) // Уменьшенная задержка для более плавной анимации
|
||
}
|
||
}
|
||
|
||
// Запускаем процесс добавления с небольшой задержкой для лучшего UX
|
||
setTimeout(() => {
|
||
addStudentWithDelay(0)
|
||
}, 300)
|
||
}
|
||
}, [ls.data?.body?.students])
|
||
|
||
// Эффект для сброса флага "новизны" студентов
|
||
useEffect(() => {
|
||
if (animatedStudents.length > 0) {
|
||
const timeoutId = setTimeout(() => {
|
||
setAnimatedStudents(students =>
|
||
students.map(student => ({...student, isNew: false}))
|
||
)
|
||
}, 2000)
|
||
|
||
return () => clearTimeout(timeoutId)
|
||
}
|
||
}, [animatedStudents])
|
||
|
||
if (acc.isLoading) {
|
||
return (
|
||
<Container maxW="container.xl">
|
||
<Center h="300px">
|
||
<Spinner
|
||
thickness="4px"
|
||
speed="0.65s"
|
||
emptyColor="gray.200"
|
||
color="blue.500"
|
||
size="xl"
|
||
/>
|
||
</Center>
|
||
</Container>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<Container maxW="container.lg" pt={4}>
|
||
{acc.isLoading && (
|
||
<Center py={4}>
|
||
<Spinner mr={2} />
|
||
<Text>{t('journal.pl.common.sending')}</Text>
|
||
</Center>
|
||
)}
|
||
|
||
{acc.isSuccess && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.3 }}
|
||
>
|
||
<Alert status="success" mb={4} borderRadius="lg">
|
||
<AlertIcon />
|
||
{t('journal.pl.common.success')}
|
||
</Alert>
|
||
</motion.div>
|
||
)}
|
||
|
||
{acc.error && (
|
||
<Box mb="6" mt="2">
|
||
<Alert status="warning" borderRadius="lg">
|
||
<AlertIcon />
|
||
{(acc as any).error?.data?.body?.errorMessage ===
|
||
'Code is expired' ? (
|
||
t('journal.pl.access.expiredCode')
|
||
) : (
|
||
<pre>{JSON.stringify(acc.error, null, 4)}</pre>
|
||
)}
|
||
</Alert>
|
||
</Box>
|
||
)}
|
||
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
transition={{ duration: 0.4 }}
|
||
>
|
||
<Box
|
||
mb={6}
|
||
p={5}
|
||
borderRadius="xl"
|
||
bg={colorMode === "light" ? "gray.50" : "gray.700"}
|
||
boxShadow="md"
|
||
>
|
||
<Heading fontSize="xl" fontWeight={600} mb={2}>
|
||
{t('journal.pl.lesson.topicTitle')}
|
||
<Box as="span" ml={2} color={colorMode === "light" ? "blue.500" : "blue.300"}>
|
||
{ls.data?.body?.name}
|
||
</Box>
|
||
</Heading>
|
||
|
||
<Flex align="center" justify="space-between" mt={3}>
|
||
<Text color={colorMode === "light" ? "gray.600" : "gray.300"}>
|
||
{formatDate(ls.data?.body?.date, t('journal.pl.lesson.dateFormat'))}
|
||
</Text>
|
||
|
||
<Badge colorScheme="green" fontSize="md" borderRadius="full" px={3} py={1}>
|
||
{t('journal.pl.common.people')}: {animatedStudents.length}
|
||
</Badge>
|
||
</Flex>
|
||
</Box>
|
||
</motion.div>
|
||
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
transition={{ duration: 0.4, delay: 0.2 }}
|
||
>
|
||
{animatedStudents.length > 0 ? (
|
||
<StudentListView>
|
||
<AnimatePresence initial={true}>
|
||
{animatedStudents.map((student) => (
|
||
<motion.li
|
||
key={student.sub}
|
||
layout
|
||
initial={{ opacity: 0, scale: 0.6, y: 20 }}
|
||
animate={{
|
||
opacity: 1,
|
||
scale: 1,
|
||
y: 0,
|
||
boxShadow: student.isNew
|
||
? ['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.6, y: 20 }}
|
||
transition={{
|
||
type: "spring",
|
||
stiffness: 300,
|
||
damping: 25,
|
||
delay: 0.03 * animatedStudents.indexOf(student), // Уменьшенная задержка для более плавного появления
|
||
boxShadow: {
|
||
repeat: student.isNew ? 3 : 0,
|
||
duration: 1.5
|
||
}
|
||
}}
|
||
>
|
||
<UserCard
|
||
width="100%"
|
||
wrapperAS="div"
|
||
student={student}
|
||
present={true}
|
||
recentlyPresent={student.isNew}
|
||
/>
|
||
</motion.li>
|
||
))}
|
||
</AnimatePresence>
|
||
</StudentListView>
|
||
) : (
|
||
ls.data && (
|
||
<Center py={10} px={5}>
|
||
<Box
|
||
textAlign="center"
|
||
p={6}
|
||
borderRadius="xl"
|
||
bg={colorMode === "light" ? "gray.50" : "gray.700"}
|
||
boxShadow="md"
|
||
width="100%"
|
||
maxWidth="500px"
|
||
>
|
||
<motion.div
|
||
initial={{ scale: 0.9, opacity: 0 }}
|
||
animate={{ scale: 1, opacity: 1 }}
|
||
transition={{ duration: 0.5, delay: 0.3 }}
|
||
>
|
||
<Heading size="md" mb={4}>{t('journal.pl.lesson.noStudents')}</Heading>
|
||
<Text>{t('journal.pl.lesson.waitForStudents')}</Text>
|
||
</motion.div>
|
||
</Box>
|
||
</Center>
|
||
)
|
||
)}
|
||
</motion.div>
|
||
</Container>
|
||
)
|
||
}
|
||
|
||
export default UserPage
|