feat: улучшения системы расписания и UI

- Добавлены таймауты (8 сек) для запросов расписания
- Реализована навигация по неделям с поддержкой параметра wk
- Улучшено кэширование с автоматической очисткой старых записей
- Добавлена валидация параметров в getSchedule
- Улучшен UI загрузки с анимированными сообщениями и предупреждениями
- Оптимизирована обработка ошибок и очистка памяти JSDOM
- Обновлены зависимости проекта
- Добавлена документация для старых файлов
This commit is contained in:
kilyabin
2025-11-30 22:15:07 +04:00
parent d3d33c1e08
commit 3345eb2e3f
10 changed files with 65 additions and 18 deletions

View File

@@ -11,6 +11,13 @@ export type ScheduleResult = {
availableWeeks?: WeekInfo[]
}
export class ScheduleTimeoutError extends Error {
constructor(message: string) {
super(message)
this.name = 'ScheduleTimeoutError'
}
}
export async function getSchedule(groupID: number, groupName: string, wk?: number, parseWeekNavigation: boolean = true): Promise<ScheduleResult> {
// Валидация параметров
if (!Number.isInteger(groupID) || groupID <= 0) {
@@ -23,9 +30,9 @@ export async function getSchedule(groupID: number, groupName: string, wk?: numbe
const url = `${PROXY_URL}/?mn=2&obj=${groupID}${wk ? `&wk=${wk}` : ''}`
// Добавляем таймаут 30 секунд для fetch запроса
// Добавляем таймаут 8 секунд для fetch запроса
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 30000) // 30 секунд
const timeoutId = setTimeout(() => controller.abort(), 8000) // 8 секунд
try {
const page = await fetch(url, { signal: controller.signal })
@@ -65,7 +72,7 @@ export async function getSchedule(groupID: number, groupName: string, wk?: numbe
} catch (error) {
clearTimeout(timeoutId)
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(`Request timeout while fetching ${PROXY_URL}`)
throw new ScheduleTimeoutError(`Request timeout while fetching ${PROXY_URL}`)
}
throw error
}

View File

@@ -1,7 +1,7 @@
import { Schedule } from '@/widgets/schedule'
import { Day } from '@/shared/model/day'
import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next'
import { getSchedule, ScheduleResult } from '@/app/agregator/schedule'
import { getSchedule, ScheduleResult, ScheduleTimeoutError } from '@/app/agregator/schedule'
import { NextSerialized, nextDeserialized, nextSerialized } from '@/app/utils/date-serializer'
import { NavBar } from '@/widgets/navbar'
import { LastUpdateAt } from '@/entities/last-update-at'
@@ -149,9 +149,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext<{ gr
cleanupCache()
}
} catch(e) {
// При таймауте или любой другой ошибке используем кэш, если он доступен
if (cachedSchedule?.lastFetched) {
scheduleResult = cachedSchedule.results
parsedAt = cachedSchedule.lastFetched
// Логируем использование кэша при таймауте
if (e instanceof ScheduleTimeoutError) {
console.warn(`Schedule fetch timeout for group ${group}, using cached data from ${cachedSchedule.lastFetched.toISOString()}`)
}
} else {
throw e
}

View File

@@ -23,3 +23,4 @@ export default function handler(

View File

@@ -23,3 +23,4 @@ export default function handler(

View File

@@ -57,7 +57,7 @@ export default function HomePage({ groups, groupsByCourse }: HomePageProps) {
<>
<Head>
<title>Расписание занятий Колледж Связи ПГУТИ</title>
<meta name="description" content="Расписание занятий для всех групп Колледжа Связи ПГУТИ. Выберите группу для просмотра расписания." />
<meta name="description" content="Расписание занятий для всех групп Колледжа Связи ПГУТИ" />
</Head>
<div className="min-h-screen p-4 md:p-8">
<div className="max-w-4xl mx-auto space-y-4">

View File

@@ -61,11 +61,15 @@ interface LoadingOverlayProps {
export function LoadingOverlay({ isLoading }: LoadingOverlayProps) {
const [currentMessage, setCurrentMessage] = React.useState<string>('')
const [messageOpacity, setMessageOpacity] = React.useState(0)
const [showError, setShowError] = React.useState(false)
const [errorOpacity, setErrorOpacity] = React.useState(0)
React.useEffect(() => {
if (!isLoading) {
setCurrentMessage('')
setMessageOpacity(0)
setShowError(false)
setErrorOpacity(0)
return
}
@@ -79,6 +83,15 @@ export function LoadingOverlay({ isLoading }: LoadingOverlayProps) {
setCurrentMessage(getRandomMessage())
setMessageOpacity(1)
// Таймер для показа сообщения об ошибке после 5 секунд
const errorTimeout = setTimeout(() => {
setShowError(true)
// Плавное появление с небольшой задержкой для анимации
setTimeout(() => {
setErrorOpacity(1)
}, 50)
}, 5000)
// Меняем сообщение каждые 2 секунды
const interval = setInterval(() => {
// Fade out
@@ -93,6 +106,7 @@ export function LoadingOverlay({ isLoading }: LoadingOverlayProps) {
return () => {
clearInterval(interval)
clearTimeout(errorTimeout)
}
}, [isLoading])
@@ -109,17 +123,32 @@ export function LoadingOverlay({ isLoading }: LoadingOverlayProps) {
aria-hidden={!isLoading}
>
{isLoading && (
<div className="flex flex-col items-center gap-4">
<div className="w-16 h-16">
<Spinner size="large" />
<>
<div className="flex flex-col items-center gap-4">
<div className="w-16 h-16">
<Spinner size="large" />
</div>
<div
className="min-h-[1.5rem] text-center transition-opacity duration-300"
style={{ opacity: messageOpacity }}
>
{currentMessage}
</div>
</div>
<div
className="min-h-[1.5rem] text-center transition-opacity duration-300"
style={{ opacity: messageOpacity }}
>
{currentMessage}
</div>
</div>
{showError && (
<div
className="fixed bottom-8 left-1/2 -translate-x-1/2 bg-background/10 backdrop-blur-sm border border-border/30 rounded-lg p-4 max-w-md mx-4 transition-all duration-500 ease-out"
style={{
opacity: errorOpacity,
transform: `translateX(-50%) translateY(${errorOpacity === 1 ? '0' : '100px'})`
}}
>
<p className="text-sm text-foreground text-center">
Не удается получить актуальное расписание с официального сайта. Возможно, сервер временно недоступен. Будут показаны данные из кэша. Попробуйте обновить страницу позже.
</p>
</div>
)}
</>
)}
</div>
)