feat: add cart and product pages, quantity controls, and first-visit development notice
Build & Deploy (develop) / build-and-deploy (push) Successful in 1m44s
Build & Deploy (develop) / build-and-deploy (push) Successful in 1m44s
This commit is contained in:
Generated
+107
-113
@@ -4,38 +4,22 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="bb7b50a5-be9c-4cd7-a05b-979f0e91b173" name="Changes" comment="refactor: remove disabled home sections and their unused assets">
|
||||
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
||||
<list default="true" id="bb7b50a5-be9c-4cd7-a05b-979f0e91b173" name="Changes" comment="refactor: migrate project to TypeScript and translate comments to English">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/app/context/LanguageContext.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/app/context/ThemeContext.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/app/layout.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/app/page.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/app/providers.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/AboutSection.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/CartDrawer.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/FeaturesSection.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/FloatingItems.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/Footer.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/Header.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/HeroSection.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/HorizontalShowcase.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/LanguageToggle.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/LocationsSection.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/MobileShowcase.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/ShowcaseSection.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/ThemeToggle.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/VendingCard.jsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/data/content.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/jsconfig.json" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/lib/axios.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/next.config.js" beforeDir="false" afterPath="$PROJECT_DIR$/next.config.js" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/package-lock.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/store/cartSlice.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/store/store.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/tailwind.config.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/app/layout.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/app/layout.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/app/page.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/app/page.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/app/providers.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/app/providers.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/AboutSection.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/components/AboutSection.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/CartDrawer.tsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/Footer.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/components/Footer.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/Header.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/components/Header.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/HorizontalShowcase.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/components/HorizontalShowcase.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/MobileShowcase.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/components/MobileShowcase.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/components/VendingCard.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/components/VendingCard.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/data/content.ts" beforeDir="false" afterPath="$PROJECT_DIR$/data/content.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/store/cartSlice.ts" beforeDir="false" afterPath="$PROJECT_DIR$/store/cartSlice.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/types/domain.ts" beforeDir="false" afterPath="$PROJECT_DIR$/types/domain.ts" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -57,61 +41,61 @@
|
||||
</component>
|
||||
<component name="NextEditCompletionFeaturesState">
|
||||
<decayedCancelled>
|
||||
<entry key="MS100" value="1.0105021478132723" />
|
||||
<entry key="MS500" value="1.6326904583013002" />
|
||||
<entry key="S2" value="3.200495115489667" />
|
||||
<entry key="S5" value="4.148160592733714" />
|
||||
<entry key="S10" value="4.983360398199462" />
|
||||
<entry key="S30" value="7.2170837140275825" />
|
||||
<entry key="S60" value="9.732495501143132" />
|
||||
<entry key="M2" value="14.025681297470134" />
|
||||
<entry key="M5" value="22.17637857625762" />
|
||||
<entry key="M10" value="27.702751951097298" />
|
||||
<entry key="M15" value="30.22928327611057" />
|
||||
<entry key="M30" value="34.21976380295617" />
|
||||
<entry key="H1" value="38.60682034968035" />
|
||||
<entry key="H2" value="42.59562362296086" />
|
||||
<entry key="H4" value="45.40713861912438" />
|
||||
<entry key="D1" value="48.33720180411775" />
|
||||
<entry key="W1" value="48.90360237164015" />
|
||||
<entry key="MS100" value="5.636161152320107E-5" />
|
||||
<entry key="MS500" value="0.2302465263814015" />
|
||||
<entry key="S2" value="1.9612794450498428" />
|
||||
<entry key="S5" value="3.4102363921166443" />
|
||||
<entry key="S10" value="4.5184216736456175" />
|
||||
<entry key="S30" value="6.985270524998366" />
|
||||
<entry key="S60" value="9.574915616687461" />
|
||||
<entry key="M2" value="13.91167227171946" />
|
||||
<entry key="M5" value="22.10409689222133" />
|
||||
<entry key="M10" value="27.65756793067821" />
|
||||
<entry key="M15" value="30.196404421732346" />
|
||||
<entry key="M30" value="34.20114919146407" />
|
||||
<entry key="H1" value="38.596318396422" />
|
||||
<entry key="H2" value="42.58982972864479" />
|
||||
<entry key="H4" value="45.40405035457018" />
|
||||
<entry key="D1" value="48.33665386421948" />
|
||||
<entry key="W1" value="48.9035231768996" />
|
||||
</decayedCancelled>
|
||||
<decayedSelected>
|
||||
<entry key="MS100" value="0.0" />
|
||||
<entry key="MS500" value="8.86528819923573E-248" />
|
||||
<entry key="S2" value="1.7255327824639203E-62" />
|
||||
<entry key="S5" value="1.9713751865934254E-25" />
|
||||
<entry key="S10" value="4.440017101986686E-13" />
|
||||
<entry key="S30" value="7.628893420815487E-5" />
|
||||
<entry key="S60" value="0.008734353680047248" />
|
||||
<entry key="M2" value="0.09345776415069663" />
|
||||
<entry key="M5" value="0.387477201784943" />
|
||||
<entry key="M10" value="0.6224766676630884" />
|
||||
<entry key="M15" value="0.7290355699678522" />
|
||||
<entry key="M30" value="0.8538357980126227" />
|
||||
<entry key="H1" value="0.9240323576653702" />
|
||||
<entry key="H2" value="0.9612660181580177" />
|
||||
<entry key="H4" value="0.9804417464378067" />
|
||||
<entry key="D1" value="0.9967134048485689" />
|
||||
<entry key="W1" value="0.9995298237292167" />
|
||||
<entry key="MS100" value="1.0" />
|
||||
<entry key="MS500" value="1.0" />
|
||||
<entry key="S2" value="1.0" />
|
||||
<entry key="S5" value="1.0" />
|
||||
<entry key="S10" value="1.0000000000004026" />
|
||||
<entry key="S30" value="1.0000738385287775" />
|
||||
<entry key="S60" value="1.0085929348174816" />
|
||||
<entry key="M2" value="1.0926980842168899" />
|
||||
<entry key="M5" value="1.3862142586684925" />
|
||||
<entry key="M10" value="1.6214613895235106" />
|
||||
<entry key="M15" value="1.7282426350470148" />
|
||||
<entry key="M30" value="1.853371334793368" />
|
||||
<entry key="H1" value="1.9237809993680148" />
|
||||
<entry key="H2" value="1.9611352659059051" />
|
||||
<entry key="H4" value="1.9803750638943771" />
|
||||
<entry key="D1" value="1.9967021063244244" />
|
||||
<entry key="W1" value="1.9995282050855745" />
|
||||
</decayedSelected>
|
||||
<decayedShown>
|
||||
<entry key="MS100" value="0.01712637149186048" />
|
||||
<entry key="MS500" value="0.9540168467150943" />
|
||||
<entry key="S2" value="2.9136793264313505" />
|
||||
<entry key="S5" value="4.013649896020289" />
|
||||
<entry key="S10" value="4.909460420460746" />
|
||||
<entry key="S30" value="7.187579804050971" />
|
||||
<entry key="S60" value="9.723843431274007" />
|
||||
<entry key="M2" value="14.107987700307802" />
|
||||
<entry key="M5" value="22.55725525698723" />
|
||||
<entry key="M10" value="28.321163439518607" />
|
||||
<entry key="M15" value="30.955381308867647" />
|
||||
<entry key="M30" value="35.07198888146704" />
|
||||
<entry key="H1" value="39.52999740754272" />
|
||||
<entry key="H2" value="43.55644480884343" />
|
||||
<entry key="H4" value="46.38735262517173" />
|
||||
<entry key="D1" value="49.33387639135037" />
|
||||
<entry key="W1" value="49.903126627030524" />
|
||||
<entry key="MS100" value="5.673108319866789E-5" />
|
||||
<entry key="MS500" value="0.2755608628441334" />
|
||||
<entry key="S2" value="2.398322303413965" />
|
||||
<entry key="S5" value="4.121762152287479" />
|
||||
<entry key="S10" value="5.358118199319534" />
|
||||
<entry key="S30" value="7.924594218734951" />
|
||||
<entry key="S60" value="10.550212525549737" />
|
||||
<entry key="M2" value="14.985181048549935" />
|
||||
<entry key="M5" value="23.480472743765223" />
|
||||
<entry key="M10" value="29.27333974135673" />
|
||||
<entry key="M15" value="31.92062506542455" />
|
||||
<entry key="M30" value="36.05236671041284" />
|
||||
<entry key="H1" value="40.518972305385134" />
|
||||
<entry key="H2" value="44.55038420189922" />
|
||||
<entry key="H4" value="47.38412968080923" />
|
||||
<entry key="D1" value="50.33330581758772" />
|
||||
<entry key="W1" value="50.9030441942503" />
|
||||
</decayedShown>
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
@@ -123,36 +107,36 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"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"
|
||||
<component name="PropertiesComponent">{
|
||||
"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"
|
||||
}
|
||||
}]]></component>
|
||||
}</component>
|
||||
<component name="RecentsManager">
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="D:\Project\GitHubRepo\robot-market-landing\public\images\icons" />
|
||||
@@ -234,7 +218,8 @@
|
||||
<workItem from="1789362025574" duration="23000" />
|
||||
<workItem from="1789362137185" duration="377000" />
|
||||
<workItem from="1789362644534" duration="6512000" />
|
||||
<workItem from="1789383269194" duration="1732000" />
|
||||
<workItem from="1789383269194" duration="2337000" />
|
||||
<workItem from="1789445417779" duration="2914000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="update">
|
||||
<option name="closed" value="true" />
|
||||
@@ -508,7 +493,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1789384147873</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="35" />
|
||||
<task id="LOCAL-00035" summary="refactor: migrate project to TypeScript and translate comments to English">
|
||||
<option name="closed" value="true" />
|
||||
<created>1789385239281</created>
|
||||
<option name="number" value="00035" />
|
||||
<option name="presentableId" value="LOCAL-00035" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1789385239281</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="36" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -530,6 +523,7 @@
|
||||
<MESSAGE value="update" />
|
||||
<MESSAGE value="feat: add landing page device images and fix machine branding" />
|
||||
<MESSAGE value="refactor: remove disabled home sections and their unused assets" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="refactor: remove disabled home sections and their unused assets" />
|
||||
<MESSAGE value="refactor: migrate project to TypeScript and translate comments to English" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="refactor: migrate project to TypeScript and translate comments to English" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type {Metadata} from 'next';
|
||||
import CartPage from '@/components/CartPage';
|
||||
|
||||
export const metadata: Metadata = {title: 'سبد خرید | روبات مارکت'};
|
||||
|
||||
export default function Page() {
|
||||
return <CartPage/>;
|
||||
}
|
||||
+9
-1
@@ -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 (
|
||||
<html lang="fa" dir="rtl" className={`${vazirmatn.variable} light`} suppressHydrationWarning>
|
||||
<body className="bg-page text-primary antialiased transition-colors duration-300">
|
||||
<Providers>{children}</Providers>
|
||||
<Providers>
|
||||
<Header/>
|
||||
{children}
|
||||
<Footer/>
|
||||
<DevelopmentNotice/>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<main className="relative bg-page">
|
||||
<Header/>
|
||||
<CartDrawer/>
|
||||
<HeroSection/>
|
||||
<ShowcaseSection/>
|
||||
<FeaturesSection/>
|
||||
<AboutSection/>
|
||||
<LocationsSection/>
|
||||
<Footer/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 <ProductDetails id={params.id}/>;
|
||||
}
|
||||
+2
-3
@@ -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 (
|
||||
<ReduxProvider store={store}>
|
||||
<CartPersistence/>
|
||||
<ThemeProvider>
|
||||
<LanguageProvider>
|
||||
<AntdBridge>{children}</AntdBridge>
|
||||
|
||||
@@ -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() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* faz 1 */}
|
||||
{/* <button
|
||||
onClick={() => 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}
|
||||
<span className={`transition-transform ${expanded ? 'rotate-180' : ''}`}>˅</span>
|
||||
</button>*/}
|
||||
<button
|
||||
onClick={() => 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}
|
||||
<span className={`transition-transform ${expanded ? 'rotate-180' : ''}`}>˅</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Drawer
|
||||
title={t.cart.title}
|
||||
placement={dir === 'rtl' ? 'right' : 'left'}
|
||||
onClose={() => dispatch(closeDrawer())}
|
||||
open={isOpen}
|
||||
width={400}
|
||||
>
|
||||
{items.length === 0 ? (
|
||||
<Empty description={<span className="text-secondary">{t.cart.empty}</span>} className="mt-16"/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
{items.map((item) => (
|
||||
<div key={item.id}
|
||||
className="flex items-center gap-3 bg-surface border border-subtle rounded-xl p-3">
|
||||
<img src={item.image} alt={item.name} className="w-14 h-20 object-contain shrink-0"/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold text-primary truncate">{item.name}</p>
|
||||
<p className="text-xs text-brand-400 mt-1">
|
||||
{toLocaleNumber(item.priceNumeric, locale)}
|
||||
{currencySuffix}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={1}
|
||||
precision={0}
|
||||
value={item.qty}
|
||||
onChange={(val) => {
|
||||
if (val !== null) dispatch(setQuantity({id: item.id, qty: val}));
|
||||
}}
|
||||
className="w-16"
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined/>}
|
||||
onClick={() => dispatch(removeFromCart(item.id))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="border-t border-subtle pt-4 mt-2">
|
||||
<div className="flex items-center justify-between text-sm text-secondary mb-4">
|
||||
<span>{t.cart.total}</span>
|
||||
<span className="text-lg font-bold text-primary">
|
||||
{toLocaleNumber(total, locale)}
|
||||
{currencySuffix}
|
||||
</span>
|
||||
</div>
|
||||
<Button type="primary" block size="large" onClick={handleCheckout}>
|
||||
{t.cart.checkout}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<main className="mx-auto max-w-7xl min-h-[65vh] px-5 md:px-8 pt-28 pb-20">
|
||||
<Link href="/#showcase" className="text-brand-400 hover:underline">{copy.continueShopping}</Link>
|
||||
<h1 className="text-3xl md:text-4xl font-extrabold text-primary mt-6">{t.cart.title}</h1>
|
||||
<p className="text-secondary mt-3 mb-10">{copy.cartDescription}</p>
|
||||
{items.length === 0 ? (
|
||||
<div className="rounded-3xl border border-subtle bg-surface p-10 text-center">
|
||||
<Empty description={t.cart.empty}/>
|
||||
<Link href="/#showcase" className="inline-block mt-6 text-brand-400 hover:underline">{copy.continueShopping}</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid lg:grid-cols-[minmax(0,1fr),340px] gap-8 items-start">
|
||||
<div className="space-y-4">
|
||||
{items.map(item => {
|
||||
const product = t.machines.find(machine => machine.id === item.id) ?? item;
|
||||
return (
|
||||
<article key={item.id} className="rounded-2xl border border-subtle bg-surface p-5 flex flex-wrap sm:flex-nowrap gap-5 items-center">
|
||||
<Link href={`/products/${item.id}`} className="shrink-0">
|
||||
<img src={product.image} alt={product.name} className="w-24 h-32 object-contain"/>
|
||||
</Link>
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link href={`/products/${item.id}`} className="font-bold text-lg text-primary hover:text-brand-400">{product.name}</Link>
|
||||
<p className="text-secondary text-sm mt-2">{copy.unitPrice}: {money(item.priceNumeric)}</p>
|
||||
<div className="flex flex-wrap gap-3 items-center mt-4">
|
||||
<QuantityControl product={product}/>
|
||||
<Button danger type="text" icon={<DeleteOutlined/>}
|
||||
aria-label={`${copy.remove}: ${product.name}`}
|
||||
onClick={() => dispatch(removeFromCart(item.id))}/>
|
||||
</div>
|
||||
<p className="mt-4 text-primary font-semibold">{copy.lineTotal}: {money(item.qty * item.priceNumeric)}</p>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<aside className="lg:sticky lg:top-28 border border-subtle bg-elevated rounded-2xl p-6">
|
||||
<h2 className="font-bold text-xl text-primary">{copy.summary}</h2>
|
||||
<div className="border-t border-subtle mt-5 pt-5 flex flex-wrap justify-between gap-3">
|
||||
<span className="text-secondary">{t.cart.total}</span>
|
||||
<strong className="text-primary">{money(total)}</strong>
|
||||
</div>
|
||||
<p className="text-secondary text-sm leading-7 my-5">{copy.checkoutUnavailable}</p>
|
||||
<Button type="primary" size="large" disabled block>{t.cart.checkout}</Button>
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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<AppStore>();
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 (
|
||||
<Modal open={open} centered title={copy.developmentTitle} onCancel={dismiss}
|
||||
footer={<Button type="primary" onClick={dismiss}>{copy.acknowledge}</Button>}>
|
||||
<ToolOutlined className="text-brand-400 text-3xl my-4"/>
|
||||
<p className="text-secondary leading-8">{copy.developmentMessage}</p>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
<div>
|
||||
<h3 className="text-primary font-bold text-lg mb-3">{t.footer.quickLinks}</h3>
|
||||
<ul className="flex flex-col gap-2 text-sm text-secondary">
|
||||
<li><a href="#hero" className="hover:text-brand-400">{t.nav.home}</a></li>
|
||||
<li><a href="#showcase" className="hover:text-brand-400">{t.nav.showcase}</a></li>
|
||||
<li><a href="#about" className="hover:text-brand-400">{t.nav.about}</a></li>
|
||||
<li><a href="#locations" className="hover:text-brand-400">{t.nav.locations}</a></li>
|
||||
<li><Link href="/#hero" className="hover:text-brand-400">{t.nav.home}</Link></li>
|
||||
<li><Link href="/#showcase" className="hover:text-brand-400">{t.nav.showcase}</Link></li>
|
||||
<li><Link href="/#about" className="hover:text-brand-400">{t.nav.about}</Link></li>
|
||||
<li><Link href="/#locations" className="hover:text-brand-400">{t.nav.locations}</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
+21
-21
@@ -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 (
|
||||
<header
|
||||
className={`fixed top-0 inset-x-0 z-50 transition-all duration-300 ${
|
||||
scrolled ? 'bg-page-85 backdrop-blur-md shadow-[0_1px_0_0_rgba(0,166,147,0.25)]' : 'bg-transparent'
|
||||
scrolled || pathname !== '/' ? 'bg-page-85 backdrop-blur-md shadow-[0_1px_0_0_rgba(0,166,147,0.25)]' : 'bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<div className="mx-auto max-w-7xl px-5 md:px-8 h-20 flex items-center justify-between gap-4">
|
||||
<a href="#hero" className="flex items-center gap-2 shrink-0">
|
||||
<Link href="/#hero" onClick={() => setMobileOpen(false)} className="flex items-center gap-2 shrink-0">
|
||||
<Image src="/images/icons/logo.png" alt="روبات مارکت" width={150} height={42} priority/>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden xl:flex items-center gap-5 2xl:gap-7">
|
||||
{navLinks.map((link) => (
|
||||
<a
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm text-secondary hover:text-brand-300 transition-colors whitespace-nowrap"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
@@ -77,13 +80,10 @@ export default function Header() {
|
||||
</Button>
|
||||
|
||||
<Badge count={cartCount} size="small" offset={[-2, 2]}>
|
||||
<Button
|
||||
shape="circle"
|
||||
size="large"
|
||||
icon={<ShoppingCartOutlined/>}
|
||||
onClick={() => dispatch(openDrawer())}
|
||||
aria-label={t.cart.title}
|
||||
/>
|
||||
<Link href="/cart" onClick={() => setMobileOpen(false)} aria-label={t.cart.title}
|
||||
className="w-11 h-11 rounded-full border border-subtle bg-elevated inline-flex items-center justify-center text-xl text-primary hover:text-brand-400">
|
||||
<ShoppingCartOutlined/>
|
||||
</Link>
|
||||
</Badge>
|
||||
|
||||
<button
|
||||
@@ -100,14 +100,14 @@ export default function Header() {
|
||||
<nav
|
||||
className="xl:hidden bg-elevated-95 backdrop-blur-md border-t border-subtle px-5 py-4 flex flex-col gap-4 max-h-[calc(100vh-5rem)] overflow-y-auto">
|
||||
{navLinks.map((link) => (
|
||||
<a
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="text-secondary text-base"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
<div className="flex items-center gap-3 pt-2 border-t border-subtle">
|
||||
<LanguageToggle/>
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import type {Machine} from '@/types/domain';
|
||||
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 {useAppDispatch} from '@/store/hooks';
|
||||
import VendingCard from './VendingCard';
|
||||
import {showcaseVideoSrc} from '@/data/content';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {addToCart} from '@/store/cartSlice';
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
@@ -18,9 +13,7 @@ export default function HorizontalShowcase() {
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
const trackRef = useRef<HTMLDivElement>(null);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
const {t} = useLanguage();
|
||||
const [quickViewMachine, setQuickViewMachine] = useState<Machine | null>(null);
|
||||
const [videoAvailable, setVideoAvailable] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -74,23 +67,6 @@ export default function HorizontalShowcase() {
|
||||
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 (
|
||||
<section id="showcase" ref={sectionRef} className="relative w-full overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
@@ -116,37 +92,10 @@ export default function HorizontalShowcase() {
|
||||
|
||||
<div ref={trackRef} className="horizontal-track relative z-10" dir="ltr">
|
||||
{t.machines.map((machine) => (
|
||||
<VendingCard key={machine.id} machine={machine} onQuickView={setQuickViewMachine}/>
|
||||
<VendingCard key={machine.id} machine={machine}/>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,108 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import type {Machine} from '@/types/domain';
|
||||
import {useState} from 'react';
|
||||
import {Modal, Button, message} from 'antd';
|
||||
import {ShoppingCartOutlined, EyeOutlined} from '@ant-design/icons';
|
||||
import {useAppDispatch} from '@/store/hooks';
|
||||
import Link from 'next/link';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {addToCart} from '@/store/cartSlice';
|
||||
import {commerceCopy} from '@/data/commerce';
|
||||
import QuantityControl from './QuantityControl';
|
||||
|
||||
export default function MobileShowcase() {
|
||||
const dispatch = useAppDispatch();
|
||||
const {t, dir} = useLanguage();
|
||||
const [quickViewMachine, setQuickViewMachine] = useState<Machine | null>(null);
|
||||
|
||||
const handleAddToCart = (machine: Machine) => {
|
||||
dispatch(
|
||||
addToCart({
|
||||
id: machine.id,
|
||||
name: machine.name,
|
||||
priceNumeric: machine.priceNumeric,
|
||||
priceLabel: machine.priceLabel,
|
||||
image: machine.image,
|
||||
})
|
||||
);
|
||||
message.success(t.cart.addToast(machine.name));
|
||||
};
|
||||
|
||||
const handleModalAddToCart = () => {
|
||||
if (!quickViewMachine) return;
|
||||
handleAddToCart(quickViewMachine);
|
||||
setQuickViewMachine(null);
|
||||
};
|
||||
const {t, dir, locale} = useLanguage();
|
||||
|
||||
return (
|
||||
<section id="showcase" className="relative w-full py-8 px-4" dir={dir}>
|
||||
<div className="flex flex-col gap-4">
|
||||
{t.machines.map((machine) => (
|
||||
<div
|
||||
key={machine.id}
|
||||
className="flex gap-4 items-center bg-surface border border-subtle rounded-2xl p-4"
|
||||
>
|
||||
<img
|
||||
src={machine.image}
|
||||
alt={machine.name}
|
||||
className="w-24 h-24 object-contain shrink-0"
|
||||
onClick={() => setQuickViewMachine(machine)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-1.5 min-w-0 flex-1">
|
||||
<span className="text-brand-400 text-xs font-semibold tracking-wide">
|
||||
{machine.shortLabel}
|
||||
</span>
|
||||
<h3 className="text-lg font-bold text-primary truncate">
|
||||
{machine.name}
|
||||
</h3>
|
||||
<p className="text-sm text-secondary line-clamp-2">
|
||||
{machine.description}
|
||||
</p>
|
||||
<p className="text-base font-bold text-primary mt-1">
|
||||
{machine.priceLabel}
|
||||
</p>
|
||||
|
||||
<div className="flex justify-start items-center mt-3">
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
icon={<ShoppingCartOutlined/>}
|
||||
onClick={() => handleAddToCart(machine)}
|
||||
>
|
||||
{t.machineActions.addToCart}
|
||||
</Button>
|
||||
{t.machines.map(machine => (
|
||||
<article key={machine.id} className="bg-surface border border-subtle rounded-2xl p-4">
|
||||
<Link href={`/products/${machine.id}`} className="flex gap-4 items-center">
|
||||
<img src={machine.image} alt={machine.name} className="w-24 h-28 object-contain shrink-0"/>
|
||||
<div className="min-w-0">
|
||||
<span className="text-brand-400 text-xs font-semibold">{machine.shortLabel}</span>
|
||||
<h3 className="text-lg font-bold text-primary mt-1">{machine.name}</h3>
|
||||
<p className="text-sm text-secondary line-clamp-2 mt-2">{machine.description}</p>
|
||||
</div>
|
||||
</Link>
|
||||
<p className="text-base font-bold text-primary my-4">{machine.priceLabel}</p>
|
||||
<div className="flex flex-wrap justify-between items-center gap-3">
|
||||
<QuantityControl product={machine}/>
|
||||
<Link href={`/products/${machine.id}`} className="text-sm text-brand-400 hover:underline">
|
||||
{commerceCopy[locale].details}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import {useState} from 'react';
|
||||
import Link from 'next/link';
|
||||
import {CheckCircleOutlined} from '@ant-design/icons';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {commerceCopy} from '@/data/commerce';
|
||||
import QuantityControl from './QuantityControl';
|
||||
|
||||
export default function ProductDetails({id}: {id: string}) {
|
||||
const {t, locale} = useLanguage();
|
||||
const copy = commerceCopy[locale];
|
||||
const machine = t.machines.find(item => item.id === id);
|
||||
const [alternate, setAlternate] = useState(false);
|
||||
if (!machine) return null;
|
||||
|
||||
return (
|
||||
<main className="max-w-7xl mx-auto px-5 md:px-8 pt-28 pb-20">
|
||||
<nav aria-label={copy.details} className="text-sm text-secondary flex flex-wrap gap-3 mb-8">
|
||||
<Link href="/" className="hover:text-brand-400">{t.nav.home}</Link>
|
||||
<span aria-hidden="true">/</span>
|
||||
<Link href="/#showcase" className="hover:text-brand-400">{t.nav.showcase}</Link>
|
||||
<span aria-hidden="true">/</span><span>{machine.shortLabel}</span>
|
||||
</nav>
|
||||
<div className="grid md:grid-cols-2 gap-10 lg:gap-16 items-start">
|
||||
<section aria-label={copy.gallery} className="rounded-3xl border border-subtle bg-surface p-6">
|
||||
<img src={alternate ? machine.imageAlt : machine.image} alt={`${machine.name} — ${alternate ? copy.alternateImage : copy.baseImage}`}
|
||||
className="w-full h-72 md:h-[450px] object-contain"/>
|
||||
<div className="flex justify-center gap-3 mt-6">
|
||||
{[false, true].map(value => (
|
||||
<button key={String(value)} onClick={() => setAlternate(value)} aria-pressed={alternate === value}
|
||||
aria-label={value ? copy.alternateImage : copy.baseImage}
|
||||
className={`rounded-xl border p-2 ${alternate === value ? 'border-brand bg-brand/10' : 'border-subtle'}`}>
|
||||
<img src={value ? machine.imageAlt : machine.image} alt="" className="h-20 w-20 object-contain"/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<p className="text-brand-400 font-semibold">{machine.shortLabel}</p>
|
||||
<h1 className="text-3xl lg:text-4xl font-extrabold text-primary mt-3 leading-relaxed">{machine.name}</h1>
|
||||
<p className="text-secondary mt-5 leading-8">{machine.description}</p>
|
||||
<p className="text-sm text-secondary mt-4">{copy.model}: <span dir="ltr">{machine.id.toUpperCase()}</span></p>
|
||||
<h2 className="font-bold text-primary text-xl mt-8 mb-4">{copy.specifications}</h2>
|
||||
<ul className="space-y-3">
|
||||
{machine.highlights.map(feature => <li key={feature} className="flex gap-3 text-secondary"><CheckCircleOutlined className="text-brand-400"/>{feature}</li>)}
|
||||
</ul>
|
||||
<div className="border-t border-subtle mt-8 pt-6">
|
||||
<p className="text-sm text-secondary">{copy.unitPrice}</p>
|
||||
<p className="text-2xl font-bold text-primary mt-2">{machine.priceLabel}</p>
|
||||
<div className="mt-5 flex flex-wrap gap-4 items-center">
|
||||
<QuantityControl product={machine}/>
|
||||
<Link href="/cart" className="text-brand-400 font-semibold hover:underline">{copy.cartLink}</Link>
|
||||
</div>
|
||||
<p className="text-secondary text-sm leading-7 mt-5">{copy.checkoutUnavailable}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import {Button} from 'antd';
|
||||
import {MinusOutlined, PlusOutlined} from '@ant-design/icons';
|
||||
import type {CartProduct} from '@/types/domain';
|
||||
import {useAppDispatch, useAppSelector} from '@/store/hooks';
|
||||
import {addToCart, decrementQty} from '@/store/cartSlice';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {commerceCopy} from '@/data/commerce';
|
||||
|
||||
export default function QuantityControl({product}: {product: CartProduct}) {
|
||||
const dispatch = useAppDispatch();
|
||||
const {locale} = useLanguage();
|
||||
const copy = commerceCopy[locale];
|
||||
const quantity = useAppSelector(state => state.cart.items.find(item => item.id === product.id)?.qty ?? 0);
|
||||
|
||||
return (
|
||||
<div role="group" aria-label={`${copy.quantity}: ${product.name}`}
|
||||
className="inline-flex items-center gap-3 rounded-xl border border-subtle bg-page p-1" dir="ltr">
|
||||
<Button icon={<MinusOutlined/>} disabled={quantity === 0}
|
||||
aria-label={`${copy.decrease}: ${product.name}`}
|
||||
onClick={() => dispatch(decrementQty(product.id))}/>
|
||||
<span aria-live="polite" aria-atomic="true" className="min-w-8 text-center font-bold text-primary">
|
||||
{quantity.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US')}
|
||||
</span>
|
||||
<Button type="primary" icon={<PlusOutlined/>}
|
||||
aria-label={`${copy.increase}: ${product.name}`}
|
||||
onClick={() => dispatch(addToCart({id: product.id, name: product.name, image: product.image,
|
||||
priceLabel: product.priceLabel, priceNumeric: product.priceNumeric}))}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,17 +4,15 @@
|
||||
import type {Machine} from '@/types/domain';
|
||||
import type {MouseEvent} from 'react';
|
||||
import {useRef} from 'react';
|
||||
import {Button, message} from 'antd';
|
||||
import {ShoppingCartOutlined, EyeOutlined} from '@ant-design/icons';
|
||||
import {useAppDispatch} from '@/store/hooks';
|
||||
import {addToCart} from '@/store/cartSlice';
|
||||
import Link from 'next/link';
|
||||
import QuantityControl from './QuantityControl';
|
||||
import {commerceCopy} from '@/data/commerce';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import FloatingItems from './FloatingItems';
|
||||
import {floatingSnacks} from '@/data/content';
|
||||
|
||||
export default function VendingCard({machine, onQuickView}: {machine: Machine; onQuickView?: (machine: Machine) => void}) {
|
||||
const dispatch = useAppDispatch();
|
||||
const {t, dir} = useLanguage();
|
||||
export default function VendingCard({machine}: {machine: Machine}) {
|
||||
const {t, dir, locale} = useLanguage();
|
||||
const stageRef = useRef<HTMLDivElement>(null);
|
||||
const lensRef = useRef<HTMLDivElement>(null);
|
||||
const revealRef = useRef<HTMLImageElement>(null);
|
||||
@@ -38,19 +36,6 @@ export default function VendingCard({machine, onQuickView}: {machine: Machine; o
|
||||
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 (
|
||||
<div className="horizontal-panel flex items-center justify-center px-6 md:px-16" dir={dir}>
|
||||
<div className="relative z-10 max-w-6xl w-full grid md:grid-cols-2 gap-0 md:gap-10 items-center">
|
||||
@@ -103,12 +88,10 @@ export default function VendingCard({machine, onQuickView}: {machine: Machine; o
|
||||
<p className="text-xl md:text-2xl font-bold text-primary mt-2">{machine.priceLabel}</p>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 mt-2 ">
|
||||
{/* <Button type="primary" size="large" icon={<ShoppingCartOutlined/>} onClick={handleAddToCart}>
|
||||
{t.machineActions.addToCart}
|
||||
</Button>*/}
|
||||
<Button size="large" icon={<EyeOutlined/>} onClick={() => onQuickView?.(machine)}>
|
||||
{t.machineActions.quickView}
|
||||
</Button>
|
||||
<QuantityControl product={machine}/>
|
||||
<Link href={`/products/${machine.id}`} className="px-5 py-3 rounded-xl border border-subtle text-primary hover:text-brand-400">
|
||||
{commerceCopy[locale].details}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type {Locale} from '@/types/domain';
|
||||
|
||||
const fa = {
|
||||
details: 'جزئیات محصول',
|
||||
increase: 'افزایش تعداد',
|
||||
decrease: 'کاهش تعداد',
|
||||
quantity: 'تعداد',
|
||||
remove: 'حذف از سبد',
|
||||
continueShopping: 'مشاهده دستگاهها',
|
||||
cartDescription: 'دستگاههای انتخابشده و تعداد آنها را اینجا بررسی کنید.',
|
||||
summary: 'خلاصه سبد خرید',
|
||||
unitPrice: 'قیمت هر دستگاه',
|
||||
lineTotal: 'جمع این دستگاه',
|
||||
currency: 'ریال',
|
||||
cartLink: 'مشاهده سبد خرید',
|
||||
specifications: 'ویژگیهای دستگاه',
|
||||
model: 'مدل دستگاه',
|
||||
gallery: 'تصاویر دستگاه',
|
||||
baseImage: 'نمای اصلی',
|
||||
alternateImage: 'نمای جایگزین',
|
||||
developmentTitle: 'روبات مارکت در حال توسعه است',
|
||||
developmentMessage: 'به روبات مارکت خوش آمدید. سایت در حال توسعه است و هنوز تمام بخشهای آن تکمیل نشدهاند. در حال حاضر میتوانید دستگاهها را بررسی و به سبد خرید اضافه کنید، اما ثبت نهایی سفارش و پرداخت آنلاین فعال نیست.',
|
||||
acknowledge: 'متوجه شدم، ادامه میدهم',
|
||||
checkoutUnavailable: 'ثبت نهایی سفارش و پرداخت آنلاین هنوز فعال نیست.',
|
||||
};
|
||||
|
||||
export const commerceCopy: Record<Locale, Record<keyof typeof fa, string>> = {
|
||||
fa,
|
||||
en: {
|
||||
details: 'Product details',
|
||||
increase: 'Increase quantity',
|
||||
decrease: 'Decrease quantity',
|
||||
quantity: 'Quantity',
|
||||
remove: 'Remove from cart',
|
||||
continueShopping: 'Browse machines',
|
||||
cartDescription: 'Review your selected machines and adjust their quantities.',
|
||||
summary: 'Cart summary',
|
||||
unitPrice: 'Unit price',
|
||||
lineTotal: 'Item total',
|
||||
currency: 'Rials',
|
||||
cartLink: 'View cart',
|
||||
specifications: 'Machine features',
|
||||
model: 'Machine model',
|
||||
gallery: 'Machine images',
|
||||
baseImage: 'Main view',
|
||||
alternateImage: 'Alternate view',
|
||||
developmentTitle: 'Robot Market is under development',
|
||||
developmentMessage: 'Welcome to Robot Market. Our website is still under development and some features are not yet complete. You can explore machines and add them to your cart, but order submission and online payment are not available yet.',
|
||||
acknowledge: 'Got it, continue',
|
||||
checkoutUnavailable: 'Order submission and online payment are not available yet.',
|
||||
},
|
||||
};
|
||||
@@ -138,7 +138,6 @@ export const content: Record<Locale, SiteContent> = {
|
||||
},
|
||||
machineActions: {
|
||||
addToCart: 'افزودن به سبد خرید',
|
||||
quickView: 'مشاهده سریع',
|
||||
wishlistAdd: 'افزودن به علاقهمندیها',
|
||||
wishlistRemove: 'حذف از علاقهمندیها',
|
||||
hoverHint: 'نشانگر را روی دستگاه حرکت دهید',
|
||||
@@ -304,7 +303,6 @@ export const content: Record<Locale, SiteContent> = {
|
||||
},
|
||||
machineActions: {
|
||||
addToCart: 'Add to Cart',
|
||||
quickView: 'Quick View',
|
||||
wishlistAdd: 'Add to Wishlist',
|
||||
wishlistRemove: 'Remove from Wishlist',
|
||||
hoverHint: 'Move your cursor over the machine',
|
||||
|
||||
+5
-16
@@ -1,17 +1,19 @@
|
||||
import type {PayloadAction} from '@reduxjs/toolkit';
|
||||
import type {CartProduct, CartState} from '@/types/domain';
|
||||
import type {CartItem, CartProduct, CartState} from '@/types/domain';
|
||||
import type {RootState} from './store';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const initialState: CartState = {
|
||||
items: [], // { id, name, priceNumeric, priceLabel, qty, image }
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
|
||||
const cartSlice = createSlice({
|
||||
name: 'cart',
|
||||
initialState,
|
||||
reducers: {
|
||||
restoreCart: (state, action: PayloadAction<CartItem[]>) => {
|
||||
state.items = action.payload;
|
||||
},
|
||||
addToCart: (state, action: PayloadAction<CartProduct>) => {
|
||||
const product = action.payload;
|
||||
const existing = state.items.find((item) => item.id === product.id);
|
||||
@@ -20,7 +22,6 @@ const cartSlice = createSlice({
|
||||
} else {
|
||||
state.items.push({ ...product, qty: 1 });
|
||||
}
|
||||
state.isDrawerOpen = true;
|
||||
},
|
||||
removeFromCart: (state, action: PayloadAction<string>) => {
|
||||
state.items = state.items.filter((item) => item.id !== action.payload);
|
||||
@@ -46,28 +47,17 @@ const cartSlice = createSlice({
|
||||
clearCart: (state) => {
|
||||
state.items = [];
|
||||
},
|
||||
openDrawer: (state) => {
|
||||
state.isDrawerOpen = true;
|
||||
},
|
||||
closeDrawer: (state) => {
|
||||
state.isDrawerOpen = false;
|
||||
},
|
||||
toggleDrawer: (state) => {
|
||||
state.isDrawerOpen = !state.isDrawerOpen;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
restoreCart,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
setQuantity,
|
||||
incrementQty,
|
||||
decrementQty,
|
||||
clearCart,
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
toggleDrawer,
|
||||
} = cartSlice.actions;
|
||||
|
||||
// Selectors
|
||||
@@ -76,6 +66,5 @@ export const selectCartCount = (state: RootState) =>
|
||||
state.cart.items.reduce((sum, item) => sum + item.qty, 0);
|
||||
export const selectCartTotal = (state: RootState) =>
|
||||
state.cart.items.reduce((sum, item) => sum + item.qty * item.priceNumeric, 0);
|
||||
export const selectIsDrawerOpen = (state: RootState) => state.cart.isDrawerOpen;
|
||||
|
||||
export default cartSlice.reducer;
|
||||
|
||||
+1
-2
@@ -17,7 +17,7 @@ export interface Machine {
|
||||
|
||||
export type CartProduct = Pick<Machine, 'id' | 'name' | 'priceNumeric' | 'priceLabel' | 'image'>;
|
||||
export interface CartItem extends CartProduct { qty: number }
|
||||
export interface CartState { items: CartItem[]; isDrawerOpen: boolean }
|
||||
export interface CartState { items: CartItem[] }
|
||||
|
||||
export interface FloatingSnack {
|
||||
id: string;
|
||||
@@ -59,7 +59,6 @@ export interface SiteContent {
|
||||
};
|
||||
machineActions: {
|
||||
addToCart: string;
|
||||
quickView: string;
|
||||
wishlistAdd: string;
|
||||
wishlistRemove: string;
|
||||
hoverHint: string;
|
||||
|
||||
Reference in New Issue
Block a user