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
+27 -9
View File
@@ -3,14 +3,9 @@
<component name="ChangeListManager">
<list default="true" id="bb7b50a5-be9c-4cd7-a05b-979f0e91b173" name="Changes" comment="update">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/jsconfig.json" beforeDir="false" afterPath="$PROJECT_DIR$/jsconfig.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/app/globals.css" beforeDir="false" afterPath="$PROJECT_DIR$/src/app/globals.css" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/app/page.js" beforeDir="false" afterPath="$PROJECT_DIR$/src/app/page.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/animations/ScrollReveal.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/animations/ScrollReveal.jsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/hero/FloatingProducts.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/hero/FloatingProducts.jsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/hero/Hero.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/hero/Hero.jsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/hero/MachineAssembly.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/hero/MachineAssembly.jsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/layout/Navbar.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/layout/Navbar.jsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/globals.css" beforeDir="false" afterPath="$PROJECT_DIR$/app/globals.css" afterDir="false" />
<change beforePath="$PROJECT_DIR$/data/content.js" beforeDir="false" afterPath="$PROJECT_DIR$/data/content.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/video/showcase-placeholder.mp4" beforeDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -45,6 +40,7 @@
&quot;git-widget-placeholder&quot;: &quot;main&quot;,
&quot;js.debugger.nextJs.config.created.client&quot;: &quot;true&quot;,
&quot;js.debugger.nextJs.config.created.server&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;D:/Project/GitHubRepo/robot-market-landing&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
@@ -84,6 +80,12 @@
<updated>1785394346997</updated>
<workItem from="1785394348169" duration="14851000" />
<workItem from="1785556493808" duration="2292000" />
<workItem from="1785581294427" duration="1693000" />
<workItem from="1785583200703" duration="181000" />
<workItem from="1785583393176" duration="49000" />
<workItem from="1785583486761" duration="9000" />
<workItem from="1785583517424" duration="145000" />
<workItem from="1785645096071" duration="26000" />
</task>
<task id="LOCAL-00001" summary="update">
<option name="closed" value="true" />
@@ -93,7 +95,23 @@
<option name="project" value="LOCAL" />
<updated>1785394671723</updated>
</task>
<option name="localTasksCounter" value="2" />
<task id="LOCAL-00002" summary="update">
<option name="closed" value="true" />
<created>1785581338739</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1785581338740</updated>
</task>
<task id="LOCAL-00003" summary="update">
<option name="closed" value="true" />
<created>1785582935941</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1785582935941</updated>
</task>
<option name="localTasksCounter" value="4" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
+2
View File
@@ -1,5 +1,7 @@
'use client';
// context زبان سایت: نگه‌داری زبان فعلی (fa/en)، ذخیره در localStorage، و تنظیم جهت صفحه (rtl/ltr).
import { createContext, useContext, useEffect, useState } from 'react';
import { content } from '@/data/content';
+8 -4
View File
@@ -5,16 +5,20 @@ import { createContext, useContext, useEffect, useState } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('dark');
// پیش‌فرض سایت حالا «روشن» (light) است. اگر کاربر قبلاً تم را عوض کرده باشد
// (ذخیره‌شده در localStorage) همان ترجیح رو حفظ می‌کنیم؛ در غیر این صورت فقط
// اگر سیستم کاربر صراحتاً روی حالت تیره تنظیم شده باشد، تیره را انتخاب می‌کنیم.
const [theme, setTheme] = useState('light');
const [mounted, setMounted] = useState(false);
// On mount: read saved preference, else fall back to system preference.
// On mount: read saved preference, else fall back to system preference
// (but default stays light unless the OS explicitly prefers dark).
useEffect(() => {
const saved = window.localStorage.getItem('rm-theme');
if (saved === 'light' || saved === 'dark') {
setTheme(saved);
} else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
setTheme('light');
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
setTheme('dark');
}
setMounted(true);
}, []);
+31 -51
View File
@@ -56,11 +56,9 @@ html[dir='rtl'] body {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-scrollbar-track);
}
::-webkit-scrollbar-thumb {
background: var(--color-scrollbar-thumb);
border-radius: 999px;
@@ -68,51 +66,21 @@ html[dir='rtl'] body {
/* Semantic surface/text utilities — read the theme variables above so every
component that uses them automatically supports dark + light mode. */
.bg-page {
background-color: var(--color-bg-page);
}
.bg-elevated {
background-color: var(--color-bg-elevated);
}
.bg-surface {
background-color: var(--color-bg-surface);
}
.bg-surface-strong {
background-color: var(--color-bg-surface-strong);
}
.border-subtle {
border-color: var(--color-border);
}
.text-primary {
color: var(--color-text-primary);
}
.text-secondary {
color: var(--color-text-secondary);
}
.bg-page { background-color: var(--color-bg-page); }
.bg-elevated { background-color: var(--color-bg-elevated); }
.bg-surface { background-color: var(--color-bg-surface); }
.bg-surface-strong { background-color: var(--color-bg-surface-strong); }
.border-subtle { border-color: var(--color-border); }
.text-primary { color: var(--color-text-primary); }
.text-secondary { color: var(--color-text-secondary); }
/* Opacity variants of the tokens above (Tailwind's /NN modifier only works on
Tailwind-generated utilities, not these hand-written classes). */
.bg-page-60 {
background-color: color-mix(in srgb, var(--color-bg-page) 60%, transparent);
}
.bg-page-85 {
background-color: color-mix(in srgb, var(--color-bg-page) 85%, transparent);
}
.bg-elevated-95 {
background-color: color-mix(in srgb, var(--color-bg-elevated) 95%, transparent);
}
.text-primary-90 {
color: color-mix(in srgb, var(--color-text-primary) 90%, transparent);
}
.bg-page-60 { background-color: color-mix(in srgb, var(--color-bg-page) 60%, transparent); }
.bg-page-70 { background-color: color-mix(in srgb, var(--color-bg-page) 70%, transparent); }
.bg-page-85 { background-color: color-mix(in srgb, var(--color-bg-page) 85%, transparent); }
.bg-elevated-95 { background-color: color-mix(in srgb, var(--color-bg-elevated) 95%, transparent); }
.text-primary-90 { color: color-mix(in srgb, var(--color-text-primary) 90%, transparent); }
/* ------------------------------------------------------------------ */
/* Horizontal scroll showcase (pinned via GSAP ScrollTrigger) */
@@ -120,7 +88,7 @@ html[dir='rtl'] body {
.horizontal-track {
display: flex;
height: 100vh;
/*width: 100vw;*/
width: max-content;
will-change: transform;
}
@@ -165,7 +133,8 @@ html[dir='rtl'] body {
height: 190px;
border-radius: 999px;
border: 2px solid rgba(0, 166, 147, 0.9);
box-shadow: 0 0 0 4px rgba(0, 166, 147, 0.15),
box-shadow:
0 0 0 4px rgba(0, 166, 147, 0.15),
0 0 40px 6px rgba(0, 166, 147, 0.45);
pointer-events: none;
transform: translate(-50%, -50%);
@@ -182,7 +151,6 @@ html[dir='rtl'] body {
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
@@ -208,12 +176,24 @@ html[dir='rtl'] body {
}
@keyframes caretBlink {
0%, 49% {
opacity: 1;
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
50%, 100% {
opacity: 0;
/* ------------------------------------------------------------------ */
/* نوار لوگوی بی‌پایان (trusted-by marquee) */
/* ------------------------------------------------------------------ */
@keyframes marqueeScroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); } /* دقیقاً نصف عرض، چون لیست دو برابر شده */
}
.animate-marquee {
animation: marqueeScroll 28s linear infinite;
}
.animate-marquee:hover {
animation-play-state: paused;
}
/* ------------------------------------------------------------------ */
+5 -4
View File
@@ -15,11 +15,12 @@ export const metadata = {
};
export default function RootLayout({ children }) {
// Default server-rendered shell: Persian + dark. ThemeProvider/LanguageProvider
// read localStorage on mount and correct lang/dir/class client-side if needed —
// suppressHydrationWarning avoids a noisy (harmless) mismatch warning for that swap.
// پوسته پیش‌فرض سمت سرور: فارسی + حالت روشن (Light).
// ThemeProvider/LanguageProvider بعد از mount شدن، اگر کاربر قبلاً تم/زبان
// دیگری را انتخاب کرده بود (ذخیره‌شده در localStorage)، آن را روی <html> اعمال
// می‌کنند؛ suppressHydrationWarning جلوی هشدار بی‌ضرر عدم‌تطابق را می‌گیرد.
return (
<html lang="fa" dir="rtl" className={`${vazirmatn.variable} dark`} suppressHydrationWarning>
<html lang="fa" dir="rtl" className={`${vazirmatn.variable} light`} suppressHydrationWarning>
<body className="bg-page text-primary antialiased transition-colors duration-300">
<Providers>{children}</Providers>
</body>
+9 -4
View File
@@ -1,6 +1,7 @@
import Header from '@/components/Header';
import HeroSection from '@/components/HeroSection';
import HorizontalShowcase from '@/components/HorizontalShowcase';
import TrustedBySection from '@/components/TrustedBySection';
import FeaturesSection from '@/components/FeaturesSection';
import AboutSection from '@/components/AboutSection';
import CatalogSection from '@/components/CatalogSection';
@@ -12,20 +13,25 @@ import Footer from '@/components/Footer';
import CartDrawer from '@/components/CartDrawer';
import ChatWidget from '@/components/ChatWidget';
// چیدمان کامل صفحه اصلی. ترتیب سکشن‌ها از بالا به پایین:
// هیرو (اسلایدر) -> ویترین محصولات (اسکرول افقی + ویدیو) -> مشتریان ما ->
// مزایا -> درباره ما -> کاتالوگ -> مکان‌های نصب -> ویدیوی معرفی -> نقشه ->
// فرم تیکت -> فوتر. همه‌ی سکشن‌ها به‌جز هیرو و ویترین محصولات، رنگ زمینه‌شان
// به‌ترتیب بین سه تن روشن/متوسط/برجسته (bg-page / bg-surface / bg-elevated)
// می‌چرخد تا هنگام اسکرول یک تغییر رنگ ظریف حس شود.
export default function HomePage() {
return (
<main className="relative bg-page">
<Header />
<CartDrawer />
<ChatWidget />
<HeroSection />
<HorizontalShowcase />
<TrustedBySection />
<FeaturesSection />
<AboutSection />
@@ -41,7 +47,6 @@ export default function HomePage() {
<TicketForm />
<Footer />
</main>
);
}
+2
View File
@@ -1,5 +1,7 @@
'use client';
// پل ارتباطی بین Redux، حالت تیره/روشن، و زبان فا/en با تنظیمات ConfigProvider آنت‌دیزاین.
import { Provider as ReduxProvider } from 'react-redux';
import { ConfigProvider, theme as antdThemeApi } from 'antd';
import faIR from 'antd/locale/fa_IR';
+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}
+134 -147
View File
@@ -1,174 +1,161 @@
'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;
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 handleManualNav = (direction) => {
if (direction === 'next') goToNext();
else goToPrev();
restartAutoplay(); // شمارش ۵ ثانیه بعد از کلیک دستی از نو شروع شود
};
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" />
{/* ------------------------------------------------------------------ */}
{/* اسلایدهای عکس — همه اسلایدها روی هم قرار دارند و فقط با 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 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 ref={subRef} className="mt-6 max-w-2xl text-secondary text-base md:text-xl opacity-0">
{t.hero.sub}
</p>
<p className="mt-5 text-secondary text-base md:text-lg max-w-xl">{slide.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"
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 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>
{/* ------------------------------------------------------------------ */}
{/* فلش‌های ناوبری دستی (قبلی / بعدی) — شبیه سایت 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>
);
+7 -5
View File
@@ -1,5 +1,9 @@
'use client';
// سکشن ویترین محصولات: با اسکرول عمودی به اسکرول افقی پین‌شده تبدیل می‌شود (GSAP ScrollTrigger)
// و یک ویدیوی پس‌زمینه دارد که با پیشرفت اسکرول جلو/عقب می‌رود. طبق درخواست کاربر، این سکشن
// و هیرو از چرخه‌ی رنگ‌بندی متناوب بقیه سکشن‌ها مستثنا هستند.
import { useEffect, useRef, useState } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
@@ -74,7 +78,7 @@ export default function HorizontalShowcase() {
};
return (
<section id="showcase" ref={sectionRef} className="relative overflow-hidden">
<section id="showcase" ref={sectionRef} className="relative w-full overflow-hidden">
<div className="absolute inset-0 z-0">
{videoAvailable && (
<video
@@ -111,12 +115,10 @@ export default function HorizontalShowcase() {
>
{quickViewMachine && (
<div className="flex flex-col gap-4">
<img src={quickViewMachine.image} alt={quickViewMachine.name}
className="w-full h-64 object-contain"/>
<img src={quickViewMachine.image} alt={quickViewMachine.name} className="w-full h-64 object-contain" />
<p className="text-secondary leading-7">{quickViewMachine.description}</p>
<p className="text-xl font-bold text-primary">{quickViewMachine.priceLabel}</p>
<Button type="primary" size="large" icon={<ShoppingCartOutlined/>}
onClick={handleModalAddToCart}>
<Button type="primary" size="large" icon={<ShoppingCartOutlined />} onClick={handleModalAddToCart}>
{t.machineActions.addToCart}
</Button>
</div>
+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';
+74 -12
View File
@@ -105,13 +105,32 @@ export const content = {
},
hero: {
loading: 'در حال بارگذاری روبات مارکت…',
typingPhrases: [
'به دنیای خودکار و هوشمند خوش آمدید!',
'تجربه‌ای نوین از خرید آسان و سریع.',
'روبات مارکت؛ فروشنده هوشمند شما، ۲۴ ساعته.',
],
sub: 'تجربه‌ای نوین از خرید آسان و سریع با دستگاه‌های وندینگ روبات مارکت.',
cta: 'مشاهده دستگاه‌ها',
// هر اسلاید یک تصویر دستگاه + یک عنوان دو خطی بزرگ (شبیه سایت Sielaff) دارد.
// برای افزودن اسلاید جدید، فقط یک آیتم دیگر به این آرایه اضافه کنید.
slides: [
{
id: 'slide-1',
kicker: 'روبات مارکت',
titleLines: ['فروش خودکار،', 'هوشمند و بی‌وقفه'],
sub: 'تجربه‌ای نوین از خرید آسان و سریع با دستگاه‌های وندینگ روبات مارکت.',
image: '/images/machine-35-base.svg',
},
{
id: 'slide-2',
kicker: 'یخچالدار',
titleLines: ['خنک‌کننده قدرتمند،', 'کیفیت تضمینی'],
sub: 'نگهداری اصولی تنقلات و نوشیدنی سرد، ۲۴ ساعته و بدون نیاز به نیروی انسانی.',
image: '/images/machine-48-base.svg',
},
{
id: 'slide-3',
kicker: 'مدیریت آنلاین',
titleLines: ['کنترل کامل کسب‌وکار', 'از راه دور'],
sub: 'مدیریت موجودی، فروش و گزارش‌گیری آنلاین، هر ساعت از شبانه‌روز.',
image: '/images/machine-60-base.svg',
},
],
},
cart: {
title: 'سبد خرید',
@@ -131,6 +150,19 @@ export const content = {
gifAlt: (name) => `نمایش عملکرد ${name}`,
},
machines: machinesFa,
trustedBy: {
eyebrow: 'اعتماد شما، افتخار ماست',
title: 'کسب‌وکارهایی که به روبات مارکت اعتماد کرده‌اند',
// اسم‌های نمونه/فرضی هستند — با نام مشتریان واقعی جایگزین کنید.
clients: [
{ id: 'c1', name: 'هتل پارسیان' },
{ id: 'c2', name: 'دانشگاه فردوسی' },
{ id: 'c3', name: 'بیمارستان رضوی' },
{ id: 'c4', name: 'فروشگاه‌های زنجیره‌ای رفاه' },
{ id: 'c5', name: 'مجتمع اداری آسمان' },
{ id: 'c6', name: 'باشگاه ورزشی تلاش' },
],
},
features: [
{ id: 'warranty', icon: 'safety', title: 'ضمانت کالا', description: 'کلیه محصولات شرکت دارای یک سال خدمات پس از فروش می‌باشند.' },
{ id: 'consulting', icon: 'consulting', title: 'مشاوره تخصصی و رایگان', description: 'خدمات مشاوره رایگان در مورد نوع دستگاه و چیدمان محصولات.' },
@@ -274,13 +306,30 @@ export const content = {
},
hero: {
loading: 'Loading Robot Market…',
typingPhrases: [
'Welcome to a smart, automated world!',
'A new experience of fast, easy shopping.',
'Robot Market — your smart seller, around the clock.',
],
sub: 'A new experience of fast, easy shopping with Robot Market vending machines.',
cta: 'View Machines',
slides: [
{
id: 'slide-1',
kicker: 'Robot Market',
titleLines: ['Automated selling,', 'smart & non-stop'],
sub: 'A new experience of fast, easy shopping with Robot Market vending machines.',
image: '/images/machine-35-base.svg',
},
{
id: 'slide-2',
kicker: 'Refrigerated',
titleLines: ['Powerful cooling,', 'guaranteed quality'],
sub: 'Proper storage for snacks and cold drinks, 24/7, with no staff required.',
image: '/images/machine-48-base.svg',
},
{
id: 'slide-3',
kicker: 'Online Management',
titleLines: ['Full control of your', 'business, remotely'],
sub: 'Manage inventory, sales, and reports online, any hour of the day.',
image: '/images/machine-60-base.svg',
},
],
},
cart: {
title: 'Cart',
@@ -300,6 +349,19 @@ export const content = {
gifAlt: (name) => `${name} in action`,
},
machines: machinesEn,
trustedBy: {
eyebrow: 'Trusted by businesses like yours',
title: 'Businesses That Trust Robot Market',
// Sample/placeholder names — replace with real client names.
clients: [
{ id: 'c1', name: 'Parsian Hotel' },
{ id: 'c2', name: 'Ferdowsi University' },
{ id: 'c3', name: 'Razavi Hospital' },
{ id: 'c4', name: 'Refah Retail Chain' },
{ id: 'c5', name: 'Asemaan Office Complex' },
{ id: 'c6', name: 'Talash Sports Club' },
],
},
features: [
{ id: 'warranty', icon: 'safety', title: 'Product Warranty', description: 'All our products come with one year of after-sales service.' },
{ id: 'consulting', icon: 'consulting', title: 'Free Expert Consulting', description: 'Free consulting on the right machine type and product layout.' },
+1 -2
View File
@@ -3,7 +3,6 @@
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
"jsx": "react"
}
}
}
+3 -3
View File
@@ -736,9 +736,9 @@
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.11.9",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.9.tgz",
"integrity": "sha512-cp447VUsGS07+n1Dqf7YSQ8maeJrjEhaDxTm1ZefbqDtypHBC5GzGMQbklR6IPR13Y8OAJRHZWEMtZipJLCttg==",
"version": "2.11.10",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.10.tgz",
"integrity": "sha512-35JEvJ5/KKlbCHjMCsONI2w6HE88STjVdHk+C7d8LtcFxUjZR1KeLP9izofn2qs0KUxX5r4z73bwH/rd+JHacw==",
"dev": true,
"license": "Apache-2.0",
"bin": {