feat: добавлены анимации загрузки и переходов между разделами

- Добавлен полноэкранный индикатор загрузки с размытием фона (LoadingOverlay)
- Реализован глобальный контекст загрузки для отслеживания переходов между страницами
- Добавлены плавные fade-анимации при переходах между страницами
- Реализованы поочередные (stagger) анимации для карточек:
  * Карточки дней на странице расписания
  * Карточки уроков внутри каждого дня
  * Карточки групп на главной странице
- Анимации работают на всех устройствах, включая мобильные
- Улучшен компонент Spinner с поддержкой разных размеров
- Исправлена ошибка гидратации с вложенными <li> элементами в навигации
- Оптимизированы задержки анимаций для более быстрого отображения контента
This commit is contained in:
kilyabin
2025-11-23 01:29:09 +04:00
parent e5262f8203
commit cf0137a8d6
11 changed files with 276 additions and 60 deletions

View File

@@ -1,9 +1,25 @@
import '@/shared/styles/globals.css'
import type { AppProps } from 'next/app'
import { ThemeProvider } from '@/shared/providers/theme-provider'
import { LoadingContextProvider, LoadingContext } from '@/shared/context/loading-context'
import { LoadingOverlay } from '@/shared/ui/loading-overlay'
import Head from 'next/head'
import React from 'react'
export default function App({ Component, pageProps }: AppProps) {
function AppContent({ Component, pageProps }: AppProps) {
const { isLoading } = React.useContext(LoadingContext)
return (
<>
<div className="page-transition-wrapper">
<Component {...pageProps} />
</div>
<LoadingOverlay isLoading={isLoading} />
</>
)
}
export default function App(props: AppProps) {
return (
<>
<Head>
@@ -15,7 +31,9 @@ export default function App({ Component, pageProps }: AppProps) {
enableSystem
disableTransitionOnChange
>
<Component {...pageProps} />
<LoadingContextProvider>
<AppContent {...props} />
</LoadingContextProvider>
</ThemeProvider>
</>
)

View File

@@ -30,6 +30,17 @@ export default function HomePage({ groups, groupsByCourse }: HomePageProps) {
const [openCourses, setOpenCourses] = React.useState<Set<number>>(new Set([1]))
const [addGroupDialogOpen, setAddGroupDialogOpen] = React.useState(false)
// Подсчитываем смещения для каждого курса для последовательной анимации
const courseOffsets = React.useMemo(() => {
const offsets: { [course: number]: number } = {}
let totalGroups = 0
for (const course of [1, 2, 3, 4, 5]) {
offsets[course] = totalGroups
totalGroups += (groupsByCourse[course] || []).length
}
return { offsets, totalGroups }
}, [groupsByCourse])
const toggleCourse = (course: number) => {
setOpenCourses(prev => {
const next = new Set(prev)
@@ -50,55 +61,80 @@ export default function HomePage({ groups, groupsByCourse }: HomePageProps) {
</Head>
<div className="min-h-screen p-4 md:p-8">
<div className="max-w-4xl mx-auto space-y-4">
<div className="text-center space-y-2 mb-8">
<div className="text-center space-y-2 mb-8 stagger-card" style={{ animationDelay: '0.05s' } as React.CSSProperties}>
<h1 className="text-3xl md:text-4xl font-bold">Расписание занятий</h1>
<p className="text-muted-foreground">Выберите группу для просмотра расписания</p>
</div>
<div className="space-y-2">
{[1, 2, 3, 4, 5].map(course => {
{[1, 2, 3, 4, 5].map((course, courseIndex) => {
const courseGroups = groupsByCourse[course] || []
const isOpen = openCourses.has(course)
const courseOffset = courseOffsets.offsets[course]
if (courseGroups.length === 0) {
return null
}
return (
<Card key={course}>
<CardHeader
className="cursor-pointer"
onClick={() => toggleCourse(course)}
>
<div className="flex items-center justify-between">
<CardTitle className="text-xl">
{course} курс
</CardTitle>
<ChevronDown
className={cn(
"h-5 w-5 transition-transform duration-200",
isOpen && "transform rotate-180"
)}
/>
</div>
</CardHeader>
{isOpen && (
<CardContent>
<div className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2">
{courseGroups.map(({ id, name }) => (
<Link key={id} href={`/${id}`}>
<Button
variant="outline"
className="w-full justify-center h-auto py-3 px-2 sm:px-4 text-sm sm:text-base whitespace-nowrap"
>
{name}
</Button>
</Link>
))}
<div
key={course}
className="stagger-card"
style={{
animationDelay: `${0.1 + courseIndex * 0.05}s`,
} as React.CSSProperties}
>
<Card>
<CardHeader
className="cursor-pointer"
onClick={() => toggleCourse(course)}
>
<div className="flex items-center justify-between">
<CardTitle className="text-xl">
{course} курс
</CardTitle>
<ChevronDown
className={cn(
"h-5 w-5 transition-transform duration-200",
isOpen && "transform rotate-180"
)}
/>
</div>
</CardContent>
)}
</Card>
</CardHeader>
{isOpen && (
<CardContent>
<div className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2">
{courseGroups.map(({ id, name }, groupIndex) => {
// Последовательная анимация: каждый следующий элемент с задержкой
// courseOffset - это количество групп во всех предыдущих курсах
// groupIndex - это индекс в текущем курсе
// Итого: последовательный счетчик для всех групп подряд
const globalIndex = courseOffset + groupIndex
const delay = 0.15 + globalIndex * 0.04
return (
<div
key={id}
className="stagger-card"
style={{
animationDelay: `${delay}s`,
} as React.CSSProperties}
>
<Link href={`/${id}`}>
<Button
variant="outline"
className="w-full justify-center h-auto py-3 px-2 sm:px-4 text-sm sm:text-base whitespace-nowrap"
>
{name}
</Button>
</Link>
</div>
)
})}
</div>
</CardContent>
)}
</Card>
</div>
)
})}
</div>
@@ -112,26 +148,39 @@ export default function HomePage({ groups, groupsByCourse }: HomePageProps) {
)}
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 mt-8">
<Button
variant="secondary"
onClick={() => setAddGroupDialogOpen(true)}
className="gap-2"
<div
className="stagger-card"
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + 0.05}s` } as React.CSSProperties}
>
<Button
variant="secondary"
onClick={() => setAddGroupDialogOpen(true)}
className="gap-2"
>
<MdAdd className="h-4 w-4" />
Добавить группу
</Button>
</div>
<div
className="relative stagger-card"
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + 0.08}s` } as React.CSSProperties}
>
<MdAdd className="h-4 w-4" />
Добавить группу
</Button>
<div className="relative">
<ThemeSwitcher />
<span className="absolute -bottom-5 left-1/2 transform -translate-x-1/2 text-xs text-muted-foreground whitespace-nowrap sm:hidden">
Тема
</span>
</div>
<Link href={GITHUB_REPO_URL} target="_blank" rel="noopener noreferrer">
<Button variant="outline" className="gap-2">
<FaGithub className="h-4 w-4" />
GitHub
</Button>
</Link>
<div
className="stagger-card"
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + 0.11}s` } as React.CSSProperties}
>
<Link href={GITHUB_REPO_URL} target="_blank" rel="noopener noreferrer">
<Button variant="outline" className="gap-2">
<FaGithub className="h-4 w-4" />
GitHub
</Button>
</Link>
</div>
</div>
</div>
</div>