diff --git a/.idea/workspace.xml b/.idea/workspace.xml index a2f171e..6b79b1c 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,10 +2,11 @@ + + - - + diff --git a/app/page.jsx b/app/page.jsx index 2832a67..4675183 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -1,6 +1,5 @@ import Header from '@/components/Header'; import HeroSection from '@/components/HeroSection'; -import HorizontalShowcase from '@/components/HorizontalShowcase'; import TrustedBySection from '@/components/TrustedBySection'; import FeaturesSection from '@/components/FeaturesSection'; import AboutSection from '@/components/AboutSection'; @@ -12,6 +11,7 @@ import TicketForm from '@/components/TicketForm'; import Footer from '@/components/Footer'; import CartDrawer from '@/components/CartDrawer'; import ChatWidget from '@/components/ChatWidget'; +import ShowcaseSection from "@/components/ShowcaseSection"; export default function HomePage() { @@ -25,7 +25,7 @@ export default function HomePage() { - + @@ -33,7 +33,7 @@ export default function HomePage() { - {/**/} + diff --git a/components/HorizontalShowcase.jsx b/components/HorizontalShowcase.jsx index 4a4b7aa..222a452 100644 --- a/components/HorizontalShowcase.jsx +++ b/components/HorizontalShowcase.jsx @@ -1,9 +1,5 @@ 'use client'; -// سکشن ویترین محصولات: با اسکرول عمودی به اسکرول افقی پین‌شده تبدیل می‌شود (GSAP ScrollTrigger) -// و یک ویدیوی پس‌زمینه دارد که با پیشرفت اسکرول جلو/عقب می‌رود. طبق درخواست کاربر، این سکشن -// و هیرو از چرخه‌ی رنگ‌بندی متناوب بقیه سکشن‌ها مستثنا هستند. - import {useEffect, useRef, useState} from 'react'; import {gsap} from 'gsap'; import {ScrollTrigger} from 'gsap/ScrollTrigger'; @@ -30,9 +26,28 @@ export default function HorizontalShowcase() { const ctx = gsap.context(() => { const track = trackRef.current; const section = sectionRef.current; + if (!track || !section) return; - const scrollDistance = () => track.scrollWidth - window.innerWidth; + const scrollDistance = () => ( + Math.max(track.scrollWidth - window.innerWidth, 0) + ); + + let animationFrameId = null; + let latestProgress = 0; + + const syncVideo = (progress) => { + latestProgress = progress; + if (animationFrameId !== null) return; + + animationFrameId = window.requestAnimationFrame(() => { + const video = videoRef.current; + if (video && Number.isFinite(video.duration) && video.duration > 0) { + video.currentTime = latestProgress * video.duration; + } + animationFrameId = null; + }); + }; const horizontalTween = gsap.to(track, { x: () => -scrollDistance(), @@ -44,16 +59,12 @@ export default function HorizontalShowcase() { scrub: 1, pin: true, invalidateOnRefresh: true, - onUpdate: (self) => { - const video = videoRef.current; - if (video && video.duration) { - video.currentTime = self.progress * video.duration; - } - }, + onUpdate: (self) => syncVideo(self.progress), }, }); return () => { + if (animationFrameId !== null) window.cancelAnimationFrame(animationFrameId); horizontalTween.scrollTrigger?.kill(); horizontalTween.kill(); }; @@ -64,6 +75,7 @@ export default function HorizontalShowcase() { const handleModalAddToCart = () => { if (!quickViewMachine) return; + dispatch( addToCart({ id: quickViewMachine.id, @@ -73,6 +85,7 @@ export default function HorizontalShowcase() { image: quickViewMachine.image, }) ); + message.success(t.cart.addToast(quickViewMachine.name)); setQuickViewMachine(null); }; @@ -115,12 +128,19 @@ export default function HorizontalShowcase() { > {quickViewMachine && (
- {quickViewMachine.name} + {quickViewMachine.name}

{quickViewMachine.description}

{quickViewMachine.priceLabel}

-
@@ -128,4 +148,4 @@ export default function HorizontalShowcase() { ); -} +} \ No newline at end of file diff --git a/components/MobileShowcase.jsx b/components/MobileShowcase.jsx new file mode 100644 index 0000000..d8658da --- /dev/null +++ b/components/MobileShowcase.jsx @@ -0,0 +1,107 @@ +'use client'; + +import {useState} from 'react'; +import {Modal, Button, message} from 'antd'; +import {ShoppingCartOutlined, EyeOutlined} from '@ant-design/icons'; +import {useDispatch} from 'react-redux'; +import {useLanguage} from '@/app/context/LanguageContext'; +import {addToCart} from '@/store/cartSlice'; + +export default function MobileShowcase() { + const dispatch = useDispatch(); + const {t, dir} = useLanguage(); + const [quickViewMachine, setQuickViewMachine] = useState(null); + + const handleAddToCart = (machine) => { + dispatch( + addToCart({ + id: machine.id, + name: machine.name, + priceNumeric: machine.priceNumeric, + priceLabel: machine.priceLabel, + image: machine.image, + }) + ); + message.success(t.cart.addToast(machine.name)); + }; + + const handleModalAddToCart = () => { + if (!quickViewMachine) return; + handleAddToCart(quickViewMachine); + setQuickViewMachine(null); + }; + + return ( +
+
+ {t.machines.map((machine) => ( +
+ {machine.name} setQuickViewMachine(machine)} + /> + +
+ + {machine.shortLabel} + +

+ {machine.name} +

+

+ {machine.description} +

+

+ {machine.priceLabel} +

+ +
+ +
+
+
+ ))} +
+ + setQuickViewMachine(null)} + footer={null} + centered + title={quickViewMachine?.name} + > + {quickViewMachine && ( +
+ {quickViewMachine.name} +

{quickViewMachine.description}

+

{quickViewMachine.priceLabel}

+ +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/components/ShowcaseSection.jsx b/components/ShowcaseSection.jsx new file mode 100644 index 0000000..9be9046 --- /dev/null +++ b/components/ShowcaseSection.jsx @@ -0,0 +1,28 @@ +'use client'; + +import {useEffect, useState} from 'react'; +import HorizontalShowcase from './HorizontalShowcase'; +import MobileShowcase from './MobileShowcase'; + +const MOBILE_BREAKPOINT = 768; + +export default function ShowcaseSection() { + const [isMobile, setIsMobile] = useState(null); + + useEffect(() => { + const mediaQuery = window.matchMedia( + `(max-width: ${MOBILE_BREAKPOINT - 1}px)` + ); + + const handleChange = (event) => setIsMobile(event.matches); + + setIsMobile(mediaQuery.matches); + mediaQuery.addEventListener('change', handleChange); + + return () => mediaQuery.removeEventListener('change', handleChange); + }, []); + + if (isMobile === null) return null; + + return isMobile ? : ; +} \ No newline at end of file