diff --git a/frontend-next-migration/src/entities/Gallery/ui/GalleryCategoriesWithModalSlider.tsx b/frontend-next-migration/src/entities/Gallery/ui/GalleryCategoriesWithModalSlider.tsx index 207b62b32..148f0ba72 100644 --- a/frontend-next-migration/src/entities/Gallery/ui/GalleryCategoriesWithModalSlider.tsx +++ b/frontend-next-migration/src/entities/Gallery/ui/GalleryCategoriesWithModalSlider.tsx @@ -1,9 +1,9 @@ /* eslint-disable complexity */ 'use client'; import Image from 'next/image'; -import { memo, useEffect, useState } from 'react'; -import Fancybox from '@/shared/ui/Fancybox/Fancybox'; +import { memo, useEffect, useState, useRef } from 'react'; import { useClientTranslation } from '@/shared/i18n'; +import { ReaderModal } from '@/entities/Gallery/ui/ReaderModal'; import cls from './styles.module.scss'; export type GalleryCategoriesWithModalSliderProps = { @@ -19,9 +19,27 @@ export type GalleryCategoriesWithModalSliderProps = { export const GalleryCategoriesWithModalSlider = memo( ({ sources, cover }: GalleryCategoriesWithModalSliderProps) => { useClientTranslation('picture-galleries'); + + const [isModalOpen, setIsModalOpen] = useState(false); const [pageIndex, setPageIndex] = useState(0); + const [isMobile, setIsMobile] = useState(false); + + const touchStartX = useRef(0); + const touchEndX = useRef(0); + + useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth <= 768); + }; + + checkMobile(); + window.addEventListener('resize', checkMobile); + + return () => window.removeEventListener('resize', checkMobile); + }, []); const filteredSources = sources.filter((src) => src !== cover.url); + const sortedSources = [...filteredSources].sort((a, b) => { const numA = parseInt(a.match(/\d+/)?.[0] || '', 10); const numB = parseInt(b.match(/\d+/)?.[0] || '', 10); @@ -29,7 +47,8 @@ export const GalleryCategoriesWithModalSlider = memo( }); const allImages = [cover.url, ...sortedSources]; - const maxPageIndex = Math.ceil(sortedSources.length / 2); + + const maxPageIndex = isMobile ? sortedSources.length : Math.ceil(sortedSources.length / 2); const changePage = (direction: 'next' | 'prev') => { setPageIndex((prev) => @@ -39,30 +58,69 @@ export const GalleryCategoriesWithModalSlider = memo( useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'ArrowLeft' && pageIndex > 0) changePage('prev'); - else if (event.key === 'ArrowRight' && pageIndex < maxPageIndex) changePage('next'); + if (event.key === 'ArrowLeft' && pageIndex > 0) { + changePage('prev'); + } + + if (event.key === 'ArrowRight' && pageIndex < maxPageIndex) { + changePage('next'); + } }; + window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [pageIndex, maxPageIndex]); - const openCurrentInFancybox = () => { - const indexToOpen = pageIndex === 0 ? 0 : 1 + (pageIndex - 1) * 2; - const el = document.getElementById( - `fancybox-image-${indexToOpen}`, - ) as HTMLAnchorElement; - if (el) el.click(); + useEffect(() => { + if (isModalOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + }, [isModalOpen]); + + const openModal = () => { + setIsModalOpen(true); + }; + + // SWIPE + const handleTouchStart = (e: React.TouchEvent) => { + touchStartX.current = e.touches[0].clientX; + }; + + const handleTouchMove = (e: React.TouchEvent) => { + touchEndX.current = e.touches[0].clientX; + }; + + const handleTouchEnd = () => { + if (!isMobile) return; + + const delta = touchStartX.current - touchEndX.current; + + if (Math.abs(delta) < 50) return; + + if (delta > 0 && pageIndex < maxPageIndex) { + changePage('next'); + } + + if (delta < 0 && pageIndex > 0) { + changePage('prev'); + } }; - const renderImages = () => { + const renderImages = (isModal = false) => { return allImages.map((src, idx) => { let visible = false; + if (pageIndex === 0) { visible = idx === 0; + } else if (isMobile) { + visible = idx === pageIndex; } else { const start = (pageIndex - 1) * 2; const leftIdx = 1 + start; const rightIdx = 2 + start; + visible = idx === leftIdx || idx === rightIdx; } @@ -72,83 +130,93 @@ export const GalleryCategoriesWithModalSlider = memo( key={idx} style={{ display: visible ? 'flex' : 'none' }} > - - {idx - + {idx ); }); }; + return (
- + {/* GALLERY */} +
- {/* Zoom Button */} -
- -
+ Zoom + +
+ +
+ {renderImages(false)} +
+
- {/* Viewer */} -
+ {/* MODAL */} + setIsModalOpen(false)} + > +
+
0 ? () => changePage('prev') : undefined} - className={`${cls.navSymbol} ${pageIndex === 0 ? cls.disabled : ''}`} - role="button" - tabIndex={pageIndex > 0 ? 0 : -1} - aria-label="Previous" + onClick={(e) => { + e.stopPropagation(); + if (pageIndex > 0) changePage('prev'); + }} + className={`${cls.navSymbol} ${ + pageIndex === 0 ? cls.disabled : '' + }`} > {'<'} - {renderImages()} + {renderImages(true)} changePage('next') : undefined - } + onClick={(e) => { + e.stopPropagation(); + if (pageIndex < maxPageIndex) changePage('next'); + }} className={`${cls.navSymbol} ${ pageIndex === maxPageIndex ? cls.disabled : '' }`} - role="button" - tabIndex={pageIndex < maxPageIndex ? 0 : -1} - aria-label="Next" > {'>'}
- {/* Page slider (✅ stays close to cover) */} + {/* Slider */}
- +
); }, diff --git a/frontend-next-migration/src/entities/Gallery/ui/ReaderModal.module.scss b/frontend-next-migration/src/entities/Gallery/ui/ReaderModal.module.scss new file mode 100644 index 000000000..0e1550016 --- /dev/null +++ b/frontend-next-migration/src/entities/Gallery/ui/ReaderModal.module.scss @@ -0,0 +1,29 @@ +.dialog { + position: fixed; + inset: 0; + + width: 100%; + height: 100%; + + margin: 0; + padding: 0; + border: none; + + background: transparent; +} + +.dialog::backdrop { + background: rgba(0, 0, 0, 0.7); +} + +.content { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + display: flex; + justify-content: center; + align-items: center; +} + diff --git a/frontend-next-migration/src/entities/Gallery/ui/ReaderModal.tsx b/frontend-next-migration/src/entities/Gallery/ui/ReaderModal.tsx new file mode 100644 index 000000000..a7e3c7419 --- /dev/null +++ b/frontend-next-migration/src/entities/Gallery/ui/ReaderModal.tsx @@ -0,0 +1,61 @@ +'use client'; +import { useEffect, useRef } from 'react'; +import cls from './ReaderModal.module.scss'; + +type ReaderModalProps = { + open: boolean; + onClose: () => void; + children: React.ReactNode; +}; + +export function ReaderModal({ open, onClose, children }: ReaderModalProps) { + const dialogRef = useRef(null); + + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + + if (open && !dialog.open) { + dialog.showModal(); + } + + if (!open && dialog.open) { + dialog.close(); + } + }, [open]); + + useEffect(() => { + if (open) { + document.body.style.overflow = 'hidden'; + document.documentElement.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + document.documentElement.style.overflow = ''; + } + + return () => { + document.body.style.overflow = ''; + document.documentElement.style.overflow = ''; + }; + }, [open]); + + const handleClose = () => { + onClose(); + }; + + return ( + +
e.stopPropagation()} + > + {children} +
+
+ ); +} diff --git a/frontend-next-migration/src/entities/Gallery/ui/styles.module.scss b/frontend-next-migration/src/entities/Gallery/ui/styles.module.scss index 5f83d8dac..3b0b58a28 100644 --- a/frontend-next-migration/src/entities/Gallery/ui/styles.module.scss +++ b/frontend-next-migration/src/entities/Gallery/ui/styles.module.scss @@ -93,11 +93,6 @@ } } -.zoomed { - transition: transform 0.2s ease; - cursor: zoom-out; -} - .sliderContainer { display: flex; align-items: center; @@ -220,3 +215,9 @@ margin-bottom: 0; } } + +.modalImage { + max-height: 80vh; + max-width: 90vw; + object-fit: contain; +}