update
This commit is contained in:
Generated
+15
-4
@@ -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
@@ -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/>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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