This commit is contained in:
Hamed
2026-08-02 13:05:28 +04:00
parent 574a18712d
commit 34f96d3830
24 changed files with 1169 additions and 1013 deletions
+103 -102
View File
@@ -2,120 +2,121 @@
// هدر ثابت بالای صفحه: لوگو، منوی ناوبری، سوییچ زبان/تم، دکمه ورود به پنل کاربری و آیکون سبد خرید.
import { useState, useEffect } from 'react';
import {useState, useEffect} from 'react';
import Image from 'next/image';
import { Badge, Button } from 'antd';
import { ShoppingCartOutlined, MenuOutlined, CloseOutlined, UserOutlined } from '@ant-design/icons';
import { useSelector, useDispatch } from 'react-redux';
import { selectCartCount, openDrawer } from '@/store/cartSlice';
import { useLanguage } from '@/app/context/LanguageContext';
import {Badge, Button} from 'antd';
import {ShoppingCartOutlined, MenuOutlined, CloseOutlined, UserOutlined} from '@ant-design/icons';
import {useSelector, useDispatch} from 'react-redux';
import {selectCartCount, openDrawer} from '@/store/cartSlice';
import {useLanguage} from '@/app/context/LanguageContext';
import ThemeToggle from './ThemeToggle';
import LanguageToggle from './LanguageToggle';
export default function Header() {
const dispatch = useDispatch();
const cartCount = useSelector(selectCartCount);
const { t } = useLanguage();
const [scrolled, setScrolled] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const dispatch = useDispatch();
const cartCount = useSelector(selectCartCount);
const {t} = useLanguage();
const [scrolled, setScrolled] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const navLinks = [
{ href: '#hero', label: t.nav.home },
{ href: '#showcase', label: t.nav.showcase },
{ href: '#about', label: t.nav.about },
{ href: '#catalog', label: t.nav.catalog },
{ href: '#locations', label: t.nav.locations },
{ href: '#video', label: t.nav.video },
{ href: '#map', label: t.nav.map },
{ href: '#support', label: t.nav.support },
];
const navLinks = [
{href: '#hero', label: t.nav.home},
{href: '#showcase', label: t.nav.showcase},
{href: '#about', label: t.nav.about},
{href: '#catalog', label: t.nav.catalog},
{href: '#locations', label: t.nav.locations},
{href: '#video', label: t.nav.video},
{href: '#map', label: t.nav.map},
{href: '#support', label: t.nav.support},
];
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<header
className={`fixed top-0 inset-x-0 z-50 transition-all duration-300 ${
scrolled ? 'bg-page-85 backdrop-blur-md shadow-[0_1px_0_0_rgba(0,166,147,0.25)]' : 'bg-transparent'
}`}
>
<div className="mx-auto max-w-7xl px-5 md:px-8 h-20 flex items-center justify-between gap-4">
<a href="#hero" className="flex items-center gap-2 shrink-0">
<Image src="/images/logo.svg" alt="روبات مارکت" width={150} height={42} priority />
</a>
return (
<header
className={`fixed top-0 inset-x-0 z-50 transition-all duration-300 ${
scrolled ? 'bg-page-85 backdrop-blur-md shadow-[0_1px_0_0_rgba(0,166,147,0.25)]' : 'bg-transparent'
}`}
>
<div className="mx-auto max-w-7xl px-5 md:px-8 h-20 flex items-center justify-between gap-4">
<a href="#hero" className="flex items-center gap-2 shrink-0">
<Image src="/images/logo.png" alt="روبات مارکت" width={150} height={42} priority/>
</a>
<nav className="hidden xl:flex items-center gap-5 2xl:gap-7">
{navLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="text-sm text-secondary hover:text-brand-300 transition-colors whitespace-nowrap"
>
{link.label}
</a>
))}
</nav>
<nav className="hidden xl:flex items-center gap-5 2xl:gap-7">
{navLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="text-sm text-secondary hover:text-brand-300 transition-colors whitespace-nowrap"
>
{link.label}
</a>
))}
</nav>
<div className="flex items-center gap-2 md:gap-3">
<div className="hidden md:flex items-center gap-2">
<LanguageToggle />
<ThemeToggle />
</div>
<div className="flex items-center gap-2 md:gap-3">
<div className="hidden md:flex items-center gap-2">
<LanguageToggle/>
<ThemeToggle/>
</div>
<Button
className="hidden sm:inline-flex"
icon={<UserOutlined />}
onClick={() => {
/* TODO: point this at the real customer panel login route */
}}
>
{t.nav.login}
</Button>
<Button
className="hidden sm:inline-flex"
icon={<UserOutlined/>}
href="https://panel.my-rm.com"
target="_blank"
rel="noopener noreferrer"
>
{t.nav.login}
</Button>
<Badge count={cartCount} size="small" offset={[-2, 2]}>
<Button
shape="circle"
size="large"
icon={<ShoppingCartOutlined />}
onClick={() => dispatch(openDrawer())}
aria-label="cart"
/>
</Badge>
<Badge count={cartCount} size="small" offset={[-2, 2]}>
<Button
shape="circle"
size="large"
icon={<ShoppingCartOutlined/>}
onClick={() => dispatch(openDrawer())}
aria-label="cart"
/>
</Badge>
<button
className="xl:hidden text-brand-400 text-2xl"
onClick={() => setMobileOpen((v) => !v)}
aria-label="menu"
>
{mobileOpen ? <CloseOutlined /> : <MenuOutlined />}
</button>
</div>
</div>
<button
className="xl:hidden text-brand-400 text-2xl"
onClick={() => setMobileOpen((v) => !v)}
aria-label="menu"
>
{mobileOpen ? <CloseOutlined/> : <MenuOutlined/>}
</button>
</div>
</div>
{mobileOpen && (
<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}
href={link.href}
onClick={() => setMobileOpen(false)}
className="text-secondary text-base"
>
{link.label}
</a>
))}
<div className="flex items-center gap-3 pt-2 border-t border-subtle">
<LanguageToggle />
<ThemeToggle />
<Button icon={<UserOutlined />} className="flex-1">
{t.nav.login}
</Button>
</div>
</nav>
)}
</header>
);
{mobileOpen && (
<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}
href={link.href}
onClick={() => setMobileOpen(false)}
className="text-secondary text-base"
>
{link.label}
</a>
))}
<div className="flex items-center gap-3 pt-2 border-t border-subtle">
<LanguageToggle/>
<ThemeToggle/>
<Button icon={<UserOutlined/>} className="flex-1">
{t.nav.login}
</Button>
</div>
</nav>
)}
</header>
);
}
+134 -134
View File
@@ -7,156 +7,156 @@
* عنوان بزرگ دو خطی، و دکمه‌های فلش قبلی/بعدی برای ناوبری دستی.
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { useLanguage } from '@/app/context/LanguageContext';
import {useCallback, useEffect, useRef, useState} from 'react';
import {LeftOutlined, RightOutlined} from '@ant-design/icons';
import {useLanguage} from '@/app/context/LanguageContext';
const AUTOPLAY_DELAY = 5000; // هر ۵ ثانیه اسلاید عوض می‌شود
export default function HeroSection() {
const { t, dir } = useLanguage();
const slides = t.hero.slides;
const [activeIndex, setActiveIndex] = useState(0);
const timerRef = useRef(null);
const {t, dir} = useLanguage();
const slides = t.hero.slides;
const [activeIndex, setActiveIndex] = useState(0);
const timerRef = useRef(null);
// رفتن به اسلاید بعدی (با چرخش به اول لیست وقتی به آخر رسید)
const goToNext = useCallback(() => {
setActiveIndex((prev) => (prev + 1) % slides.length);
}, [slides.length]);
// رفتن به اسلاید بعدی (با چرخش به اول لیست وقتی به آخر رسید)
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 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]);
// تایمر پخش خودکار: هر بار که اسلاید عوض می‌شود، تایمر قبلی پاک و یک تایمر
// جدید ۵ ثانیه‌ای ست می‌شود. با کلیک دستی روی فلش‌ها هم همین تابع دوباره
// صدا زده می‌شود تا شمارش از نو شروع شود (تجربه کاربری بهتر).
const restartAutoplay = useCallback(() => {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(goToNext, AUTOPLAY_DELAY);
}, [goToNext]);
useEffect(() => {
restartAutoplay();
return () => {
if (timerRef.current) clearInterval(timerRef.current);
useEffect(() => {
restartAutoplay();
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [restartAutoplay]);
const handleManualNav = (direction) => {
if (direction === 'next') goToNext();
else goToPrev();
restartAutoplay(); // شمارش ۵ ثانیه بعد از کلیک دستی از نو شروع شود
};
}, [restartAutoplay]);
const handleManualNav = (direction) => {
if (direction === 'next') goToNext();
else goToPrev();
restartAutoplay(); // شمارش ۵ ثانیه بعد از کلیک دستی از نو شروع شود
};
const slide = slides[activeIndex];
const slide = slides[activeIndex];
return (
<section id="hero" className="relative h-screen w-full overflow-hidden bg-page">
{/* ------------------------------------------------------------------ */}
{/* شکل مورب تزئینی سمت راست/چپ — نسخه سبز برند به‌جای قرمز Sielaff */}
{/* ------------------------------------------------------------------ */}
<div
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%)',
}}
/>
{/* ------------------------------------------------------------------ */}
{/* اسلایدهای عکس — همه اسلایدها روی هم قرار دارند و فقط با 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"
return (
<section id="hero" className="relative h-screen w-full overflow-hidden bg-page">
{/* ------------------------------------------------------------------ */}
{/* شکل مورب تزئینی سمت راست/چپ — نسخه سبز برند به‌جای قرمز Sielaff */}
{/* ------------------------------------------------------------------ */}
<div
className="absolute inset-y-0 w-1/3 hidden md:block"
style={{
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>
))}
</div>
{/* لایه‌ی نیمه‌شفاف پشت متن، برای خوانایی روی هر عکسی */}
<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%)',
}}
/>
{/* ------------------------------------------------------------------ */}
{/* اسلایدهای عکس — همه اسلایدها روی هم قرار دارند و فقط با 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>
{/* ------------------------------------------------------------------ */}
{/* متن روی اسلاید: کیکر کوچک + عنوان بزرگ دو خطی + توضیح + دکمه 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">
{/* لایه‌ی نیمه‌شفاف پشت متن، برای خوانایی روی هر عکسی */}
<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-white drop-shadow-[0_4px_12px_rgba(0,0,0,0.9)]">
{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>
))}
</h1>
<p className="mt-5 text-white/90 text-base md:text-lg max-w-xl drop-shadow-lg">{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>
{/* ------------------------------------------------------------------ */}
{/* فلش‌های ناوبری دستی (قبلی / بعدی) — وسط دو طرف */}
{/* ------------------------------------------------------------------ */}
<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>
);
{/* نقطه‌های شمارشگر اسلاید پایین صفحه — کلیک روی هرکدام مستقیم به همان اسلاید می‌رود */}
<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>
);
}
+56
View File
@@ -0,0 +1,56 @@
'use client';
/**
* سکشن «مشتریانی که به ما اعتماد کرده‌اند» — یک نوار لوگوی بی‌پایان (marquee)
* که به‌آرامی و پیوسته حرکت می‌کند. برای ساخت افکت حلقه‌ی بی‌پایان، لیست
* لوگوها را دو بار پشت‌سرهم رندر می‌کنیم و با انیمیشن CSS آن را جابه‌جا می‌کنیم.
*
* نکته فنی: مسیر (track) همیشه جهت LTR دارد، چون منطق «حرکت به چپ» انیمیشن
* فرض بر همین جهت است — دقیقاً همان مشکلی که در اسکرول افقی محصولات داشتیم و
* حلش کردیم. این‌جا هم برای جلوگیری از همان باگ، جهت را ثابت نگه داشتیم.
*/
import { useLanguage } from '@/app/context/LanguageContext';
export default function TrustedBySection() {
const { t } = useLanguage();
const clients = t.trustedBy.clients;
// لیست لوگوها را دو برابر می‌کنیم تا وقتی نیمه اول با انیمیشن رد شد،
// نیمه دوم دقیقاً از همان‌جا ادامه بدهد و چشم متوجه پرش/شکاف نشود.
const doubledClients = [...clients, ...clients];
return (
<section className="relative px-6 md:px-16 py-16 md:py-20 bg-page border-t border-subtle overflow-hidden">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-10">
<span className="text-brand-400 text-sm font-semibold tracking-wide">{t.trustedBy.eyebrow}</span>
<h2 className="text-xl md:text-2xl font-extrabold text-primary mt-2">{t.trustedBy.title}</h2>
</div>
</div>
{/* یک محو تدریجی (fade mask) دو طرف نوار، تا لبه‌های لوگوها بریده به‌نظر نرسد */}
<div
className="relative"
style={{
maskImage: 'linear-gradient(90deg, transparent 0%, black 8%, black 92%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(90deg, transparent 0%, black 8%, black 92%, transparent 100%)',
}}
>
<div className="flex w-max animate-marquee gap-10" dir="ltr">
{doubledClients.map((client, i) => (
<div
key={`${client.id}-${i}`}
className="w-40 md:w-48 shrink-0 flex items-center justify-center bg-surface border border-subtle rounded-xl p-4 opacity-80 hover:opacity-100 transition-opacity"
>
<img
src={`/images/clients/client-${((i % clients.length) + 1)}.svg`}
alt={client.name}
className="w-full h-auto"
/>
</div>
))}
</div>
</div>
</section>
);
}