From 3f7f5d358eaa348563b29d86438e674fa1dbc57d Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 21 Nov 2025 15:40:08 +0300 Subject: [PATCH 01/18] =?UTF-8?q?feature:=20=D0=90=D0=B4=D0=B0=D0=BF=D1=82?= =?UTF-8?q?=D0=B8=D0=B2=D0=BD=D1=8B=D0=B5=20=D1=81=D1=82=D0=B8=D0=BB=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Button/Button.module.scss | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/shared/ui/Button/Button.module.scss b/src/shared/ui/Button/Button.module.scss index 7fed480..0c4c970 100644 --- a/src/shared/ui/Button/Button.module.scss +++ b/src/shared/ui/Button/Button.module.scss @@ -40,18 +40,30 @@ height: 40px; font-size: 14px; + + @media (width <=768px) { + height: 20px; + } } &.medium { height: 50px; font-size: 18px; + + @media (width <=768px) { + height: 25px; + } } &.large { height: 60px; font-size: 24px; + + @media (width <=768px) { + height: 30px; + } } // Color Schemes From f34f1a28de9f61f5b00b2d3f8c339f15d38fd970 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 21 Nov 2025 15:40:27 +0300 Subject: [PATCH 02/18] =?UTF-8?q?feature:=20=D0=90=D0=B4=D0=B0=D0=BF=D1=82?= =?UTF-8?q?=D0=B8=D0=B2=D0=BD=D1=8B=D0=B5=20=D1=81=D1=82=D0=B8=D0=BB=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=20Card?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Card/Card.module.scss | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/shared/ui/Card/Card.module.scss b/src/shared/ui/Card/Card.module.scss index afeb97b..fa57ef4 100644 --- a/src/shared/ui/Card/Card.module.scss +++ b/src/shared/ui/Card/Card.module.scss @@ -9,11 +9,19 @@ font-weight: 400; font-size: 25px; line-height: 120%; + + @media (width <=768px) { + font-size: 16px; + } } .description { color: var(--color-text); font-weight: 400; font-size: 20px; - line-height: 30px; + line-height: 145%; + + @media (width <=768px) { + font-size: 14px; + } } \ No newline at end of file From 364c5e1ff1a4f207fd44692761e303edcd7bf36e Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 21 Nov 2025 15:41:57 +0300 Subject: [PATCH 03/18] =?UTF-8?q?feature:=20=D0=9D=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BD=20=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B8=D0=B2?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=B0=D1=80=D1=83=D1=81=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EventsCarousel/EventsCarousel.module.scss | 20 +++++++++++++ .../ui/EventsCarousel/EventsCarousel.tsx | 1 + .../ui/EventsCarousel/constants.ts | 29 +++++++++++++++---- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.module.scss b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.module.scss index 68c1485..d968c58 100644 --- a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.module.scss +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.module.scss @@ -30,4 +30,24 @@ opacity: 0; pointer-events: none; +} + +:global(.swiper-slide-next) { + transition: opacity 0.3s ease; + + @media (width <=576px) { + opacity: 0.4; + } +} + +:global(.swiper-slide-prev) { + transition: opacity 0.3s ease; + + @media (width <=576px) { + opacity: 0.4; + } +} + +:global(.swiper-slide-active) { + opacity: 1; } \ No newline at end of file diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx index f6e7d00..cd12507 100644 --- a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx @@ -145,6 +145,7 @@ export const EventsCarousel = memo( diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts b/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts index b6149e8..7697fff 100644 --- a/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts @@ -1,5 +1,7 @@ import { Navigation } from 'swiper/modules' +import styles from './EventsCarousel.module.scss' + import type { SwiperOptions } from 'swiper/types' /** @@ -7,16 +9,31 @@ import type { SwiperOptions } from 'swiper/types' */ export const EVENT_CAROUSEL_CONFIG: SwiperOptions = { modules: [Navigation], - spaceBetween: 30, slidesPerView: 1.5, - breakpoints: { - 768: { - slidesPerView: 3.5, - }, - }, + spaceBetween: 25, navigation: { prevEl: '.swiper-button-prev-custom', nextEl: '.swiper-button-next-custom', + enabled: false, + }, + breakpoints: { + 576: { + slidesPerView: 2, + }, + 768: { + slidesPerView: 3, + navigation: { + enabled: true, + }, + spaceBetween: 30, + }, + 1440: { + slidesPerView: 3, + navigation: { + enabled: true, + }, + spaceBetween: 80, + }, }, } From 2958cc734a8f3d89f8b10321055143b611315fe4 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 21 Nov 2025 15:42:39 +0300 Subject: [PATCH 04/18] =?UTF-8?q?feature:=20=D0=9D=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BD=20=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B8=D0=B2?= =?UTF-8?q?=20=D0=B2=D0=B8=D0=B4=D0=B6=D0=B5=D1=82=D0=B0=20TimeFrameSlider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TimeFrameSlider.module.scss | 93 ++++++++++++++++--- .../ui/TimeFrameSlider/TimeFrameSlider.tsx | 37 +++++--- 2 files changed, 107 insertions(+), 23 deletions(-) diff --git a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss index ead27c0..ac0ff57 100644 --- a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss +++ b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss @@ -24,8 +24,9 @@ overflow: hidden; @media (width <=768px) { - min-height: auto; - padding: 20px; + padding: 60px 20px 20px; + + background-image: unset; } } @@ -47,10 +48,15 @@ @media (width <=768px) { + position: relative; + inset: unset; + margin-bottom: 20px; padding-left: 0; font-size: 20px; + + border: none; } } @@ -70,10 +76,16 @@ background-size: 100% 1px; @media (width <=768px) { + position: unset; + display: flex; flex-direction: column; + width: 100%; height: auto; + margin: 0; + + background-image: unset; } } @@ -90,10 +102,13 @@ transform-origin: left; @media (width <=768px) { - position: static; + left: 20px; + bottom: 13px; order: 2; + gap: 10px; + margin-top: 20px; padding: 0; } @@ -107,6 +122,59 @@ .buttons { display: flex; gap: 20px; + + @media (width <=768px) { + gap: 8px; + } +} + +.chevronIcon { + width: 9px; + height: 14px; + + @media (width <=768px) { + width: 6px; + height: 11.5px; + } +} + +.dots { + display: none; + + @media (width <=768px) { + position: absolute; + left: 50%; + bottom: 32px; + + display: flex; + gap: 10px; + justify-content: center; + + width: 100%; + + transform: translate(-50%, -50%); + } +} + +.dot { + width: 6px; + height: 6px; + padding: 0; + + border: none; + border-radius: 50%; + + background-color: var(--color-primary); + + cursor: pointer; + + opacity: 0.4; + + transition: opacity 0.3s ease; + + &.activeDot { + opacity: 1; + } } .rotated { @@ -120,7 +188,7 @@ z-index: 0; display: flex; - gap: 40px; + gap: 60px; color: var(--color-text); font-weight: 700; @@ -161,15 +229,12 @@ display: block; - margin-bottom: 40px; - padding-top: 40px; + margin-bottom: 20px; color: var(--color-text); font-weight: 700; - font-size: 20px; - text-align: center; - - border-top: 1px solid var(--color-border); + font-size: 16px; + text-align: left; } } @@ -179,6 +244,12 @@ } } -.eventCarousel { +.carouselContainer { padding: 55px 80px 105px; + + @media (width <=768px) { + padding: 0; + + border-top: 1px solid #C7CDD9; + } } \ No newline at end of file diff --git a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx index 2cc1e4e..edb806b 100644 --- a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx +++ b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx @@ -3,6 +3,7 @@ * Главный компонент временной шкалы с круговой диаграммой и каруселью событий */ +import classNames from 'classnames' import gsap from 'gsap' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -127,18 +128,19 @@ export const TimeFrameSlider = memo(() => {
{currentPeriod.yearFrom} - {'\u00A0'} {currentPeriod.yearTo}
{currentPeriod.label}
- +
+ +
@@ -153,7 +155,7 @@ export const TimeFrameSlider = memo(() => { onClick={handlePrev} aria-label='Предыдущий период' > - +
-
+
+ +
+ {HISTORICAL_PERIODS.map((_, index) => ( +
) }) From 38785cce94abf87ed58b832fa25adeb17b0b0edc Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 13:14:20 +0300 Subject: [PATCH 05/18] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B8=D0=BE=D0=B4=D0=BE=D0=B2?= =?UTF-8?q?=20=D1=81=20=D0=B0=D0=BD=D0=B8=D0=BC=D0=B0=D1=86=D0=B8=D0=B5?= =?UTF-8?q?=D0=B9=20=D0=BF=D0=BE=D1=8F=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CircleTimeline/CircleTimeline.module.scss | 23 +++++++++-- .../ui/CircleTimeline/CircleTimeline.tsx | 39 +++++++++++++++++-- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.module.scss b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.module.scss index c44ddf0..0e1f208 100644 --- a/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.module.scss +++ b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.module.scss @@ -20,7 +20,7 @@ margin-top: -28px; margin-left: -28px; - border: 25px solid transparent; + border: 25px solid transparent; border-radius: 50%; background: var(--color-text); @@ -46,15 +46,30 @@ background-clip: padding-box; } - &:hover .label, - &.active .label { + &:hover .number, + &.active .number { display: block; } - .label { + .number { display: none; color: var(--color-text); font-size: 20px; } + + .title { + position: absolute; + left: 100%; + + margin-left: 20px; + + color: var(--color-text); + font-weight: 700; + font-size: 20px; + white-space: nowrap; + + opacity: 0; + visibility: hidden; + } } \ No newline at end of file diff --git a/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.tsx b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.tsx index e72302c..35ab81b 100644 --- a/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.tsx +++ b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.tsx @@ -67,6 +67,9 @@ export const CircleTimeline = memo(function CircleTimeline({ // Реф для массива точек периодов const pointsRef = useRef<(HTMLDivElement | null)[]>([]) + // Реф для заголовков периодов + const titlesRef = useRef<(HTMLSpanElement | null)[]>([]) + /** * Эффект для анимации поворота круга и контр-поворота точек * Запускается при изменении rotation @@ -82,16 +85,38 @@ export const CircleTimeline = memo(function CircleTimeline({ } // Контр-поворот точек, чтобы текст оставался читаемым - pointsRef.current.forEach((point) => { + pointsRef.current.forEach((point, index) => { if (point) { gsap.to(point, { rotation: -rotation, duration: 0, ease: ANIMATION_EASE, }) + + // Анимация заголовка + const title = titlesRef.current[index] + if (title) { + // Сбрасываем предыдущие анимации для этого элемента + gsap.killTweensOf(title) + + if (index === activeIndex) { + gsap.to(title, { + opacity: 1, + visibility: 'visible', + duration: 0.5, + delay: ANIMATION_DURATION, // Ждем окончания вращения + }) + } else { + gsap.to(title, { + opacity: 0, + visibility: 'hidden', + duration: 0.2, + }) + } + } } }) - }, [rotation]) + }, [rotation, activeIndex]) /** * Мемоизированный расчет позиций точек на круге @@ -138,7 +163,15 @@ export const CircleTimeline = memo(function CircleTimeline({ aria-label={`Period ${index + 1}: ${period.label}`} aria-current={index === activeIndex ? 'true' : 'false'} > - {index + 1} + {index + 1} + { + titlesRef.current[index] = el + }} + > + {period.label} + ) })} From e892e69277a1c5f3bc792f3ca57173381b7f6983 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 13:15:30 +0300 Subject: [PATCH 06/18] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B8=D0=BE=D0=B4=D0=B0=20?= =?UTF-8?q?=D1=81=20=D0=B0=D0=BD=D0=B8=D0=BC=D0=B0=D1=86=D0=B8=D0=B5=D0=B9?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=8F=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BC=D0=BE=D0=B1=D0=B8=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=80=D0=B0=D0=B7=D1=80=D0=B5=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/TimeFrameSlider/TimeFrameSlider.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx index edb806b..36d67e9 100644 --- a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx +++ b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx @@ -35,6 +35,7 @@ export const TimeFrameSlider = memo(() => { const startYearRef = useRef(null) const endYearRef = useRef(null) const containerRef = useRef(null) + const periodLabelRef = useRef(null) // Текущий период const currentPeriod = useMemo( @@ -100,6 +101,14 @@ export const TimeFrameSlider = memo(() => { prevYearFromRef.current = currentPeriod.yearFrom prevYearToRef.current = currentPeriod.yearTo + + if (periodLabelRef.current) { + gsap.fromTo( + periodLabelRef.current, + { opacity: 0, visibility: 'hidden' }, + { opacity: 1, visibility: 'visible', duration: 1 } + ) + } }, containerRef) return () => ctx.revert() @@ -131,7 +140,9 @@ export const TimeFrameSlider = memo(() => { {currentPeriod.yearTo} -
{currentPeriod.label}
+
+ {currentPeriod.label} +
Date: Sun, 23 Nov 2025 13:15:45 +0300 Subject: [PATCH 07/18] feat: --- .../EventsCarousel/EventsCarousel.module.scss | 10 ++++-- .../ui/EventsCarousel/constants.ts | 7 +++++ .../TimeFrameSlider.module.scss | 31 +++++++++++++++++-- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.module.scss b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.module.scss index d968c58..06d766a 100644 --- a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.module.scss +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.module.scss @@ -32,10 +32,16 @@ pointer-events: none; } +:global(.swiper) { + @media (width <=768px) { + padding: 0 20px; + } +} + :global(.swiper-slide-next) { transition: opacity 0.3s ease; - @media (width <=576px) { + @media (width <=768px) { opacity: 0.4; } } @@ -43,7 +49,7 @@ :global(.swiper-slide-prev) { transition: opacity 0.3s ease; - @media (width <=576px) { + @media (width <=768px) { opacity: 0.4; } } diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts b/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts index 7697fff..b7f3c96 100644 --- a/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts @@ -21,6 +21,13 @@ export const EVENT_CAROUSEL_CONFIG: SwiperOptions = { slidesPerView: 2, }, 768: { + slidesPerView: 2, + navigation: { + enabled: true, + }, + spaceBetween: 25, + }, + 1024: { slidesPerView: 3, navigation: { enabled: true, diff --git a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss index ac0ff57..b4ba1ef 100644 --- a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss +++ b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss @@ -23,6 +23,10 @@ overflow: hidden; + @media (width <=1024px) { + padding-top: 100px; + } + @media (width <=768px) { padding: 60px 20px 20px; @@ -47,6 +51,11 @@ border-image: var(--gradient-primary) 1; + @media (width <=1024px) { + top: 80px; + font-size: 40px; + } + @media (width <=768px) { position: relative; inset: unset; @@ -101,6 +110,11 @@ transform-origin: left; + @media (width <=1024px) { + left: 100px; + bottom: 40px; + } + @media (width <=768px) { left: 20px; bottom: 13px; @@ -199,6 +213,12 @@ pointer-events: none; + @media (width <=1024px) { + font-size: 140px; + line-height: 120px; + gap: 40px; + } + @media (width <=768px) { position: static; @@ -229,16 +249,21 @@ display: block; - margin-bottom: 20px; + padding-bottom: 20px; color: var(--color-text); font-weight: 700; font-size: 16px; text-align: left; + + border-bottom: 1px solid #C7CDD9; } } .circleContainer { + width: 100%; + height: 100%; + @media (width <=768px) { display: none; } @@ -248,8 +273,8 @@ padding: 55px 80px 105px; @media (width <=768px) { + margin: 0 -20px; + width: calc(100% + 40px); padding: 0; - - border-top: 1px solid #C7CDD9; } } \ No newline at end of file From c4a7653d7b9a8d98ecea317bd27acba272bbbcf8 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 13:31:46 +0300 Subject: [PATCH 08/18] =?UTF-8?q?fix:=20=D0=9F=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=20stylel?= =?UTF-8?q?int=20=D0=B4=D0=BB=D1=8F=20=D0=B3=D0=BB=D0=BE=D0=B1=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D1=85=20=D1=81=D1=82=D0=B8=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- stylelint.config.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/stylelint.config.js b/stylelint.config.js index 3cbcb32..7ead2f2 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -53,5 +53,11 @@ module.exports = { 'order/properties-order': propertyOrdering, 'declaration-empty-line-before': null, 'no-descending-specificity': null, // Отключаем из-за конфликта с order/order + 'selector-pseudo-class-no-unknown': [ + true, + { + ignorePseudoClasses: ['global'], + }, + ], }, } From d27906fd479f8863a4462e1dace4dc1d062824fd Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 13:34:15 +0300 Subject: [PATCH 09/18] =?UTF-8?q?refactor:=20=D0=98=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B0=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3?= =?UTF-8?q?=D1=83=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20webpack=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B8=D0=B7=D0=B2=D0=BE=D0=B4=D0=B8=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20=D0=B8=20?= =?UTF-8?q?=D1=81=D0=BA=D0=BE=D1=80=D0=BE=D1=81=D1=82=D0=B8=20=D1=81=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/build/buildLoaders.ts | 10 ++++---- config/build/buildWebpackConfig.ts | 6 +++++ config/build/loaders/buildBabelLoader.ts | 9 ++++++-- config/build/loaders/buildTypescriptLoader.ts | 23 ------------------- 4 files changed, 18 insertions(+), 30 deletions(-) delete mode 100644 config/build/loaders/buildTypescriptLoader.ts diff --git a/config/build/buildLoaders.ts b/config/build/buildLoaders.ts index 6f43be8..4fd1e06 100644 --- a/config/build/buildLoaders.ts +++ b/config/build/buildLoaders.ts @@ -4,7 +4,6 @@ import { buildBabelLoader } from './loaders/buildBabelLoader' import { buildCssLoader } from './loaders/buildCssLoader' import { buildFileLoader } from './loaders/buildFileLoader' import { buildSvgrLoader } from './loaders/buildSvgrLoader' -import { buildTypescriptLoader } from './loaders/buildTypescriptLoader' import { BuildOptions } from './types/config' /** @@ -23,15 +22,16 @@ import { BuildOptions } from './types/config' * @returns {webpack.RuleSetRule[]} Массив правил для webpack */ export function buildLoaders({ isDev }: BuildOptions): webpack.RuleSetRule[] { - const babelLoader = buildBabelLoader(isDev) + // Используем babel-loader для обработки JS и TS файлов + // Это ускоряет сборку, так как babel работает быстрее ts-loader + // Проверка типов должна выполняться отдельно (например, через tsc --noEmit) + const codeBabelLoader = buildBabelLoader(isDev) const fileLoader = buildFileLoader() const svgrLoader = buildSvgrLoader() - const typescriptLoader = buildTypescriptLoader() - const cssLoader = buildCssLoader(isDev) - return [fileLoader, svgrLoader, babelLoader, typescriptLoader, cssLoader] + return [fileLoader, svgrLoader, codeBabelLoader, cssLoader] } diff --git a/config/build/buildWebpackConfig.ts b/config/build/buildWebpackConfig.ts index 55eab69..f9fbd46 100644 --- a/config/build/buildWebpackConfig.ts +++ b/config/build/buildWebpackConfig.ts @@ -56,5 +56,11 @@ export function buildWebpackConfig( plugins: buildPlugins(options), devtool: isDev ? 'inline-source-map' : undefined, devServer: isDev ? buildDevServer(options) : undefined, + optimization: { + splitChunks: { + chunks: 'all', // Разделяем код на чанки для оптимизации загрузки + }, + runtimeChunk: 'single', // Выносим рантайм webpack в отдельный файл + }, } } diff --git a/config/build/loaders/buildBabelLoader.ts b/config/build/loaders/buildBabelLoader.ts index 0f0bbad..f5bbc79 100644 --- a/config/build/loaders/buildBabelLoader.ts +++ b/config/build/loaders/buildBabelLoader.ts @@ -13,12 +13,17 @@ */ export function buildBabelLoader(isDev: boolean) { const babelLoader = { - test: /\.(js|jsx|tsx)$/, + test: /\.(js|jsx|tsx|ts)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { - presets: ['@babel/preset-env'], + cacheDirectory: true, // Включаем кеширование для ускорения пересборки + presets: [ + '@babel/preset-env', + ['@babel/preset-react', { runtime: 'automatic' }], // Поддержка React 17+ (новый JSX transform) + '@babel/preset-typescript', // Поддержка TypeScript через Babel + ], plugins: [isDev && require.resolve('react-refresh/babel')].filter( Boolean ), diff --git a/config/build/loaders/buildTypescriptLoader.ts b/config/build/loaders/buildTypescriptLoader.ts deleted file mode 100644 index 41ac83d..0000000 --- a/config/build/loaders/buildTypescriptLoader.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Конфигурация TypeScript loader для webpack - * - * Компилирует TypeScript файлы (.ts, .tsx) в JavaScript. - * Выполняет проверку типов во время сборки. - * - * @returns {Object} Конфигурация ts-loader - * - * @example - * // Обрабатывает файлы: - * // - Component.tsx - * // - utils.ts - * // - types.d.ts - */ -export function buildTypescriptLoader() { - const typescriptLoader = { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: '/node-modules/', - } - - return typescriptLoader -} From 5c408ccff4755b59819153412147bed770c7c739 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 13:34:40 +0300 Subject: [PATCH 10/18] =?UTF-8?q?fix:=20=D0=9F=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BB=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/CircleTimeline/CircleTimeline.module.scss | 3 ++- .../ui/TimeFrameSlider/TimeFrameSlider.module.scss | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.module.scss b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.module.scss index 0e1f208..bcdfddc 100644 --- a/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.module.scss +++ b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.module.scss @@ -69,7 +69,8 @@ font-size: 20px; white-space: nowrap; - opacity: 0; visibility: hidden; + + opacity: 0; } } \ No newline at end of file diff --git a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss index b4ba1ef..f75e634 100644 --- a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss +++ b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss @@ -53,6 +53,7 @@ @media (width <=1024px) { top: 80px; + font-size: 40px; } @@ -214,9 +215,10 @@ pointer-events: none; @media (width <=1024px) { + gap: 40px; + font-size: 140px; line-height: 120px; - gap: 40px; } @media (width <=768px) { @@ -273,8 +275,8 @@ padding: 55px 80px 105px; @media (width <=768px) { - margin: 0 -20px; width: calc(100% + 40px); + margin: 0 -20px; padding: 0; } } \ No newline at end of file From da0309f714b8f742287d67db2405a4b0d65af5ae Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 13:46:06 +0300 Subject: [PATCH 11/18] =?UTF-8?q?feat:=20=D0=A3=D1=81=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B1=D0=B8=D0=B1=D0=BB?= =?UTF-8?q?=D0=B8=D0=BE=D1=82=D0=B5=D0=BA=D0=B0=20identify-object-proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/package.json b/package.json index 40e5551..31baf1f 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "globals": "^15.12.0", "html-webpack-plugin": "^5.6.0", "husky": "^9.1.0", + "identity-obj-proxy": "^3.0.0", "jest": "^30.2.0", "jest-environment-jsdom": "^30.2.0", "mini-css-extract-plugin": "^2.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 992bc36..2ea9f2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,6 +138,9 @@ importers: husky: specifier: ^9.1.0 version: 9.1.7 + identity-obj-proxy: + specifier: ^3.0.0 + version: 3.0.0 jest: specifier: ^30.2.0 version: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.25.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) @@ -3517,6 +3520,9 @@ packages: handle-thing@2.0.1: resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + harmony-reflect@1.6.2: + resolution: {integrity: sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -3654,6 +3660,10 @@ packages: peerDependencies: postcss: ^8.1.0 + identity-obj-proxy@3.0.0: + resolution: {integrity: sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==} + engines: {node: '>=4'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -9672,6 +9682,8 @@ snapshots: handle-thing@2.0.1: {} + harmony-reflect@1.6.2: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -9817,6 +9829,10 @@ snapshots: dependencies: postcss: 8.5.6 + identity-obj-proxy@3.0.0: + dependencies: + harmony-reflect: 1.6.2 + ignore@5.3.2: {} ignore@7.0.5: {} From ec6867d7a0fd729e63fbf37e98863dc368109362 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 13:46:48 +0300 Subject: [PATCH 12/18] feat: --- config/jest/setupTests.ts | 15 +++++++++++++++ tsconfig.json | 7 ++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/config/jest/setupTests.ts b/config/jest/setupTests.ts index 738e6bb..1dd6186 100644 --- a/config/jest/setupTests.ts +++ b/config/jest/setupTests.ts @@ -1,2 +1,17 @@ import '@testing-library/jest-dom' import 'regenerator-runtime/runtime' + +// Глобальный мок для GSAP +jest.mock('gsap', () => { + return { + to: jest.fn(), + fromTo: jest.fn(), + killTweensOf: jest.fn(), + context: jest.fn(() => ({ + revert: jest.fn(), + })), + Power2: { + easeOut: 'power2.out', + }, + } +}) diff --git a/tsconfig.json b/tsconfig.json index 92de296..c540bc3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,12 @@ "./src/*" ] }, - "strict": true + "strict": true, + "types": [ + "node", + "jest", + "@testing-library/jest-dom" + ] }, "ts-node": { "compilerOptions": { From 6de84f31439e4f7deca9d3cb86ea2f02c94c6d1e Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 13:48:50 +0300 Subject: [PATCH 13/18] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20Rea?= =?UTF-8?q?ct=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=BE=D0=B2.=20=D0=A2=D0=B5=D1=81=D1=82=D1=8B=20=D0=B1=D0=B0?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=BE=D0=B9=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA?= =?UTF-8?q?=D0=B8,=20=D0=93=D1=80=D0=B0=D0=BD=D0=B8=D1=87=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2,=20=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Button/Button.test.tsx | 34 ++++++++ src/shared/ui/Card/Card.test.tsx | 18 ++++ .../ui/CircleTimeline/CircleTimeline.test.tsx | 56 +++++++++++++ .../ui/EventsCarousel/EventsCarousel.test.tsx | 52 ++++++++++++ .../TimeFrameSlider/TimeFrameSlider.test.tsx | 82 +++++++++++++++++++ 5 files changed, 242 insertions(+) create mode 100644 src/shared/ui/Button/Button.test.tsx create mode 100644 src/shared/ui/Card/Card.test.tsx create mode 100644 src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.test.tsx create mode 100644 src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.test.tsx create mode 100644 src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.test.tsx diff --git a/src/shared/ui/Button/Button.test.tsx b/src/shared/ui/Button/Button.test.tsx new file mode 100644 index 0000000..bb0b329 --- /dev/null +++ b/src/shared/ui/Button/Button.test.tsx @@ -0,0 +1,34 @@ +import { fireEvent, render, screen } from '@testing-library/react' + +import { Button } from './Button' + +describe('Button', () => { + // Тест на рендеринг кнопки + it('должна рендериться корректно', () => { + render() + expect(screen.getByText('Test Button')).toBeInTheDocument() + }) + + // Тест на применение класса варианта + it('должна применять класс варианта', () => { + render() + const button = screen.getByText('Regular Button') + // Проверяем наличие класса, который генерируется CSS модулями (частичное совпадение) + expect(button.className).toMatch(/regular/) + }) + + // Тест на обработку клика + it('должна вызывать обработчик onClick при клике', () => { + const handleClick = jest.fn() + render() + + fireEvent.click(screen.getByText('Click Me')) + expect(handleClick).toHaveBeenCalledTimes(1) + }) + + // Тест на отключенное состояние + it('должна быть отключена при передаче пропса disabled', () => { + render() + expect(screen.getByText('Disabled Button')).toBeDisabled() + }) +}) diff --git a/src/shared/ui/Card/Card.test.tsx b/src/shared/ui/Card/Card.test.tsx new file mode 100644 index 0000000..e823dab --- /dev/null +++ b/src/shared/ui/Card/Card.test.tsx @@ -0,0 +1,18 @@ +import { render, screen } from '@testing-library/react' + +import { Card } from './Card' + +describe('Card', () => { + // Тест на рендеринг заголовка и описания + it('должна рендерить заголовок и описание', () => { + render() + expect(screen.getByText('1992')).toBeInTheDocument() + expect(screen.getByText('Test Description')).toBeInTheDocument() + }) + + // Тест на рендеринг числового заголовка + it('должна корректно рендерить числовой заголовок', () => { + render() + expect(screen.getByText('2023')).toBeInTheDocument() + }) +}) diff --git a/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.test.tsx b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.test.tsx new file mode 100644 index 0000000..60da310 --- /dev/null +++ b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.test.tsx @@ -0,0 +1,56 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import gsap from 'gsap' + +import { HISTORICAL_PERIODS } from '@/entities/TimePeriod' + +import { CircleTimeline } from './CircleTimeline' + +describe('CircleTimeline', () => { + const mockOnPeriodChange = jest.fn() + const defaultProps = { + periods: HISTORICAL_PERIODS, + activeIndex: 0, + onPeriodChange: mockOnPeriodChange, + rotation: 0, + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + // Тест на рендеринг правильного количества точек + it('должна рендерить правильное количество точек', () => { + render() + const points = screen.getAllByRole('button') + expect(points).toHaveLength(HISTORICAL_PERIODS.length) + }) + + // Тест на активную точку + it('должна корректно отображать активную точку', () => { + render() + const points = screen.getAllByRole('button') + + // Проверяем aria-current для доступности + expect(points[1]).toHaveAttribute('aria-current', 'true') + expect(points[0]).toHaveAttribute('aria-current', 'false') + }) + + // Тест на клик по точке + it('должна вызывать onPeriodChange при клике по точке', () => { + render() + const points = screen.getAllByRole('button') + + fireEvent.click(points[2]) + expect(mockOnPeriodChange).toHaveBeenCalledWith(2) + }) + + // Тест на вызов GSAP анимации + it('должна вызывать GSAP анимацию при изменении rotation', () => { + const { rerender } = render() + + rerender() + + // Проверяем, что gsap.to был вызван + expect(gsap.to).toHaveBeenCalled() + }) +}) diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.test.tsx b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.test.tsx new file mode 100644 index 0000000..f32eebd --- /dev/null +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.test.tsx @@ -0,0 +1,52 @@ +import { render, screen } from '@testing-library/react' + +import { EventsCarousel } from './EventsCarousel' + +// Мокаем Swiper +jest.mock('swiper/react', () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Swiper: ({ children }: any) =>
{children}
, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + SwiperSlide: ({ children }: any) => ( +
{children}
+ ), +})) + +jest.mock('swiper/modules', () => ({ + Navigation: jest.fn(), + Pagination: jest.fn(), + FreeMode: jest.fn(), +})) + +// Мокаем стили Swiper +jest.mock('swiper/css', () => ({})) +jest.mock('swiper/css/navigation', () => ({})) +jest.mock('swiper/css/pagination', () => ({})) + +describe('EventsCarousel', () => { + const mockEvents = [ + { id: 1, year: 1990, description: 'Event 1' }, + { id: 2, year: 1991, description: 'Event 2' }, + ] + + // Тест на рендеринг событий + it('должен рендерить переданные события', () => { + render() + + // Проверяем, что слайды отрендерились + const slides = screen.getAllByTestId('swiper-slide') + expect(slides).toHaveLength(mockEvents.length) + + // Проверяем контент внутри слайдов (Card компонент) + expect(screen.getByText('1990')).toBeInTheDocument() + expect(screen.getByText('Event 1')).toBeInTheDocument() + }) + + // Тест на видимость + it('должен применять класс visible, когда visible=true', () => { + render() + // В реальном компоненте класс применяется к контейнеру Swiper или обертке + // Здесь мы проверяем наличие контента, так как opacity управляется через CSS/GSAP + expect(screen.getByTestId('swiper')).toBeInTheDocument() + }) +}) diff --git a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.test.tsx b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.test.tsx new file mode 100644 index 0000000..ac91efb --- /dev/null +++ b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.test.tsx @@ -0,0 +1,82 @@ +import { fireEvent, render, screen } from '@testing-library/react' + +import { HISTORICAL_PERIODS } from '@/entities/TimePeriod' + +import { TimeFrameSlider } from './TimeFrameSlider' + +// Мокаем дочерние компоненты, чтобы тестировать изолированно +jest.mock('../CircleTimeline/CircleTimeline', () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + CircleTimeline: ({ activeIndex, onPeriodChange }: any) => ( +
+ + Active: {activeIndex} +
+ ), +})) + +jest.mock('../EventsCarousel/EventsCarousel', () => ({ + EventsCarousel: () =>
Carousel
, +})) + +describe('TimeFrameSlider', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + // Тест на рендеринг заголовка + it('должен рендерить заголовок', () => { + render() + expect(screen.getByText('Исторические даты')).toBeInTheDocument() + }) + + // Тест на отображение начального периода + it('должен отображать начальный период (первый в списке)', () => { + render() + const firstPeriod = HISTORICAL_PERIODS[0] + expect(screen.getByText(firstPeriod.yearFrom)).toBeInTheDocument() + expect(screen.getByText(firstPeriod.yearTo)).toBeInTheDocument() + }) + + // Тест на переключение вперед + it('должен переключать период вперед при клике на кнопку "Следующий"', () => { + render() + + const nextButton = screen.getByLabelText('Следующий период') + fireEvent.click(nextButton) + + // Проверяем, что отображается второй период + const secondPeriod = HISTORICAL_PERIODS[1] + expect(screen.getByText(secondPeriod.yearFrom)).toBeInTheDocument() + }) + + // Тест на переключение назад + it('должен переключать период назад при клике на кнопку "Предыдущий"', () => { + render() + + // Сначала переключаем вперед, чтобы не быть на первом элементе (хотя логика циклична) + const nextButton = screen.getByLabelText('Следующий период') + fireEvent.click(nextButton) + + // Теперь назад + const prevButton = screen.getByLabelText('Предыдущий период') + fireEvent.click(prevButton) + + // Должны вернуться к первому периоду + const firstPeriod = HISTORICAL_PERIODS[0] + expect(screen.getByText(firstPeriod.yearFrom)).toBeInTheDocument() + }) + + // Тест на пагинацию (точки) + it('должен переключать период при клике на точку пагинации', () => { + render() + + const dots = screen.getAllByLabelText(/Перейти к периоду/) + fireEvent.click(dots[2]) // Клик по 3-й точке + + const thirdPeriod = HISTORICAL_PERIODS[2] + expect(screen.getByText(thirdPeriod.yearFrom)).toBeInTheDocument() + }) +}) From 013d32f09d50dc6cd79a44de32d94ae9ae3127ed Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 14:12:27 +0300 Subject: [PATCH 14/18] =?UTF-8?q?feat:=20=D0=9D=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BD=20webpack=20=D0=BF=D0=BB=D0=B0=D0=B3=D0=B8?= =?UTF-8?q?=D0=BD=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=B6=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D1=8F=20prod=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/build/buildPlugins.ts | 15 +++++++++++++++ package.json | 1 + pnpm-lock.yaml | 15 +++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/config/build/buildPlugins.ts b/config/build/buildPlugins.ts index 1734125..b8f7cb8 100644 --- a/config/build/buildPlugins.ts +++ b/config/build/buildPlugins.ts @@ -1,4 +1,5 @@ import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin' +import CompressionPlugin from 'compression-webpack-plugin' import HtmlWebpackPlugin from 'html-webpack-plugin' import MiniCssExtractPlugin from 'mini-css-extract-plugin' import webpack from 'webpack' @@ -22,6 +23,9 @@ import { BuildOptions } from './types/config' * - ReactRefreshWebpackPlugin: обеспечивает быструю перезагрузку React компонентов * - HotModuleReplacementPlugin: включает горячую замену модулей (HMR) * + * Плагины только для production: + * - CompressionPlugin: создает gzip-сжатые версии файлов для уменьшения размера передачи + * * @param {BuildOptions} options - Опции сборки * @param {BuildPaths} options.paths - Пути проекта * @param {boolean} options.isDev - Флаг режима разработки @@ -68,6 +72,17 @@ export function buildPlugins({ ) plugins.push(new ReactRefreshWebpackPlugin({ overlay: false })) plugins.push(new webpack.HotModuleReplacementPlugin()) + } else { + // Сжатие файлов для production сборки + // Создает .gz файлы для всех JS и CSS файлов больше 10KB + plugins.push( + new CompressionPlugin({ + algorithm: 'gzip', + test: /\.(js|css|html|svg)$/, + threshold: 10240, // Сжимаем только файлы больше 10KB + minRatio: 0.8, // Сжимаем только если размер уменьшается минимум на 20% + }) + ) } return plugins } diff --git a/package.json b/package.json index 31baf1f..2acf48a 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@typescript-eslint/eslint-plugin": "^8.16.0", "@typescript-eslint/parser": "^8.16.0", "babel-loader": "^9.2.0", + "compression-webpack-plugin": "^11.1.0", "css-loader": "^7.1.0", "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ea9f2f..770ab62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,9 @@ importers: babel-loader: specifier: ^9.2.0 version: 9.2.1(@babel/core@7.28.5)(webpack@5.103.0) + compression-webpack-plugin: + specifier: ^11.1.0 + version: 11.1.0(webpack@5.103.0) css-loader: specifier: ^7.1.0 version: 7.1.2(webpack@5.103.0) @@ -2620,6 +2623,12 @@ packages: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} + compression-webpack-plugin@11.1.0: + resolution: {integrity: sha512-zDOQYp10+upzLxW+VRSjEpRRwBXJdsb5lBMlRxx1g8hckIFBpe3DTI0en2w7h+beuq89576RVzfiXrkdPGrHhA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + webpack: ^5.1.0 + compression@1.8.1: resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} engines: {node: '>= 0.8.0'} @@ -8614,6 +8623,12 @@ snapshots: dependencies: mime-db: 1.54.0 + compression-webpack-plugin@11.1.0(webpack@5.103.0): + dependencies: + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + webpack: 5.103.0(esbuild@0.25.12)(webpack-cli@5.1.4) + compression@1.8.1: dependencies: bytes: 3.1.2 From 2a72e1077c1efa5239f0e70c0e92ebd3f4dcdeef Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 14:13:21 +0300 Subject: [PATCH 15/18] =?UTF-8?q?feat:=20=D0=9D=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B9=D0=BA=D0=B8=20webpack=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=83=D0=BC=D0=B5=D0=BD=D1=8C=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=80=D0=B0=20=D0=B1=D0=B0=D0=BD?= =?UTF-8?q?=D0=B4=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/build/buildLoaders.ts | 1 + config/build/loaders/buildBabelLoader.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/config/build/buildLoaders.ts b/config/build/buildLoaders.ts index 4fd1e06..b08ae5e 100644 --- a/config/build/buildLoaders.ts +++ b/config/build/buildLoaders.ts @@ -25,6 +25,7 @@ export function buildLoaders({ isDev }: BuildOptions): webpack.RuleSetRule[] { // Используем babel-loader для обработки JS и TS файлов // Это ускоряет сборку, так как babel работает быстрее ts-loader // Проверка типов должна выполняться отдельно (например, через tsc --noEmit) + // Исключаем тестовые и storybook файлы из production сборки const codeBabelLoader = buildBabelLoader(isDev) const fileLoader = buildFileLoader() diff --git a/config/build/loaders/buildBabelLoader.ts b/config/build/loaders/buildBabelLoader.ts index f5bbc79..6e1939e 100644 --- a/config/build/loaders/buildBabelLoader.ts +++ b/config/build/loaders/buildBabelLoader.ts @@ -14,7 +14,12 @@ export function buildBabelLoader(isDev: boolean) { const babelLoader = { test: /\.(js|jsx|tsx|ts)$/, - exclude: /node_modules/, + exclude: [ + /node_modules/, + /\.test\.(ts|tsx)$/, // Исключаем тестовые файлы + /\.spec\.(ts|tsx)$/, // Исключаем spec файлы + /\.stories\.(ts|tsx)$/, // Исключаем Storybook файлы + ], use: { loader: 'babel-loader', options: { From e4c563075f4c3b32dad3ed0ed1ab617105ccc48c Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 14:13:52 +0300 Subject: [PATCH 16/18] =?UTF-8?q?feat:=20=D0=9E=D0=BF=D1=82=D0=B8=D0=BC?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B8=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/jest/setupTests.ts | 17 ----------------- .../ui/CircleTimeline/CircleTimeline.tsx | 2 +- .../ui/EventsCarousel/EventsCarousel.tsx | 2 +- .../ui/EventsCarousel/constants.ts | 2 -- .../ui/TimeFrameSlider/TimeFrameSlider.tsx | 2 +- 5 files changed, 3 insertions(+), 22 deletions(-) delete mode 100644 config/jest/setupTests.ts diff --git a/config/jest/setupTests.ts b/config/jest/setupTests.ts deleted file mode 100644 index 1dd6186..0000000 --- a/config/jest/setupTests.ts +++ /dev/null @@ -1,17 +0,0 @@ -import '@testing-library/jest-dom' -import 'regenerator-runtime/runtime' - -// Глобальный мок для GSAP -jest.mock('gsap', () => { - return { - to: jest.fn(), - fromTo: jest.fn(), - killTweensOf: jest.fn(), - context: jest.fn(() => ({ - revert: jest.fn(), - })), - Power2: { - easeOut: 'power2.out', - }, - } -}) diff --git a/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.tsx b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.tsx index 35ab81b..f803983 100644 --- a/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.tsx +++ b/src/widgets/TimeFrameSlider/ui/CircleTimeline/CircleTimeline.tsx @@ -9,7 +9,7 @@ */ import classNames from 'classnames' -import gsap from 'gsap' +import { gsap } from 'gsap' import { memo, useCallback, useEffect, useMemo, useRef } from 'react' import styles from './CircleTimeline.module.scss' diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx index cd12507..0e9c4df 100644 --- a/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/EventsCarousel.tsx @@ -5,7 +5,7 @@ */ import classNames from 'classnames' -import gsap from 'gsap' +import { gsap } from 'gsap' import { memo, useEffect, useRef, useState } from 'react' import { Swiper, SwiperSlide } from 'swiper/react' diff --git a/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts b/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts index b7f3c96..47ec773 100644 --- a/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts +++ b/src/widgets/TimeFrameSlider/ui/EventsCarousel/constants.ts @@ -1,7 +1,5 @@ import { Navigation } from 'swiper/modules' -import styles from './EventsCarousel.module.scss' - import type { SwiperOptions } from 'swiper/types' /** diff --git a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx index 36d67e9..5293b6d 100644 --- a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx +++ b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx @@ -4,7 +4,7 @@ */ import classNames from 'classnames' -import gsap from 'gsap' +import { gsap } from 'gsap' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { HISTORICAL_PERIODS } from '@/entities/TimePeriod' From 246371c43141eb736af9916688bebe098ea39e1e Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 14:15:45 +0300 Subject: [PATCH 17/18] =?UTF-8?q?feat:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BD=D0=B0=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D0=B8=D1=8F=20setupTests=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/jest/setupTests.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 config/jest/setupTests.ts diff --git a/config/jest/setupTests.ts b/config/jest/setupTests.ts new file mode 100644 index 0000000..b5c24bc --- /dev/null +++ b/config/jest/setupTests.ts @@ -0,0 +1,24 @@ +import '@testing-library/jest-dom' +import 'regenerator-runtime/runtime' + +// Глобальный мок для GSAP +jest.mock('gsap', () => { + const gsapMock = { + to: jest.fn(), + fromTo: jest.fn(), + killTweensOf: jest.fn(), + context: jest.fn(() => ({ + revert: jest.fn(), + })), + Power2: { + easeOut: 'power2.out', + }, + } + + return { + __esModule: true, + default: gsapMock, // Для default import + gsap: gsapMock, // Для named import + Power2: gsapMock.Power2, // Экспортируем Power2 отдельно + } +}) From 1c81deaed0bde3f3eeefa461b01e4e79e9b649c7 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 23 Nov 2025 14:47:07 +0300 Subject: [PATCH 18/18] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=84=D0=B0=D0=B9=D0=BB=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..6650a26 --- /dev/null +++ b/README.md @@ -0,0 +1,231 @@ +# Only Task - Интерактивная временная шкала + +Современное React-приложение с интерактивной круговой временной шкалой и каруселью исторических событий. + +## 🚀 Технологии + +- **React 19** - библиотека для создания пользовательских интерфейсов +- **TypeScript 5** - типизированный JavaScript +- **GSAP** - библиотека для плавных анимаций +- **Swiper** - современная библиотека для каруселей +- **Webpack 5** - сборщик модулей +- **SCSS** - препроцессор CSS с модулями +- **Jest** - фреймворк для тестирования +- **ESLint + Prettier** - линтинг и форматирование кода + +## 📋 Требования + +- **Node.js** >= 18.0.0 +- **pnpm** >= 8.0.0 + +## 🛠️ Установка + +1. Клонируйте репозиторий: +```bash +git clone +cd only-task +``` + +2. Установите зависимости: +```bash +pnpm install +``` + +## 🎯 Доступные команды + +### Разработка + +```bash +# Запуск dev-сервера на порту 3000 +pnpm dev +``` + +Приложение будет доступно по адресу: http://localhost:3000 + +### Сборка + +```bash +# Production сборка +pnpm build:prod + +# Development сборка +pnpm build:dev +``` + +Результат сборки будет в папке `dist/` + +### Тестирование + +```bash +# Запуск всех unit-тестов +pnpm test:unit + +# Проверка типов TypeScript +pnpm type-check +``` + +### Линтинг + +```bash +# Проверка JavaScript/TypeScript кода +pnpm lint + +# Автоматическое исправление ошибок +pnpm lint --fix + +# Проверка стилей (SCSS) +pnpm lint:styles + +# Автоматическое исправление стилей +pnpm lint:styles --fix +``` + +### Storybook + +```bash +# Запуск Storybook для разработки компонентов +pnpm storybook + +# Сборка статической версии Storybook +pnpm build-storybook +``` + +### Pre-push проверки + +```bash +# Запуск всех проверок перед push +pnpm pre-push +``` + +Эта команда выполняет: +- ✅ Проверку типов TypeScript +- ✅ Линтинг кода +- ✅ Линтинг стилей +- ✅ Запуск unit-тестов +- ✅ Production сборку + +## 📁 Структура проекта + +``` +only-task/ +├── config/ # Конфигурация сборки +│ ├── build/ # Webpack конфигурация +│ ├── jest/ # Jest конфигурация +│ └── storybook/ # Storybook конфигурация +├── dist/ # Production сборка (генерируется) +├── src/ # Исходный код +│ ├── app/ # Корневой компонент приложения +│ ├── entities/ # Бизнес-сущности (типы данных) +│ ├── shared/ # Переиспользуемые компоненты и утилиты +│ │ ├── assets/ # Статические ресурсы (SVG, изображения) +│ │ └── ui/ # UI компоненты (Button, Card) +│ └── widgets/ # Сложные компоненты (TimeFrameSlider) +├── .husky/ # Git hooks +├── package.json # Зависимости и скрипты +└── tsconfig.json # Конфигурация TypeScript +``` + +## 🎨 Основные компоненты + +### TimeFrameSlider +Главный виджет приложения, объединяющий круговую временную шкалу и карусель событий. + +**Особенности:** +- Интерактивная круговая диаграмма с периодами +- Плавные GSAP анимации при переключении +- Адаптивный дизайн для мобильных, планшетов и десктопа +- Кастомная навигация + +### CircleTimeline +Круговая временная шкала с точками периодов. + +**Особенности:** +- Автоматический поворот к активному периоду +- Клик по точкам для переключения +- Анимация заголовков периодов + +### EventsCarousel +Карусель исторических событий с использованием Swiper. + +**Особенности:** +- Адаптивное количество слайдов +- Кастомная навигация +- Плавная анимация появления/исчезновения + +## ⚡ Оптимизация производительности + +Проект оптимизирован для production: + +- ✅ **Code splitting** - разделение на chunks (vendors, runtime, main) +- ✅ **Tree shaking** - удаление неиспользуемого кода +- ✅ **Gzip compression** - сжатие файлов (~68% уменьшение размера) +- ✅ **Minification** - минификация JS и CSS +- ✅ **CSS Modules** - изолированные стили компонентов +- ✅ **Babel caching** - кеширование для быстрой пересборки + +**Размер бандла:** +- Uncompressed: ~371 KiB +- Gzipped: ~117 KiB + +## 🧪 Тестирование + +Проект покрыт unit-тестами: + +- ✅ Тесты компонентов (Button, Card, CircleTimeline, TimeFrameSlider, EventsCarousel) +- ✅ Тесты утилит (calculateCoordinates) +- ✅ Моки для GSAP и Swiper +- ✅ 39 тестов, 100% успешных + +## 📱 Адаптивность + +Приложение адаптировано для всех устройств: + +- **Desktop** (>1440px) - полная версия с круговой диаграммой +- **Tablet** (768px - 1024px) - оптимизированная версия +- **Mobile** (<768px) - упрощенная версия с навигацией внизу + +## 🔧 Конфигурация + +### Webpack +- Babel для транспиляции (быстрее ts-loader) +- CSS Modules для изоляции стилей +- SVGR для импорта SVG как React компонентов +- Hot Module Replacement для разработки +- Bundle Analyzer для анализа размера + +### TypeScript +- Strict mode включен +- Path aliases (`@/*` → `src/*`) +- Проверка типов отдельно от сборки + +### ESLint +- Правила для React, TypeScript, Jest +- Prettier интеграция +- Import sorting + +## 🤝 Git Hooks + +Pre-push hook автоматически запускает: +1. Type checking +2. ESLint +3. Stylelint +4. Unit tests +5. Production build + +Это гарантирует, что в репозиторий попадает только рабочий код. + +## 📝 Лицензия + +ISC + +## 👨‍💻 Разработка + +Для разработки рекомендуется: +1. Запустить `pnpm dev` для dev-сервера +2. Использовать `pnpm storybook` для разработки компонентов в изоляции +3. Писать тесты для новых компонентов +4. Следовать существующей структуре проекта + +--- + +**Приятной разработки! 🚀**