update
This commit is contained in:
Generated
+15
-4
@@ -2,10 +2,11 @@
|
||||
<project version="4">
|
||||
<component name="ChangeListManager">
|
||||
<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$/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$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/HorizontalShowcase.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/components/HorizontalShowcase.jsx" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -68,7 +69,7 @@
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<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>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
@@ -89,6 +90,8 @@
|
||||
<workItem from="1785645096071" duration="26000" />
|
||||
<workItem from="1785646314677" duration="16621000" />
|
||||
<workItem from="1785730130350" duration="12049000" />
|
||||
<workItem from="1785757166936" duration="5000" />
|
||||
<workItem from="1785901279535" duration="9314000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="update">
|
||||
<option name="closed" value="true" />
|
||||
@@ -218,7 +221,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1785754201946</updated>
|
||||
</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 />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
|
||||
+3
-3
@@ -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() {
|
||||
|
||||
<HeroSection/>
|
||||
|
||||
<HorizontalShowcase/>
|
||||
<ShowcaseSection/>
|
||||
|
||||
<TrustedBySection/>
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function HomePage() {
|
||||
|
||||
<AboutSection/>
|
||||
|
||||
{/*<CatalogSection/>*/}
|
||||
<CatalogSection/>
|
||||
|
||||
<LocationsSection/>
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<img src={quickViewMachine.image} alt={quickViewMachine.name}
|
||||
className="w-full h-64 object-contain"/>
|
||||
<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}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<ShoppingCartOutlined/>}
|
||||
onClick={handleModalAddToCart}
|
||||
>
|
||||
{t.machineActions.addToCart}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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/>;
|
||||
}
|
||||
Reference in New Issue
Block a user