2025-11-20 10:42:33 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* EventsCarousel Component
|
|
|
|
|
|
* Карусель событий с использованием Swiper
|
|
|
|
|
|
* Отображает список исторических событий в виде слайдера
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-11-23 15:39:04 +03:00
|
|
|
|
import { useGSAP } from '@gsap/react'
|
2025-11-20 10:42:33 +03:00
|
|
|
|
import classNames from 'classnames'
|
2025-11-23 14:13:52 +03:00
|
|
|
|
import { gsap } from 'gsap'
|
2025-11-23 15:39:04 +03:00
|
|
|
|
import { memo, useRef, useState } from 'react'
|
2025-11-20 10:42:33 +03:00
|
|
|
|
import { Swiper, SwiperSlide } from 'swiper/react'
|
|
|
|
|
|
|
|
|
|
|
|
import 'swiper/css'
|
|
|
|
|
|
import 'swiper/css/navigation'
|
|
|
|
|
|
import 'swiper/css/pagination'
|
|
|
|
|
|
|
2025-11-20 12:11:09 +03:00
|
|
|
|
import ChevronSvg from '@/shared/assets/chevron--left.svg'
|
2025-11-20 10:42:33 +03:00
|
|
|
|
import { Button } from '@/shared/ui/Button'
|
|
|
|
|
|
import { Card } from '@/shared/ui/Card'
|
|
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
EVENT_CAROUSEL_CONFIG,
|
|
|
|
|
|
HIDE_DURATION,
|
|
|
|
|
|
SHOW_DELAY,
|
|
|
|
|
|
SHOW_DURATION,
|
|
|
|
|
|
SHOW_Y_OFFSET,
|
|
|
|
|
|
} from './constants'
|
|
|
|
|
|
import styles from './EventsCarousel.module.scss'
|
|
|
|
|
|
|
|
|
|
|
|
import type { HistoricalEvent } from '@/entities/TimePeriod'
|
|
|
|
|
|
import type { Swiper as SwiperType } from 'swiper'
|
|
|
|
|
|
|
|
|
|
|
|
export interface EventsCarouselProps {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Массив исторических событий для отображения
|
|
|
|
|
|
*/
|
|
|
|
|
|
readonly events: readonly HistoricalEvent[]
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Флаг видимости карусели (управляет анимацией появления/исчезновения)
|
|
|
|
|
|
*/
|
|
|
|
|
|
readonly visible: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Компонент карусели исторических событий
|
|
|
|
|
|
*
|
|
|
|
|
|
* Использует Swiper для создания слайдера с кастомной навигацией.
|
|
|
|
|
|
* Поддерживает адаптивное количество слайдов на разных размерах экрана.
|
2025-11-23 15:39:04 +03:00
|
|
|
|
* Анимирует появление/исчезновение с помощью GSAP useGSAP hook.
|
2025-11-20 10:42:33 +03:00
|
|
|
|
*
|
|
|
|
|
|
* @example
|
|
|
|
|
|
* ```tsx
|
|
|
|
|
|
* <EventsCarousel
|
|
|
|
|
|
* events={ HISTORICAL_PERIODS[0].events }
|
|
|
|
|
|
* visible={ true }
|
|
|
|
|
|
* />
|
|
|
|
|
|
* ```
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const EventsCarousel = memo(
|
|
|
|
|
|
({ events, visible }: EventsCarouselProps) => {
|
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
const [isBeginning, setIsBeginning] = useState(true)
|
|
|
|
|
|
const [isEnd, setIsEnd] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-11-23 15:39:04 +03:00
|
|
|
|
* Анимация появления/исчезновения карусели
|
|
|
|
|
|
* Использует useGSAP hook для автоматической очистки анимаций
|
2025-11-20 10:42:33 +03:00
|
|
|
|
*/
|
2025-11-23 15:39:04 +03:00
|
|
|
|
useGSAP(
|
|
|
|
|
|
() => {
|
2025-11-20 10:42:33 +03:00
|
|
|
|
if (visible) {
|
|
|
|
|
|
gsap.fromTo(
|
|
|
|
|
|
containerRef.current,
|
|
|
|
|
|
{ opacity: 0, y: SHOW_Y_OFFSET },
|
|
|
|
|
|
{
|
|
|
|
|
|
opacity: 1,
|
|
|
|
|
|
y: 0,
|
|
|
|
|
|
duration: SHOW_DURATION,
|
|
|
|
|
|
delay: SHOW_DELAY,
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
gsap.to(containerRef.current, {
|
|
|
|
|
|
opacity: 0,
|
|
|
|
|
|
duration: HIDE_DURATION,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-11-23 15:39:04 +03:00
|
|
|
|
},
|
|
|
|
|
|
{ scope: containerRef, dependencies: [visible, events] }
|
|
|
|
|
|
)
|
2025-11-20 10:42:33 +03:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Обработчик инициализации Swiper
|
|
|
|
|
|
* Устанавливает начальное состояние кнопок навигации
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleSwiperInit = (swiper: SwiperType) => {
|
|
|
|
|
|
setIsBeginning(swiper.isBeginning)
|
|
|
|
|
|
setIsEnd(swiper.isEnd)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Обработчик изменения состояния Swiper
|
|
|
|
|
|
* Обновляет состояние кнопок навигации
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleSlideChange = (swiper: SwiperType) => {
|
|
|
|
|
|
setIsBeginning(swiper.isBeginning)
|
|
|
|
|
|
setIsEnd(swiper.isEnd)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className={styles.container} ref={containerRef}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={classNames(styles.prevButtonWrapper, {
|
|
|
|
|
|
[styles.hidden]: isBeginning,
|
|
|
|
|
|
})}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant='round'
|
|
|
|
|
|
size='small'
|
|
|
|
|
|
colorScheme='secondary'
|
|
|
|
|
|
className='swiper-button-prev-custom'
|
|
|
|
|
|
aria-label='Предыдущий слайд'
|
|
|
|
|
|
>
|
2025-11-20 12:11:09 +03:00
|
|
|
|
<ChevronSvg width={6} height={9} stroke='#3877EE' />
|
2025-11-20 10:42:33 +03:00
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={classNames(styles.nextButtonWrapper, {
|
|
|
|
|
|
[styles.hidden]: isEnd,
|
|
|
|
|
|
})}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant='round'
|
|
|
|
|
|
size='small'
|
|
|
|
|
|
colorScheme='secondary'
|
|
|
|
|
|
className='swiper-button-next-custom'
|
|
|
|
|
|
aria-label='Следующий слайд'
|
|
|
|
|
|
>
|
2025-11-20 12:11:09 +03:00
|
|
|
|
<ChevronSvg width={6} height={9} stroke='#3877EE' />
|
2025-11-20 10:42:33 +03:00
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Swiper
|
|
|
|
|
|
{...EVENT_CAROUSEL_CONFIG}
|
2025-11-21 15:41:57 +03:00
|
|
|
|
watchSlidesProgress
|
2025-11-20 10:42:33 +03:00
|
|
|
|
onInit={handleSwiperInit}
|
|
|
|
|
|
onSlideChange={handleSlideChange}
|
|
|
|
|
|
>
|
|
|
|
|
|
{events.map((event) => (
|
|
|
|
|
|
<SwiperSlide
|
|
|
|
|
|
key={`${event.year}-${event.description.slice(0, 20)}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Card title={event.year} description={event.description} />
|
|
|
|
|
|
</SwiperSlide>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Swiper>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
EventsCarousel.displayName = 'EventsCarousel'
|