diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index b3a64c6..2d970a2 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,38 +4,22 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -57,61 +41,61 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{
@@ -123,36 +107,36 @@
- {
+ "keyToString": {
+ "ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true",
+ "ModuleVcsDetector.initialDetectionPerformed": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "RunOnceActivity.typescript.service.memoryLimit.init": "true",
+ "codeWithMe.voiceChat.enabledByDefault": "false",
+ "com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
+ "git-widget-placeholder": "develop",
+ "javascript.nodejs.core.library.configured.version": "24.20.0",
+ "javascript.nodejs.core.library.typings.version": "24.13.3",
+ "javascript.preferred.runtime.type.id": "node",
+ "js.debugger.nextJs.config.created.client": "true",
+ "js.debugger.nextJs.config.created.server": "true",
+ "junie.onboarding.icon.badge.shown": "true",
+ "kronk.content.tree.expanded.paths": "",
+ "last_opened_file_path": "/home/hamed/Documents/Projects/GItLabRepo/device_web",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "settings.editor.selected.configurable": "editor.preferences.fonts.default",
+ "to.speed.mode.migration.done": "true",
+ "ts.external.directory.path": "/home/hamed/Documents/Projects/GIthubRepo/robot-market-landing/node_modules/typescript/lib",
+ "vue.rearranger.settings.migration": "true"
}
-}]]>
+}
@@ -234,7 +218,8 @@
-
+
+
@@ -508,7 +493,15 @@
1789384147873
-
+
+
+ 1789385239281
+
+
+
+ 1789385239281
+
+
@@ -530,6 +523,7 @@
-
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 717639f..a196b54 100644
--- a/README.md
+++ b/README.md
@@ -31,14 +31,20 @@ npm start
## ساختار و بخشهای فعال
-- `app/page.tsx`: صفحه اصلی شامل هدر، سبد خرید، هیرو، ویترین دستگاهها، مزایا، درباره ما، محلهای مناسب نصب و فوتر.
+- `app/page.tsx`: صفحه اصلی شامل هیرو، ویترین دستگاهها، مزایا، درباره ما و محلهای مناسب نصب.
+- `app/layout.tsx`: هدر، فوتر و پیام توسعه مشترک بین صفحات.
+- `app/cart/page.tsx`: صفحه مستقل سبد خرید.
+- `app/products/[id]/page.tsx`: صفحه جزئیات هر دستگاه، تصاویر، ویژگیها و کنترل تعداد.
- `components/HeroSection.tsx`: اسلایدر تصاویر دستگاهها.
- `components/ShowcaseSection.tsx`: انتخاب نمایش موبایل یا ویترین افقی دسکتاپ.
- `components/HorizontalShowcase.tsx`: ویترین افقی با GSAP و ویدیوی پسزمینه وابسته به اسکرول.
- `components/MobileShowcase.tsx`: فهرست دستگاهها در موبایل.
- `components/VendingCard.tsx` و `components/FloatingItems.tsx`: نمایش دستگاه و جلوههای تصویری.
-- `components/CartDrawer.tsx` و `store/`: سبد خرید و مدیریت تعداد دستگاهها با Redux.
+- `components/CartPage.tsx`، `components/QuantityControl.tsx` و `store/`: سبد خرید و کنترل افزایش و کاهش تعداد با Redux.
+- `components/CartPersistence.tsx`: حفظ شناسه و تعداد دستگاهها در مرورگر.
+- `components/DevelopmentNotice.tsx`: پیام اولین بازدید؛ پس از بستهشدن دوباره در همان مرورگر نمایش داده نمیشود.
- `data/content.ts`: محتوای فارسی و انگلیسی، مشخصات و قیمت دستگاهها و مسیر رسانهها.
+- `data/commerce.ts`: متنهای دوزبانه سبد خرید، جزئیات محصول و پیام توسعه.
- `app/context/` و `app/providers.tsx`: زبان، جهت صفحه، تم و تنظیمات Ant Design.
- `app/globals.css`: رنگها و استایلهای مشترک.
- `public/`: تصاویر دستگاهها، اقلام شناور، گیفها و ویدیوی ویترین.
@@ -51,8 +57,9 @@ npm start
## ورود و سبد خرید
دکمه ورود به https://panel.my-rm.com متصل است.
-سبد خرید در حافظه نگهداری میشود و با بارگذاری مجدد صفحه پاک میشود.
-پرداخت هنوز متصل نیست؛ دکمه پرداخت در `CartDrawer.tsx` پیام نمایشی نشان میدهد.
+سبد خرید در مسیر `/cart` نمایش داده میشود و انتخابها پس از بارگذاری مجدد حفظ میشوند.
+کنترلهای مثبت و منفی در کارتها، صفحه محصول و سبد هماهنگاند؛ کاهش تعداد از یک به صفر دستگاه را از سبد حذف میکند.
+پرداخت هنوز متصل نیست؛ دکمه پرداخت غیرفعال است و توضیح آن نمایش داده میشود.
## تنظیم API
diff --git a/app/cart/page.tsx b/app/cart/page.tsx
new file mode 100644
index 0000000..75ba5ad
--- /dev/null
+++ b/app/cart/page.tsx
@@ -0,0 +1,8 @@
+import type {Metadata} from 'next';
+import CartPage from '@/components/CartPage';
+
+export const metadata: Metadata = {title: 'سبد خرید | روبات مارکت'};
+
+export default function Page() {
+ return ;
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index d7f0b6e..5c21771 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -3,6 +3,9 @@ import type {ReactNode} from 'react';
import {Vazirmatn} from 'next/font/google';
import './globals.css';
import Providers from './providers';
+import Header from '@/components/Header';
+import Footer from '@/components/Footer';
+import DevelopmentNotice from '@/components/DevelopmentNotice';
const vazirmatn = Vazirmatn({
subsets: ['arabic'],
@@ -28,7 +31,12 @@ export default function RootLayout({children}: {children: ReactNode}) {
return (
- {children}
+
+
+ {children}
+
+
+
);
diff --git a/app/page.tsx b/app/page.tsx
index e89b60c..df17ea7 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -3,22 +3,16 @@ import ShowcaseSection from "@/components/ShowcaseSection";
import FeaturesSection from '@/components/FeaturesSection';
import AboutSection from '@/components/AboutSection';
import HeroSection from '@/components/HeroSection';
-import CartDrawer from '@/components/CartDrawer';
-import Footer from '@/components/Footer';
-import Header from '@/components/Header';
export default function HomePage() {
return (
-
-
-
);
}
diff --git a/app/products/[id]/page.tsx b/app/products/[id]/page.tsx
new file mode 100644
index 0000000..9fb4dd5
--- /dev/null
+++ b/app/products/[id]/page.tsx
@@ -0,0 +1,21 @@
+import type {Metadata} from 'next';
+import {notFound} from 'next/navigation';
+import {content} from '@/data/content';
+import ProductDetails from '@/components/ProductDetails';
+
+type Props = {params: {id: string}};
+
+export function generateStaticParams() {
+ return content.fa.machines.map(machine => ({id: machine.id}));
+}
+
+export function generateMetadata({params}: Props): Metadata {
+ const machine = content.fa.machines.find(item => item.id === params.id);
+ if (!machine) return {};
+ return {title: `${machine.name} | روبات مارکت`, description: machine.description};
+}
+
+export default function ProductPage({params}: Props) {
+ if (!content.fa.machines.some(machine => machine.id === params.id)) notFound();
+ return ;
+}
diff --git a/app/providers.tsx b/app/providers.tsx
index 198e13d..98402b4 100644
--- a/app/providers.tsx
+++ b/app/providers.tsx
@@ -11,6 +11,7 @@ import enUS from 'antd/locale/en_US';
import {store} from '@/store/store';
import {ThemeProvider, useTheme} from './context/ThemeContext';
import {LanguageProvider, useLanguage} from './context/LanguageContext';
+import CartPersistence from '@/components/CartPersistence';
function AntdBridge({children}: {children: ReactNode}) {
const {theme} = useTheme();
@@ -37,9 +38,6 @@ function AntdBridge({children}: {children: ReactNode}) {
headerBg: theme === 'dark' ? '#0c2b24' : '#ffffff',
titleColor: theme === 'dark' ? '#e6f7f4' : '#04231f',
},
- Drawer: {
- colorBgElevated: theme === 'dark' ? '#0c2b24' : '#ffffff',
- },
},
};
@@ -53,6 +51,7 @@ function AntdBridge({children}: {children: ReactNode}) {
export default function Providers({children}: {children: ReactNode}) {
return (
+
{children}
diff --git a/components/AboutSection.tsx b/components/AboutSection.tsx
index ba5717d..d4cec21 100644
--- a/components/AboutSection.tsx
+++ b/components/AboutSection.tsx
@@ -7,7 +7,7 @@ import {useLanguage} from '@/app/context/LanguageContext';
export default function AboutSection() {
const {t} = useLanguage();
- const [expanded, setExpanded] = useState(true);
+ const [expanded, setExpanded] = useState(false);
const visibleParagraphs = expanded ? t.about.paragraphs : t.about.paragraphs.slice(0, 2);
return (
@@ -26,14 +26,13 @@ export default function AboutSection() {
))}
- {/* faz 1 */}
- {/* */}
+
);
diff --git a/components/CartDrawer.tsx b/components/CartDrawer.tsx
deleted file mode 100644
index 19d9432..0000000
--- a/components/CartDrawer.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-'use client';
-
-// Cart drawer with quantity editing, totals, and placeholder checkout.
-
-import {Drawer, Button, Empty, InputNumber, message} from 'antd';
-import {DeleteOutlined} from '@ant-design/icons';
-import {useAppSelector, useAppDispatch} from '@/store/hooks';
-import {
- selectCartItems,
- selectCartTotal,
- selectIsDrawerOpen,
- closeDrawer,
- removeFromCart,
- setQuantity,
-} from '@/store/cartSlice';
-import {useLanguage} from '@/app/context/LanguageContext';
-import type {Locale} from '@/types/domain';
-import api from "@/lib/axios";
-
-function toLocaleNumber(n: number, locale: Locale) {
- return n.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US');
-}
-
-export default function CartDrawer() {
- const dispatch = useAppDispatch();
- const isOpen = useAppSelector(selectIsDrawerOpen);
- const items = useAppSelector(selectCartItems);
- const total = useAppSelector(selectCartTotal);
- const {t, locale, dir} = useLanguage();
-
- const currencySuffix = locale === 'fa' ? ' ریال' : ' Rials';
-
- const handleCheckout = async () => {
- // -------------------------------------------------------------------
- // TODO: Connect to payment gateway API..
-
- // 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('...');
- // }
-
- // api() <===> todo : use this func to fetch ...
-
- // -------------------------------------------------------------------
- await 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 !== null) dispatch(setQuantity({id: item.id, qty: val}));
- }}
- className="w-16"
- />
-
-
-
- ))}
-
-
-
- {t.cart.total}
-
- {toLocaleNumber(total, locale)}
- {currencySuffix}
-
-
-
-
-
- )}
-
- );
-}
diff --git a/components/CartPage.tsx b/components/CartPage.tsx
new file mode 100644
index 0000000..fef0dd0
--- /dev/null
+++ b/components/CartPage.tsx
@@ -0,0 +1,68 @@
+'use client';
+
+import Link from 'next/link';
+import {Button, Empty} from 'antd';
+import {DeleteOutlined} from '@ant-design/icons';
+import {useAppDispatch, useAppSelector} from '@/store/hooks';
+import {removeFromCart, selectCartItems, selectCartTotal} from '@/store/cartSlice';
+import {useLanguage} from '@/app/context/LanguageContext';
+import {commerceCopy} from '@/data/commerce';
+import QuantityControl from './QuantityControl';
+
+export default function CartPage() {
+ const dispatch = useAppDispatch();
+ const items = useAppSelector(selectCartItems);
+ const total = useAppSelector(selectCartTotal);
+ const {t, locale} = useLanguage();
+ const copy = commerceCopy[locale];
+ const money = (value: number) => `${value.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US')} ${copy.currency}`;
+
+ return (
+
+ {copy.continueShopping}
+ {t.cart.title}
+ {copy.cartDescription}
+ {items.length === 0 ? (
+
+
+ {copy.continueShopping}
+
+ ) : (
+
+
+ {items.map(item => {
+ const product = t.machines.find(machine => machine.id === item.id) ?? item;
+ return (
+
+
+
+
+
+
{product.name}
+
{copy.unitPrice}: {money(item.priceNumeric)}
+
+
+
+
{copy.lineTotal}: {money(item.qty * item.priceNumeric)}
+
+
+ );
+ })}
+
+
+
+ )}
+
+ );
+}
diff --git a/components/CartPersistence.tsx b/components/CartPersistence.tsx
new file mode 100644
index 0000000..c5a5025
--- /dev/null
+++ b/components/CartPersistence.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import {useEffect} from 'react';
+import {useStore} from 'react-redux';
+import type {AppStore} from '@/store/store';
+import type {CartItem} from '@/types/domain';
+import {restoreCart} from '@/store/cartSlice';
+import {content} from '@/data/content';
+
+const STORAGE_KEY = 'rm-cart';
+const useAppStore = useStore.withTypes();
+
+export default function CartPersistence() {
+ const store = useAppStore();
+
+ useEffect(() => {
+ try {
+ const saved: unknown = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '[]');
+ if (Array.isArray(saved)) {
+ const items: CartItem[] = [];
+ for (const entry of saved) {
+ if (!entry || typeof entry !== 'object' || typeof entry.id !== 'string'
+ || !Number.isSafeInteger(entry.qty) || entry.qty < 1) continue;
+ const machine = content.fa.machines.find(item => item.id === entry.id);
+ if (!machine || items.some(item => item.id === machine.id)) continue;
+ // Restore identifiers and quantities; use current catalog prices and images.
+ items.push({id: machine.id, name: machine.name, image: machine.image,
+ priceNumeric: machine.priceNumeric, priceLabel: machine.priceLabel, qty: entry.qty});
+ }
+ store.dispatch(restoreCart(items));
+ }
+ } catch {
+ // Invalid or unavailable storage must not prevent shopping.
+ }
+
+ return store.subscribe(() => {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(store.getState().cart.items.map(({id, qty}) => ({id, qty}))));
+ } catch {
+ // Keep the in-memory cart usable when storage is full or disabled.
+ }
+ });
+ }, [store]);
+
+ return null;
+}
diff --git a/components/DevelopmentNotice.tsx b/components/DevelopmentNotice.tsx
new file mode 100644
index 0000000..14a1032
--- /dev/null
+++ b/components/DevelopmentNotice.tsx
@@ -0,0 +1,41 @@
+'use client';
+
+import {useEffect, useState} from 'react';
+import {Button, Modal} from 'antd';
+import {ToolOutlined} from '@ant-design/icons';
+import {useLanguage} from '@/app/context/LanguageContext';
+import {commerceCopy} from '@/data/commerce';
+
+const STORAGE_KEY = 'rm-development-notice-seen';
+
+export default function DevelopmentNotice() {
+ const [open, setOpen] = useState(false);
+ const {locale} = useLanguage();
+ const copy = commerceCopy[locale];
+
+ useEffect(() => {
+ try {
+ setOpen(localStorage.getItem(STORAGE_KEY) !== 'true');
+ } catch {
+ // Show the notice even when browser storage is unavailable.
+ setOpen(true);
+ }
+ }, []);
+
+ const dismiss = () => {
+ setOpen(false);
+ try {
+ localStorage.setItem(STORAGE_KEY, 'true');
+ } catch {
+ // Dismissing the notice must not depend on storage access.
+ }
+ };
+
+ return (
+ {copy.acknowledge}}>
+
+ {copy.developmentMessage}
+
+ );
+}
diff --git a/components/Footer.tsx b/components/Footer.tsx
index 26a01ab..c731efe 100644
--- a/components/Footer.tsx
+++ b/components/Footer.tsx
@@ -2,6 +2,7 @@
// Company information, quick links, social links, and contact details.
+import Link from 'next/link';
import { PhoneOutlined, EnvironmentOutlined } from '@ant-design/icons';
import { useLanguage } from '@/app/context/LanguageContext';
@@ -33,10 +34,10 @@ export default function Footer() {
diff --git a/components/Header.tsx b/components/Header.tsx
index 91dd278..28c48a8 100644
--- a/components/Header.tsx
+++ b/components/Header.tsx
@@ -4,30 +4,33 @@
import {useState, useEffect} from 'react';
import Image from 'next/image';
+import Link from 'next/link';
+import {usePathname} from 'next/navigation';
import {Badge, Button} from 'antd';
import {ShoppingCartOutlined, MenuOutlined, CloseOutlined, UserOutlined} from '@ant-design/icons';
-import {useAppSelector, useAppDispatch} from '@/store/hooks';
-import {selectCartCount, openDrawer} from '@/store/cartSlice';
+import {useAppSelector} from '@/store/hooks';
+import {selectCartCount} from '@/store/cartSlice';
import {useLanguage} from '@/app/context/LanguageContext';
import ThemeToggle from './ThemeToggle';
import LanguageToggle from './LanguageToggle';
export default function Header() {
- const dispatch = useAppDispatch();
+ const pathname = usePathname();
const cartCount = useAppSelector(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: '#locations', label: t.nav.locations},
+ {href: '/#hero', label: t.nav.home},
+ {href: '/#showcase', label: t.nav.showcase},
+ {href: '/#about', label: t.nav.about},
+ {href: '/#locations', label: t.nav.locations},
];
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24);
+ onScroll();
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
@@ -40,23 +43,23 @@ export default function Header() {
return (