update
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
// سکشن معرفی شرکت با متن بلند؛ بهخاطر طولانی بودن متن، یک دکمه «ادامه مطلب» برای باز/بسته کردن دارد.
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useLanguage } from '@/app/context/LanguageContext';
|
||||
|
||||
@@ -9,7 +11,7 @@ export default function AboutSection() {
|
||||
const visibleParagraphs = expanded ? t.about.paragraphs : t.about.paragraphs.slice(0, 2);
|
||||
|
||||
return (
|
||||
<section id="about" className="relative px-6 md:px-16 py-20 md:py-28 bg-surface border-t border-subtle">
|
||||
<section id="about" className="relative px-6 md:px-16 py-20 md:py-28 bg-elevated border-t border-subtle">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<span className="text-brand-400 text-sm font-semibold tracking-wide">{t.about.eyebrow}</span>
|
||||
<h2 className="text-2xl md:text-4xl font-extrabold brand-gradient-text mt-3 mb-8">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
// کشوی (Drawer) سبد خرید: نمایش آیتمهای انتخابی، تغییر تعداد، جمع کل، و دکمه پرداخت (placeholder).
|
||||
|
||||
import { Drawer, Button, Empty, InputNumber, message } from 'antd';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
'use client';
|
||||
|
||||
// کاتالوگ محصولات جانبی (زعفران، تنقلات، نوشیدنی و ...) که مشتریان دستگاه با قیمت کمتر از بازار
|
||||
// میتوانند سفارش دهند. دکمه «افزودن به سفارش» از همان سبد خرید Redux دستگاهها استفاده میکند.
|
||||
|
||||
import { Button, message } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -29,7 +32,7 @@ export default function CatalogSection() {
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="catalog" className="relative px-6 md:px-16 py-20 md:py-28 bg-elevated border-t border-subtle">
|
||||
<section id="catalog" className="relative px-6 md:px-16 py-20 md:py-28 bg-page border-t border-subtle">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-4">
|
||||
<span className="text-brand-400 text-sm font-semibold tracking-wide">{t.catalog.eyebrow}</span>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
// آیکون شناور چت گوشه صفحه؛ با کلیک یک مودال گفتگوی ساده باز میشود (فعلاً بدون اتصال به بکاند واقعی).
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Modal, Input, Button, Tooltip } from 'antd';
|
||||
import { MessageOutlined, SendOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
// نوار مزایا: ضمانت کالا، مشاوره رایگان، خرید اقساطی، بستهبندی ایمن.
|
||||
|
||||
import {
|
||||
SafetyCertificateOutlined,
|
||||
CustomerServiceOutlined,
|
||||
@@ -19,7 +21,7 @@ export default function FeaturesSection() {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<section className="relative px-6 md:px-16 py-16 md:py-20 bg-page border-t border-subtle">
|
||||
<section className="relative px-6 md:px-16 py-16 md:py-20 bg-surface border-t border-subtle">
|
||||
<div className="max-w-6xl mx-auto grid sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||
{t.features.map((feature) => {
|
||||
const Icon = ICONS[feature.icon];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
// رندر آیتمهای شناور (چیپس/نوشابه) دور دستگاه با حرکت تصادفی و ملایم GSAP.
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
// فوتر سایت: درباره ما کوتاه، لینکهای شبکه اجتماعی، دسترسی سریع، و اطلاعات تماس/آدرس.
|
||||
|
||||
import { PhoneOutlined, EnvironmentOutlined } from '@ant-design/icons';
|
||||
import { useLanguage } from '@/app/context/LanguageContext';
|
||||
|
||||
@@ -8,7 +10,7 @@ export default function Footer() {
|
||||
const c = t.contactInfo;
|
||||
|
||||
return (
|
||||
<footer id="contact" className="relative border-t border-subtle bg-surface">
|
||||
<footer id="contact" className="relative border-t border-subtle bg-elevated">
|
||||
<div className="max-w-6xl mx-auto px-6 md:px-16 py-14 grid md:grid-cols-3 gap-10 text-center md:text-start">
|
||||
<div>
|
||||
<h3 className="text-primary font-bold text-lg mb-3">{t.footer.aboutLabel}</h3>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
// هدر ثابت بالای صفحه: لوگو، منوی ناوبری، سوییچ زبان/تم، دکمه ورود به پنل کاربری و آیکون سبد خرید.
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { Badge, Button } from 'antd';
|
||||
@@ -45,7 +47,7 @@ export default function Header() {
|
||||
<Image src="/images/logo.svg" alt="روبات مارکت" width={150} height={42} priority />
|
||||
</a>
|
||||
|
||||
<nav className="hidden lg:flex items-center gap-6 xl:gap-8">
|
||||
<nav className="hidden xl:flex items-center gap-5 2xl:gap-7">
|
||||
{navLinks.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
@@ -84,7 +86,7 @@ export default function Header() {
|
||||
</Badge>
|
||||
|
||||
<button
|
||||
className="lg:hidden text-brand-400 text-2xl"
|
||||
className="xl:hidden text-brand-400 text-2xl"
|
||||
onClick={() => setMobileOpen((v) => !v)}
|
||||
aria-label="menu"
|
||||
>
|
||||
@@ -94,7 +96,7 @@ export default function Header() {
|
||||
</div>
|
||||
|
||||
{mobileOpen && (
|
||||
<nav className="lg:hidden bg-elevated-95 backdrop-blur-md border-t border-subtle px-5 py-4 flex flex-col gap-4">
|
||||
<nav className="xl:hidden bg-elevated-95 backdrop-blur-md border-t border-subtle px-5 py-4 flex flex-col gap-4 max-h-[calc(100vh-5rem)] overflow-y-auto">
|
||||
{navLinks.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
|
||||
+139
-152
@@ -1,175 +1,162 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
/**
|
||||
* سکشن اول سایت (Hero) — ایده کلی از صفحه وندینگ سایت Sielaff گرفته شده:
|
||||
* یک اسلایدر تمامعرض که هر ۵ ثانیه بهصورت خودکار به اسلاید بعدی میرود،
|
||||
* همراه با یک شکل مورب رنگی (بهجای قرمز آنها، رنگ سبز برند خودمان #00a693)،
|
||||
* عنوان بزرگ دو خطی، و دکمههای فلش قبلی/بعدی برای ناوبری دستی.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { useLanguage } from '@/app/context/LanguageContext';
|
||||
import { useTheme } from '@/app/context/ThemeContext';
|
||||
|
||||
// Simple typewriter: types each phrase, pauses, deletes, moves to next phrase, loops.
|
||||
function useTypewriter(phrases, { typeSpeed = 55, deleteSpeed = 30, holdTime = 1600 } = {}) {
|
||||
const [text, setText] = useState('');
|
||||
const [phraseIndex, setPhraseIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!phrases?.length) return undefined;
|
||||
let charIndex = 0;
|
||||
let deleting = false;
|
||||
let timeoutId;
|
||||
|
||||
const tick = () => {
|
||||
const current = phrases[phraseIndex % phrases.length];
|
||||
|
||||
if (!deleting) {
|
||||
charIndex += 1;
|
||||
setText(current.slice(0, charIndex));
|
||||
if (charIndex >= current.length) {
|
||||
deleting = true;
|
||||
timeoutId = setTimeout(tick, holdTime);
|
||||
return;
|
||||
}
|
||||
timeoutId = setTimeout(tick, typeSpeed);
|
||||
} else {
|
||||
charIndex -= 1;
|
||||
setText(current.slice(0, charIndex));
|
||||
if (charIndex <= 0) {
|
||||
deleting = false;
|
||||
setPhraseIndex((i) => (i + 1) % phrases.length);
|
||||
timeoutId = setTimeout(tick, 300);
|
||||
return;
|
||||
}
|
||||
timeoutId = setTimeout(tick, deleteSpeed);
|
||||
}
|
||||
};
|
||||
|
||||
timeoutId = setTimeout(tick, 400);
|
||||
return () => clearTimeout(timeoutId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [phraseIndex, phrases]);
|
||||
|
||||
return text;
|
||||
}
|
||||
const AUTOPLAY_DELAY = 5000; // هر ۵ ثانیه اسلاید عوض میشود
|
||||
|
||||
export default function HeroSection() {
|
||||
const { t } = useLanguage();
|
||||
const { theme } = useTheme();
|
||||
const introRef = useRef(null);
|
||||
const logoRef = useRef(null);
|
||||
const bgColorRef = useRef(null);
|
||||
const subRef = useRef(null);
|
||||
const ctaRef = useRef(null);
|
||||
const { t, dir } = useLanguage();
|
||||
const slides = t.hero.slides;
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const timerRef = useRef(null);
|
||||
|
||||
const typedHeadline = useTypewriter(t.hero.typingPhrases);
|
||||
const isDark = theme === 'dark';
|
||||
// رفتن به اسلاید بعدی (با چرخش به اول لیست وقتی به آخر رسید)
|
||||
const goToNext = useCallback(() => {
|
||||
setActiveIndex((prev) => (prev + 1) % slides.length);
|
||||
}, [slides.length]);
|
||||
|
||||
const goToPrev = useCallback(() => {
|
||||
setActiveIndex((prev) => (prev - 1 + slides.length) % slides.length);
|
||||
}, [slides.length]);
|
||||
|
||||
// تایمر پخش خودکار: هر بار که اسلاید عوض میشود، تایمر قبلی پاک و یک تایمر
|
||||
// جدید ۵ ثانیهای ست میشود. با کلیک دستی روی فلشها هم همین تابع دوباره
|
||||
// صدا زده میشود تا شمارش از نو شروع شود (تجربه کاربری بهتر).
|
||||
const restartAutoplay = useCallback(() => {
|
||||
if (timerRef.current) clearInterval(timerRef.current);
|
||||
timerRef.current = setInterval(goToNext, AUTOPLAY_DELAY);
|
||||
}, [goToNext]);
|
||||
|
||||
// Intro loader fade-in / fade-out.
|
||||
useEffect(() => {
|
||||
const ctx = gsap.context(() => {
|
||||
const tl = gsap.timeline();
|
||||
tl.fromTo(logoRef.current, { opacity: 0, scale: 0.85 }, { opacity: 1, scale: 1, duration: 0.9, ease: 'power2.out' })
|
||||
.to(logoRef.current, { opacity: 0, duration: 0.5, delay: 0.4, ease: 'power1.in' })
|
||||
.to(introRef.current, { opacity: 0, duration: 0.7, ease: 'power2.inOut', pointerEvents: 'none' }, '-=0.1')
|
||||
.fromTo(subRef.current, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.8, ease: 'power3.out' }, '-=0.5')
|
||||
.fromTo(ctaRef.current, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.7, ease: 'power3.out' }, '-=0.4');
|
||||
});
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
restartAutoplay();
|
||||
return () => {
|
||||
if (timerRef.current) clearInterval(timerRef.current);
|
||||
};
|
||||
}, [restartAutoplay]);
|
||||
|
||||
// Background behavior differs by theme:
|
||||
// - Dark mode: looping color cycle (brand green -> black -> white -> green),
|
||||
// kept readable by the dark scrim below.
|
||||
// - Light mode: a calm, static light background (no cycling), paired with
|
||||
// dark text and a light scrim instead.
|
||||
useEffect(() => {
|
||||
const el = bgColorRef.current;
|
||||
if (!el) return undefined;
|
||||
const handleManualNav = (direction) => {
|
||||
if (direction === 'next') goToNext();
|
||||
else goToPrev();
|
||||
restartAutoplay(); // شمارش ۵ ثانیه بعد از کلیک دستی از نو شروع شود
|
||||
};
|
||||
|
||||
if (isDark) {
|
||||
const tl = gsap.timeline({ repeat: -1 });
|
||||
tl.to(el, { backgroundColor: '#04120f', duration: 5, ease: 'sine.inOut' })
|
||||
.to(el, { backgroundColor: '#ffffff', duration: 5, ease: 'sine.inOut' })
|
||||
.to(el, { backgroundColor: '#00a693', duration: 5, ease: 'sine.inOut' });
|
||||
return () => tl.kill();
|
||||
}
|
||||
|
||||
gsap.set(el, { backgroundColor: '#f5fbf9' });
|
||||
return undefined;
|
||||
}, [isDark]);
|
||||
|
||||
const scrimStyle = isDark
|
||||
? {
|
||||
background:
|
||||
'radial-gradient(circle at 50% 40%, rgba(4,18,15,0.35), rgba(4,18,15,0.75) 70%), linear-gradient(180deg, rgba(4,18,15,0.55) 0%, rgba(4,18,15,0.35) 45%, rgba(4,18,15,0.7) 100%)',
|
||||
}
|
||||
: {
|
||||
background:
|
||||
'radial-gradient(circle at 50% 40%, rgba(255,255,255,0.15), rgba(255,255,255,0.55) 70%), linear-gradient(180deg, rgba(255,255,255,0.45) 0%, rgba(255,255,255,0.25) 45%, rgba(255,255,255,0.55) 100%)',
|
||||
};
|
||||
const slide = slides[activeIndex];
|
||||
|
||||
return (
|
||||
<section id="hero" className="relative h-screen w-full overflow-hidden">
|
||||
{/* Intro loader overlay */}
|
||||
<div ref={introRef} className="fixed inset-0 z-[100] bg-page flex items-center justify-center">
|
||||
<div ref={logoRef} className="flex flex-col items-center gap-4">
|
||||
<div className="w-16 h-16 rounded-2xl bg-brand flex items-center justify-center shadow-brandGlow">
|
||||
<span className="text-2xl font-bold text-ink-950">R</span>
|
||||
</div>
|
||||
<p className="text-brand-400 tracking-widest text-sm">{t.hero.loading}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background: cycling colors in dark mode, static light tone in light mode */}
|
||||
<div ref={bgColorRef} className="absolute inset-0" style={{ backgroundColor: '#00a693' }} />
|
||||
|
||||
{/*
|
||||
TODO: swap the background above for a video once the asset is ready:
|
||||
|
||||
<video
|
||||
autoPlay muted loop playsInline
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
src="/video/hero-background.mp4"
|
||||
/>
|
||||
|
||||
Keep the scrim div below on top of it so the headline stays readable.
|
||||
*/}
|
||||
|
||||
{/* Scrim to keep text legible — tuned per theme so it never washes out the copy */}
|
||||
<div className="absolute inset-0" style={scrimStyle} />
|
||||
|
||||
{/* Decorative dot grid — dark dots on light backgrounds, light dots on dark */}
|
||||
<section id="hero" className="relative h-screen w-full overflow-hidden bg-page">
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
{/* شکل مورب تزئینی سمت راست/چپ — نسخه سبز برند بهجای قرمز Sielaff */}
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
<div
|
||||
className={`absolute inset-0 opacity-[0.08] [background-size:26px_26px] ${
|
||||
isDark
|
||||
? '[background-image:radial-gradient(#ffffff_1px,transparent_1px)]'
|
||||
: '[background-image:radial-gradient(#04231f_1px,transparent_1px)]'
|
||||
}`}
|
||||
className="absolute inset-y-0 w-1/3 hidden md:block"
|
||||
style={{
|
||||
[dir === 'rtl' ? 'left' : 'right']: '-6%',
|
||||
background: 'linear-gradient(135deg, #00a693 0%, #017065 60%, #0b3d38 100%)',
|
||||
clipPath:
|
||||
dir === 'rtl'
|
||||
? 'polygon(30% 0, 100% 0, 100% 100%, 0% 100%)'
|
||||
: 'polygon(0 0, 70% 0, 100% 100%, 0% 100%)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 h-full flex flex-col items-center justify-center text-center px-6">
|
||||
<h1 className="text-3xl md:text-6xl font-extrabold leading-tight max-w-4xl text-primary min-h-[1.4em]">
|
||||
{typedHeadline}
|
||||
<span className="typewriter-caret h-[0.9em] align-middle" />
|
||||
</h1>
|
||||
<p ref={subRef} className="mt-6 max-w-2xl text-secondary text-base md:text-xl opacity-0">
|
||||
{t.hero.sub}
|
||||
</p>
|
||||
<a
|
||||
ref={ctaRef}
|
||||
href="#showcase"
|
||||
className="opacity-0 mt-10 inline-flex items-center gap-2 bg-brand hover:bg-brand-400 text-ink-950 font-bold px-8 py-3.5 rounded-full transition-colors shadow-brandGlow"
|
||||
>
|
||||
{t.hero.cta}
|
||||
</a>
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
{/* اسلایدهای عکس — همه اسلایدها روی هم قرار دارند و فقط با opacity */}
|
||||
{/* محو/نمایان میشوند (transition نرم بهجای پرش ناگهانی) */}
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
<div className="absolute inset-0">
|
||||
{slides.map((s, i) => (
|
||||
<div
|
||||
key={s.id}
|
||||
className="absolute inset-0 flex items-center justify-center transition-opacity duration-1000 ease-in-out"
|
||||
style={{ opacity: i === activeIndex ? 1 : 0, pointerEvents: i === activeIndex ? 'auto' : 'none' }}
|
||||
aria-hidden={i !== activeIndex}
|
||||
>
|
||||
<img
|
||||
src={s.image}
|
||||
alt={s.titleLines.join(' ')}
|
||||
className="h-[55%] md:h-[70%] w-auto object-contain drop-shadow-2xl"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-8 inset-x-0 flex justify-center z-10">
|
||||
<div
|
||||
className={`w-6 h-10 rounded-full border-2 flex items-start justify-center p-1.5 ${
|
||||
isDark ? 'border-white/50' : 'border-ink-900/30'
|
||||
}`}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full animate-pulseSoft ${isDark ? 'bg-white' : 'bg-ink-900'}`} />
|
||||
{/* لایهی نیمهشفاف پشت متن، برای خوانایی روی هر عکسی */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(4,18,15,0.05) 0%, rgba(4,18,15,0.15) 60%, rgba(4,18,15,0.35) 100%)',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
{/* متن روی اسلاید: کیکر کوچک + عنوان بزرگ دو خطی + توضیح + دکمه CTA */}
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
<div className="relative z-10 h-full flex flex-col justify-end md:justify-center px-6 md:px-16 pb-28 md:pb-0">
|
||||
<div className="max-w-2xl">
|
||||
<span className="text-brand-400 font-semibold tracking-widest text-sm uppercase">
|
||||
{slide.kicker}
|
||||
</span>
|
||||
<h1 className="mt-3 text-3xl sm:text-4xl md:text-6xl font-extrabold leading-tight text-primary">
|
||||
{slide.titleLines.map((line, i) => (
|
||||
<span key={i} className="block">
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</h1>
|
||||
<p className="mt-5 text-secondary text-base md:text-lg max-w-xl">{slide.sub}</p>
|
||||
<a
|
||||
href="#showcase"
|
||||
className="mt-8 inline-flex items-center gap-2 bg-brand hover:bg-brand-400 text-ink-950 font-bold px-8 py-3.5 rounded-full transition-colors shadow-brandGlow"
|
||||
>
|
||||
{t.hero.cta}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
{/* فلشهای ناوبری دستی (قبلی / بعدی) — شبیه سایت Sielaff، وسط دو طرف */}
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
<button
|
||||
onClick={() => handleManualNav('prev')}
|
||||
aria-label="previous slide"
|
||||
className="hidden sm:flex absolute top-1/2 -translate-y-1/2 start-4 z-20 w-11 h-11 rounded-full bg-page-70 border border-subtle items-center justify-center text-primary hover:bg-brand hover:text-ink-950 transition-colors"
|
||||
>
|
||||
<LeftOutlined />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleManualNav('next')}
|
||||
aria-label="next slide"
|
||||
className="hidden sm:flex absolute top-1/2 -translate-y-1/2 end-4 z-20 w-11 h-11 rounded-full bg-page-70 border border-subtle items-center justify-center text-primary hover:bg-brand hover:text-ink-950 transition-colors"
|
||||
>
|
||||
<RightOutlined />
|
||||
</button>
|
||||
|
||||
{/* نقطههای شمارشگر اسلاید پایین صفحه — کلیک روی هرکدام مستقیم به همان اسلاید میرود */}
|
||||
<div className="absolute bottom-8 inset-x-0 z-20 flex justify-center gap-2">
|
||||
{slides.map((s, i) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => {
|
||||
setActiveIndex(i);
|
||||
restartAutoplay();
|
||||
}}
|
||||
aria-label={`slide ${i + 1}`}
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
i === activeIndex ? 'w-8 bg-brand' : 'w-2 bg-brand/30 hover:bg-brand/60'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
+114
-112
@@ -1,127 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
import {gsap} from 'gsap';
|
||||
import {ScrollTrigger} from 'gsap/ScrollTrigger';
|
||||
import {Modal, Button, message} from 'antd';
|
||||
import {ShoppingCartOutlined} from '@ant-design/icons';
|
||||
import {useDispatch} from 'react-redux';
|
||||
// سکشن ویترین محصولات: با اسکرول عمودی به اسکرول افقی پینشده تبدیل میشود (GSAP ScrollTrigger)
|
||||
// و یک ویدیوی پسزمینه دارد که با پیشرفت اسکرول جلو/عقب میرود. طبق درخواست کاربر، این سکشن
|
||||
// و هیرو از چرخهی رنگبندی متناوب بقیه سکشنها مستثنا هستند.
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import { Modal, Button, message } from 'antd';
|
||||
import { ShoppingCartOutlined } from '@ant-design/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import VendingCard from './VendingCard';
|
||||
import {showcaseVideoSrc} from '@/data/content';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {addToCart} from '@/store/cartSlice';
|
||||
import { showcaseVideoSrc } from '@/data/content';
|
||||
import { useLanguage } from '@/app/context/LanguageContext';
|
||||
import { addToCart } from '@/store/cartSlice';
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
export default function HorizontalShowcase() {
|
||||
const sectionRef = useRef(null);
|
||||
const trackRef = useRef(null);
|
||||
const videoRef = useRef(null);
|
||||
const dispatch = useDispatch();
|
||||
const {t} = useLanguage();
|
||||
const [quickViewMachine, setQuickViewMachine] = useState(null);
|
||||
const [videoAvailable, setVideoAvailable] = useState(true);
|
||||
const sectionRef = useRef(null);
|
||||
const trackRef = useRef(null);
|
||||
const videoRef = useRef(null);
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useLanguage();
|
||||
const [quickViewMachine, setQuickViewMachine] = useState(null);
|
||||
const [videoAvailable, setVideoAvailable] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const ctx = gsap.context(() => {
|
||||
const track = trackRef.current;
|
||||
const section = sectionRef.current;
|
||||
if (!track || !section) return;
|
||||
useEffect(() => {
|
||||
const ctx = gsap.context(() => {
|
||||
const track = trackRef.current;
|
||||
const section = sectionRef.current;
|
||||
if (!track || !section) return;
|
||||
|
||||
const scrollDistance = () => track.scrollWidth - window.innerWidth;
|
||||
const scrollDistance = () => track.scrollWidth - window.innerWidth;
|
||||
|
||||
const horizontalTween = gsap.to(track, {
|
||||
x: () => -scrollDistance(),
|
||||
ease: 'none',
|
||||
scrollTrigger: {
|
||||
trigger: section,
|
||||
start: 'top top',
|
||||
end: () => `+=${scrollDistance()}`,
|
||||
scrub: 1,
|
||||
pin: true,
|
||||
invalidateOnRefresh: true,
|
||||
onUpdate: (self) => {
|
||||
const video = videoRef.current;
|
||||
if (video && video.duration) {
|
||||
video.currentTime = self.progress * video.duration;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
const horizontalTween = gsap.to(track, {
|
||||
x: () => -scrollDistance(),
|
||||
ease: 'none',
|
||||
scrollTrigger: {
|
||||
trigger: section,
|
||||
start: 'top top',
|
||||
end: () => `+=${scrollDistance()}`,
|
||||
scrub: 1,
|
||||
pin: true,
|
||||
invalidateOnRefresh: true,
|
||||
onUpdate: (self) => {
|
||||
const video = videoRef.current;
|
||||
if (video && video.duration) {
|
||||
video.currentTime = self.progress * video.duration;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
horizontalTween.scrollTrigger?.kill();
|
||||
horizontalTween.kill();
|
||||
};
|
||||
}, sectionRef);
|
||||
return () => {
|
||||
horizontalTween.scrollTrigger?.kill();
|
||||
horizontalTween.kill();
|
||||
};
|
||||
}, sectionRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
const handleModalAddToCart = () => {
|
||||
if (!quickViewMachine) return;
|
||||
dispatch(
|
||||
addToCart({
|
||||
id: quickViewMachine.id,
|
||||
name: quickViewMachine.name,
|
||||
priceNumeric: quickViewMachine.priceNumeric,
|
||||
priceLabel: quickViewMachine.priceLabel,
|
||||
image: quickViewMachine.image,
|
||||
})
|
||||
);
|
||||
message.success(t.cart.addToast(quickViewMachine.name));
|
||||
setQuickViewMachine(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="showcase" ref={sectionRef} className="relative overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
{videoAvailable && (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={showcaseVideoSrc}
|
||||
muted
|
||||
playsInline
|
||||
preload="auto"
|
||||
className="w-full h-full object-cover opacity-60"
|
||||
onError={() => setVideoAvailable(false)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(4,18,15,0.55) 0%, rgba(7,30,25,0.3) 50%, rgba(4,18,15,0.6) 100%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div ref={trackRef} className="horizontal-track relative z-10" dir="ltr">
|
||||
{t.machines.map((machine) => (
|
||||
<VendingCard key={machine.id} machine={machine} onQuickView={setQuickViewMachine}/>
|
||||
))}
|
||||
</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>
|
||||
const handleModalAddToCart = () => {
|
||||
if (!quickViewMachine) return;
|
||||
dispatch(
|
||||
addToCart({
|
||||
id: quickViewMachine.id,
|
||||
name: quickViewMachine.name,
|
||||
priceNumeric: quickViewMachine.priceNumeric,
|
||||
priceLabel: quickViewMachine.priceLabel,
|
||||
image: quickViewMachine.image,
|
||||
})
|
||||
);
|
||||
message.success(t.cart.addToast(quickViewMachine.name));
|
||||
setQuickViewMachine(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="showcase" ref={sectionRef} className="relative w-full overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
{videoAvailable && (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={showcaseVideoSrc}
|
||||
muted
|
||||
playsInline
|
||||
preload="auto"
|
||||
className="w-full h-full object-cover opacity-60"
|
||||
onError={() => setVideoAvailable(false)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(4,18,15,0.55) 0%, rgba(7,30,25,0.3) 50%, rgba(4,18,15,0.6) 100%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div ref={trackRef} className="horizontal-track relative z-10" dir="ltr">
|
||||
{t.machines.map((machine) => (
|
||||
<VendingCard key={machine.id} machine={machine} onQuickView={setQuickViewMachine} />
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
// سکشن مستقل ویدیوی معرفی شرکت (جدا از ویدیوی پسزمینه ویترین محصولات).
|
||||
|
||||
import { useLanguage } from '@/app/context/LanguageContext';
|
||||
import { introVideoSrc, introVideoPoster } from '@/data/content';
|
||||
|
||||
@@ -7,7 +9,7 @@ export default function IntroVideoSection() {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<section id="video" className="relative px-6 md:px-16 py-20 md:py-28 bg-surface border-t border-subtle">
|
||||
<section id="video" className="relative px-6 md:px-16 py-20 md:py-28 bg-elevated border-t border-subtle">
|
||||
<div className="max-w-5xl mx-auto text-center">
|
||||
<span className="text-brand-400 text-sm font-semibold tracking-wide">{t.introVideo.eyebrow}</span>
|
||||
<h2 className="text-2xl md:text-4xl font-extrabold brand-gradient-text mt-3 mb-3">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
// دکمه سوییچ زبان فارسی/انگلیسی.
|
||||
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { useLanguage } from '@/app/context/LanguageContext';
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
// گرید «مکان مناسب برای نصب» — هتلها، مدارس، داروخانهها، بیمارستانها، شیفت شب، محصولات تخصصی.
|
||||
|
||||
import {
|
||||
HomeOutlined,
|
||||
ReadOutlined,
|
||||
@@ -23,7 +25,7 @@ export default function LocationsSection() {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<section id="locations" className="relative px-6 md:px-16 py-20 md:py-28 bg-page border-t border-subtle">
|
||||
<section id="locations" className="relative px-6 md:px-16 py-20 md:py-28 bg-surface border-t border-subtle">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-2xl md:text-4xl font-extrabold brand-gradient-text">{t.locations.title}</h2>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
'use client';
|
||||
|
||||
// نقشه نشان (Neshan) — از Static Map API استفاده میکند. تا وقتی کلید API خالی باشد،
|
||||
// بهجای نقشه یک پیام راهنما نشان داده میشود (نه تصویر خراب).
|
||||
|
||||
import { useState } from 'react';
|
||||
import { EnvironmentOutlined } from '@ant-design/icons';
|
||||
import { useLanguage } from '@/app/context/LanguageContext';
|
||||
@@ -71,7 +74,7 @@ export default function MapSection() {
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="map" className="relative px-6 md:px-16 py-20 md:py-28 bg-elevated border-t border-subtle">
|
||||
<section id="map" className="relative px-6 md:px-16 py-20 md:py-28 bg-page border-t border-subtle">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-brand-400 text-sm font-semibold tracking-wide">{t.map.eyebrow}</span>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
// دکمه سوییچ حالت روشن/تیره.
|
||||
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { SunOutlined, MoonOutlined } from '@ant-design/icons';
|
||||
import { useTheme } from '@/app/context/ThemeContext';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
// فرم ثبت تیکت پشتیبانی با اعتبارسنجی antd؛ ارسال واقعی به بکاند در بلوک TODO داخل کد مشخص شده.
|
||||
|
||||
import { Form, Input, Select, Button, message } from 'antd';
|
||||
import { useLanguage } from '@/app/context/LanguageContext';
|
||||
|
||||
@@ -28,7 +30,7 @@ export default function TicketForm() {
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="support" className="relative px-6 md:px-16 py-20 md:py-28 bg-page border-t border-subtle">
|
||||
<section id="support" className="relative px-6 md:px-16 py-20 md:py-28 bg-surface border-t border-subtle">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="text-center mb-10">
|
||||
<span className="text-brand-400 text-sm font-semibold tracking-wide">{t.ticket.eyebrow}</span>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
'use client';
|
||||
|
||||
// کارت نمایش هر دستگاه داخل ویترین: شامل جلوه اسپاتلایت (هاور روی عکس دستگاه رنگ جایگزین را نشان میدهد)،
|
||||
// آیتمهای شناور، گیف+توضیحات، و دکمههای افزودن به سبد / مشاهده سریع / علاقهمندی.
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
import { Button, Tooltip, message } from 'antd';
|
||||
import { ShoppingCartOutlined, EyeOutlined, HeartOutlined, HeartFilled } from '@ant-design/icons';
|
||||
|
||||
Reference in New Issue
Block a user