diff --git a/.env.local.example b/.env.local.example
new file mode 100644
index 0000000..32c64a0
--- /dev/null
+++ b/.env.local.example
@@ -0,0 +1,5 @@
+# Copy this file to .env.local and fill in your real keys.
+
+# Neshan (نشان) map API key — get one from https://platform.neshan.org
+# Used by components/MapSection.jsx for the static map image.
+NEXT_PUBLIC_NESHAN_API_KEY=
diff --git a/app/context/LanguageContext.jsx b/app/context/LanguageContext.jsx
new file mode 100644
index 0000000..82f35f7
--- /dev/null
+++ b/app/context/LanguageContext.jsx
@@ -0,0 +1,40 @@
+'use client';
+
+import { createContext, useContext, useEffect, useState } from 'react';
+import { content } from '@/data/content';
+
+const LanguageContext = createContext(null);
+
+export function LanguageProvider({ children }) {
+ const [locale, setLocale] = useState('fa');
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ const saved = window.localStorage.getItem('rm-locale');
+ if (saved === 'fa' || saved === 'en') setLocale(saved);
+ setMounted(true);
+ }, []);
+
+ useEffect(() => {
+ const root = document.documentElement;
+ root.lang = locale;
+ root.dir = content[locale].dir;
+ window.localStorage.setItem('rm-locale', locale);
+ }, [locale]);
+
+ const toggleLocale = () => setLocale((l) => (l === 'fa' ? 'en' : 'fa'));
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useLanguage() {
+ const ctx = useContext(LanguageContext);
+ if (!ctx) throw new Error('useLanguage must be used within LanguageProvider');
+ return ctx;
+}
diff --git a/app/context/ThemeContext.jsx b/app/context/ThemeContext.jsx
new file mode 100644
index 0000000..ad8d05e
--- /dev/null
+++ b/app/context/ThemeContext.jsx
@@ -0,0 +1,43 @@
+'use client';
+
+import { createContext, useContext, useEffect, useState } from 'react';
+
+const ThemeContext = createContext(null);
+
+export function ThemeProvider({ children }) {
+ const [theme, setTheme] = useState('dark');
+ const [mounted, setMounted] = useState(false);
+
+ // On mount: read saved preference, else fall back to system preference.
+ 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');
+ }
+ setMounted(true);
+ }, []);
+
+ // Reflect theme on class + persist.
+ useEffect(() => {
+ const root = document.documentElement;
+ root.classList.remove('light', 'dark');
+ root.classList.add(theme);
+ window.localStorage.setItem('rm-theme', theme);
+ }, [theme]);
+
+ const toggleTheme = () => setTheme((t) => (t === 'dark' ? 'light' : 'dark'));
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useTheme() {
+ const ctx = useContext(ThemeContext);
+ if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
+ return ctx;
+}
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..a12d10e
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,228 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* ------------------------------------------------------------------ */
+/* Theme tokens — dark is the default; .light overrides on */
+/* ------------------------------------------------------------------ */
+:root {
+ --brand: #00a693;
+ --color-bg-page: #04120f;
+ --color-bg-elevated: #0c2b24;
+ --color-bg-surface: rgba(255, 255, 255, 0.045);
+ --color-bg-surface-strong: rgba(255, 255, 255, 0.08);
+ --color-border: rgba(0, 166, 147, 0.28);
+ --color-text-primary: #ffffff;
+ --color-text-secondary: rgba(230, 247, 244, 0.72);
+ --color-scrollbar-track: #071e19;
+ --color-scrollbar-thumb: #0b5c50;
+ color-scheme: dark;
+}
+
+html.light {
+ --color-bg-page: #f5fbf9;
+ --color-bg-elevated: #ffffff;
+ --color-bg-surface: rgba(0, 89, 79, 0.045);
+ --color-bg-surface-strong: rgba(0, 89, 79, 0.08);
+ --color-border: rgba(0, 166, 147, 0.25);
+ --color-text-primary: #04231f;
+ --color-text-secondary: rgba(4, 35, 31, 0.68);
+ --color-scrollbar-track: #e1f3ef;
+ --color-scrollbar-thumb: #8fdccc;
+ color-scheme: light;
+}
+
+html,
+body {
+ padding: 0;
+ margin: 0;
+ scroll-behavior: smooth;
+}
+
+html[dir='rtl'] body {
+ font-family: var(--font-vazirmatn), Tahoma, sans-serif;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+::selection {
+ background: rgba(0, 166, 147, 0.35);
+ color: var(--color-text-primary);
+}
+
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--color-scrollbar-track);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--color-scrollbar-thumb);
+ border-radius: 999px;
+}
+
+/* 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);
+}
+
+/* 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);
+}
+
+/* ------------------------------------------------------------------ */
+/* Horizontal scroll showcase (pinned via GSAP ScrollTrigger) */
+/* ------------------------------------------------------------------ */
+.horizontal-track {
+ display: flex;
+ height: 100vh;
+ /*width: 100vw;*/
+ will-change: transform;
+}
+
+.horizontal-panel {
+ height: 100vh;
+ width: 100vw;
+ flex-shrink: 0;
+ position: relative;
+ overflow: hidden;
+}
+
+/* ------------------------------------------------------------------ */
+/* Cursor spotlight / lens reveal effect over the vending machine */
+/* ------------------------------------------------------------------ */
+.spotlight-stage {
+ position: relative;
+ cursor: none;
+ isolation: isolate;
+}
+
+.spotlight-stage .base-layer,
+.spotlight-stage .reveal-layer {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ pointer-events: none;
+ user-select: none;
+}
+
+.spotlight-stage .reveal-layer {
+ clip-path: circle(0px at 50% 50%);
+ transition: clip-path 0.05s linear;
+}
+
+.spotlight-lens {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 190px;
+ 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),
+ 0 0 40px 6px rgba(0, 166, 147, 0.45);
+ pointer-events: none;
+ transform: translate(-50%, -50%);
+ opacity: 0;
+ transition: opacity 0.15s ease;
+ z-index: 30;
+}
+
+.spotlight-stage:hover .spotlight-lens {
+ opacity: 1;
+}
+
+/* Utility: hide native scrollbar but keep scroll functionality */
+.no-scrollbar::-webkit-scrollbar {
+ display: none;
+}
+
+.no-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+/* Gradient text helper for headings */
+.brand-gradient-text {
+ background: linear-gradient(90deg, #6fe0c9 0%, #00a693 55%, #017065 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+
+/* ------------------------------------------------------------------ */
+/* Typewriter caret for the hero headline */
+/* ------------------------------------------------------------------ */
+.typewriter-caret {
+ display: inline-block;
+ width: 3px;
+ margin-inline-start: 4px;
+ background-color: var(--brand);
+ animation: caretBlink 0.9s steps(1) infinite;
+}
+
+@keyframes caretBlink {
+ 0%, 49% {
+ opacity: 1;
+ }
+ 50%, 100% {
+ opacity: 0;
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* Chat widget */
+/* ------------------------------------------------------------------ */
+.chat-bubble {
+ max-width: 80%;
+ padding: 10px 14px;
+ border-radius: 14px;
+ font-size: 0.875rem;
+ line-height: 1.6;
+}
diff --git a/app/layout.jsx b/app/layout.jsx
new file mode 100644
index 0000000..058da35
--- /dev/null
+++ b/app/layout.jsx
@@ -0,0 +1,28 @@
+import { Vazirmatn } from 'next/font/google';
+import './globals.css';
+import Providers from './providers';
+
+const vazirmatn = Vazirmatn({
+ subsets: ['arabic'],
+ variable: '--font-vazirmatn',
+ display: 'swap',
+});
+
+export const metadata = {
+ title: 'روبات مارکت | دستگاههای وندینگ هوشمند',
+ description:
+ 'روبات مارکت؛ دستگاههای فروش خودکار هوشمند با مدیریت آنلاین، تحویل نرم و طراحی مدرن.',
+};
+
+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.
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/app/page.jsx b/app/page.jsx
new file mode 100644
index 0000000..39c1da9
--- /dev/null
+++ b/app/page.jsx
@@ -0,0 +1,47 @@
+import Header from '@/components/Header';
+import HeroSection from '@/components/HeroSection';
+import HorizontalShowcase from '@/components/HorizontalShowcase';
+import FeaturesSection from '@/components/FeaturesSection';
+import AboutSection from '@/components/AboutSection';
+import CatalogSection from '@/components/CatalogSection';
+import LocationsSection from '@/components/LocationsSection';
+import IntroVideoSection from '@/components/IntroVideoSection';
+import MapSection from '@/components/MapSection';
+import TicketForm from '@/components/TicketForm';
+import Footer from '@/components/Footer';
+import CartDrawer from '@/components/CartDrawer';
+import ChatWidget from '@/components/ChatWidget';
+
+export default function HomePage() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/providers.jsx b/app/providers.jsx
new file mode 100644
index 0000000..0320a39
--- /dev/null
+++ b/app/providers.jsx
@@ -0,0 +1,59 @@
+'use client';
+
+import { Provider as ReduxProvider } from 'react-redux';
+import { ConfigProvider, theme as antdThemeApi } from 'antd';
+import faIR from 'antd/locale/fa_IR';
+import enUS from 'antd/locale/en_US';
+import { store } from '@/store/store';
+import { ThemeProvider, useTheme } from './context/ThemeContext';
+import { LanguageProvider, useLanguage } from './context/LanguageContext';
+
+function AntdBridge({ children }) {
+ const { theme } = useTheme();
+ const { locale, dir } = useLanguage();
+
+ const antdTheme = {
+ algorithm: theme === 'dark' ? antdThemeApi.darkAlgorithm : antdThemeApi.defaultAlgorithm,
+ token: {
+ colorPrimary: '#00a693',
+ colorInfo: '#00a693',
+ borderRadius: 10,
+ fontFamily: 'var(--font-vazirmatn), Tahoma, sans-serif',
+ colorBgContainer: theme === 'dark' ? '#0c2b24' : '#ffffff',
+ colorBgElevated: theme === 'dark' ? '#0f332b' : '#ffffff',
+ colorBorder: theme === 'dark' ? '#0b3d38' : '#c0ebe3',
+ colorText: theme === 'dark' ? '#e6f7f4' : '#04231f',
+ colorTextSecondary: theme === 'dark' ? 'rgba(230,247,244,0.7)' : 'rgba(4,35,31,0.65)',
+ },
+ components: {
+ Button: { controlHeight: 44, fontWeight: 600 },
+ Badge: { colorError: '#ff6b57' },
+ Modal: {
+ contentBg: theme === 'dark' ? '#0c2b24' : '#ffffff',
+ headerBg: theme === 'dark' ? '#0c2b24' : '#ffffff',
+ titleColor: theme === 'dark' ? '#e6f7f4' : '#04231f',
+ },
+ Drawer: {
+ colorBgElevated: theme === 'dark' ? '#0c2b24' : '#ffffff',
+ },
+ },
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default function Providers({ children }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/components/AboutSection.jsx b/components/AboutSection.jsx
new file mode 100644
index 0000000..1b9c1aa
--- /dev/null
+++ b/components/AboutSection.jsx
@@ -0,0 +1,37 @@
+'use client';
+
+import { useState } from 'react';
+import { useLanguage } from '@/app/context/LanguageContext';
+
+export default function AboutSection() {
+ const { t } = useLanguage();
+ const [expanded, setExpanded] = useState(false);
+ const visibleParagraphs = expanded ? t.about.paragraphs : t.about.paragraphs.slice(0, 2);
+
+ return (
+
+
+
{t.about.eyebrow}
+
+ {t.about.legalName}
+
+
+
+ {visibleParagraphs.map((p, i) => (
+
+ {p}
+
+ ))}
+
+
+
setExpanded((v) => !v)}
+ className="mt-6 inline-flex items-center gap-2 text-brand-400 hover:text-brand-300 text-sm font-semibold transition-colors"
+ >
+ {expanded ? t.about.less : t.about.more}
+ ˅
+
+
+
+ );
+}
diff --git a/components/CartDrawer.jsx b/components/CartDrawer.jsx
new file mode 100644
index 0000000..e19bed2
--- /dev/null
+++ b/components/CartDrawer.jsx
@@ -0,0 +1,109 @@
+'use client';
+
+import { Drawer, Button, Empty, InputNumber, message } from 'antd';
+import { DeleteOutlined } from '@ant-design/icons';
+import { useSelector, useDispatch } from 'react-redux';
+import {
+ selectCartItems,
+ selectCartTotal,
+ selectIsDrawerOpen,
+ closeDrawer,
+ removeFromCart,
+ incrementQty,
+ decrementQty,
+} from '@/store/cartSlice';
+import { useLanguage } from '@/app/context/LanguageContext';
+
+function toLocaleNumber(n, locale) {
+ return n.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US');
+}
+
+export default function CartDrawer() {
+ const dispatch = useDispatch();
+ const isOpen = useSelector(selectIsDrawerOpen);
+ const items = useSelector(selectCartItems);
+ const total = useSelector(selectCartTotal);
+ const { t, locale, dir } = useLanguage();
+
+ const currencySuffix = locale === 'fa' ? ' ریال' : ' Rials';
+
+ const handleCheckout = async () => {
+ // -------------------------------------------------------------------
+ // TODO: Connect to payment gateway API.
+ //
+ // import axios from 'axios';
+ //
+ // try {
+ // const { data } = await axios.post('/api/payment/create', {
+ // items: items.map((i) => ({ id: i.id, qty: i.qty })),
+ // totalAmount: total,
+ // });
+ // window.location.href = data.paymentRedirectUrl;
+ // } catch (err) {
+ // message.error('...');
+ // }
+ // -------------------------------------------------------------------
+ message.info(t.cart.checkoutToast);
+ };
+
+ return (
+ dispatch(closeDrawer())}
+ open={isOpen}
+ width={400}
+ >
+ {items.length === 0 ? (
+ {t.cart.empty}} className="mt-16" />
+ ) : (
+
+ {items.map((item) => (
+
+
+
+
{item.name}
+
+ {toLocaleNumber(item.priceNumeric, locale)}
+ {currencySuffix}
+
+
+ {
+ if (val > item.qty) dispatch(incrementQty(item.id));
+ else if (val < item.qty) dispatch(decrementQty(item.id));
+ }}
+ className="w-16"
+ />
+ }
+ onClick={() => dispatch(removeFromCart(item.id))}
+ />
+
+
+
+ ))}
+
+
+
+ {t.cart.total}
+
+ {toLocaleNumber(total, locale)}
+ {currencySuffix}
+
+
+
+ {t.cart.checkout}
+
+
+
+ )}
+
+ );
+}
diff --git a/components/CatalogSection.jsx b/components/CatalogSection.jsx
new file mode 100644
index 0000000..a6704e3
--- /dev/null
+++ b/components/CatalogSection.jsx
@@ -0,0 +1,82 @@
+'use client';
+
+import { Button, message } from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
+import { useDispatch } from 'react-redux';
+import { addToCart } from '@/store/cartSlice';
+import { useLanguage } from '@/app/context/LanguageContext';
+
+function toLocaleNumber(n, locale) {
+ return n.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US');
+}
+
+export default function CatalogSection() {
+ const { t, locale } = useLanguage();
+ const dispatch = useDispatch();
+ const currencySuffix = locale === 'fa' ? ' ریال' : ' Rials';
+
+ const handleAdd = (item) => {
+ dispatch(
+ addToCart({
+ id: `catalog-${item.id}`,
+ name: item.name,
+ priceNumeric: item.priceNumeric,
+ priceLabel: `${toLocaleNumber(item.priceNumeric, locale)}${currencySuffix}`,
+ image: item.image,
+ })
+ );
+ message.success(t.catalog.addToast(item.name));
+ };
+
+ return (
+
+
+
+ {t.catalog.eyebrow}
+
{t.catalog.title}
+
+
+ {t.catalog.subtitle}
+
+
+
+ {t.catalog.items.map((item) => (
+
+
+
+
+ {item.category}
+
{item.name}
+
+
+
+
+
+
+ {toLocaleNumber(item.originalPriceNumeric, locale)}
+ {currencySuffix}
+
+
+ {toLocaleNumber(item.priceNumeric, locale)}
+ {currencySuffix}
+
+
+
}
+ onClick={() => handleAdd(item)}
+ >
+ {t.catalog.addToOrder}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/components/ChatWidget.jsx b/components/ChatWidget.jsx
new file mode 100644
index 0000000..d6544dd
--- /dev/null
+++ b/components/ChatWidget.jsx
@@ -0,0 +1,99 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+import { Modal, Input, Button, Tooltip } from 'antd';
+import { MessageOutlined, SendOutlined, CloseOutlined } from '@ant-design/icons';
+import { useLanguage } from '@/app/context/LanguageContext';
+
+export default function ChatWidget() {
+ const { t, dir } = useLanguage();
+ const [open, setOpen] = useState(false);
+ const [draft, setDraft] = useState('');
+ const [messages, setMessages] = useState([]);
+ const scrollRef = useRef(null);
+
+ // Seed the bot greeting once, in the current language.
+ useEffect(() => {
+ setMessages([{ from: 'bot', text: t.chat.initialBotMessage }]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [t.chat.initialBotMessage]);
+
+ useEffect(() => {
+ scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' });
+ }, [messages, open]);
+
+ const handleSend = () => {
+ const text = draft.trim();
+ if (!text) return;
+
+ setMessages((prev) => [...prev, { from: 'user', text }]);
+ setDraft('');
+
+ // -----------------------------------------------------------------
+ // TODO: Connect to a real chat/support backend (e.g. via axios or a
+ // websocket) instead of this local placeholder reply.
+ //
+ // import axios from 'axios';
+ // const { data } = await axios.post('/api/chat', { message: text });
+ // setMessages((prev) => [...prev, { from: 'bot', text: data.reply }]);
+ // -----------------------------------------------------------------
+ setTimeout(() => {
+ setMessages((prev) => [...prev, { from: 'bot', text: t.chat.fallbackReply }]);
+ }, 600);
+ };
+
+ return (
+ <>
+
+ setOpen(true)}
+ aria-label={t.chat.fabLabel}
+ className="fixed bottom-6 z-40 w-14 h-14 rounded-full bg-brand hover:bg-brand-400 text-ink-950 shadow-brandGlow flex items-center justify-center text-2xl transition-transform hover:scale-105"
+ style={dir === 'rtl' ? { left: '1.5rem' } : { right: '1.5rem' }}
+ >
+
+
+
+
+ setOpen(false)}
+ footer={null}
+ closeIcon={ }
+ title={t.chat.title}
+ centered
+ >
+
+ {messages.map((m, i) => (
+
+ {m.text}
+
+ ))}
+
+
+
+ setDraft(e.target.value)}
+ onPressEnter={handleSend}
+ placeholder={t.chat.placeholder}
+ />
+ } onClick={handleSend}>
+ {t.chat.send}
+
+
+
+ >
+ );
+}
diff --git a/components/FeaturesSection.jsx b/components/FeaturesSection.jsx
new file mode 100644
index 0000000..12cf2fa
--- /dev/null
+++ b/components/FeaturesSection.jsx
@@ -0,0 +1,42 @@
+'use client';
+
+import {
+ SafetyCertificateOutlined,
+ CustomerServiceOutlined,
+ CreditCardOutlined,
+ InboxOutlined,
+} from '@ant-design/icons';
+import { useLanguage } from '@/app/context/LanguageContext';
+
+const ICONS = {
+ safety: SafetyCertificateOutlined,
+ consulting: CustomerServiceOutlined,
+ installment: CreditCardOutlined,
+ packaging: InboxOutlined,
+};
+
+export default function FeaturesSection() {
+ const { t } = useLanguage();
+
+ return (
+
+
+ {t.features.map((feature) => {
+ const Icon = ICONS[feature.icon];
+ return (
+
+
+ {Icon && }
+
+
{feature.title}
+
{feature.description}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/components/FloatingItems.jsx b/components/FloatingItems.jsx
new file mode 100644
index 0000000..78a8fc3
--- /dev/null
+++ b/components/FloatingItems.jsx
@@ -0,0 +1,55 @@
+'use client';
+
+import { useEffect, useRef } from 'react';
+import { gsap } from 'gsap';
+
+/**
+ * Renders the centralized `floatingSnacks` config as absolutely-positioned
+ * images and gives each one a gentle, randomized floating drift via GSAP.
+ * Positions/sizes/images all come from data/content.js — edit there only.
+ */
+export default function FloatingItems({ items }) {
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ const nodes = containerRef.current?.querySelectorAll('[data-float-item]');
+ if (!nodes) return;
+
+ const tweens = Array.from(nodes).map((node, i) => {
+ const drift = 14 + Math.random() * 14;
+ const rotate = 4 + Math.random() * 6;
+ return gsap.to(node, {
+ y: `+=${drift}`,
+ rotate: i % 2 === 0 ? rotate : -rotate,
+ duration: 3.5 + Math.random() * 2.5,
+ ease: 'sine.inOut',
+ yoyo: true,
+ repeat: -1,
+ delay: Math.random() * 1.5,
+ });
+ });
+
+ return () => tweens.forEach((t) => t.kill());
+ }, [items]);
+
+ return (
+
+ {items.map((item) => (
+
+ ))}
+
+ );
+}
diff --git a/components/Footer.jsx b/components/Footer.jsx
new file mode 100644
index 0000000..73b2c59
--- /dev/null
+++ b/components/Footer.jsx
@@ -0,0 +1,80 @@
+'use client';
+
+import { PhoneOutlined, EnvironmentOutlined } from '@ant-design/icons';
+import { useLanguage } from '@/app/context/LanguageContext';
+
+export default function Footer() {
+ const { t } = useLanguage();
+ const c = t.contactInfo;
+
+ return (
+
+ );
+}
diff --git a/components/Header.jsx b/components/Header.jsx
new file mode 100644
index 0000000..4e653e6
--- /dev/null
+++ b/components/Header.jsx
@@ -0,0 +1,119 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import Image from 'next/image';
+import { Badge, Button } from 'antd';
+import { ShoppingCartOutlined, MenuOutlined, CloseOutlined, UserOutlined } from '@ant-design/icons';
+import { useSelector, useDispatch } from 'react-redux';
+import { selectCartCount, openDrawer } from '@/store/cartSlice';
+import { useLanguage } from '@/app/context/LanguageContext';
+import ThemeToggle from './ThemeToggle';
+import LanguageToggle from './LanguageToggle';
+
+export default function Header() {
+ const dispatch = useDispatch();
+ const cartCount = useSelector(selectCartCount);
+ const { t } = useLanguage();
+ const [scrolled, setScrolled] = useState(false);
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ const navLinks = [
+ { href: '#hero', label: t.nav.home },
+ { href: '#showcase', label: t.nav.showcase },
+ { href: '#about', label: t.nav.about },
+ { href: '#catalog', label: t.nav.catalog },
+ { href: '#locations', label: t.nav.locations },
+ { href: '#video', label: t.nav.video },
+ { href: '#map', label: t.nav.map },
+ { href: '#support', label: t.nav.support },
+ ];
+
+ useEffect(() => {
+ const onScroll = () => setScrolled(window.scrollY > 24);
+ window.addEventListener('scroll', onScroll);
+ return () => window.removeEventListener('scroll', onScroll);
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/components/HeroSection.jsx b/components/HeroSection.jsx
new file mode 100644
index 0000000..0a8df3b
--- /dev/null
+++ b/components/HeroSection.jsx
@@ -0,0 +1,175 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+import { gsap } from 'gsap';
+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;
+}
+
+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 typedHeadline = useTypewriter(t.hero.typingPhrases);
+ const isDark = theme === 'dark';
+
+ // 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();
+ }, []);
+
+ // 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%)',
+ };
+
+ return (
+
+ {/* Intro loader overlay */}
+
+
+
+ R
+
+
{t.hero.loading}
+
+
+
+ {/* Background: cycling colors in dark mode, static light tone in light mode */}
+
+
+ {/*
+ TODO: swap the background above for a video once the asset is ready:
+
+
+
+ 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 */}
+
+
+ {/* Decorative dot grid — dark dots on light backgrounds, light dots on dark */}
+
+
+
+
+
+
+ );
+}
diff --git a/components/HorizontalShowcase.jsx b/components/HorizontalShowcase.jsx
new file mode 100644
index 0000000..d59ff5a
--- /dev/null
+++ b/components/HorizontalShowcase.jsx
@@ -0,0 +1,127 @@
+'use client';
+
+import {useEffect, useRef, useState} from 'react';
+import {gsap} from 'gsap';
+import {ScrollTrigger} from 'gsap/ScrollTrigger';
+import {Modal, Button, message} from 'antd';
+import {ShoppingCartOutlined} from '@ant-design/icons';
+import {useDispatch} from 'react-redux';
+import VendingCard from './VendingCard';
+import {showcaseVideoSrc} from '@/data/content';
+import {useLanguage} from '@/app/context/LanguageContext';
+import {addToCart} from '@/store/cartSlice';
+
+gsap.registerPlugin(ScrollTrigger);
+
+export default function HorizontalShowcase() {
+ const sectionRef = useRef(null);
+ const trackRef = useRef(null);
+ const videoRef = useRef(null);
+ const dispatch = useDispatch();
+ const {t} = useLanguage();
+ const [quickViewMachine, setQuickViewMachine] = useState(null);
+ const [videoAvailable, setVideoAvailable] = useState(true);
+
+ useEffect(() => {
+ const ctx = gsap.context(() => {
+ const track = trackRef.current;
+ const section = sectionRef.current;
+ if (!track || !section) return;
+
+ const scrollDistance = () => track.scrollWidth - window.innerWidth;
+
+ const horizontalTween = gsap.to(track, {
+ x: () => -scrollDistance(),
+ ease: 'none',
+ scrollTrigger: {
+ trigger: section,
+ start: 'top top',
+ end: () => `+=${scrollDistance()}`,
+ scrub: 1,
+ pin: true,
+ invalidateOnRefresh: true,
+ onUpdate: (self) => {
+ const video = videoRef.current;
+ if (video && video.duration) {
+ video.currentTime = self.progress * video.duration;
+ }
+ },
+ },
+ });
+
+ return () => {
+ horizontalTween.scrollTrigger?.kill();
+ horizontalTween.kill();
+ };
+ }, sectionRef);
+
+ return () => ctx.revert();
+ }, []);
+
+ const handleModalAddToCart = () => {
+ if (!quickViewMachine) return;
+ dispatch(
+ addToCart({
+ id: quickViewMachine.id,
+ name: quickViewMachine.name,
+ priceNumeric: quickViewMachine.priceNumeric,
+ priceLabel: quickViewMachine.priceLabel,
+ image: quickViewMachine.image,
+ })
+ );
+ message.success(t.cart.addToast(quickViewMachine.name));
+ setQuickViewMachine(null);
+ };
+
+ return (
+
+
+ {videoAvailable && (
+
setVideoAvailable(false)}
+ />
+ )}
+
+
+
+
+ {t.machines.map((machine) => (
+
+ ))}
+
+
+ setQuickViewMachine(null)}
+ footer={null}
+ centered
+ title={quickViewMachine?.name}
+ >
+ {quickViewMachine && (
+
+
+
{quickViewMachine.description}
+
{quickViewMachine.priceLabel}
+
}
+ onClick={handleModalAddToCart}>
+ {t.machineActions.addToCart}
+
+
+ )}
+
+
+ );
+}
diff --git a/components/IntroVideoSection.jsx b/components/IntroVideoSection.jsx
new file mode 100644
index 0000000..c15b5d0
--- /dev/null
+++ b/components/IntroVideoSection.jsx
@@ -0,0 +1,33 @@
+'use client';
+
+import { useLanguage } from '@/app/context/LanguageContext';
+import { introVideoSrc, introVideoPoster } from '@/data/content';
+
+export default function IntroVideoSection() {
+ const { t } = useLanguage();
+
+ return (
+
+
+
{t.introVideo.eyebrow}
+
+ {t.introVideo.title}
+
+
{t.introVideo.subtitle}
+
+ {/*
+ TODO: replace /public/video/company-intro-placeholder.mp4 with the real
+ company introduction video (keep the same filename, or update
+ `introVideoSrc` in data/content.js). The poster image shows before
+ playback starts — swap /public/images/video-poster.svg the same way.
+ */}
+
+
+
+ {t.introVideo.videoNotSupported}
+
+
+
+
+ );
+}
diff --git a/components/LanguageToggle.jsx b/components/LanguageToggle.jsx
new file mode 100644
index 0000000..b06336a
--- /dev/null
+++ b/components/LanguageToggle.jsx
@@ -0,0 +1,16 @@
+'use client';
+
+import { Button, Tooltip } from 'antd';
+import { useLanguage } from '@/app/context/LanguageContext';
+
+export default function LanguageToggle() {
+ const { locale, toggleLocale } = useLanguage();
+
+ return (
+
+
+ {locale === 'fa' ? 'EN' : 'فا'}
+
+
+ );
+}
diff --git a/components/LocationsSection.jsx b/components/LocationsSection.jsx
new file mode 100644
index 0000000..e4915d1
--- /dev/null
+++ b/components/LocationsSection.jsx
@@ -0,0 +1,52 @@
+'use client';
+
+import {
+ HomeOutlined,
+ ReadOutlined,
+ ShopOutlined,
+ MedicineBoxOutlined,
+ ClockCircleOutlined,
+ AppstoreOutlined,
+} from '@ant-design/icons';
+import { useLanguage } from '@/app/context/LanguageContext';
+
+const ICONS = {
+ hotel: HomeOutlined,
+ school: ReadOutlined,
+ pharmacy: ShopOutlined,
+ hospital: MedicineBoxOutlined,
+ night: ClockCircleOutlined,
+ specialty: AppstoreOutlined,
+};
+
+export default function LocationsSection() {
+ const { t } = useLanguage();
+
+ return (
+
+
+
+
{t.locations.title}
+
+
+
+ {t.locations.items.map((loc) => {
+ const Icon = ICONS[loc.icon];
+ return (
+
+
+ {Icon && }
+
+
{loc.title}
+
{loc.description}
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/components/MapSection.jsx b/components/MapSection.jsx
new file mode 100644
index 0000000..4c886cd
--- /dev/null
+++ b/components/MapSection.jsx
@@ -0,0 +1,113 @@
+'use client';
+
+import { useState } from 'react';
+import { EnvironmentOutlined } from '@ant-design/icons';
+import { useLanguage } from '@/app/context/LanguageContext';
+import { useTheme } from '@/app/context/ThemeContext';
+import { mapLocations } from '@/data/content';
+
+// ---------------------------------------------------------------------------
+// Neshan (نشان) map integration.
+//
+// TODO: put your Neshan API key here, or (preferred) set it as an environment
+// variable in `.env.local`:
+// NEXT_PUBLIC_NESHAN_API_KEY=your-key-here
+// Get a key from https://platform.neshan.org
+// ---------------------------------------------------------------------------
+const NESHAN_API_KEY = process.env.NEXT_PUBLIC_NESHAN_API_KEY || '';
+
+// Neshan's Static Map API — a plain image, no JS SDK/script tag needed.
+// Verify query param names/values against the current Neshan docs
+// (https://platform.neshan.org/api/static-map/) if this ever needs updating —
+// `type` accepts their day/night map styles, e.g. "standard-day" / "standard-night".
+function staticMapUrl({ lat, lng }, mapStyle) {
+ const params = new URLSearchParams({
+ key: NESHAN_API_KEY,
+ type: mapStyle,
+ center: `${lat},${lng}`,
+ zoom: '15',
+ size: '600x400',
+ marker: `${lat},${lng}`,
+ });
+ return `https://api.neshan.org/v5/static?${params.toString()}`;
+}
+
+function neshanViewUrl({ lat, lng }) {
+ return `https://neshan.org/maps/@${lat},${lng},15z`;
+}
+
+function MapThumb({ coords, mapStyle, alt }) {
+ const [errored, setErrored] = useState(false);
+ const { t } = useLanguage();
+
+ if (!NESHAN_API_KEY || errored) {
+ return (
+
+
+
{t.map.configNotice}
+
+ );
+ }
+
+ return (
+ // eslint-disable-next-line @next/next/no-img-element
+ setErrored(true)}
+ className="w-full h-full object-cover"
+ />
+ );
+}
+
+export default function MapSection() {
+ const { t } = useLanguage();
+ const { theme } = useTheme();
+ const mapStyle = theme === 'dark' ? 'standard-night' : 'standard-day';
+
+ const pins = [
+ { key: 'factory', label: t.map.factoryLabel, coords: mapLocations.factory, address: t.contactInfo.factoryAddress },
+ { key: 'sales', label: t.map.salesLabel, coords: mapLocations.salesOffice, address: t.contactInfo.salesOfficeAddress },
+ ];
+
+ return (
+
+
+
+ {t.map.eyebrow}
+
{t.map.title}
+
+
+
+
+
+
+
+
+ {pins.map((pin) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/components/ThemeToggle.jsx b/components/ThemeToggle.jsx
new file mode 100644
index 0000000..9d3e78c
--- /dev/null
+++ b/components/ThemeToggle.jsx
@@ -0,0 +1,21 @@
+'use client';
+
+import { Button, Tooltip } from 'antd';
+import { SunOutlined, MoonOutlined } from '@ant-design/icons';
+import { useTheme } from '@/app/context/ThemeContext';
+
+export default function ThemeToggle() {
+ const { theme, toggleTheme } = useTheme();
+
+ return (
+
+ : }
+ />
+
+ );
+}
diff --git a/components/TicketForm.jsx b/components/TicketForm.jsx
new file mode 100644
index 0000000..e0d74a9
--- /dev/null
+++ b/components/TicketForm.jsx
@@ -0,0 +1,114 @@
+'use client';
+
+import { Form, Input, Select, Button, message } from 'antd';
+import { useLanguage } from '@/app/context/LanguageContext';
+
+const { TextArea } = Input;
+
+export default function TicketForm() {
+ const { t, locale } = useLanguage();
+ const [form] = Form.useForm();
+
+ const handleFinish = async (values) => {
+ // -------------------------------------------------------------------
+ // TODO: Connect to the real ticketing/support API.
+ //
+ // import axios from 'axios';
+ //
+ // try {
+ // await axios.post('/api/tickets', values);
+ // } catch (err) {
+ // message.error('...');
+ // return;
+ // }
+ // -------------------------------------------------------------------
+ console.log('Ticket submitted (placeholder):', values);
+ message.success(t.ticket.successMsg);
+ form.resetFields();
+ };
+
+ return (
+
+
+
+
{t.ticket.eyebrow}
+
+ {t.ticket.title}
+
+
{t.ticket.subtitle}
+
+
+
{t.ticket.subject}}
+ name="subject"
+ rules={[{ required: true, message: t.ticket.requiredMsg }]}
+ >
+
+
+
+
{t.ticket.message}}
+ name="message"
+ rules={[{ required: true, message: t.ticket.requiredMsg }]}
+ >
+
+
+
+
+
+ {t.ticket.submit}
+
+
+
+
+
+ );
+}
diff --git a/components/VendingCard.jsx b/components/VendingCard.jsx
new file mode 100644
index 0000000..f1b3223
--- /dev/null
+++ b/components/VendingCard.jsx
@@ -0,0 +1,120 @@
+'use client';
+
+import { useRef, useState } from 'react';
+import { Button, Tooltip, message } from 'antd';
+import { ShoppingCartOutlined, EyeOutlined, HeartOutlined, HeartFilled } from '@ant-design/icons';
+import { useDispatch } from 'react-redux';
+import { addToCart } from '@/store/cartSlice';
+import { useLanguage } from '@/app/context/LanguageContext';
+import FloatingItems from './FloatingItems';
+import { floatingSnacks } from '@/data/content';
+
+export default function VendingCard({ machine, onQuickView }) {
+ const dispatch = useDispatch();
+ const { t, dir } = useLanguage();
+ const [wishlisted, setWishlisted] = useState(false);
+ const stageRef = useRef(null);
+ const lensRef = useRef(null);
+ const revealRef = useRef(null);
+
+ const handleMouseMove = (e) => {
+ const stage = stageRef.current;
+ if (!stage) return;
+ const rect = stage.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ if (lensRef.current) {
+ lensRef.current.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%)`;
+ }
+ if (revealRef.current) {
+ revealRef.current.style.clipPath = `circle(95px at ${x}px ${y}px)`;
+ }
+ };
+
+ const handleMouseLeave = () => {
+ if (revealRef.current) revealRef.current.style.clipPath = 'circle(0px at 50% 50%)';
+ };
+
+ const handleAddToCart = () => {
+ dispatch(
+ addToCart({
+ id: machine.id,
+ name: machine.name,
+ priceNumeric: machine.priceNumeric,
+ priceLabel: machine.priceLabel,
+ image: machine.image,
+ })
+ );
+ message.success(t.cart.addToast(machine.name));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {t.machineActions.hoverHint}
+
+
+
+
+
{machine.shortLabel}
+
{machine.name}
+
+
+
+
{machine.description}
+
+
+
+ {machine.highlights.map((h) => (
+
+ {h}
+
+ ))}
+
+
+
{machine.priceLabel}
+
+
+ } onClick={handleAddToCart}>
+ {t.machineActions.addToCart}
+
+ } onClick={() => onQuickView?.(machine)}>
+ {t.machineActions.quickView}
+
+
+ : }
+ onClick={() => setWishlisted((v) => !v)}
+ />
+
+
+
+
+
+ );
+}
diff --git a/data/content.js b/data/content.js
new file mode 100644
index 0000000..c81e8fb
--- /dev/null
+++ b/data/content.js
@@ -0,0 +1,447 @@
+// ---------------------------------------------------------------------------
+// Single bilingual content source (fa/en). Every UI string and every piece of
+// business copy lives here, per locale. Components read via useLanguage().
+// Asset paths (images/gifs/video) are locale-independent and stay the same.
+// ---------------------------------------------------------------------------
+
+const machinesFa = [
+ {
+ id: 'rm-35',
+ name: 'روبات مارکت ۳۵ کانال',
+ shortLabel: '۳۵ کانال',
+ priceLabel: '۳,۸۶۰,۰۰۰,۰۰۰ ریال',
+ priceNumeric: 3860000000,
+ description: 'یخچالدار، ۳۵ کانال اختصاصی، سیستم خنککننده قدرتمند، تحویل نرم، مدیریت آنلاین ۲۴ ساعته.',
+ highlights: ['یخچالدار', '۳۵ کانال اختصاصی', 'تحویل نرم', 'مدیریت آنلاین ۲۴ ساعته'],
+ image: '/images/machine-35-base.svg',
+ imageAlt: '/images/machine-35-white.svg',
+ gif: '/gifs/machine-35.gif',
+ },
+ {
+ id: 'rm-48',
+ name: 'روبات مارکت ۴۸ کانال',
+ shortLabel: '۴۸ کانال',
+ priceLabel: '۴,۴۸۰,۰۰۰,۰۰۰ ریال',
+ priceNumeric: 4480000000,
+ description: 'یخچالدار، ۴۸ کانال اختصاصی، قابلیتهای پیشرفته مدیریت آنلاین و تحویل کالا.',
+ highlights: ['یخچالدار', '۴۸ کانال اختصاصی', 'مدیریت آنلاین پیشرفته', 'تحویل هوشمند کالا'],
+ image: '/images/machine-48-base.svg',
+ imageAlt: '/images/machine-48-white.svg',
+ gif: '/gifs/machine-48.gif',
+ },
+ {
+ id: 'rm-60',
+ name: 'روبات مارکت ۶۰ کانال',
+ shortLabel: '۶۰ کانال',
+ priceLabel: '۴,۹۸۰,۰۰۰,۰۰۰ ریال',
+ priceNumeric: 4980000000,
+ description: 'یخچالدار، ۶۰ کانال اختصاصی برای کسبوکارهای پرتردد.',
+ highlights: ['یخچالدار', '۶۰ کانال اختصاصی', 'مناسب اماکن پرتردد', 'ظرفیت بالا'],
+ image: '/images/machine-60-base.svg',
+ imageAlt: '/images/machine-60-white.svg',
+ gif: '/gifs/machine-60.gif',
+ },
+];
+
+const machinesEn = [
+ {
+ id: 'rm-35',
+ name: 'Robot Market RM-35',
+ shortLabel: '35 Channels',
+ priceLabel: '3,860,000,000 Rials',
+ priceNumeric: 3860000000,
+ description: 'Refrigerated, 35 dedicated channels, powerful cooling system, soft delivery, 24/7 online management.',
+ highlights: ['Refrigerated', '35 dedicated channels', 'Soft delivery', '24/7 online management'],
+ image: '/images/machine-35-base.svg',
+ imageAlt: '/images/machine-35-white.svg',
+ gif: '/gifs/machine-35.gif',
+ },
+ {
+ id: 'rm-48',
+ name: 'Robot Market RM-48',
+ shortLabel: '48 Channels',
+ priceLabel: '4,480,000,000 Rials',
+ priceNumeric: 4480000000,
+ description: 'Refrigerated, 48 dedicated channels, advanced online management and delivery capabilities.',
+ highlights: ['Refrigerated', '48 dedicated channels', 'Advanced online management', 'Smart delivery'],
+ image: '/images/machine-48-base.svg',
+ imageAlt: '/images/machine-48-white.svg',
+ gif: '/gifs/machine-48.gif',
+ },
+ {
+ id: 'rm-60',
+ name: 'Robot Market RM-60',
+ shortLabel: '60 Channels',
+ priceLabel: '4,980,000,000 Rials',
+ priceNumeric: 4980000000,
+ description: 'Refrigerated, 60 dedicated channels for high-traffic businesses.',
+ highlights: ['Refrigerated', '60 dedicated channels', 'Ideal for high-traffic venues', 'High capacity'],
+ image: '/images/machine-60-base.svg',
+ imageAlt: '/images/machine-60-white.svg',
+ gif: '/gifs/machine-60.gif',
+ },
+];
+
+export const content = {
+ fa: {
+ dir: 'rtl',
+ lang: 'fa',
+ meta: {
+ title: 'روبات مارکت | دستگاههای وندینگ هوشمند',
+ description:
+ 'روبات مارکت؛ دستگاههای فروش خودکار هوشمند با مدیریت آنلاین، تحویل نرم و طراحی مدرن.',
+ },
+ nav: {
+ home: 'خانه',
+ showcase: 'دستگاهها',
+ about: 'درباره ما',
+ locations: 'مکانهای نصب',
+ catalog: 'کاتالوگ محصولات',
+ video: 'ویدیوی معرفی',
+ map: 'نقشه',
+ support: 'پشتیبانی',
+ contact: 'تماس با ما',
+ login: 'ورود به پنل کاربری',
+ },
+ hero: {
+ loading: 'در حال بارگذاری روبات مارکت…',
+ typingPhrases: [
+ 'به دنیای خودکار و هوشمند خوش آمدید!',
+ 'تجربهای نوین از خرید آسان و سریع.',
+ 'روبات مارکت؛ فروشنده هوشمند شما، ۲۴ ساعته.',
+ ],
+ sub: 'تجربهای نوین از خرید آسان و سریع با دستگاههای وندینگ روبات مارکت.',
+ cta: 'مشاهده دستگاهها',
+ },
+ cart: {
+ title: 'سبد خرید',
+ empty: 'سبد خرید شما خالی است',
+ total: 'جمع کل',
+ checkout: 'پرداخت / تسویه حساب',
+ checkoutToast: 'درگاه پرداخت بهزودی متصل میشود.',
+ addToast: (name) => `${name} به سبد خرید اضافه شد`,
+ },
+ machineActions: {
+ addToCart: 'افزودن به سبد خرید',
+ quickView: 'مشاهده سریع',
+ wishlistAdd: 'افزودن به علاقهمندیها',
+ wishlistRemove: 'حذف از علاقهمندیها',
+ hoverHint: 'نشانگر را روی دستگاه حرکت دهید',
+ altVariant: 'رنگ جایگزین',
+ gifAlt: (name) => `نمایش عملکرد ${name}`,
+ },
+ machines: machinesFa,
+ features: [
+ { id: 'warranty', icon: 'safety', title: 'ضمانت کالا', description: 'کلیه محصولات شرکت دارای یک سال خدمات پس از فروش میباشند.' },
+ { id: 'consulting', icon: 'consulting', title: 'مشاوره تخصصی و رایگان', description: 'خدمات مشاوره رایگان در مورد نوع دستگاه و چیدمان محصولات.' },
+ { id: 'installment', icon: 'installment', title: 'امکان خرید اقساطی', description: 'امکان خرید اقساط با شرایط آسان و مناسب جهت کلیه مشاغل.' },
+ { id: 'packaging', icon: 'packaging', title: 'بستهبندی ایمن', description: 'با درخواست شما، محصولات با بستهبندی باکس پالت با هزینهای اندک ارسال میشود.' },
+ ],
+ about: {
+ eyebrow: 'معرفی شرکت',
+ legalName: 'شرکت نگین سازان پیشگام رادمهر آتوسا',
+ more: 'ادامه مطلب',
+ less: 'نمایش کمتر',
+ paragraphs: [
+ 'شرکت نگین سازان پیشگام رادمهر آتوسا با شماره ثبت ۸۴۰۹۴ با نامهای «ایده آوران پیشگام آتوسا» به شماره ثبت ۶۳۰۹۷ و «پیشگام گستر نگین آتوسا» به شماره ثبت ۷۳۴۱۶، از سال ۱۳۹۶ فعالیت خود را با تولید دستگاههای دقیق فرآوری زعفران آغاز نمود و با تشکیل شبکه گسترده توزیع و پخش زعفران در سراسر کشور، در سال ۱۴۰۱ با تولید دستگاههای وندینگ ماشین با نام تجاری «روبات مارکت» ویژه فروش زعفران وارد این حوزه شد.',
+ 'سال ۱۴۰۲ برای حفظ کیفیت زعفران، قابلیت سرمایش به این دستگاهها افزوده شد و روبات مارکت قابلیت عرضه و فروش تنقلات و نوشیدنی سرد را نیز به دست آورد؛ تا اینکه در بهار سال ۱۴۰۳ با معرفی گسترده دستگاههای «روبات مارکت»، این رباتها بهعنوان فروشنده رسمی تنقلات و نوشیدنیهای سرد در ایران معرفی شدند.',
+ 'روبات مارکت تنها یک نام نیست، بلکه نماد تفکری نوین و پیشرو در صنعت فروش خودکار است. آنچه در روبات مارکت اتفاق میافتد با تکیه بر دانش فنی مهندسین و بهرهگیری از تکنولوژی روز دنیا، تجلی پیدا کرده است. هر دستگاه روبات مارکت یک دستیار هوشمند و خستگیناپذیر است که با دقت و سرعت بالا، پلی میان نیاز مصرفکنندگان و اهداف تجاری شما میسازد.',
+ 'تعهد ما تولید دستگاههایی با کیفیت بالا، طراحی مدرن و عملکردی بینقص است تا تجربهای متفاوت از خرید و درآمدزایی رقم بخورد. روبات مارکت نماد اعتماد، نوآوری و تلاش برای پیشرفت صنعت فروش خودکار است.',
+ 'در کالبد روبات مارکت، صدها ساعت کار مهندسی و تستهای عملیاتی نهفته است. ما بر این باوریم که دوام و طول عمر دستگاهها مهمترین شاخص برای سرمایهگذاری مشتریان ماست. به همین دلیل، در تمامی مراحل تولید، از انتخاب بهترین متریال و قطعات الکترونیک گرفته تا مونتاژ نهایی، استانداردهای سختگیرانهای را رعایت میکنیم تا دستگاهها در شرایط مختلف، عملکردی پایدار و بدون نقص داشته باشند.',
+ 'دستگاههای هوشمند ما با رابط کاربری ساده و در عین حال پیشرفته، تجربهای لذتبخش را برای کاربران و خریداران فراهم میکنند. سیستمهای پرداخت امن، قابلیت اتصال به اینترنت برای مدیریت آنلاین و نظارت بر قیمت اجناس، از جمله ویژگیهایی است که روبات مارکت را از سایر رقبا متمایز میکند. ما تلاش کردهایم فرآیند خرید را به سادگی چند لمس ساده تبدیل کنیم تا مشتریان شما در هر ساعت از شبانهروز، در کمترین زمان به کالای موردنظر خود دسترسی پیدا کنند. روبات مارکت تنها یک تولیدکننده نیست، بلکه مشاوری قابل اعتماد برای توسعه کسبوکار شماست؛ لذا راهکارهای فروش خودکار متناسب با نیازهای شما طراحی و ارائه میشوند.',
+ 'فرقی نمیکند یک فروشگاه زنجیرهای بزرگ داشته باشید یا مدیر یک مجموعه اداری کوچک باشید؛ دستگاههای ما این امکان را میدهند که بدون محدودیت زمانی و مکانی و بدون نگرانی از مشکلات نیروی انسانی، فروش خود را افزایش دهید.',
+ 'آنچه از ورای تولیدات تخصصی و پیشروی روبات مارکت قابل مشاهده است، تکنولوژیای است که جایگزین نمونههای خارجی شده و نهتنها نیازهای داخلی را برطرف میکند، بلکه در آیندهای نچندان دور سهم قابل رقابتی را در بازارهای خارجی از آن خود خواهد کرد. افتخار ما این است که محصولی ایرانی میسازیم که با استانداردهای جهانی رقابت میکند و گامی کوچک اما استوار در جهت خودکفایی و شکوفایی اقتصاد کشور برمیداریم.',
+ ],
+ },
+ locations: {
+ title: 'مکان مناسب برای نصب روبات مارکت',
+ items: [
+ { id: 'hotels', icon: 'hotel', title: 'هتلها و مراکز اقامتی', description: 'هر جایی که جمعیت زیادی در صف انتظار قرار دارند، بهجز درآمد، روبات مارکت جزو امکانات رفاهی آن مکان محسوب شده و میزان رضایت مراجعان را نیز افزایش میدهد.' },
+ { id: 'schools', icon: 'school', title: 'مراکز آموزشی و مدارس', description: 'در صورتی که هر یک از دانشآموزان و دانشجویان برای خرید مایحتاج خود به خارج از مجموعه مراجعه کنند و خدایناکرده اتفاقی بیفتد، مسئولیت آن مستقیماً به عهده مدیر آموزشگاه است. در نتیجه وجود یک روبات مارکت در هر آموزشگاه ضروری است.' },
+ { id: 'pharmacies', icon: 'pharmacy', title: 'داروخانهها و سوپرمارکتها', description: 'برای فروش محصولات جنسی و پیشگیری، ویژه مشتریانی که برای خرید آن معذوریت دارند.' },
+ { id: 'hospitals', icon: 'hospital', title: 'بیمارستانها و مراکز درمانی', description: 'برای فروش محصولات جنسی و پیشگیری، ویژه مشتریانی که برای خرید آن معذوریت دارند.' },
+ { id: 'night-shift', icon: 'night', title: 'کشیک شیفت شب', description: 'امکان تعبیه روبات مارکت پشت شیشه سکوریت مغازهها نیز وجود دارد تا در زمان تعطیلی فروشگاه و حتی نیمهشبها، روبات زحمتکش در حال خدمتدهی به مشتریان عزیز شما باشد.' },
+ { id: 'specialty', icon: 'specialty', title: 'محصولات تخصصی', description: 'اگر تولیدکننده هستید یا نماینده تولیدکننده محصولی خاص هستید، یک روبات مارکت میتواند مسئولیت معرفی و عرضه محصولات تخصصی شما را نیز بر عهده بگیرد.' },
+ ],
+ },
+ catalog: {
+ eyebrow: 'کاتالوگ محصولات',
+ title: 'محصولات قابل سفارش برای دستگاه شما',
+ subtitle:
+ 'با خرید دستگاه روبات مارکت، میتوانید این محصولات را با قیمت عمده و کمتر از بازار سفارش دهید؛ تیم ما هم چیدمان اولیه و شارژ دستگاه شما را انجام میدهد.',
+ addToOrder: 'افزودن به سفارش',
+ originalPriceLabel: 'قیمت بازار',
+ machinePriceLabel: 'قیمت ویژه مشتریان روبات مارکت',
+ addToast: (name) => `${name} به لیست سفارش اضافه شد`,
+ items: [
+ { id: 'saffron', name: 'زعفران ممتاز درجهیک', category: 'زعفران', image: '/images/catalog/saffron.svg', originalPriceNumeric: 12000000, priceNumeric: 8900000 },
+ { id: 'chips', name: 'چیپس و پفک (بسته عمده)', category: 'تنقلات', image: '/images/snacks/chips-1.svg', originalPriceNumeric: 450000, priceNumeric: 320000 },
+ { id: 'soda', name: 'نوشابه و نوشیدنی گازدار', category: 'نوشیدنی', image: '/images/snacks/soda-1.svg', originalPriceNumeric: 250000, priceNumeric: 180000 },
+ { id: 'water', name: 'آب معدنی', category: 'نوشیدنی', image: '/images/catalog/water.svg', originalPriceNumeric: 120000, priceNumeric: 80000 },
+ { id: 'coffee', name: 'قهوه فوری', category: 'گرم', image: '/images/catalog/coffee.svg', originalPriceNumeric: 650000, priceNumeric: 470000 },
+ { id: 'tea', name: 'چای کیسهای', category: 'گرم', image: '/images/catalog/tea.svg', originalPriceNumeric: 380000, priceNumeric: 270000 },
+ ],
+ },
+ introVideo: {
+ eyebrow: 'ویدیوی معرفی',
+ title: 'با روبات مارکت آشنا شوید',
+ subtitle: 'نگاهی کوتاه به کارخانه، فرآیند تولید و عملکرد دستگاههای روبات مارکت.',
+ videoNotSupported: 'مرورگر شما از پخش ویدیو پشتیبانی نمیکند.',
+ },
+ map: {
+ eyebrow: 'موقعیت ما روی نقشه',
+ title: 'کارخانه و دفاتر روبات مارکت',
+ factoryLabel: 'کارخانه',
+ salesLabel: 'دفتر فروش مرکزی',
+ openInMaps: 'مشاهده در نشان',
+ configNotice: 'برای فعال شدن نقشه، کلید API نشان را در تنظیمات پروژه قرار دهید.',
+ },
+ ticket: {
+ eyebrow: 'پشتیبانی',
+ title: 'ثبت تیکت پشتیبانی',
+ subtitle: 'سوال یا درخواستی دارید؟ فرم زیر را پر کنید تا تیم پشتیبانی در سریعترین زمان با شما تماس بگیرد.',
+ name: 'نام و نام خانوادگی',
+ namePlaceholder: 'مثال: علی رضایی',
+ phone: 'شماره تماس',
+ phonePlaceholder: '09xxxxxxxxx',
+ email: 'ایمیل (اختیاری)',
+ emailPlaceholder: 'example@email.com',
+ subject: 'موضوع',
+ subjectPlaceholder: 'موضوع درخواست خود را بنویسید',
+ priority: 'اولویت',
+ priorities: [
+ { value: 'low', label: 'کم' },
+ { value: 'medium', label: 'متوسط' },
+ { value: 'high', label: 'فوری' },
+ ],
+ message: 'پیام شما',
+ messagePlaceholder: 'توضیحات کامل درخواست یا مشکل خود را بنویسید…',
+ submit: 'ارسال تیکت',
+ requiredMsg: 'این فیلد الزامی است',
+ invalidPhone: 'شماره تماس معتبر نیست',
+ invalidEmail: 'ایمیل معتبر نیست',
+ successMsg: 'تیکت شما با موفقیت ثبت شد. بهزودی با شما تماس میگیریم.',
+ },
+ chat: {
+ fabLabel: 'گفتگوی آنلاین',
+ title: 'پشتیبانی آنلاین روبات مارکت',
+ placeholder: 'پیام خود را بنویسید…',
+ send: 'ارسال',
+ initialBotMessage: 'سلام! 👋 چطور میتونم امروز کمکتون کنم؟',
+ fallbackReply: 'ممنون از پیامتون؛ همکاران ما بهزودی پاسخ میدهند.',
+ },
+ footer: {
+ aboutLabel: 'روبات مارکت',
+ aboutText: 'ارائهدهنده دستگاههای وندینگ هوشمند برای کسبوکارهای مدرن — محصولی از شرکت نگین سازان پیشگام رادمهر آتوسا.',
+ quickLinks: 'دسترسی سریع',
+ contactTitle: 'تماس با ما',
+ factoryLabel: 'کارخانه',
+ salesLabel: 'دفتر فروش مرکزی',
+ rights: 'تمامی حقوق محفوظ است.',
+ },
+ contactInfo: {
+ orderPhone: '۰۹۱۵۱۶۰۱۸۴۷',
+ orderPhoneHref: '09151601847',
+ factoryAddress: 'مشهد، کیلومتر ۸ جاده مشهد - چناران، دور برگردان اصلی شهرک صنعتی توس به سمت مشهد، بعد از نیروگاه، اولین فرعی سمت راست (خیابان کاله)، اولین کوچه سمت راست، پلاک ۱۱۰، شرکت نگین سازان پیشگام رادمهر آتوسا',
+ salesOfficeAddress: 'مشهد، نبش پیامبر اعظم ۷، جنب بیمارستان رضوی، برج نخل، واحد ۴۰۱',
+ socialLinks: [
+ { id: 'instagram', label: 'صفحه اینستاگرام', href: 'http://instagram.com/vending_machin_life' },
+ { id: 'bale-join', label: 'کانال پیامرسان بله', href: 'https://ble.ir/join/AiTbV6UpBA' },
+ { id: 'bale-site', label: 'وبسایت', href: 'https://my-rm.com/' },
+ ],
+ },
+ },
+
+ en: {
+ dir: 'ltr',
+ lang: 'en',
+ meta: {
+ title: 'Robot Market | Smart Vending Machines',
+ description:
+ 'Robot Market — smart vending machines with online management, soft delivery and modern design.',
+ },
+ nav: {
+ home: 'Home',
+ showcase: 'Machines',
+ about: 'About Us',
+ locations: 'Where to Install',
+ catalog: 'Product Catalog',
+ video: 'Intro Video',
+ map: 'Map',
+ support: 'Support',
+ contact: 'Contact',
+ login: 'Sign In',
+ },
+ 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',
+ },
+ cart: {
+ title: 'Cart',
+ empty: 'Your cart is empty',
+ total: 'Total',
+ checkout: 'Checkout',
+ checkoutToast: 'Payment gateway will be connected soon.',
+ addToast: (name) => `${name} added to cart`,
+ },
+ machineActions: {
+ addToCart: 'Add to Cart',
+ quickView: 'Quick View',
+ wishlistAdd: 'Add to Wishlist',
+ wishlistRemove: 'Remove from Wishlist',
+ hoverHint: 'Move your cursor over the machine',
+ altVariant: 'Alternate color',
+ gifAlt: (name) => `${name} in action`,
+ },
+ machines: machinesEn,
+ 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.' },
+ { id: 'installment', icon: 'installment', title: 'Installment Purchase', description: 'Flexible, easy installment plans suitable for any business.' },
+ { id: 'packaging', icon: 'packaging', title: 'Secure Packaging', description: 'On request, products ship in pallet-box packaging for a small fee.' },
+ ],
+ about: {
+ eyebrow: 'Company Profile',
+ legalName: 'Negin Sazan Pishgam Radmehr Atousa Co.',
+ more: 'Read more',
+ less: 'Show less',
+ paragraphs: [
+ 'Negin Sazan Pishgam Radmehr Atousa Co. (registration No. 84094), also known as "Ideh Avaran Pishgam Atousa" (No. 63097) and "Pishgam Gostar Negin Atousa" (No. 73416), began operations in 2017 producing precision saffron-processing machines. After building a nationwide saffron distribution network, in 2022 the company entered the vending-machine industry with saffron-dedicated machines under the "Robot Market" brand.',
+ 'In 2023, cooling capability was added to preserve saffron quality, and Robot Market gained the ability to sell snacks and cold drinks as well. By spring 2024, with the wide rollout of "Robot Market" machines, these robots were introduced as an official retailer of snacks and cold drinks in Iran.',
+ 'Robot Market is not just a name — it is a symbol of a modern, forward-looking way of thinking in the vending industry. What happens inside Robot Market is the result of our engineers\' technical expertise combined with today\'s technology. Every Robot Market machine is a tireless, intelligent assistant that bridges consumer needs and your business goals with precision and speed.',
+ 'Our commitment is to build high-quality machines with modern design and flawless performance, delivering a different experience of shopping and revenue generation. Robot Market stands for trust, innovation, and continuous progress in the vending industry.',
+ 'Hundreds of hours of engineering work and operational testing go into every Robot Market unit. We believe durability and machine lifespan are the most important measures of our customers\' investment. That is why, from selecting the best materials and electronic components to final assembly, we follow strict standards so our machines perform reliably in any condition.',
+ 'Our smart machines offer a simple yet advanced user interface for an enjoyable experience. Secure payment systems and internet connectivity for online management and price monitoring set Robot Market apart from competitors. We have made the purchase process as simple as a few taps, so your customers can access what they need, any time of day, in the shortest time possible. Robot Market is not just a manufacturer — it is a trusted advisor for growing your business, designing automated sales solutions tailored to your needs.',
+ 'Whether you run a large retail chain or manage a small office, our machines let you increase sales without time or location limits, and without worrying about staffing issues.',
+ 'Behind Robot Market\'s specialized, forward-looking products lies technology that replaces foreign alternatives — meeting domestic needs today, and poised to compete meaningfully in export markets in the near future. We take pride in building an Iranian product that competes with global standards — a small but firm step toward self-sufficiency and economic growth.',
+ ],
+ },
+ locations: {
+ title: 'Ideal Locations for a Robot Market Machine',
+ items: [
+ { id: 'hotels', icon: 'hotel', title: 'Hotels & Accommodations', description: 'Anywhere large crowds wait in line — beyond the revenue, Robot Market becomes an amenity that increases guest satisfaction.' },
+ { id: 'schools', icon: 'school', title: 'Schools & Educational Centers', description: 'If a student has to leave campus to buy something and, God forbid, an incident occurs, responsibility falls directly on the school administrator. A Robot Market on-site is essential.' },
+ { id: 'pharmacies', icon: 'pharmacy', title: 'Pharmacies & Supermarkets', description: 'For selling personal/health products discreetly, for customers who prefer not to purchase them in person.' },
+ { id: 'hospitals', icon: 'hospital', title: 'Hospitals & Medical Centers', description: 'For selling personal/health products discreetly, for customers who prefer not to purchase them in person.' },
+ { id: 'night-shift', icon: 'night', title: 'Night-Shift Coverage', description: 'Robot Market can be installed behind a shop\'s security glass, serving customers even after closing time and in the middle of the night.' },
+ { id: 'specialty', icon: 'specialty', title: 'Specialty Products', description: 'If you manufacture or represent a specific product line, a Robot Market machine can take on introducing and offering your specialty products too.' },
+ ],
+ },
+ catalog: {
+ eyebrow: 'Product Catalog',
+ title: 'Products You Can Order for Your Machine',
+ subtitle:
+ 'When you buy a Robot Market machine, you can order these products at a wholesale price below market rate — our team also handles the initial stocking and layout for you.',
+ addToOrder: 'Add to Order',
+ originalPriceLabel: 'Market price',
+ machinePriceLabel: 'Robot Market customer price',
+ addToast: (name) => `${name} added to your order`,
+ items: [
+ { id: 'saffron', name: 'Premium Grade-1 Saffron', category: 'Saffron', image: '/images/catalog/saffron.svg', originalPriceNumeric: 12000000, priceNumeric: 8900000 },
+ { id: 'chips', name: 'Chips & Crisps (bulk pack)', category: 'Snacks', image: '/images/snacks/chips-1.svg', originalPriceNumeric: 450000, priceNumeric: 320000 },
+ { id: 'soda', name: 'Soda & Carbonated Drinks', category: 'Drinks', image: '/images/snacks/soda-1.svg', originalPriceNumeric: 250000, priceNumeric: 180000 },
+ { id: 'water', name: 'Mineral Water', category: 'Drinks', image: '/images/catalog/water.svg', originalPriceNumeric: 120000, priceNumeric: 80000 },
+ { id: 'coffee', name: 'Instant Coffee', category: 'Hot Drinks', image: '/images/catalog/coffee.svg', originalPriceNumeric: 650000, priceNumeric: 470000 },
+ { id: 'tea', name: 'Tea Bags', category: 'Hot Drinks', image: '/images/catalog/tea.svg', originalPriceNumeric: 380000, priceNumeric: 270000 },
+ ],
+ },
+ introVideo: {
+ eyebrow: 'Intro Video',
+ title: 'Get to Know Robot Market',
+ subtitle: 'A short look at the factory, our production process, and the machines in action.',
+ videoNotSupported: 'Your browser does not support video playback.',
+ },
+ map: {
+ eyebrow: 'Find Us on the Map',
+ title: 'Robot Market Factory & Offices',
+ factoryLabel: 'Factory',
+ salesLabel: 'Central Sales Office',
+ openInMaps: 'Open in Neshan',
+ configNotice: 'Add your Neshan API key in the project settings to activate the map.',
+ },
+ ticket: {
+ eyebrow: 'Support',
+ title: 'Submit a Support Ticket',
+ subtitle: 'Have a question or request? Fill out the form below and our support team will get back to you as soon as possible.',
+ name: 'Full Name',
+ namePlaceholder: 'e.g. John Smith',
+ phone: 'Phone Number',
+ phonePlaceholder: '09xxxxxxxxx',
+ email: 'Email (optional)',
+ emailPlaceholder: 'example@email.com',
+ subject: 'Subject',
+ subjectPlaceholder: 'Write the subject of your request',
+ priority: 'Priority',
+ priorities: [
+ { value: 'low', label: 'Low' },
+ { value: 'medium', label: 'Medium' },
+ { value: 'high', label: 'Urgent' },
+ ],
+ message: 'Your Message',
+ messagePlaceholder: 'Describe your request or issue in full…',
+ submit: 'Submit Ticket',
+ requiredMsg: 'This field is required',
+ invalidPhone: 'Invalid phone number',
+ invalidEmail: 'Invalid email address',
+ successMsg: 'Your ticket was submitted successfully. We will contact you soon.',
+ },
+ chat: {
+ fabLabel: 'Live Chat',
+ title: 'Robot Market Live Support',
+ placeholder: 'Type your message…',
+ send: 'Send',
+ initialBotMessage: "Hi! 👋 How can we help you today?",
+ fallbackReply: "Thanks for your message — our team will reply shortly.",
+ },
+ footer: {
+ aboutLabel: 'Robot Market',
+ aboutText: 'Smart vending machines for modern businesses — a product of Negin Sazan Pishgam Radmehr Atousa Co.',
+ quickLinks: 'Quick Links',
+ contactTitle: 'Contact Us',
+ factoryLabel: 'Factory',
+ salesLabel: 'Central Sales Office',
+ rights: 'All rights reserved.',
+ },
+ contactInfo: {
+ orderPhone: '09151601847',
+ orderPhoneHref: '09151601847',
+ factoryAddress: 'Mashhad, Mashhad–Chenaran Rd. km 8, main roundabout of Toos Industrial Town toward Mashhad, after the power plant, 1st street on the right (Kaleh St.), 1st alley on the right, No. 110 — Negin Sazan Pishgam Radmehr Atousa Co.',
+ salesOfficeAddress: 'Mashhad, corner of Payambar-e Azam 7, next to Razavi Hospital, Nakhl Tower, Unit 401',
+ socialLinks: [
+ { id: 'instagram', label: 'Instagram', href: 'http://instagram.com/vending_machin_life' },
+ { id: 'bale-join', label: 'Bale Channel', href: 'https://ble.ir/join/AiTbV6UpBA' },
+ { id: 'bale-site', label: 'Website', href: 'https://my-rm.com/' },
+ ],
+ },
+ },
+};
+
+// Locale-independent asset config (positions/paths only — same in both languages).
+export const floatingSnacks = [
+ { id: 'chips-1', image: '/images/snacks/chips-1.svg', size: 76, top: '18%', left: '8%', duration: 5.5, delay: 0 },
+ { id: 'soda-1', image: '/images/snacks/soda-1.svg', size: 64, top: '68%', left: '12%', duration: 6.2, delay: 0.6 },
+ { id: 'chips-2', image: '/images/snacks/chips-2.svg', size: 58, top: '22%', left: '85%', duration: 4.8, delay: 0.3 },
+ { id: 'soda-2', image: '/images/snacks/soda-2.svg', size: 70, top: '72%', left: '88%', duration: 5.9, delay: 0.9 },
+ { id: 'chips-3', image: '/images/snacks/chips-1.svg', size: 48, top: '42%', left: '90%', duration: 6.6, delay: 1.2 },
+ { id: 'soda-3', image: '/images/snacks/soda-1.svg', size: 52, top: '46%', left: '4%', duration: 5.1, delay: 1.5 },
+];
+
+export const showcaseVideoSrc = '/video/showcase-placeholder.mp4';
+
+// Dedicated company introduction video (different from the horizontal-showcase
+// scroll-scrubbed background video above). Drop the real file at this path.
+export const introVideoSrc = '/video/company-intro-placeholder.mp4';
+export const introVideoPoster = '/images/video-poster.svg';
+
+// Placeholder coordinates for Toos Industrial Town, Mashhad (approximate).
+// Replace with the exact factory pin — see README.
+export const mapLocations = {
+ factory: { lat: 36.42, lng: 59.445 },
+ salesOffice: { lat: 36.3110, lng: 59.5683 }, // approximate central Mashhad placeholder
+};
diff --git a/jsconfig.json b/jsconfig.json
index 9c33383..fe655bc 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -3,6 +3,7 @@
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
- }
+ },
+ "jsx": "react"
}
}
diff --git a/next.config.js b/next.config.js
new file mode 100644
index 0000000..ebf72fe
--- /dev/null
+++ b/next.config.js
@@ -0,0 +1,8 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ reactStrictMode: true,
+ // AntD ships CJS + uses some patterns that benefit from transpilation in the App Router.
+ transpilePackages: ['antd', '@ant-design/icons', 'rc-util', 'rc-pagination', 'rc-picker'],
+};
+
+module.exports = nextConfig;
diff --git a/public/gifs/machine-35.gif b/public/gifs/machine-35.gif
new file mode 100644
index 0000000..92467e6
Binary files /dev/null and b/public/gifs/machine-35.gif differ
diff --git a/public/gifs/machine-48.gif b/public/gifs/machine-48.gif
new file mode 100644
index 0000000..a3de12f
Binary files /dev/null and b/public/gifs/machine-48.gif differ
diff --git a/public/gifs/machine-60.gif b/public/gifs/machine-60.gif
new file mode 100644
index 0000000..49a43f1
Binary files /dev/null and b/public/gifs/machine-60.gif differ
diff --git a/public/images/catalog/coffee.svg b/public/images/catalog/coffee.svg
new file mode 100644
index 0000000..7b90c2b
--- /dev/null
+++ b/public/images/catalog/coffee.svg
@@ -0,0 +1,6 @@
+
+
+
+
+ قهوه
+
diff --git a/public/images/catalog/saffron.svg b/public/images/catalog/saffron.svg
new file mode 100644
index 0000000..bfb8e56
--- /dev/null
+++ b/public/images/catalog/saffron.svg
@@ -0,0 +1,6 @@
+
+
+
+
+ زعفران
+
diff --git a/public/images/catalog/tea.svg b/public/images/catalog/tea.svg
new file mode 100644
index 0000000..2eaee13
--- /dev/null
+++ b/public/images/catalog/tea.svg
@@ -0,0 +1,6 @@
+
+
+
+
+ چای
+
diff --git a/public/images/catalog/water.svg b/public/images/catalog/water.svg
new file mode 100644
index 0000000..694306f
--- /dev/null
+++ b/public/images/catalog/water.svg
@@ -0,0 +1,5 @@
+
+
+
+ آب
+
diff --git a/public/images/logo.svg b/public/images/logo.svg
new file mode 100644
index 0000000..d6c9ae4
--- /dev/null
+++ b/public/images/logo.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+ روبات مارکت
+ Robot Market
+
diff --git a/public/images/machine-35-base.svg b/public/images/machine-35-base.svg
new file mode 100644
index 0000000..0d3b47e
--- /dev/null
+++ b/public/images/machine-35-base.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ RM-35
+
diff --git a/public/images/machine-35-white.svg b/public/images/machine-35-white.svg
new file mode 100644
index 0000000..818fff9
--- /dev/null
+++ b/public/images/machine-35-white.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ RM-35 · White
+
diff --git a/public/images/machine-48-base.svg b/public/images/machine-48-base.svg
new file mode 100644
index 0000000..19c8240
--- /dev/null
+++ b/public/images/machine-48-base.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ RM-48
+
diff --git a/public/images/machine-48-white.svg b/public/images/machine-48-white.svg
new file mode 100644
index 0000000..a937942
--- /dev/null
+++ b/public/images/machine-48-white.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ RM-48 · White
+
diff --git a/public/images/machine-60-base.svg b/public/images/machine-60-base.svg
new file mode 100644
index 0000000..336e102
--- /dev/null
+++ b/public/images/machine-60-base.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ RM-60
+
diff --git a/public/images/machine-60-white.svg b/public/images/machine-60-white.svg
new file mode 100644
index 0000000..7269c06
--- /dev/null
+++ b/public/images/machine-60-white.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ RM-60 · White
+
diff --git a/public/images/snacks/chips-1.svg b/public/images/snacks/chips-1.svg
new file mode 100644
index 0000000..30ea8e3
--- /dev/null
+++ b/public/images/snacks/chips-1.svg
@@ -0,0 +1,6 @@
+
+
+
+
+ چیپس
+
diff --git a/public/images/snacks/chips-2.svg b/public/images/snacks/chips-2.svg
new file mode 100644
index 0000000..d267d86
--- /dev/null
+++ b/public/images/snacks/chips-2.svg
@@ -0,0 +1,6 @@
+
+
+
+
+ چیپس
+
diff --git a/public/images/snacks/soda-1.svg b/public/images/snacks/soda-1.svg
new file mode 100644
index 0000000..96404cf
--- /dev/null
+++ b/public/images/snacks/soda-1.svg
@@ -0,0 +1,6 @@
+
+
+
+ نوشابه
+
+
diff --git a/public/images/snacks/soda-2.svg b/public/images/snacks/soda-2.svg
new file mode 100644
index 0000000..522778b
--- /dev/null
+++ b/public/images/snacks/soda-2.svg
@@ -0,0 +1,6 @@
+
+
+
+ نوشابه
+
+
diff --git a/public/images/video-poster.svg b/public/images/video-poster.svg
new file mode 100644
index 0000000..297bccb
--- /dev/null
+++ b/public/images/video-poster.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+ ویدیوی معرفی روبات مارکت — placeholder
+
diff --git a/public/video/company-intro-placeholder.mp4 b/public/video/company-intro-placeholder.mp4
new file mode 100644
index 0000000..52bfa8b
Binary files /dev/null and b/public/video/company-intro-placeholder.mp4 differ
diff --git a/public/video/showcase-placeholder.mp4 b/public/video/showcase-placeholder.mp4
new file mode 100644
index 0000000..f0754bf
Binary files /dev/null and b/public/video/showcase-placeholder.mp4 differ
diff --git a/store/cartSlice.js b/store/cartSlice.js
new file mode 100644
index 0000000..91cec02
--- /dev/null
+++ b/store/cartSlice.js
@@ -0,0 +1,71 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+const initialState = {
+ items: [], // { id, name, priceNumeric, priceLabel, qty, image }
+ isDrawerOpen: false,
+};
+
+const cartSlice = createSlice({
+ name: 'cart',
+ initialState,
+ reducers: {
+ addToCart: (state, action) => {
+ const product = action.payload;
+ const existing = state.items.find((item) => item.id === product.id);
+ if (existing) {
+ existing.qty += 1;
+ } else {
+ state.items.push({ ...product, qty: 1 });
+ }
+ state.isDrawerOpen = true;
+ },
+ removeFromCart: (state, action) => {
+ state.items = state.items.filter((item) => item.id !== action.payload);
+ },
+ incrementQty: (state, action) => {
+ const item = state.items.find((item) => item.id === action.payload);
+ if (item) item.qty += 1;
+ },
+ decrementQty: (state, action) => {
+ const item = state.items.find((item) => item.id === action.payload);
+ if (item && item.qty > 1) {
+ item.qty -= 1;
+ } else if (item) {
+ state.items = state.items.filter((i) => i.id !== action.payload);
+ }
+ },
+ clearCart: (state) => {
+ state.items = [];
+ },
+ openDrawer: (state) => {
+ state.isDrawerOpen = true;
+ },
+ closeDrawer: (state) => {
+ state.isDrawerOpen = false;
+ },
+ toggleDrawer: (state) => {
+ state.isDrawerOpen = !state.isDrawerOpen;
+ },
+ },
+});
+
+export const {
+ addToCart,
+ removeFromCart,
+ incrementQty,
+ decrementQty,
+ clearCart,
+ openDrawer,
+ closeDrawer,
+ toggleDrawer,
+} = cartSlice.actions;
+
+// Selectors
+export const selectCartItems = (state) => state.cart.items;
+export const selectCartCount = (state) =>
+ state.cart.items.reduce((sum, item) => sum + item.qty, 0);
+export const selectCartTotal = (state) =>
+ state.cart.items.reduce((sum, item) => sum + item.qty * item.priceNumeric, 0);
+export const selectIsDrawerOpen = (state) => state.cart.isDrawerOpen;
+
+export default cartSlice.reducer;
diff --git a/store/store.js b/store/store.js
new file mode 100644
index 0000000..e4de347
--- /dev/null
+++ b/store/store.js
@@ -0,0 +1,12 @@
+import { configureStore } from '@reduxjs/toolkit';
+import cartReducer from './cartSlice';
+
+export const makeStore = () =>
+ configureStore({
+ reducer: {
+ cart: cartReducer,
+ },
+ });
+
+// Singleton store for the client (App Router renders providers on the client tree).
+export const store = makeStore();