Обновлены стили компонента UserCard для улучшения визуального восприятия и добавлены анимации при наведении. Реализована поддержка отображения недавно присутствующих студентов с помощью анимации. Обновлен компонент LessonDetail для отслеживания новых студентов и их анимации при появлении. Улучшены стили списков студентов для лучшей адаптивности и пользовательского опыта.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
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 dayjs from 'dayjs'
|
||||
@@ -12,18 +13,64 @@ import {
|
||||
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 (
|
||||
@@ -42,13 +89,30 @@ const UserPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{acc.isLoading && <h1>{t('journal.pl.common.sending')}</h1>}
|
||||
{acc.isSuccess && <h1>{t('journal.pl.common.success')}</h1>}
|
||||
<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">
|
||||
<Alert status="warning" borderRadius="lg">
|
||||
<AlertIcon />
|
||||
{(acc as any).error?.data?.body?.errorMessage ===
|
||||
'Code is expired' ? (
|
||||
@@ -60,31 +124,106 @@ const UserPage = () => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box mb={6}>
|
||||
<Text fontSize={18} fontWeight={600} as="h1" mt="4" mb="3">
|
||||
{t('journal.pl.lesson.topicTitle')} {ls.data?.body?.name}
|
||||
</Text>
|
||||
|
||||
<span>{dayjs(ls.data?.body?.date).format(t('journal.pl.lesson.dateFormat'))}</span>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
as="ul"
|
||||
display="flex"
|
||||
flexWrap="wrap"
|
||||
justifyContent="center"
|
||||
gap={3}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
{ls.data?.body?.students?.map((student) => (
|
||||
<UserCard
|
||||
width="40%"
|
||||
wrapperAS="li"
|
||||
key={student.sub}
|
||||
student={student}
|
||||
present
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<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"}>
|
||||
{dayjs(ls.data?.body?.date).format(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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user