feat: Добавлен компонент слайдера событий EventsCarousel
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
164
src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx
Normal file
164
src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx
Normal 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'
|
||||
30
src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts
Normal file
30
src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts
Normal 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
|
||||
2
src/widgets/TimeFrameSlider/ui/EventsCarousel/index.ts
Normal file
2
src/widgets/TimeFrameSlider/ui/EventsCarousel/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { EventsCarousel } from './EventsCarousel'
|
||||
export type { EventsCarouselProps } from './EventsCarousel'
|
||||
Reference in New Issue
Block a user