Обновлен компонент CourseCard: добавлены адаптивные размеры и улучшена компоновка для различных экранов. Реализована возможность сворачивания/разворачивания списка уроков. Удален компонент CourseDetails, его функциональность интегрирована в CourseCard. Обновлен компонент CoursesList для поддержки адаптивного дизайна и улучшения пользовательского интерфейса.
This commit is contained in:
@@ -34,13 +34,16 @@ import {
|
||||
TagLeftIcon,
|
||||
Wrap,
|
||||
WrapItem,
|
||||
useBreakpointValue,
|
||||
useMediaQuery,
|
||||
Icon
|
||||
} from '@chakra-ui/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FaExpand, FaCompress } from 'react-icons/fa'
|
||||
|
||||
import { api } from '../../__data__/api/api'
|
||||
import { ArrowUpIcon, LinkIcon, CalendarIcon, ViewIcon, WarningIcon, StarIcon, TimeIcon } from '@chakra-ui/icons'
|
||||
import { Course } from '../../__data__/model'
|
||||
import { CourseDetails } from './course-details'
|
||||
|
||||
export const CourseCard = ({ course }: { course: Course }) => {
|
||||
const [getLessonList, populatedCourse] = api.useLazyGetCourseByIdQuery()
|
||||
@@ -51,9 +54,25 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
}),
|
||||
})
|
||||
const [isOpened, setIsOpened] = useState(false)
|
||||
const [isLessonsExpanded, setIsLessonsExpanded] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const { colorMode } = useColorMode()
|
||||
|
||||
// Адаптивные размеры и компоновка для различных размеров экрана
|
||||
const headingSize = useBreakpointValue({ base: 'sm', md: 'md' })
|
||||
const buttonSize = useBreakpointValue({ base: 'xs', md: 'md' })
|
||||
const tagSize = useBreakpointValue({ base: 'sm', md: 'md' })
|
||||
const avatarSize = useBreakpointValue({ base: 'xs', md: 'sm' })
|
||||
const cardPadding = useBreakpointValue({ base: 2, md: 4 })
|
||||
|
||||
// Используем медиа-запросы для определения направления бейджей
|
||||
const [isLargerThanSm] = useMediaQuery("(min-width: 480px)")
|
||||
const [badgeDirection, setBadgeDirection] = useState<'column' | 'row'>('row')
|
||||
|
||||
useEffect(() => {
|
||||
setBadgeDirection(isLargerThanSm ? 'row' : 'column')
|
||||
}, [isLargerThanSm])
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpened) {
|
||||
getLessonList(course.id, true)
|
||||
@@ -64,6 +83,10 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
setIsOpened((opened) => !opened)
|
||||
}, [setIsOpened])
|
||||
|
||||
const handleToggleExpand = useCallback(() => {
|
||||
setIsLessonsExpanded((expanded) => !expanded)
|
||||
}, [setIsLessonsExpanded])
|
||||
|
||||
// Рассчитываем статистику курса и посещаемости
|
||||
const stats = useMemo(() => {
|
||||
if (!populatedCourse.data) {
|
||||
@@ -173,9 +196,9 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
bg={colorMode === 'dark' ? 'gray.700' : 'white'}
|
||||
borderColor={colorMode === 'dark' ? 'gray.600' : 'gray.200'}
|
||||
>
|
||||
<CardHeader pb={2}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Heading as="h2" size="md">
|
||||
<CardHeader pb={2} px={{ base: 3, md: 5 }}>
|
||||
<Flex justify="space-between" align="center" flexWrap="wrap">
|
||||
<Heading as="h2" size={headingSize} mb={{ base: 2, md: 0 }}>
|
||||
{course.name}
|
||||
</Heading>
|
||||
<Tooltip label={isOpened ? t('journal.pl.close') : t('journal.pl.course.viewDetails')}>
|
||||
@@ -190,21 +213,37 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<HStack spacing={2} mt={2}>
|
||||
<Badge colorScheme="blue">
|
||||
<HStack spacing={1}>
|
||||
<CalendarIcon boxSize="3" />
|
||||
<Text>{dayjs(course.startDt).format('DD.MM.YYYY')}</Text>
|
||||
<Flex gap={2} mt={2} flexWrap="wrap">
|
||||
{badgeDirection === 'column' ? (
|
||||
<VStack align="start" spacing={2} width="100%">
|
||||
<Badge colorScheme="blue">
|
||||
<HStack spacing={1}>
|
||||
<CalendarIcon boxSize="3" />
|
||||
<Text>{dayjs(course.startDt).format('DD.MM.YYYY')}</Text>
|
||||
</HStack>
|
||||
</Badge>
|
||||
<Badge colorScheme="purple">
|
||||
{stats.totalLessons} {t('journal.pl.common.lesson').toLowerCase()}
|
||||
</Badge>
|
||||
</VStack>
|
||||
) : (
|
||||
<HStack spacing={2}>
|
||||
<Badge colorScheme="blue">
|
||||
<HStack spacing={1}>
|
||||
<CalendarIcon boxSize="3" />
|
||||
<Text>{dayjs(course.startDt).format('DD.MM.YYYY')}</Text>
|
||||
</HStack>
|
||||
</Badge>
|
||||
<Badge colorScheme="purple">
|
||||
{stats.totalLessons} {t('journal.pl.common.lesson').toLowerCase()}
|
||||
</Badge>
|
||||
</HStack>
|
||||
</Badge>
|
||||
<Badge colorScheme="purple">
|
||||
{stats.totalLessons} {t('journal.pl.common.lesson').toLowerCase()}
|
||||
</Badge>
|
||||
</HStack>
|
||||
)}
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
|
||||
{!isOpened && (
|
||||
<CardBody pt={2} pb={3}>
|
||||
<CardBody pt={2} pb={3} px={{ base: 3, md: 5 }}>
|
||||
{lessonListLoading ? (
|
||||
<Flex justify="center" py={3}>
|
||||
<Spinner size="sm" />
|
||||
@@ -214,7 +253,7 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
<Text fontSize="sm" fontWeight="medium" mb={2}>
|
||||
{t('journal.pl.attendance.stats.topStudents')}:
|
||||
</Text>
|
||||
<AvatarGroup size="sm" max={3} mb={1}>
|
||||
<AvatarGroup size={avatarSize} max={3} mb={1}>
|
||||
{attendanceStats.topStudents.map(student => (
|
||||
<Avatar
|
||||
key={student.id}
|
||||
@@ -233,8 +272,8 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
)}
|
||||
|
||||
{isOpened && (
|
||||
<CardBody pt={2}>
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4} mb={4}>
|
||||
<CardBody pt={2} px={{ base: 3, md: 5 }}>
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={{ base: 3, md: 4 }} mb={4}>
|
||||
<Stat>
|
||||
<StatLabel>{t('journal.pl.course.completedLessons')}</StatLabel>
|
||||
<HStack align="baseline">
|
||||
@@ -253,9 +292,9 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
|
||||
<Stat>
|
||||
<StatLabel>{t('journal.pl.course.upcomingLessons')}</StatLabel>
|
||||
<HStack align="baseline">
|
||||
<HStack align="baseline" flexWrap="wrap">
|
||||
<StatNumber>{stats.upcomingLessons}</StatNumber>
|
||||
<Text color="gray.500">
|
||||
<Text color="gray.500" fontSize={{ base: 'xs', md: 'sm' }}>
|
||||
<TimeIcon ml={1} mr={1} />
|
||||
{populatedCourse.data?.lessons
|
||||
.filter(lesson => dayjs(lesson.date).isAfter(dayjs()))
|
||||
@@ -290,9 +329,9 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
<VStack align="stretch" spacing={2}>
|
||||
{attendanceStats.topStudents.map((student, index) => (
|
||||
<HStack key={student.id} spacing={2}>
|
||||
<Avatar size="sm" name={student.name} src={student.avatarUrl} />
|
||||
<Avatar size={avatarSize} name={student.name} src={student.avatarUrl} />
|
||||
<Box flex="1">
|
||||
<Text fontSize="sm" fontWeight="medium">{student.name}</Text>
|
||||
<Text fontSize="sm" fontWeight="medium" isTruncated maxWidth={{ base: '120px', sm: '100%' }}>{student.name}</Text>
|
||||
<Progress
|
||||
value={student.percent}
|
||||
size="xs"
|
||||
@@ -318,9 +357,9 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
<VStack align="stretch" spacing={2}>
|
||||
{attendanceStats.lowAttendanceStudents.map((student) => (
|
||||
<HStack key={student.id} spacing={2}>
|
||||
<Avatar size="sm" name={student.name} src={student.avatarUrl} />
|
||||
<Avatar size={avatarSize} name={student.name} src={student.avatarUrl} />
|
||||
<Box flex="1">
|
||||
<Text fontSize="sm" fontWeight="medium">{student.name}</Text>
|
||||
<Text fontSize="sm" fontWeight="medium" isTruncated maxWidth={{ base: '120px', sm: '100%' }}>{student.name}</Text>
|
||||
<Progress
|
||||
value={student.percent}
|
||||
size="xs"
|
||||
@@ -349,8 +388,19 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
|
||||
{!populatedCourse.isFetching && populatedCourse.isSuccess && populatedCourse.data && (
|
||||
<>
|
||||
<Heading size="sm" mb={3}>{t('journal.pl.lesson.list')}</Heading>
|
||||
<VStack align="stretch" spacing={2} maxH="300px" overflowY="auto" pr={2}>
|
||||
<Flex justify="space-between" align="center" mb={3}>
|
||||
<Heading size="sm">{t('journal.pl.lesson.list')}</Heading>
|
||||
<Tooltip label={isLessonsExpanded ? t('journal.pl.lesson.collapse') : t('journal.pl.lesson.expand')}>
|
||||
<IconButton
|
||||
aria-label={isLessonsExpanded ? t('journal.pl.lesson.collapse') : t('journal.pl.lesson.expand')}
|
||||
icon={isLessonsExpanded ? <Icon as={FaCompress} /> : <Icon as={FaExpand} />}
|
||||
size="xs"
|
||||
onClick={handleToggleExpand}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
|
||||
<VStack align="stretch" spacing={2} maxH={isLessonsExpanded ? "none" : "300px"} overflowY={isLessonsExpanded ? "visible" : "auto"} pr={2}>
|
||||
{[...populatedCourse.data.lessons]
|
||||
.sort((a, b) => dayjs(b.date).valueOf() - dayjs(a.date).valueOf())
|
||||
.map(lesson => {
|
||||
@@ -379,10 +429,18 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
(colorMode === 'dark' ? 'blue.400' : 'blue.500')
|
||||
}
|
||||
>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Flex justify="space-between" align={{ base: 'flex-start', sm: 'center' }} flexDirection={{ base: 'column', sm: 'row' }} gap={{ base: 2, sm: 0 }}>
|
||||
<Box>
|
||||
<Text fontWeight="medium">{lesson.name}</Text>
|
||||
<HStack spacing={2} mt={1}>
|
||||
<Text
|
||||
fontWeight="medium"
|
||||
fontSize={{ base: 'sm', md: 'md' }}
|
||||
noOfLines={2}
|
||||
wordBreak="break-word"
|
||||
maxWidth={{ base: '100%', sm: '200px', md: '300px' }}
|
||||
>
|
||||
{lesson.name}
|
||||
</Text>
|
||||
<HStack spacing={2} mt={1} flexWrap="wrap">
|
||||
<Tag size="sm" colorScheme={isPast ? "green" : "blue"} borderRadius="full">
|
||||
<TagLeftIcon as={CalendarIcon} boxSize='10px' />
|
||||
<TagLabel>{dayjs(lesson.date).format('DD.MM.YYYY')}</TagLabel>
|
||||
@@ -405,6 +463,8 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
variant="ghost"
|
||||
colorScheme="blue"
|
||||
leftIcon={<ViewIcon />}
|
||||
ml={{ base: 0, sm: 'auto' }}
|
||||
alignSelf={{ base: 'flex-end', sm: 'center' }}
|
||||
>
|
||||
{t('journal.pl.common.open')}
|
||||
</Button>
|
||||
@@ -418,16 +478,17 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
</CardBody>
|
||||
)}
|
||||
|
||||
<CardFooter pt={2}>
|
||||
<ButtonGroup spacing={2} width="100%">
|
||||
<CardFooter pt={2} px={{ base: 3, md: 5 }}>
|
||||
<ButtonGroup spacing={{ base: 1, md: 2 }} width="100%" flexDirection={{ base: 'column', sm: 'row' }}>
|
||||
<Tooltip label={t('journal.pl.lesson.list')}>
|
||||
<Button
|
||||
leftIcon={<ViewIcon />}
|
||||
as={ConnectedLink}
|
||||
colorScheme="blue"
|
||||
size="md"
|
||||
size={buttonSize}
|
||||
flexGrow={1}
|
||||
to={`${getNavigationValue('journal.main')}/lessons-list/${course._id}`}
|
||||
mb={{ base: 2, sm: 0 }}
|
||||
>
|
||||
{t('journal.pl.lesson.list')}
|
||||
</Button>
|
||||
@@ -440,7 +501,7 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
as={ConnectedLink}
|
||||
variant="outline"
|
||||
colorScheme="blue"
|
||||
size="md"
|
||||
size={buttonSize}
|
||||
flexGrow={1}
|
||||
to={generatePath(
|
||||
`${getNavigationValue('journal.main')}${getNavigationValue('link.journal.attendance')}`,
|
||||
|
||||
Reference in New Issue
Block a user