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"> <component name="ChangeListManager">
<list default="true" id="bb7b50a5-be9c-4cd7-a05b-979f0e91b173" name="Changes" comment="update"> <list default="true" id="bb7b50a5-be9c-4cd7-a05b-979f0e91b173" name="Changes" comment="update">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/jsconfig.json" beforeDir="false" afterPath="$PROJECT_DIR$/jsconfig.json" afterDir="false" /> <change beforePath="$PROJECT_DIR$/app/globals.css" beforeDir="false" afterPath="$PROJECT_DIR$/app/globals.css" 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$/data/content.js" beforeDir="false" afterPath="$PROJECT_DIR$/data/content.js" 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$/public/video/showcase-placeholder.mp4" beforeDir="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" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -45,6 +40,7 @@
&quot;git-widget-placeholder&quot;: &quot;main&quot;, &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.client&quot;: &quot;true&quot;,
&quot;js.debugger.nextJs.config.created.server&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.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;, &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;, &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
@@ -84,6 +80,12 @@
<updated>1785394346997</updated> <updated>1785394346997</updated>
<workItem from="1785394348169" duration="14851000" /> <workItem from="1785394348169" duration="14851000" />
<workItem from="1785556493808" duration="2292000" /> <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>
<task id="LOCAL-00001" summary="update"> <task id="LOCAL-00001" summary="update">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -93,7 +95,23 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1785394671723</updated> <updated>1785394671723</updated>
</task> </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 /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
+2
View File
@@ -1,5 +1,7 @@
'use client'; 'use client';
// context زبان سایت: نگه‌داری زبان فعلی (fa/en)، ذخیره در localStorage، و تنظیم جهت صفحه (rtl/ltr).
import { createContext, useContext, useEffect, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
import { content } from '@/data/content'; import { content } from '@/data/content';
+8 -4
View File
@@ -5,16 +5,20 @@ import { createContext, useContext, useEffect, useState } from 'react';
const ThemeContext = createContext(null); const ThemeContext = createContext(null);
export function ThemeProvider({ children }) { export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('dark'); // پیش‌فرض سایت حالا «روشن» (light) است. اگر کاربر قبلاً تم را عوض کرده باشد
// (ذخیره‌شده در localStorage) همان ترجیح رو حفظ می‌کنیم؛ در غیر این صورت فقط
// اگر سیستم کاربر صراحتاً روی حالت تیره تنظیم شده باشد، تیره را انتخاب می‌کنیم.
const [theme, setTheme] = useState('light');
const [mounted, setMounted] = useState(false); 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(() => { useEffect(() => {
const saved = window.localStorage.getItem('rm-theme'); const saved = window.localStorage.getItem('rm-theme');
if (saved === 'light' || saved === 'dark') { if (saved === 'light' || saved === 'dark') {
setTheme(saved); setTheme(saved);
} else if (window.matchMedia('(prefers-color-scheme: light)').matches) { } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
setTheme('light'); setTheme('dark');
} }
setMounted(true); setMounted(true);
}, []); }, []);
+116 -136
View File
@@ -6,223 +6,203 @@
/* Theme tokens — dark is the default; .light overrides on <html> */ /* Theme tokens — dark is the default; .light overrides on <html> */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
:root { :root {
--brand: #00a693; --brand: #00a693;
--color-bg-page: #04120f; --color-bg-page: #04120f;
--color-bg-elevated: #0c2b24; --color-bg-elevated: #0c2b24;
--color-bg-surface: rgba(255, 255, 255, 0.045); --color-bg-surface: rgba(255, 255, 255, 0.045);
--color-bg-surface-strong: rgba(255, 255, 255, 0.08); --color-bg-surface-strong: rgba(255, 255, 255, 0.08);
--color-border: rgba(0, 166, 147, 0.28); --color-border: rgba(0, 166, 147, 0.28);
--color-text-primary: #ffffff; --color-text-primary: #ffffff;
--color-text-secondary: rgba(230, 247, 244, 0.72); --color-text-secondary: rgba(230, 247, 244, 0.72);
--color-scrollbar-track: #071e19; --color-scrollbar-track: #071e19;
--color-scrollbar-thumb: #0b5c50; --color-scrollbar-thumb: #0b5c50;
color-scheme: dark; color-scheme: dark;
} }
html.light { html.light {
--color-bg-page: #f5fbf9; --color-bg-page: #f5fbf9;
--color-bg-elevated: #ffffff; --color-bg-elevated: #ffffff;
--color-bg-surface: rgba(0, 89, 79, 0.045); --color-bg-surface: rgba(0, 89, 79, 0.045);
--color-bg-surface-strong: rgba(0, 89, 79, 0.08); --color-bg-surface-strong: rgba(0, 89, 79, 0.08);
--color-border: rgba(0, 166, 147, 0.25); --color-border: rgba(0, 166, 147, 0.25);
--color-text-primary: #04231f; --color-text-primary: #04231f;
--color-text-secondary: rgba(4, 35, 31, 0.68); --color-text-secondary: rgba(4, 35, 31, 0.68);
--color-scrollbar-track: #e1f3ef; --color-scrollbar-track: #e1f3ef;
--color-scrollbar-thumb: #8fdccc; --color-scrollbar-thumb: #8fdccc;
color-scheme: light; color-scheme: light;
} }
html, html,
body { body {
padding: 0; padding: 0;
margin: 0; margin: 0;
scroll-behavior: smooth; scroll-behavior: smooth;
} }
html[dir='rtl'] body { html[dir='rtl'] body {
font-family: var(--font-vazirmatn), Tahoma, sans-serif; font-family: var(--font-vazirmatn), Tahoma, sans-serif;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
::selection { ::selection {
background: rgba(0, 166, 147, 0.35); background: rgba(0, 166, 147, 0.35);
color: var(--color-text-primary); color: var(--color-text-primary);
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px; height: 8px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: var(--color-scrollbar-track); background: var(--color-scrollbar-track);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--color-scrollbar-thumb); background: var(--color-scrollbar-thumb);
border-radius: 999px; border-radius: 999px;
} }
/* Semantic surface/text utilities — read the theme variables above so every /* Semantic surface/text utilities — read the theme variables above so every
component that uses them automatically supports dark + light mode. */ component that uses them automatically supports dark + light mode. */
.bg-page { .bg-page { background-color: var(--color-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); }
.bg-elevated { .border-subtle { border-color: var(--color-border); }
background-color: var(--color-bg-elevated); .text-primary { color: var(--color-text-primary); }
} .text-secondary { color: var(--color-text-secondary); }
.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 /* Opacity variants of the tokens above (Tailwind's /NN modifier only works on
Tailwind-generated utilities, not these hand-written classes). */ Tailwind-generated utilities, not these hand-written classes). */
.bg-page-60 { .bg-page-60 { background-color: color-mix(in srgb, var(--color-bg-page) 60%, transparent); }
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); }
.bg-page-85 { .text-primary-90 { color: color-mix(in srgb, var(--color-text-primary) 90%, transparent); }
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) */ /* Horizontal scroll showcase (pinned via GSAP ScrollTrigger) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
.horizontal-track { .horizontal-track {
display: flex; display: flex;
height: 100vh; height: 100vh;
/*width: 100vw;*/ width: max-content;
will-change: transform; will-change: transform;
} }
.horizontal-panel { .horizontal-panel {
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
flex-shrink: 0; flex-shrink: 0;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Cursor spotlight / lens reveal effect over the vending machine */ /* Cursor spotlight / lens reveal effect over the vending machine */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
.spotlight-stage { .spotlight-stage {
position: relative; position: relative;
cursor: none; cursor: none;
isolation: isolate; isolation: isolate;
} }
.spotlight-stage .base-layer, .spotlight-stage .base-layer,
.spotlight-stage .reveal-layer { .spotlight-stage .reveal-layer {
position: absolute; position: absolute;
inset: 0; inset: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
} }
.spotlight-stage .reveal-layer { .spotlight-stage .reveal-layer {
clip-path: circle(0px at 50% 50%); clip-path: circle(0px at 50% 50%);
transition: clip-path 0.05s linear; transition: clip-path 0.05s linear;
} }
.spotlight-lens { .spotlight-lens {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 190px; width: 190px;
height: 190px; height: 190px;
border-radius: 999px; border-radius: 999px;
border: 2px solid rgba(0, 166, 147, 0.9); 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); 0 0 40px 6px rgba(0, 166, 147, 0.45);
pointer-events: none; pointer-events: none;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
opacity: 0; opacity: 0;
transition: opacity 0.15s ease; transition: opacity 0.15s ease;
z-index: 30; z-index: 30;
} }
.spotlight-stage:hover .spotlight-lens { .spotlight-stage:hover .spotlight-lens {
opacity: 1; opacity: 1;
} }
/* Utility: hide native scrollbar but keep scroll functionality */ /* Utility: hide native scrollbar but keep scroll functionality */
.no-scrollbar::-webkit-scrollbar { .no-scrollbar::-webkit-scrollbar {
display: none; display: none;
} }
.no-scrollbar { .no-scrollbar {
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;
} }
/* Gradient text helper for headings */ /* Gradient text helper for headings */
.brand-gradient-text { .brand-gradient-text {
background: linear-gradient(90deg, #6fe0c9 0%, #00a693 55%, #017065 100%); background: linear-gradient(90deg, #6fe0c9 0%, #00a693 55%, #017065 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
color: transparent; color: transparent;
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Typewriter caret for the hero headline */ /* Typewriter caret for the hero headline */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
.typewriter-caret { .typewriter-caret {
display: inline-block; display: inline-block;
width: 3px; width: 3px;
margin-inline-start: 4px; margin-inline-start: 4px;
background-color: var(--brand); background-color: var(--brand);
animation: caretBlink 0.9s steps(1) infinite; animation: caretBlink 0.9s steps(1) infinite;
} }
@keyframes caretBlink { @keyframes caretBlink {
0%, 49% { 0%, 49% { opacity: 1; }
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;
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Chat widget */ /* Chat widget */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
.chat-bubble { .chat-bubble {
max-width: 80%; max-width: 80%;
padding: 10px 14px; padding: 10px 14px;
border-radius: 14px; border-radius: 14px;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.6; line-height: 1.6;
} }
+5 -4
View File
@@ -15,11 +15,12 @@ export const metadata = {
}; };
export default function RootLayout({ children }) { export default function RootLayout({ children }) {
// Default server-rendered shell: Persian + dark. ThemeProvider/LanguageProvider // پوسته پیش‌فرض سمت سرور: فارسی + حالت روشن (Light).
// read localStorage on mount and correct lang/dir/class client-side if needed — // ThemeProvider/LanguageProvider بعد از mount شدن، اگر کاربر قبلاً تم/زبان
// suppressHydrationWarning avoids a noisy (harmless) mismatch warning for that swap. // دیگری را انتخاب کرده بود (ذخیره‌شده در localStorage)، آن را روی <html> اعمال
// می‌کنند؛ suppressHydrationWarning جلوی هشدار بی‌ضرر عدم‌تطابق را می‌گیرد.
return ( 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"> <body className="bg-page text-primary antialiased transition-colors duration-300">
<Providers>{children}</Providers> <Providers>{children}</Providers>
</body> </body>
+25 -20
View File
@@ -1,6 +1,7 @@
import Header from '@/components/Header'; import Header from '@/components/Header';
import HeroSection from '@/components/HeroSection'; import HeroSection from '@/components/HeroSection';
import HorizontalShowcase from '@/components/HorizontalShowcase'; import HorizontalShowcase from '@/components/HorizontalShowcase';
import TrustedBySection from '@/components/TrustedBySection';
import FeaturesSection from '@/components/FeaturesSection'; import FeaturesSection from '@/components/FeaturesSection';
import AboutSection from '@/components/AboutSection'; import AboutSection from '@/components/AboutSection';
import CatalogSection from '@/components/CatalogSection'; import CatalogSection from '@/components/CatalogSection';
@@ -12,36 +13,40 @@ import Footer from '@/components/Footer';
import CartDrawer from '@/components/CartDrawer'; import CartDrawer from '@/components/CartDrawer';
import ChatWidget from '@/components/ChatWidget'; import ChatWidget from '@/components/ChatWidget';
// چیدمان کامل صفحه اصلی. ترتیب سکشن‌ها از بالا به پایین:
// هیرو (اسلایدر) -> ویترین محصولات (اسکرول افقی + ویدیو) -> مشتریان ما ->
// مزایا -> درباره ما -> کاتالوگ -> مکان‌های نصب -> ویدیوی معرفی -> نقشه ->
// فرم تیکت -> فوتر. همه‌ی سکشن‌ها به‌جز هیرو و ویترین محصولات، رنگ زمینه‌شان
// به‌ترتیب بین سه تن روشن/متوسط/برجسته (bg-page / bg-surface / bg-elevated)
// می‌چرخد تا هنگام اسکرول یک تغییر رنگ ظریف حس شود.
export default function HomePage() { export default function HomePage() {
return ( return (
<main className="relative bg-page"> <main className="relative bg-page">
<Header />
<CartDrawer />
<ChatWidget />
<Header/> <HeroSection />
<CartDrawer/> <HorizontalShowcase />
<ChatWidget/> <TrustedBySection />
<HeroSection/> <FeaturesSection />
<HorizontalShowcase/> <AboutSection />
<FeaturesSection/> <CatalogSection />
<AboutSection/> <LocationsSection />
<CatalogSection/> <IntroVideoSection />
<LocationsSection/> <MapSection />
<IntroVideoSection/> <TicketForm />
<MapSection/> <Footer />
</main>
<TicketForm/> );
<Footer/>
</main>
);
} }
+2
View File
@@ -1,5 +1,7 @@
'use client'; 'use client';
// پل ارتباطی بین Redux، حالت تیره/روشن، و زبان فا/en با تنظیمات ConfigProvider آنت‌دیزاین.
import { Provider as ReduxProvider } from 'react-redux'; import { Provider as ReduxProvider } from 'react-redux';
import { ConfigProvider, theme as antdThemeApi } from 'antd'; import { ConfigProvider, theme as antdThemeApi } from 'antd';
import faIR from 'antd/locale/fa_IR'; import faIR from 'antd/locale/fa_IR';
+3 -1
View File
@@ -1,5 +1,7 @@
'use client'; 'use client';
// سکشن معرفی شرکت با متن بلند؛ به‌خاطر طولانی بودن متن، یک دکمه «ادامه مطلب» برای باز/بسته کردن دارد.
import { useState } from 'react'; import { useState } from 'react';
import { useLanguage } from '@/app/context/LanguageContext'; 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); const visibleParagraphs = expanded ? t.about.paragraphs : t.about.paragraphs.slice(0, 2);
return ( 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"> <div className="max-w-4xl mx-auto">
<span className="text-brand-400 text-sm font-semibold tracking-wide">{t.about.eyebrow}</span> <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"> <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'; 'use client';
// کشوی (Drawer) سبد خرید: نمایش آیتم‌های انتخابی، تغییر تعداد، جمع کل، و دکمه پرداخت (placeholder).
import { Drawer, Button, Empty, InputNumber, message } from 'antd'; import { Drawer, Button, Empty, InputNumber, message } from 'antd';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
+4 -1
View File
@@ -1,5 +1,8 @@
'use client'; 'use client';
// کاتالوگ محصولات جانبی (زعفران، تنقلات، نوشیدنی و ...) که مشتریان دستگاه با قیمت کمتر از بازار
// می‌توانند سفارش دهند. دکمه «افزودن به سفارش» از همان سبد خرید Redux دستگاه‌ها استفاده می‌کند.
import { Button, message } from 'antd'; import { Button, message } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
@@ -29,7 +32,7 @@ export default function CatalogSection() {
}; };
return ( 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="max-w-6xl mx-auto">
<div className="text-center mb-4"> <div className="text-center mb-4">
<span className="text-brand-400 text-sm font-semibold tracking-wide">{t.catalog.eyebrow}</span> <span className="text-brand-400 text-sm font-semibold tracking-wide">{t.catalog.eyebrow}</span>
+2
View File
@@ -1,5 +1,7 @@
'use client'; 'use client';
// آیکون شناور چت گوشه صفحه؛ با کلیک یک مودال گفتگوی ساده باز می‌شود (فعلاً بدون اتصال به بک‌اند واقعی).
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Modal, Input, Button, Tooltip } from 'antd'; import { Modal, Input, Button, Tooltip } from 'antd';
import { MessageOutlined, SendOutlined, CloseOutlined } from '@ant-design/icons'; import { MessageOutlined, SendOutlined, CloseOutlined } from '@ant-design/icons';
+3 -1
View File
@@ -1,5 +1,7 @@
'use client'; 'use client';
// نوار مزایا: ضمانت کالا، مشاوره رایگان، خرید اقساطی، بسته‌بندی ایمن.
import { import {
SafetyCertificateOutlined, SafetyCertificateOutlined,
CustomerServiceOutlined, CustomerServiceOutlined,
@@ -19,7 +21,7 @@ export default function FeaturesSection() {
const { t } = useLanguage(); const { t } = useLanguage();
return ( 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"> <div className="max-w-6xl mx-auto grid sm:grid-cols-2 lg:grid-cols-4 gap-5">
{t.features.map((feature) => { {t.features.map((feature) => {
const Icon = ICONS[feature.icon]; const Icon = ICONS[feature.icon];
+2
View File
@@ -1,5 +1,7 @@
'use client'; 'use client';
// رندر آیتم‌های شناور (چیپس/نوشابه) دور دستگاه با حرکت تصادفی و ملایم GSAP.
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { gsap } from 'gsap'; import { gsap } from 'gsap';
+3 -1
View File
@@ -1,5 +1,7 @@
'use client'; 'use client';
// فوتر سایت: درباره ما کوتاه، لینک‌های شبکه اجتماعی، دسترسی سریع، و اطلاعات تماس/آدرس.
import { PhoneOutlined, EnvironmentOutlined } from '@ant-design/icons'; import { PhoneOutlined, EnvironmentOutlined } from '@ant-design/icons';
import { useLanguage } from '@/app/context/LanguageContext'; import { useLanguage } from '@/app/context/LanguageContext';
@@ -8,7 +10,7 @@ export default function Footer() {
const c = t.contactInfo; const c = t.contactInfo;
return ( 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 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> <div>
<h3 className="text-primary font-bold text-lg mb-3">{t.footer.aboutLabel}</h3> <h3 className="text-primary font-bold text-lg mb-3">{t.footer.aboutLabel}</h3>
+5 -3
View File
@@ -1,5 +1,7 @@
'use client'; 'use client';
// هدر ثابت بالای صفحه: لوگو، منوی ناوبری، سوییچ زبان/تم، دکمه ورود به پنل کاربری و آیکون سبد خرید.
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { Badge, Button } from 'antd'; import { Badge, Button } from 'antd';
@@ -45,7 +47,7 @@ export default function Header() {
<Image src="/images/logo.svg" alt="روبات مارکت" width={150} height={42} priority /> <Image src="/images/logo.svg" alt="روبات مارکت" width={150} height={42} priority />
</a> </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) => ( {navLinks.map((link) => (
<a <a
key={link.href} key={link.href}
@@ -84,7 +86,7 @@ export default function Header() {
</Badge> </Badge>
<button <button
className="lg:hidden text-brand-400 text-2xl" className="xl:hidden text-brand-400 text-2xl"
onClick={() => setMobileOpen((v) => !v)} onClick={() => setMobileOpen((v) => !v)}
aria-label="menu" aria-label="menu"
> >
@@ -94,7 +96,7 @@ export default function Header() {
</div> </div>
{mobileOpen && ( {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) => ( {navLinks.map((link) => (
<a <a
key={link.href} key={link.href}
+139 -152
View File
@@ -1,175 +1,162 @@
'use client'; '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 { useLanguage } from '@/app/context/LanguageContext';
import { useTheme } from '@/app/context/ThemeContext';
// Simple typewriter: types each phrase, pauses, deletes, moves to next phrase, loops. const AUTOPLAY_DELAY = 5000; // هر ۵ ثانیه اسلاید عوض می‌شود
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;
}
export default function HeroSection() { export default function HeroSection() {
const { t } = useLanguage(); const { t, dir } = useLanguage();
const { theme } = useTheme(); const slides = t.hero.slides;
const introRef = useRef(null); const [activeIndex, setActiveIndex] = useState(0);
const logoRef = useRef(null); const timerRef = useRef(null);
const bgColorRef = useRef(null);
const subRef = useRef(null);
const ctaRef = 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(() => { useEffect(() => {
const ctx = gsap.context(() => { restartAutoplay();
const tl = gsap.timeline(); return () => {
tl.fromTo(logoRef.current, { opacity: 0, scale: 0.85 }, { opacity: 1, scale: 1, duration: 0.9, ease: 'power2.out' }) if (timerRef.current) clearInterval(timerRef.current);
.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') }, [restartAutoplay]);
.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();
}, []);
// Background behavior differs by theme: const handleManualNav = (direction) => {
// - Dark mode: looping color cycle (brand green -> black -> white -> green), if (direction === 'next') goToNext();
// kept readable by the dark scrim below. else goToPrev();
// - Light mode: a calm, static light background (no cycling), paired with restartAutoplay(); // شمارش ۵ ثانیه بعد از کلیک دستی از نو شروع شود
// dark text and a light scrim instead. };
useEffect(() => {
const el = bgColorRef.current;
if (!el) return undefined;
if (isDark) { const slide = slides[activeIndex];
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%)',
};
return ( return (
<section id="hero" className="relative h-screen w-full overflow-hidden"> <section id="hero" className="relative h-screen w-full overflow-hidden bg-page">
{/* Intro loader overlay */} {/* ------------------------------------------------------------------ */}
<div ref={introRef} className="fixed inset-0 z-[100] bg-page flex items-center justify-center"> {/* شکل مورب تزئینی سمت راست/چپ — نسخه سبز برند به‌جای قرمز Sielaff */}
<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 */}
<div <div
className={`absolute inset-0 opacity-[0.08] [background-size:26px_26px] ${ className="absolute inset-y-0 w-1/3 hidden md:block"
isDark style={{
? '[background-image:radial-gradient(#ffffff_1px,transparent_1px)]' [dir === 'rtl' ? 'left' : 'right']: '-6%',
: '[background-image:radial-gradient(#04231f_1px,transparent_1px)]' 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]"> {/* اسلایدهای عکس — همه اسلایدها روی هم قرار دارند و فقط با opacity */}
{typedHeadline} {/* محو/نمایان می‌شوند (transition نرم به‌جای پرش ناگهانی) */}
<span className="typewriter-caret h-[0.9em] align-middle" /> {/* ------------------------------------------------------------------ */}
</h1> <div className="absolute inset-0">
<p ref={subRef} className="mt-6 max-w-2xl text-secondary text-base md:text-xl opacity-0"> {slides.map((s, i) => (
{t.hero.sub} <div
</p> key={s.id}
<a className="absolute inset-0 flex items-center justify-center transition-opacity duration-1000 ease-in-out"
ref={ctaRef} style={{ opacity: i === activeIndex ? 1 : 0, pointerEvents: i === activeIndex ? 'auto' : 'none' }}
href="#showcase" aria-hidden={i !== activeIndex}
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" >
> <img
{t.hero.cta} src={s.image}
</a> alt={s.titleLines.join(' ')}
className="h-[55%] md:h-[70%] w-auto object-contain drop-shadow-2xl"
/>
</div>
))}
</div> </div>
<div className="absolute bottom-8 inset-x-0 flex justify-center z-10"> {/* لایه‌ی نیمه‌شفاف پشت متن، برای خوانایی روی هر عکسی */}
<div <div
className={`w-6 h-10 rounded-full border-2 flex items-start justify-center p-1.5 ${ className="absolute inset-0"
isDark ? 'border-white/50' : 'border-ink-900/30' 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%)',
<span className={`w-1.5 h-1.5 rounded-full animate-pulseSoft ${isDark ? 'bg-white' : 'bg-ink-900'}`} /> }}
/>
{/* ------------------------------------------------------------------ */}
{/* متن روی اسلاید: کیکر کوچک + عنوان بزرگ دو خطی + توضیح + دکمه 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>
</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> </section>
); );
} }
+114 -112
View File
@@ -1,127 +1,129 @@
'use client'; 'use client';
import {useEffect, useRef, useState} from 'react'; // سکشن ویترین محصولات: با اسکرول عمودی به اسکرول افقی پین‌شده تبدیل می‌شود (GSAP ScrollTrigger)
import {gsap} from 'gsap'; // و یک ویدیوی پس‌زمینه دارد که با پیشرفت اسکرول جلو/عقب می‌رود. طبق درخواست کاربر، این سکشن
import {ScrollTrigger} from 'gsap/ScrollTrigger'; // و هیرو از چرخه‌ی رنگ‌بندی متناوب بقیه سکشن‌ها مستثنا هستند.
import {Modal, Button, message} from 'antd';
import {ShoppingCartOutlined} from '@ant-design/icons'; import { useEffect, useRef, useState } from 'react';
import {useDispatch} from 'react-redux'; 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 VendingCard from './VendingCard';
import {showcaseVideoSrc} from '@/data/content'; import { showcaseVideoSrc } from '@/data/content';
import {useLanguage} from '@/app/context/LanguageContext'; import { useLanguage } from '@/app/context/LanguageContext';
import {addToCart} from '@/store/cartSlice'; import { addToCart } from '@/store/cartSlice';
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
export default function HorizontalShowcase() { export default function HorizontalShowcase() {
const sectionRef = useRef(null); const sectionRef = useRef(null);
const trackRef = useRef(null); const trackRef = useRef(null);
const videoRef = useRef(null); const videoRef = useRef(null);
const dispatch = useDispatch(); const dispatch = useDispatch();
const {t} = useLanguage(); const { t } = useLanguage();
const [quickViewMachine, setQuickViewMachine] = useState(null); const [quickViewMachine, setQuickViewMachine] = useState(null);
const [videoAvailable, setVideoAvailable] = useState(true); const [videoAvailable, setVideoAvailable] = useState(true);
useEffect(() => { useEffect(() => {
const ctx = gsap.context(() => { const ctx = gsap.context(() => {
const track = trackRef.current; const track = trackRef.current;
const section = sectionRef.current; const section = sectionRef.current;
if (!track || !section) return; if (!track || !section) return;
const scrollDistance = () => track.scrollWidth - window.innerWidth; const scrollDistance = () => track.scrollWidth - window.innerWidth;
const horizontalTween = gsap.to(track, { const horizontalTween = gsap.to(track, {
x: () => -scrollDistance(), x: () => -scrollDistance(),
ease: 'none', ease: 'none',
scrollTrigger: { scrollTrigger: {
trigger: section, trigger: section,
start: 'top top', start: 'top top',
end: () => `+=${scrollDistance()}`, end: () => `+=${scrollDistance()}`,
scrub: 1, scrub: 1,
pin: true, pin: true,
invalidateOnRefresh: true, invalidateOnRefresh: true,
onUpdate: (self) => { onUpdate: (self) => {
const video = videoRef.current; const video = videoRef.current;
if (video && video.duration) { if (video && video.duration) {
video.currentTime = self.progress * video.duration; video.currentTime = self.progress * video.duration;
} }
}, },
}, },
}); });
return () => { return () => {
horizontalTween.scrollTrigger?.kill(); horizontalTween.scrollTrigger?.kill();
horizontalTween.kill(); horizontalTween.kill();
}; };
}, sectionRef); }, sectionRef);
return () => ctx.revert(); return () => ctx.revert();
}, []); }, []);
const handleModalAddToCart = () => { const handleModalAddToCart = () => {
if (!quickViewMachine) return; if (!quickViewMachine) return;
dispatch( dispatch(
addToCart({ addToCart({
id: quickViewMachine.id, id: quickViewMachine.id,
name: quickViewMachine.name, name: quickViewMachine.name,
priceNumeric: quickViewMachine.priceNumeric, priceNumeric: quickViewMachine.priceNumeric,
priceLabel: quickViewMachine.priceLabel, priceLabel: quickViewMachine.priceLabel,
image: quickViewMachine.image, 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>
); );
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'; 'use client';
// سکشن مستقل ویدیوی معرفی شرکت (جدا از ویدیوی پس‌زمینه ویترین محصولات).
import { useLanguage } from '@/app/context/LanguageContext'; import { useLanguage } from '@/app/context/LanguageContext';
import { introVideoSrc, introVideoPoster } from '@/data/content'; import { introVideoSrc, introVideoPoster } from '@/data/content';
@@ -7,7 +9,7 @@ export default function IntroVideoSection() {
const { t } = useLanguage(); const { t } = useLanguage();
return ( 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"> <div className="max-w-5xl mx-auto text-center">
<span className="text-brand-400 text-sm font-semibold tracking-wide">{t.introVideo.eyebrow}</span> <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"> <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'; 'use client';
// دکمه سوییچ زبان فارسی/انگلیسی.
import { Button, Tooltip } from 'antd'; import { Button, Tooltip } from 'antd';
import { useLanguage } from '@/app/context/LanguageContext'; import { useLanguage } from '@/app/context/LanguageContext';
+3 -1
View File
@@ -1,5 +1,7 @@
'use client'; 'use client';
// گرید «مکان مناسب برای نصب» — هتل‌ها، مدارس، داروخانه‌ها، بیمارستان‌ها، شیفت شب، محصولات تخصصی.
import { import {
HomeOutlined, HomeOutlined,
ReadOutlined, ReadOutlined,
@@ -23,7 +25,7 @@ export default function LocationsSection() {
const { t } = useLanguage(); const { t } = useLanguage();
return ( 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="max-w-6xl mx-auto">
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-2xl md:text-4xl font-extrabold brand-gradient-text">{t.locations.title}</h2> <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'; 'use client';
// نقشه نشان (Neshan) — از Static Map API استفاده می‌کند. تا وقتی کلید API خالی باشد،
// به‌جای نقشه یک پیام راهنما نشان داده می‌شود (نه تصویر خراب).
import { useState } from 'react'; import { useState } from 'react';
import { EnvironmentOutlined } from '@ant-design/icons'; import { EnvironmentOutlined } from '@ant-design/icons';
import { useLanguage } from '@/app/context/LanguageContext'; import { useLanguage } from '@/app/context/LanguageContext';
@@ -71,7 +74,7 @@ export default function MapSection() {
]; ];
return ( 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="max-w-6xl mx-auto">
<div className="text-center mb-12"> <div className="text-center mb-12">
<span className="text-brand-400 text-sm font-semibold tracking-wide">{t.map.eyebrow}</span> <span className="text-brand-400 text-sm font-semibold tracking-wide">{t.map.eyebrow}</span>
+2
View File
@@ -1,5 +1,7 @@
'use client'; 'use client';
// دکمه سوییچ حالت روشن/تیره.
import { Button, Tooltip } from 'antd'; import { Button, Tooltip } from 'antd';
import { SunOutlined, MoonOutlined } from '@ant-design/icons'; import { SunOutlined, MoonOutlined } from '@ant-design/icons';
import { useTheme } from '@/app/context/ThemeContext'; import { useTheme } from '@/app/context/ThemeContext';
+3 -1
View File
@@ -1,5 +1,7 @@
'use client'; 'use client';
// فرم ثبت تیکت پشتیبانی با اعتبارسنجی antd؛ ارسال واقعی به بک‌اند در بلوک TODO داخل کد مشخص شده.
import { Form, Input, Select, Button, message } from 'antd'; import { Form, Input, Select, Button, message } from 'antd';
import { useLanguage } from '@/app/context/LanguageContext'; import { useLanguage } from '@/app/context/LanguageContext';
@@ -28,7 +30,7 @@ export default function TicketForm() {
}; };
return ( 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="max-w-2xl mx-auto">
<div className="text-center mb-10"> <div className="text-center mb-10">
<span className="text-brand-400 text-sm font-semibold tracking-wide">{t.ticket.eyebrow}</span> <span className="text-brand-400 text-sm font-semibold tracking-wide">{t.ticket.eyebrow}</span>
+3
View File
@@ -1,5 +1,8 @@
'use client'; 'use client';
// کارت نمایش هر دستگاه داخل ویترین: شامل جلوه اسپات‌لایت (هاور روی عکس دستگاه رنگ جایگزین را نشان می‌دهد)،
// آیتم‌های شناور، گیف+توضیحات، و دکمه‌های افزودن به سبد / مشاهده سریع / علاقه‌مندی.
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { Button, Tooltip, message } from 'antd'; import { Button, Tooltip, message } from 'antd';
import { ShoppingCartOutlined, EyeOutlined, HeartOutlined, HeartFilled } from '@ant-design/icons'; import { ShoppingCartOutlined, EyeOutlined, HeartOutlined, HeartFilled } from '@ant-design/icons';
+74 -12
View File
@@ -105,13 +105,32 @@ export const content = {
}, },
hero: { hero: {
loading: 'در حال بارگذاری روبات مارکت…', loading: 'در حال بارگذاری روبات مارکت…',
typingPhrases: [
'به دنیای خودکار و هوشمند خوش آمدید!',
'تجربه‌ای نوین از خرید آسان و سریع.',
'روبات مارکت؛ فروشنده هوشمند شما، ۲۴ ساعته.',
],
sub: 'تجربه‌ای نوین از خرید آسان و سریع با دستگاه‌های وندینگ روبات مارکت.',
cta: 'مشاهده دستگاه‌ها', 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: { cart: {
title: 'سبد خرید', title: 'سبد خرید',
@@ -131,6 +150,19 @@ export const content = {
gifAlt: (name) => `نمایش عملکرد ${name}`, gifAlt: (name) => `نمایش عملکرد ${name}`,
}, },
machines: machinesFa, 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: [ features: [
{ id: 'warranty', icon: 'safety', title: 'ضمانت کالا', description: 'کلیه محصولات شرکت دارای یک سال خدمات پس از فروش می‌باشند.' }, { id: 'warranty', icon: 'safety', title: 'ضمانت کالا', description: 'کلیه محصولات شرکت دارای یک سال خدمات پس از فروش می‌باشند.' },
{ id: 'consulting', icon: 'consulting', title: 'مشاوره تخصصی و رایگان', description: 'خدمات مشاوره رایگان در مورد نوع دستگاه و چیدمان محصولات.' }, { id: 'consulting', icon: 'consulting', title: 'مشاوره تخصصی و رایگان', description: 'خدمات مشاوره رایگان در مورد نوع دستگاه و چیدمان محصولات.' },
@@ -274,13 +306,30 @@ export const content = {
}, },
hero: { hero: {
loading: 'Loading Robot Market…', 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', 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: { cart: {
title: 'Cart', title: 'Cart',
@@ -300,6 +349,19 @@ export const content = {
gifAlt: (name) => `${name} in action`, gifAlt: (name) => `${name} in action`,
}, },
machines: machinesEn, 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: [ features: [
{ id: 'warranty', icon: 'safety', title: 'Product Warranty', description: 'All our products come with one year of after-sales service.' }, { 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.' }, { 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": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./*"] "@/*": ["./*"]
}, }
"jsx": "react"
} }
} }
+3 -3
View File
@@ -736,9 +736,9 @@
} }
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.11.9", "version": "2.11.10",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.9.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.10.tgz",
"integrity": "sha512-cp447VUsGS07+n1Dqf7YSQ8maeJrjEhaDxTm1ZefbqDtypHBC5GzGMQbklR6IPR13Y8OAJRHZWEMtZipJLCttg==", "integrity": "sha512-35JEvJ5/KKlbCHjMCsONI2w6HE88STjVdHk+C7d8LtcFxUjZR1KeLP9izofn2qs0KUxX5r4z73bwH/rd+JHacw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {