From 3d383f2e259f59066e6d7f949939d4b15b784a62 Mon Sep 17 00:00:00 2001
From: primakov <primakovpro@gmail.com>
Date: Sun, 23 Mar 2025 21:45:16 +0300
Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?=
 =?UTF-8?q?=D0=BD=D1=8B=20=D1=81=D1=82=D0=B8=D0=BB=D0=B8=20=D0=BA=D0=BE?=
 =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=B0=20UserCard=20?=
 =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?=
 =?UTF-8?q?=D0=B8=D1=8F=20=D0=B2=D0=B8=D0=B7=D1=83=D0=B0=D0=BB=D1=8C=D0=BD?=
 =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D0=B2=D0=BE=D1=81=D0=BF=D1=80=D0=B8=D1=8F?=
 =?UTF-8?q?=D1=82=D0=B8=D1=8F=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?=
 =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B0=D0=BD=D0=B8=D0=BC=D0=B0=D1=86?=
 =?UTF-8?q?=D0=B8=D0=B8=20=D0=BF=D1=80=D0=B8=20=D0=BD=D0=B0=D0=B2=D0=B5?=
 =?UTF-8?q?=D0=B4=D0=B5=D0=BD=D0=B8=D0=B8.=20=D0=A0=D0=B5=D0=B0=D0=BB?=
 =?UTF-8?q?=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4?=
 =?UTF-8?q?=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B0=20=D0=BE=D1=82=D0=BE=D0=B1?=
 =?UTF-8?q?=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BD=D0=B5=D0=B4?=
 =?UTF-8?q?=D0=B0=D0=B2=D0=BD=D0=BE=20=D0=BF=D1=80=D0=B8=D1=81=D1=83=D1=82?=
 =?UTF-8?q?=D1=81=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B8=D1=85=20=D1=81=D1=82?=
 =?UTF-8?q?=D1=83=D0=B4=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20=D1=81=20=D0=BF?=
 =?UTF-8?q?=D0=BE=D0=BC=D0=BE=D1=89=D1=8C=D1=8E=20=D0=B0=D0=BD=D0=B8=D0=BC?=
 =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?=
 =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?=
 =?UTF-8?q?=D0=BD=D1=82=20LessonDetail=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82?=
 =?UTF-8?q?=D1=81=D0=BB=D0=B5=D0=B6=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20?=
 =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D1=8B=D1=85=20=D1=81=D1=82=D1=83=D0=B4=D0=B5?=
 =?UTF-8?q?=D0=BD=D1=82=D0=BE=D0=B2=20=D0=B8=20=D0=B8=D1=85=20=D0=B0=D0=BD?=
 =?UTF-8?q?=D0=B8=D0=BC=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BF=D1=80=D0=B8=20?=
 =?UTF-8?q?=D0=BF=D0=BE=D1=8F=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B8.=20?=
 =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D1=8B=20=D1=81=D1=82?=
 =?UTF-8?q?=D0=B8=D0=BB=D0=B8=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=BE=D0=B2?=
 =?UTF-8?q?=20=D1=81=D1=82=D1=83=D0=B4=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20?=
 =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=B9=20?=
 =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B8=D0=B2=D0=BD=D0=BE=D1=81?=
 =?UTF-8?q?=D1=82=D0=B8=20=D0=B8=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?=
 =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D1=81=D0=BA=D0=BE=D0=B3?=
 =?UTF-8?q?=D0=BE=20=D0=BE=D0=BF=D1=8B=D1=82=D0=B0.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/components/user-card/style.ts             | 137 +++++++++---
 src/components/user-card/user-card.tsx        |  40 ++--
 src/pages/lesson-details.tsx                  | 123 ++++++++---
 src/pages/style.ts                            |  99 ++++++++-
 src/pages/user-page.tsx                       | 197 +++++++++++++++---
 .../lessons/access-code/create/success.json   |  32 +--
 6 files changed, 497 insertions(+), 131 deletions(-)

diff --git a/src/components/user-card/style.ts b/src/components/user-card/style.ts
index 1e90f6e..1691921 100644
--- a/src/components/user-card/style.ts
+++ b/src/components/user-card/style.ts
@@ -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);
   }
 `
\ No newline at end of file
diff --git a/src/components/user-card/user-card.tsx b/src/components/user-card/user-card.tsx
index 48246df..c9c667b 100644
--- a/src/components/user-card/user-card.tsx
+++ b/src/components/user-card/user-card.tsx
@@ -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,
-}
diff --git a/src/pages/lesson-details.tsx b/src/pages/lesson-details.tsx
index c405320..5699f00 100644
--- a/src/pages/lesson-details.tsx
+++ b/src/pages/lesson-details.tsx
@@ -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>
     </>
diff --git a/src/pages/style.ts b/src/pages/style.ts
index 2588042..d245088 100644
--- a/src/pages/style.ts
+++ b/src/pages/style.ts
@@ -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);
   }
 `
 
diff --git a/src/pages/user-page.tsx b/src/pages/user-page.tsx
index 1bf4392..ebddd0e 100644
--- a/src/pages/user-page.tsx
+++ b/src/pages/user-page.tsx
@@ -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>
   )
 }
diff --git a/stubs/mocks/lessons/access-code/create/success.json b/stubs/mocks/lessons/access-code/create/success.json
index 0f3ec54..8a51377 100644
--- a/stubs/mocks/lessons/access-code/create/success.json
+++ b/stubs/mocks/lessons/access-code/create/success.json
@@ -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",