feat: Добавлен компонент слайдера событий EventsCarousel

This commit is contained in:
Ilia Mashkov
2025-11-20 10:42:33 +03:00
parent 7f0d6d902a
commit 71330e4f78
5 changed files with 299 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
.container {
position: relative;
opacity: 0;
}
.prevButtonWrapper {
position: absolute;
top: 50%;
left: -25px;
z-index: 10;
transform: translateY(-50%);
transition: opacity 0.3s ease;
}
.nextButtonWrapper {
position: absolute;
top: 50%;
right: -25px;
z-index: 10;
transform: translateY(-50%) rotate(180deg);
transition: opacity 0.3s ease;
}
.hidden {
opacity: 0;
pointer-events: none;
}

View File

@@ -0,0 +1,70 @@
import { HISTORICAL_PERIODS } from '@/entities/TimePeriod'
import { EventsCarousel } from './EventsCarousel'
import type { Meta, StoryObj } from '@storybook/react'
const meta = {
title: 'Widgets/EventsCarousel',
component: EventsCarousel,
parameters: {
layout: 'fullwidth',
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div style={{ padding: '0 50px' }}>
<Story />
</div>
),
],
argTypes: {
visible: {
control: 'boolean',
description: 'Видимость карусели (управляет анимацией)',
},
},
} satisfies Meta<typeof EventsCarousel>
export default meta
type Story = StoryObj<typeof meta>
/**
* Базовая карусель с событиями первого периода
*/
export const Default: Story = {
args: {
events: HISTORICAL_PERIODS[0].events,
visible: true,
},
}
/**
* Карусель с событиями второго периода (Cinema)
*/
export const CinemaPeriod: Story = {
args: {
events: HISTORICAL_PERIODS[1].events,
visible: true,
},
}
/**
* Скрытая карусель (для демонстрации анимации)
*/
export const Hidden: Story = {
args: {
events: HISTORICAL_PERIODS[0].events,
visible: false,
},
}
/**
* Карусель с малым количеством событий
*/
export const FewEvents: Story = {
args: {
events: HISTORICAL_PERIODS[0].events.slice(0, 2),
visible: true,
},
}

View File

@@ -0,0 +1,164 @@
/**
* EventsCarousel Component
* Карусель событий с использованием Swiper
* Отображает список исторических событий в виде слайдера
*/
import classNames from 'classnames'
import gsap from 'gsap'
import { memo, useEffect, useRef, useState } from 'react'
import { Swiper, SwiperSlide } from 'swiper/react'
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
import ChevronIcon from '@/shared/assets/chevron--left.svg'
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 для создания слайдера с кастомной навигацией.
* Поддерживает адаптивное количество слайдов на разных размерах экрана.
* Анимирует появление/исчезновение с помощью GSAP.
*
* @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)
/**
* Эффект для анимации появления/исчезновения карусели
* Использует GSAP для плавной анимации opacity и y-позиции
*/
useEffect(() => {
if (!containerRef.current) return
const ctx = gsap.context(() => {
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,
})
}
}, containerRef)
return () => ctx.revert()
}, [visible])
/**
* Обработчик инициализации 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='Предыдущий слайд'
>
<ChevronIcon />
</Button>
</div>
<div
className={classNames(styles.nextButtonWrapper, {
[styles.hidden]: isEnd,
})}
>
<Button
variant='round'
size='small'
colorScheme='secondary'
className='swiper-button-next-custom'
aria-label='Следующий слайд'
>
<ChevronIcon />
</Button>
</div>
<Swiper
{...EVENT_CAROUSEL_CONFIG}
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'

View File

@@ -0,0 +1,30 @@
import gsap from 'gsap'
import { Navigation } from 'swiper/modules'
import type { SwiperOptions } from 'swiper/types'
/**
* Полная конфигурация Swiper для карусели событий
*/
export const EVENT_CAROUSEL_CONFIG: SwiperOptions = {
modules: [Navigation],
spaceBetween: 30,
slidesPerView: 1.5,
breakpoints: {
768: {
slidesPerView: 3.5,
},
},
navigation: {
prevEl: '.swiper-button-prev-custom',
nextEl: '.swiper-button-next-custom',
},
}
/**
* Константы для GSAP анимаций
*/
export const SHOW_DURATION: gsap.TweenVars['duration'] = 0.5
export const SHOW_DELAY: gsap.TweenVars['delay'] = 0.2
export const SHOW_Y_OFFSET: gsap.TweenVars['y'] = 20
export const HIDE_DURATION: gsap.TweenVars['duration'] = 0.3

View File

@@ -0,0 +1,2 @@
export { EventsCarousel } from './EventsCarousel'
export type { EventsCarouselProps } from './EventsCarousel'