This commit is contained in:
Hamed
2026-08-02 08:52:45 +04:00
parent 9697c98999
commit 574a18712d
27 changed files with 560 additions and 465 deletions
+3 -1
View File
@@ -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">
+2
View File
@@ -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';
+4 -1
View File
@@ -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>
+2
View File
@@ -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';
+3 -1
View File
@@ -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];
+2
View File
@@ -1,5 +1,7 @@
'use client';
// رندر آیتم‌های شناور (چیپس/نوشابه) دور دستگاه با حرکت تصادفی و ملایم GSAP.
import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
+3 -1
View File
@@ -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>
+5 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
}
+3 -1
View File
@@ -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">
+2
View File
@@ -1,5 +1,7 @@
'use client';
// دکمه سوییچ زبان فارسی/انگلیسی.
import { Button, Tooltip } from 'antd';
import { useLanguage } from '@/app/context/LanguageContext';
+3 -1
View File
@@ -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>
+4 -1
View File
@@ -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>
+2
View File
@@ -1,5 +1,7 @@
'use client';
// دکمه سوییچ حالت روشن/تیره.
import { Button, Tooltip } from 'antd';
import { SunOutlined, MoonOutlined } from '@ant-design/icons';
import { useTheme } from '@/app/context/ThemeContext';
+3 -1
View File
@@ -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>
+3
View File
@@ -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';