Обновлены стили компонента 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

View File

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