From 71330e4f78f2ec9792d108b00d81d2746ce0ee1e Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 20 Nov 2025 10:42:33 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=20=D1=81=D0=BB=D0=B0=D0=B9=D0=B4=D0=B5=D1=80=D0=B0=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20EventsCarousel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EventsCarousel/EventsCarousel.module.scss | 33 ++++ .../EventsCarousel/EventsCarousel.stories.tsx | 70 ++++++++ .../ui/EventsCarousel/EventsCarousel.tsx | 164 ++++++++++++++++++ .../ui/EventsCarousel/constants.ts | 30 ++++ .../ui/EventsCarousel/index.ts | 2 + 5 files changed, 299 insertions(+) create mode 100644 src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.module.scss create mode 100644 src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.stories.tsx create mode 100644 src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx create mode 100644 src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts create mode 100644 src/widgets/TimeFrameSlider/ui/EventsCarousel/index.ts diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.module.scss b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.module.scss new file mode 100644 index 0000000..f48d676 --- /dev/null +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.module.scss @@ -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; +} \ No newline at end of file diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.stories.tsx b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.stories.tsx new file mode 100644 index 0000000..82458b3 --- /dev/null +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.stories.tsx @@ -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) => ( +
+ +
+ ), + ], + argTypes: { + visible: { + control: 'boolean', + description: 'Видимость карусели (управляет анимацией)', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +/** + * Базовая карусель с событиями первого периода + */ +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, + }, +} diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx new file mode 100644 index 0000000..03ca121 --- /dev/null +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx @@ -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 + * + * ``` + */ +export const EventsCarousel = memo( + ({ events, visible }: EventsCarouselProps) => { + const containerRef = useRef(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 ( +
+
+ +
+ +
+ +
+ + + {events.map((event) => ( + + + + ))} + +
+ ) + } +) + +EventsCarousel.displayName = 'EventsCarousel' diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts b/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts new file mode 100644 index 0000000..3765c66 --- /dev/null +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts @@ -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 diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/index.ts b/src/widgets/TimeFrameSlider/ui/EventsCarousel/index.ts new file mode 100644 index 0000000..fcc2dc8 --- /dev/null +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/index.ts @@ -0,0 +1,2 @@ +export { EventsCarousel } from './EventsCarousel' +export type { EventsCarouselProps } from './EventsCarousel'