This commit is contained in:
Hamed
2026-08-05 11:28:05 +04:00
parent fbacd2ca22
commit 121287e105
5 changed files with 189 additions and 23 deletions
+15 -4
View File
@@ -2,10 +2,11 @@
<project version="4"> <project version="4">
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="bb7b50a5-be9c-4cd7-a05b-979f0e91b173" name="Changes" comment="update"> <list default="true" id="bb7b50a5-be9c-4cd7-a05b-979f0e91b173" name="Changes" comment="update">
<change afterPath="$PROJECT_DIR$/components/MobileShowcase.jsx" afterDir="false" />
<change afterPath="$PROJECT_DIR$/components/ShowcaseSection.jsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/page.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/app/page.jsx" afterDir="false" /> <change beforePath="$PROJECT_DIR$/app/page.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/app/page.jsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/components/FloatingItems.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/components/FloatingItems.jsx" afterDir="false" /> <change beforePath="$PROJECT_DIR$/components/HorizontalShowcase.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/components/HorizontalShowcase.jsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -68,7 +69,7 @@
<component name="SharedIndexes"> <component name="SharedIndexes">
<attachedChunks> <attachedChunks>
<set> <set>
<option value="bundled-js-predefined-d6986cc7102b-31caf2ab9e3c-JavaScript-WS-261.23567.141" /> <option value="bundled-js-predefined-d6986cc7102b-31caf2ab9e3c-JavaScript-WS-261.26222.58" />
</set> </set>
</attachedChunks> </attachedChunks>
</component> </component>
@@ -89,6 +90,8 @@
<workItem from="1785645096071" duration="26000" /> <workItem from="1785645096071" duration="26000" />
<workItem from="1785646314677" duration="16621000" /> <workItem from="1785646314677" duration="16621000" />
<workItem from="1785730130350" duration="12049000" /> <workItem from="1785730130350" duration="12049000" />
<workItem from="1785757166936" duration="5000" />
<workItem from="1785901279535" duration="9314000" />
</task> </task>
<task id="LOCAL-00001" summary="update"> <task id="LOCAL-00001" summary="update">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -218,7 +221,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1785754201946</updated> <updated>1785754201946</updated>
</task> </task>
<option name="localTasksCounter" value="17" /> <task id="LOCAL-00017" summary="update">
<option name="closed" value="true" />
<created>1785755999255</created>
<option name="number" value="00017" />
<option name="presentableId" value="LOCAL-00017" />
<option name="project" value="LOCAL" />
<updated>1785755999255</updated>
</task>
<option name="localTasksCounter" value="18" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
+3 -3
View File
@@ -1,6 +1,5 @@
import Header from '@/components/Header'; import Header from '@/components/Header';
import HeroSection from '@/components/HeroSection'; import HeroSection from '@/components/HeroSection';
import HorizontalShowcase from '@/components/HorizontalShowcase';
import TrustedBySection from '@/components/TrustedBySection'; import TrustedBySection from '@/components/TrustedBySection';
import FeaturesSection from '@/components/FeaturesSection'; import FeaturesSection from '@/components/FeaturesSection';
import AboutSection from '@/components/AboutSection'; import AboutSection from '@/components/AboutSection';
@@ -12,6 +11,7 @@ import TicketForm from '@/components/TicketForm';
import Footer from '@/components/Footer'; import Footer from '@/components/Footer';
import CartDrawer from '@/components/CartDrawer'; import CartDrawer from '@/components/CartDrawer';
import ChatWidget from '@/components/ChatWidget'; import ChatWidget from '@/components/ChatWidget';
import ShowcaseSection from "@/components/ShowcaseSection";
export default function HomePage() { export default function HomePage() {
@@ -25,7 +25,7 @@ export default function HomePage() {
<HeroSection/> <HeroSection/>
<HorizontalShowcase/> <ShowcaseSection/>
<TrustedBySection/> <TrustedBySection/>
@@ -33,7 +33,7 @@ export default function HomePage() {
<AboutSection/> <AboutSection/>
{/*<CatalogSection/>*/} <CatalogSection/>
<LocationsSection/> <LocationsSection/>
+35 -15
View File
@@ -1,9 +1,5 @@
'use client'; 'use client';
// سکشن ویترین محصولات: با اسکرول عمودی به اسکرول افقی پین‌شده تبدیل می‌شود (GSAP ScrollTrigger)
// و یک ویدیوی پس‌زمینه دارد که با پیشرفت اسکرول جلو/عقب می‌رود. طبق درخواست کاربر، این سکشن
// و هیرو از چرخه‌ی رنگ‌بندی متناوب بقیه سکشن‌ها مستثنا هستند.
import {useEffect, useRef, useState} from 'react'; import {useEffect, useRef, useState} from 'react';
import {gsap} from 'gsap'; import {gsap} from 'gsap';
import {ScrollTrigger} from 'gsap/ScrollTrigger'; import {ScrollTrigger} from 'gsap/ScrollTrigger';
@@ -30,9 +26,28 @@ export default function HorizontalShowcase() {
const ctx = gsap.context(() => { const ctx = gsap.context(() => {
const track = trackRef.current; const track = trackRef.current;
const section = sectionRef.current; const section = sectionRef.current;
if (!track || !section) return; 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, { const horizontalTween = gsap.to(track, {
x: () => -scrollDistance(), x: () => -scrollDistance(),
@@ -44,16 +59,12 @@ export default function HorizontalShowcase() {
scrub: 1, scrub: 1,
pin: true, pin: true,
invalidateOnRefresh: true, invalidateOnRefresh: true,
onUpdate: (self) => { onUpdate: (self) => syncVideo(self.progress),
const video = videoRef.current;
if (video && video.duration) {
video.currentTime = self.progress * video.duration;
}
},
}, },
}); });
return () => { return () => {
if (animationFrameId !== null) window.cancelAnimationFrame(animationFrameId);
horizontalTween.scrollTrigger?.kill(); horizontalTween.scrollTrigger?.kill();
horizontalTween.kill(); horizontalTween.kill();
}; };
@@ -64,6 +75,7 @@ export default function HorizontalShowcase() {
const handleModalAddToCart = () => { const handleModalAddToCart = () => {
if (!quickViewMachine) return; if (!quickViewMachine) return;
dispatch( dispatch(
addToCart({ addToCart({
id: quickViewMachine.id, id: quickViewMachine.id,
@@ -73,6 +85,7 @@ export default function HorizontalShowcase() {
image: quickViewMachine.image, image: quickViewMachine.image,
}) })
); );
message.success(t.cart.addToast(quickViewMachine.name)); message.success(t.cart.addToast(quickViewMachine.name));
setQuickViewMachine(null); setQuickViewMachine(null);
}; };
@@ -115,12 +128,19 @@ export default function HorizontalShowcase() {
> >
{quickViewMachine && ( {quickViewMachine && (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<img src={quickViewMachine.image} alt={quickViewMachine.name} <img
className="w-full h-64 object-contain"/> src={quickViewMachine.image}
alt={quickViewMachine.name}
className="w-full h-64 object-contain"
/>
<p className="text-secondary leading-7">{quickViewMachine.description}</p> <p className="text-secondary leading-7">{quickViewMachine.description}</p>
<p className="text-xl font-bold text-primary">{quickViewMachine.priceLabel}</p> <p className="text-xl font-bold text-primary">{quickViewMachine.priceLabel}</p>
<Button type="primary" size="large" icon={<ShoppingCartOutlined/>} <Button
onClick={handleModalAddToCart}> type="primary"
size="large"
icon={<ShoppingCartOutlined/>}
onClick={handleModalAddToCart}
>
{t.machineActions.addToCart} {t.machineActions.addToCart}
</Button> </Button>
</div> </div>
+107
View File
@@ -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 (
<section id="showcase" className="relative w-full py-8 px-4" dir={dir}>
<div className="flex flex-col gap-4">
{t.machines.map((machine) => (
<div
key={machine.id}
className="flex gap-4 items-center bg-surface border border-subtle rounded-2xl p-4"
>
<img
src={machine.image}
alt={machine.name}
className="w-24 h-24 object-contain shrink-0"
onClick={() => setQuickViewMachine(machine)}
/>
<div className="flex flex-col gap-1.5 min-w-0 flex-1">
<span className="text-brand-400 text-xs font-semibold tracking-wide">
{machine.shortLabel}
</span>
<h3 className="text-lg font-bold text-primary truncate">
{machine.name}
</h3>
<p className="text-sm text-secondary line-clamp-2">
{machine.description}
</p>
<p className="text-base font-bold text-primary mt-1">
{machine.priceLabel}
</p>
<div className="flex justify-start items-center mt-3">
<Button
type="primary"
size="middle"
icon={<ShoppingCartOutlined/>}
onClick={() => handleAddToCart(machine)}
>
{t.machineActions.addToCart}
</Button>
</div>
</div>
</div>
))}
</div>
<Modal
open={!!quickViewMachine}
onCancel={() => setQuickViewMachine(null)}
footer={null}
centered
title={quickViewMachine?.name}
>
{quickViewMachine && (
<div className="flex flex-col gap-4">
<img
src={quickViewMachine.image}
alt={quickViewMachine.name}
className="w-full h-64 object-contain"
/>
<p className="text-secondary leading-7">{quickViewMachine.description}</p>
<p className="text-xl font-bold text-primary">{quickViewMachine.priceLabel}</p>
<Button
type="primary"
size="large"
icon={<ShoppingCartOutlined/>}
onClick={handleModalAddToCart}
>
{t.machineActions.addToCart}
</Button>
</div>
)}
</Modal>
</section>
);
}
+28
View File
@@ -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 ? <MobileShowcase/> : <HorizontalShowcase/>;
}