Обновлены стили компонента UserCard для улучшения визуального восприятия и добавлены анимации при наведении. Реализована поддержка отображения недавно присутствующих студентов с помощью анимации. Обновлен компонент LessonDetail для отслеживания новых студентов и их анимации при появлении. Улучшены стили списков студентов для лучшей адаптивности и пользовательского опыта.
This commit is contained in:
parent
570ae4b171
commit
3d383f2e25
@ -1,26 +1,96 @@
|
||||
import styled from '@emotion/styled'
|
||||
import { css, keyframes } from '@emotion/react'
|
||||
|
||||
export const Avatar = styled.img`
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin: 0 auto;
|
||||
border-radius: 6px;
|
||||
// Правильное определение анимации с помощью keyframes
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const pulse = keyframes`
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(72, 187, 120, 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(72, 187, 120, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(72, 187, 120, 0);
|
||||
}
|
||||
`
|
||||
|
||||
export const Avatar = styled.img`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
`
|
||||
|
||||
export const NameOverlay = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 8px;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||
color: white;
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
.chakra-ui-dark & {
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||
}
|
||||
`
|
||||
|
||||
// Стили без интерполяций компонентов
|
||||
export const Wrapper = styled.div<{ warn?: boolean; width?: string | number }>`
|
||||
list-style: none;
|
||||
background-color: var(--chakra-colors-white);
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 2px 2px 6px var(--chakra-colors-blackAlpha-400);
|
||||
transition: all 0.5;
|
||||
position: relative;
|
||||
width: 180px;
|
||||
min-height: 190px;
|
||||
max-height: 200px;
|
||||
margin-right: 12px;
|
||||
padding-bottom: 22px;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
animation: ${fadeIn} 0.5s ease;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:hover > div:last-of-type:not(button) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.recent {
|
||||
animation: ${pulse} 1.5s infinite;
|
||||
border: 2px solid var(--chakra-colors-green-400);
|
||||
}
|
||||
|
||||
.chakra-ui-dark & {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&.recent {
|
||||
border: 2px solid var(--chakra-colors-green-300);
|
||||
}
|
||||
}
|
||||
|
||||
${({ width }) =>
|
||||
width
|
||||
? css`
|
||||
@ -31,35 +101,36 @@ export const Wrapper = styled.div<{ warn?: boolean; width?: string | number }>`
|
||||
${(props) =>
|
||||
props.warn
|
||||
? css`
|
||||
background-color: var(--chakra-colors-blackAlpha-800);
|
||||
opacity: 0.7;
|
||||
color: var(--chakra-colors-gray-200);
|
||||
filter: grayscale(0.8);
|
||||
`
|
||||
: ''}
|
||||
|
||||
.chakra-ui-dark & {
|
||||
background-color: var(--chakra-colors-gray-700);
|
||||
color: var(--chakra-colors-white);
|
||||
box-shadow: 2px 2px 6px var(--chakra-colors-blackAlpha-600);
|
||||
}
|
||||
|
||||
.chakra-ui-dark &.warn {
|
||||
background-color: var(--chakra-colors-blackAlpha-900);
|
||||
color: var(--chakra-colors-gray-300);
|
||||
}
|
||||
`
|
||||
|
||||
export const AddMissedButton = styled.button`
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 12px;
|
||||
right: 8px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
opacity: 0.2;
|
||||
color: inherit;
|
||||
background-color: var(--chakra-colors-blue-500);
|
||||
color: white;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
|
||||
:hover {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.chakra-ui-dark & {
|
||||
background-color: var(--chakra-colors-blue-400);
|
||||
}
|
||||
`
|
@ -1,10 +1,11 @@
|
||||
import React from 'react'
|
||||
import { sha256 } from 'js-sha256'
|
||||
import { useColorMode } from '@chakra-ui/react'
|
||||
import { Box, useColorMode } from '@chakra-ui/react'
|
||||
import { CheckCircleIcon, AddIcon } from '@chakra-ui/icons'
|
||||
|
||||
import { User } from '../../__data__/model'
|
||||
|
||||
import { AddMissedButton, Avatar, Wrapper } from './style'
|
||||
import { AddMissedButton, Avatar, Wrapper, NameOverlay } from './style'
|
||||
|
||||
export function getGravatarURL(email, user) {
|
||||
if (!email) return void 0
|
||||
@ -17,15 +18,17 @@ export function getGravatarURL(email, user) {
|
||||
export const UserCard = ({
|
||||
student,
|
||||
present,
|
||||
onAddUser,
|
||||
wrapperAS,
|
||||
width
|
||||
onAddUser = undefined,
|
||||
wrapperAS = 'div',
|
||||
width,
|
||||
recentlyPresent = false
|
||||
}: {
|
||||
student: User
|
||||
present: boolean
|
||||
width?: string | number
|
||||
onAddUser?: (user: User) => void
|
||||
wrapperAS?: React.ElementType<any, keyof React.JSX.IntrinsicElements>;
|
||||
recentlyPresent?: boolean
|
||||
}) => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
@ -34,25 +37,22 @@ export const UserCard = ({
|
||||
warn={!present}
|
||||
as={wrapperAS}
|
||||
width={width}
|
||||
className={!present ? 'warn' : ''}
|
||||
className={!present ? 'warn' : recentlyPresent ? 'recent' : ''}
|
||||
>
|
||||
<Avatar src={student.picture || getGravatarURL(student.email, null)} />
|
||||
<p style={{
|
||||
marginTop: 6,
|
||||
color: colorMode === 'light' ? 'inherit' : 'var(--chakra-colors-gray-100)'
|
||||
}}>
|
||||
{student.name || student.preferred_username}{' '}
|
||||
</p>
|
||||
<Avatar src={student.picture || getGravatarURL(student.email, null)} alt={student.name || student.preferred_username} />
|
||||
<NameOverlay>
|
||||
{student.name || student.preferred_username}
|
||||
{present && (
|
||||
<Box as="span" ml={2} display="inline-block" color={recentlyPresent ? "green.100" : "green.300"}>
|
||||
<CheckCircleIcon boxSize={3} />
|
||||
</Box>
|
||||
)}
|
||||
</NameOverlay>
|
||||
{onAddUser && !present && (
|
||||
<AddMissedButton onClick={() => onAddUser(student)}>
|
||||
add
|
||||
<AddMissedButton onClick={() => onAddUser(student)} aria-label="Отметить присутствие">
|
||||
<AddIcon boxSize={3} />
|
||||
</AddMissedButton>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
UserCard.defaultProps = {
|
||||
wrapperAS: 'div',
|
||||
onAddUser: void 0,
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -16,19 +16,96 @@ const reveal = keyframes`
|
||||
`
|
||||
|
||||
export const StudentList = styled.ul`
|
||||
padding-left: 0px;
|
||||
height: 600px;
|
||||
justify-content: space-evenly;
|
||||
padding-right: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
height: auto;
|
||||
max-height: 600px;
|
||||
padding-right: 0;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
/* Стили для motion.li элементов */
|
||||
li {
|
||||
list-style: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chakra-ui-dark &::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.chakra-ui-dark &::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
`
|
||||
|
||||
export const StudentListView = styled.ul`
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
|
||||
/* Адаптивные отступы на разных экранах */
|
||||
@media (max-width: 768px) {
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
}
|
||||
|
||||
/* Стили для контейнеров карточек */
|
||||
li {
|
||||
list-style: none;
|
||||
height: 100%;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
/* Добавляем плавные переходы между состояниями */
|
||||
li:hover {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Стилизация скроллбара */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chakra-ui-dark &::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.chakra-ui-dark &::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
`
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -7,19 +7,25 @@
|
||||
"name": "ВВОДНАЯ ПО JS.ПРИМЕНЕНИЕ И СПОСОБЫ ПОДКЛЮЧЕНИЯ НА СТРАНИЦЕ. LET, CONST. БАЗОВЫЕ ТИПЫ ДАННЫХ, ПРИВЕДЕНИЕ ТИПОВ. ПЕРЕМЕННЫЕ, ОБЛАСТЬ ВИДИМОСТИ ПЕРЕМЕННЫХ",
|
||||
"students": [
|
||||
{
|
||||
"sub": "f62905b1-e223-40ca-910f-c8d84c6137c1",
|
||||
"email_verified": true,
|
||||
"gravatar": "true",
|
||||
"name": "Александр Примаков",
|
||||
"groups": [
|
||||
"/inno-staff",
|
||||
"/microfrontend-admin-user"
|
||||
],
|
||||
"preferred_username": "primakov",
|
||||
"given_name": "Александр",
|
||||
"family_name": "Примаков",
|
||||
"email": "primakovpro@gmail.com"
|
||||
}
|
||||
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||
"email_verified": true,
|
||||
"name": "Мария Капитанова",
|
||||
"preferred_username": "maryaKapitan@gmail.com",
|
||||
"given_name": "Мария",
|
||||
"family_name": "Капитанова",
|
||||
"email": "maryaKapitan@gmail.com",
|
||||
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJgIjjOFD2YUSyRF5kH4jaysE6X5p-kq0Cg0CFncfMi=s96-c"
|
||||
},
|
||||
{
|
||||
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||
"email_verified": true,
|
||||
"name": "Евгения Жужова",
|
||||
"preferred_username": "zhuzhova@gmail.com",
|
||||
"given_name": "Евгения",
|
||||
"family_name": "Жужова",
|
||||
"email": "zhuzhova@gmail.com",
|
||||
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJUtJBAVBm642AxoGpMDDMV8CPu3MEoLjU3hmO7oisG=s96-c"
|
||||
}
|
||||
],
|
||||
"date": "2024-02-28T20:37:00.057Z",
|
||||
"created": "2024-02-28T20:37:00.057Z",
|
||||
|
Loading…
x
Reference in New Issue
Block a user