diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index e7609ee..19dbb64 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -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 ( + <> +
+ +
+ + + ) +} + +export default function App(props: AppProps) { return ( <> @@ -15,7 +31,9 @@ export default function App({ Component, pageProps }: AppProps) { enableSystem disableTransitionOnChange > - + + + ) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index b9c2f64..233ca06 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -30,6 +30,17 @@ export default function HomePage({ groups, groupsByCourse }: HomePageProps) { const [openCourses, setOpenCourses] = React.useState>(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) {
-
+

Расписание занятий

Выберите группу для просмотра расписания

- {[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 ( - - toggleCourse(course)} - > -
- - {course} курс - - -
-
- {isOpen && ( - -
- {courseGroups.map(({ id, name }) => ( - - - - ))} +
+ + toggleCourse(course)} + > +
+ + {course} курс + +
- - )} -
+ + {isOpen && ( + +
+ {courseGroups.map(({ id, name }, groupIndex) => { + // Последовательная анимация: каждый следующий элемент с задержкой + // courseOffset - это количество групп во всех предыдущих курсах + // groupIndex - это индекс в текущем курсе + // Итого: последовательный счетчик для всех групп подряд + const globalIndex = courseOffset + groupIndex + const delay = 0.15 + globalIndex * 0.04 + return ( +
+ + + +
+ ) + })} +
+
+ )} + +
) })}
@@ -112,26 +148,39 @@ export default function HomePage({ groups, groupsByCourse }: HomePageProps) { )}
- +
+
- - Добавить группу - -
Тема
- - - +
+ + + +
diff --git a/src/shared/context/loading-context.tsx b/src/shared/context/loading-context.tsx new file mode 100644 index 0000000..217a3be --- /dev/null +++ b/src/shared/context/loading-context.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { useRouter } from 'next/router' + +interface LoadingContextType { + isLoading: boolean +} + +export const LoadingContext = React.createContext({ + isLoading: false, +}) + +export function LoadingContextProvider({ children }: React.PropsWithChildren) { + const [isLoading, setIsLoading] = React.useState(false) + const router = useRouter() + + React.useEffect(() => { + const handleRouteChangeStart = () => { + setIsLoading(true) + } + + const handleRouteChangeComplete = () => { + setIsLoading(false) + } + + const handleRouteChangeError = () => { + setIsLoading(false) + } + + router.events.on('routeChangeStart', handleRouteChangeStart) + router.events.on('routeChangeComplete', handleRouteChangeComplete) + router.events.on('routeChangeError', handleRouteChangeError) + + return () => { + router.events.off('routeChangeStart', handleRouteChangeStart) + router.events.off('routeChangeComplete', handleRouteChangeComplete) + router.events.off('routeChangeError', handleRouteChangeError) + } + }, [router]) + + return ( + + {children} + + ) +} + diff --git a/src/shared/styles/globals.css b/src/shared/styles/globals.css index 8a3c148..49576be 100644 --- a/src/shared/styles/globals.css +++ b/src/shared/styles/globals.css @@ -146,4 +146,37 @@ height: auto; } } +} + +/* Page transition animations */ +@layer utilities { + .page-transition-wrapper { + animation: fadeIn 0.3s ease-in-out; + } + + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + /* Stagger animation for cards - works on all devices */ + .stagger-card { + animation: fadeInSlideUp 0.3s ease-out forwards; + opacity: 0; + } + + @keyframes fadeInSlideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } } \ No newline at end of file diff --git a/src/shared/ui/loading-overlay.tsx b/src/shared/ui/loading-overlay.tsx new file mode 100644 index 0000000..97fc510 --- /dev/null +++ b/src/shared/ui/loading-overlay.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import { Spinner } from './spinner' +import { cn } from '@/shared/utils' + +interface LoadingOverlayProps { + isLoading: boolean +} + +export function LoadingOverlay({ isLoading }: LoadingOverlayProps) { + return ( +
+ {isLoading && ( +
+
+ +
+
+ )} +
+ ) +} + diff --git a/src/shared/ui/spinner.tsx b/src/shared/ui/spinner.tsx index cdb3f37..0096198 100644 --- a/src/shared/ui/spinner.tsx +++ b/src/shared/ui/spinner.tsx @@ -1,7 +1,19 @@ import styles from './styles.module.scss' +import { cn } from '@/shared/utils' -export function Spinner() { +interface SpinnerProps { + size?: 'small' | 'large' + className?: string +} + +export function Spinner({ size = 'small', className }: SpinnerProps) { return ( -
+
) } \ No newline at end of file diff --git a/src/shared/ui/styles.module.scss b/src/shared/ui/styles.module.scss index 04917c2..6757d48 100644 --- a/src/shared/ui/styles.module.scss +++ b/src/shared/ui/styles.module.scss @@ -9,6 +9,16 @@ margin-right: 8px; } +.spinnerLarge { + width: 64px; + height: 64px; + border-width: 4px; + margin-right: 0; + border-color: hsl(var(--foreground) / 0.3); + border-left-color: hsl(var(--foreground)); + border-top-color: hsl(var(--foreground)); +} + @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } diff --git a/src/widgets/navbar/index.tsx b/src/widgets/navbar/index.tsx index 20280e3..6a55665 100644 --- a/src/widgets/navbar/index.tsx +++ b/src/widgets/navbar/index.tsx @@ -116,7 +116,7 @@ function NavBarItem({ url, children }: React.PropsWithChildren<{ ) return ( -
  • + <> {isLoading && isLoading === url ? ( button ) : ( @@ -124,6 +124,6 @@ function NavBarItem({ url, children }: React.PropsWithChildren<{ {button} )} -
  • + ) } \ No newline at end of file diff --git a/src/widgets/schedule/day.tsx b/src/widgets/schedule/day.tsx index e0c7db0..7cbb9a2 100644 --- a/src/widgets/schedule/day.tsx +++ b/src/widgets/schedule/day.tsx @@ -1,3 +1,4 @@ +import React from 'react' import type { Day as DayType } from '@/shared/model/day' import { getDayOfWeek } from '@/shared/utils' import { Lesson } from '@/widgets/schedule/lesson' @@ -48,9 +49,10 @@ export function Day({ day }: {
    {day.lessons.map((lesson, i) => ( ))}
    diff --git a/src/widgets/schedule/index.tsx b/src/widgets/schedule/index.tsx index e633d37..58971de 100644 --- a/src/widgets/schedule/index.tsx +++ b/src/widgets/schedule/index.tsx @@ -50,7 +50,15 @@ export function Schedule({ days }: { return (
    {days.map((day, i) => ( - +
    + +
    ))}
    ) diff --git a/src/widgets/schedule/lesson.tsx b/src/widgets/schedule/lesson.tsx index 6aa4ded..5c72d1d 100644 --- a/src/widgets/schedule/lesson.tsx +++ b/src/widgets/schedule/lesson.tsx @@ -22,9 +22,10 @@ import { BsFillGeoAltFill } from 'react-icons/bs' import { RiGroup2Fill } from 'react-icons/ri' import { ResourcesDialog } from '@/widgets/schedule/resources-dialog' -export function Lesson({ lesson, width = 350 }: { +export function Lesson({ lesson, width = 350, animationDelay }: { lesson: LessonType width: number + animationDelay?: number }) { const [resourcesDialogOpened, setResourcesDialogOpened] = React.useState(false) @@ -63,7 +64,12 @@ export function Lesson({ lesson, width = 350 }: { } return ( - + {lesson.isChange &&
    }