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