feat: исправление мобильной версии и улучшение UX
- Исправлена мобильная версия: добавлена горизонтальная прокрутка навигации, оптимизированы отступы и размеры элементов для touch-интерфейсов - Устранено зависание на мобильных: удален бесконечный цикл в date-serializer.ts - Улучшена читаемость: сделаны светлее описание пар, дни недели и текст последнего обновления (текущий день остается выделенным) - Добавлена автоматическая прокрутка до текущего дня при загрузке страницы - Добавлено отображение 'Пары нет' для отмененных пар при замене - Оптимизированы скрипты установки: добавлена проверка зависимостей перед установкой для ускорения повторных запусков - Исправлено отображение адреса и аудитории на мобильных устройствах - Улучшены диалоги и touch-цели для мобильных устройств
This commit is contained in:
@@ -21,19 +21,29 @@ export function Day({ day }: {
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const dayPassed = day.date.getTime() < today.getTime()
|
||||
const dayDate = new Date(day.date)
|
||||
dayDate.setHours(0, 0, 0, 0)
|
||||
const dayPassed = dayDate.getTime() < today.getTime()
|
||||
const isToday = dayDate.getTime() === today.getTime()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 md:gap-5">
|
||||
<h2 className={cx('scroll-m-20 text-2xl md:text-4xl font-extrabold tracking-tight lg:text-5xl', { 'text-[hsl(var(--grayed-out))]': dayPassed })} id={getDayOfWeek(day.date)}>
|
||||
{dayOfWeek} <span className={cx('ml-3', { 'text-border': !dayPassed })}>{Intl.DateTimeFormat('ru-RU', {
|
||||
<h2 className={cx('scroll-m-20 text-xl md:text-2xl lg:text-4xl font-extrabold tracking-tight', {
|
||||
'text-[hsl(var(--grayed-out))]': dayPassed,
|
||||
'text-foreground': isToday,
|
||||
'text-muted-foreground': !dayPassed && !isToday
|
||||
})} id={getDayOfWeek(day.date)}>
|
||||
{dayOfWeek} <span className={cx('ml-2 md:ml-3', {
|
||||
'text-border': isToday,
|
||||
'text-muted-foreground/70': !dayPassed && !isToday
|
||||
})}>{Intl.DateTimeFormat('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
// year: 'numeric'
|
||||
}).format(day.date)}</span>
|
||||
</h2>
|
||||
<div>
|
||||
<div className='overflow-auto md:snap-x md:snap-proximity md:-translate-x-16 md:w-[calc(100%+8rem)] scrollbar-hide'>
|
||||
<div className='overflow-x-hidden md:overflow-x-auto md:snap-x md:snap-proximity md:-translate-x-16 md:w-[calc(100%+8rem)] scrollbar-hide'>
|
||||
<div className="flex flex-col md:flex-row gap-4 w-full md:w-max">
|
||||
<div className='snap-start hidden md:block' style={{ flex: '0 0 3rem' }} />
|
||||
{day.lessons.map((lesson, i) => (
|
||||
|
||||
@@ -1,14 +1,54 @@
|
||||
import type { Day as DayType } from '@/shared/model/day'
|
||||
import { Day } from '@/widgets/schedule/day'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { getDayOfWeek } from '@/shared/utils'
|
||||
|
||||
export function Schedule({ days }: {
|
||||
days: DayType[]
|
||||
}) {
|
||||
const group = useRouter().query['group']
|
||||
const hasScrolledRef = React.useRef(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasScrolledRef.current || typeof window === 'undefined') return
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
// Находим текущий день
|
||||
const todayDay = days.find(day => {
|
||||
const dayDate = new Date(day.date)
|
||||
dayDate.setHours(0, 0, 0, 0)
|
||||
return dayDate.getTime() === today.getTime()
|
||||
})
|
||||
|
||||
if (todayDay) {
|
||||
// Небольшая задержка для завершения рендеринга
|
||||
const timeoutId = setTimeout(() => {
|
||||
const elementId = getDayOfWeek(todayDay.date)
|
||||
const element = document.getElementById(elementId)
|
||||
|
||||
if (element) {
|
||||
// Прокручиваем с отступом для sticky header
|
||||
const headerOffset = 100
|
||||
const elementPosition = element.getBoundingClientRect().top
|
||||
const offsetPosition = elementPosition + window.pageYOffset - headerOffset
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
hasScrolledRef.current = true
|
||||
}
|
||||
}, 100)
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
}
|
||||
}, [days])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-8 md:p-16 gap-12 md:gap-14">
|
||||
<div className="flex flex-col p-4 md:p-8 lg:p-16 gap-6 md:gap-12 lg:gap-14">
|
||||
{days.map((day, i) => (
|
||||
<Day day={day} key={`${group}_day${i}`} />
|
||||
))}
|
||||
|
||||
@@ -34,6 +34,9 @@ export function Lesson({ lesson, width = 350 }: {
|
||||
const hasPlace = 'place' in lesson && lesson.place
|
||||
|
||||
const isFallbackDiscipline = 'fallbackDiscipline' in lesson && lesson.fallbackDiscipline
|
||||
const hasSubject = 'subject' in lesson && lesson.subject
|
||||
const hasContent = hasSubject || (isFallbackDiscipline && lesson.fallbackDiscipline) || (lesson.topic && lesson.topic.trim())
|
||||
const isCancelled = lesson.isChange && !hasContent
|
||||
|
||||
const getTeacherPhoto = (url?: string) => {
|
||||
if(url) {
|
||||
@@ -63,9 +66,9 @@ export function Lesson({ lesson, width = 350 }: {
|
||||
<Card className={`w-full ${width === 450 ? 'md:w-[450px] md:min-w-[450px] md:max-w-[450px]' : 'md:w-[350px] md:min-w-[350px] md:max-w-[350px]'} flex flex-col relative overflow-hidden snap-start scroll-ml-16 shrink-0`}>
|
||||
{lesson.isChange && <div className='absolute top-0 left-0 w-full h-full bg-gradient-to-br from-[#ffc60026] to-[#95620026] pointer-events-none'></div>}
|
||||
<CardHeader>
|
||||
<div className='flex gap-4'>
|
||||
<div className='flex gap-2 md:gap-4'>
|
||||
{hasTeacher ? (
|
||||
<Avatar>
|
||||
<Avatar className="flex-shrink-0">
|
||||
<AvatarImage
|
||||
src={getTeacherPhoto(teacherObj?.picture)!}
|
||||
alt={lesson.teacher}
|
||||
@@ -76,45 +79,61 @@ export function Lesson({ lesson, width = 350 }: {
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar>
|
||||
<Avatar className="flex-shrink-0">
|
||||
<AvatarFallback><MdSchool /></AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
<div className='flex flex-col gap-1'>
|
||||
{'subject' in lesson && <CardTitle className='hyphens-auto'>{lesson.subject}</CardTitle>}
|
||||
<CardDescription>
|
||||
<div className='flex flex-col gap-1 min-w-0 flex-1'>
|
||||
{isCancelled ? (
|
||||
<CardTitle className='hyphens-auto break-words text-base md:text-lg'>Пары нет</CardTitle>
|
||||
) : (
|
||||
hasSubject && <CardTitle className='hyphens-auto break-words text-base md:text-lg'>{lesson.subject}</CardTitle>
|
||||
)}
|
||||
<CardDescription className="text-xs md:text-sm">
|
||||
{lesson.time.start} - {lesson.time.end}{
|
||||
}{lesson.time.hint && <span className='font-bold'> ({lesson.time.hint})</span>}
|
||||
</CardDescription>
|
||||
{hasTeacher && lesson.teacher && (
|
||||
<CardDescription className='text-sm font-medium'>
|
||||
{!isCancelled && hasTeacher && lesson.teacher && (
|
||||
<CardDescription className='text-xs md:text-sm font-medium break-words'>
|
||||
{lesson.teacher}
|
||||
</CardDescription>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{lesson.type && <><Badge>{lesson.type}</Badge>{' '} </>}
|
||||
{isFallbackDiscipline && (
|
||||
<span className='leading-relaxed hyphens-auto block'>{lesson.fallbackDiscipline}</span>
|
||||
)}
|
||||
{lesson.topic ? (
|
||||
<span className='leading-relaxed hyphens-auto'>{lesson.topic}</span>
|
||||
<CardContent className="text-sm md:text-base">
|
||||
{isCancelled ? (
|
||||
<span className='text-muted-foreground italic'>Пара отменена</span>
|
||||
) : (
|
||||
!isFallbackDiscipline && <span className='text-border font-semibold'>Нет описания пары</span>
|
||||
<>
|
||||
{lesson.type && <><Badge className="text-xs md:text-sm">{lesson.type}</Badge>{' '} </>}
|
||||
{isFallbackDiscipline && (
|
||||
<span className='leading-relaxed hyphens-auto block break-words text-muted-foreground'>{lesson.fallbackDiscipline}</span>
|
||||
)}
|
||||
{lesson.topic ? (
|
||||
<span className='leading-relaxed hyphens-auto break-words text-muted-foreground'>{lesson.topic}</span>
|
||||
) : (
|
||||
!isFallbackDiscipline && hasSubject && <span className='text-border font-semibold'>Нет описания пары</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isCancelled && ('place' in lesson && lesson.place) && (
|
||||
<div className='flex flex-col text-muted-foreground text-xs break-words mt-3 md:hidden'>
|
||||
<span className='flex items-center gap-2'><BsFillGeoAltFill /> <span className="break-words">{lesson.place.address}</span></span>
|
||||
<span className='font-bold flex items-center gap-2'><RiGroup2Fill /> {lesson.place.classroom}</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
{(Boolean(lesson.resources.length) || hasPlace) && (
|
||||
<CardFooter className="flex justify-between mt-auto">
|
||||
{!isCancelled && (Boolean(lesson.resources.length) || ('place' in lesson && lesson.place)) && (
|
||||
<CardFooter className="flex flex-col sm:flex-row justify-between gap-2 mt-auto">
|
||||
{('place' in lesson && lesson.place) ? (
|
||||
<div className='flex flex-col text-muted-foreground text-xs'>
|
||||
<span className='flex items-center gap-2'><BsFillGeoAltFill /> {lesson.place.address}</span>
|
||||
<div className='hidden md:flex flex-col text-muted-foreground text-xs break-words'>
|
||||
<span className='flex items-center gap-2'><BsFillGeoAltFill /> <span className="break-words">{lesson.place.address}</span></span>
|
||||
<span className='font-bold flex items-center gap-2'><RiGroup2Fill /> {lesson.place.classroom}</span>
|
||||
</div>
|
||||
) : <span />}
|
||||
{Boolean(lesson.resources.length) && (
|
||||
<Button onClick={handleOpenResources}><AiOutlineFolderView /> Материалы</Button>
|
||||
<Button onClick={handleOpenResources} className="min-h-[44px] w-full sm:w-auto"><AiOutlineFolderView /> Материалы</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user