refactor: migrate project to TypeScript and translate comments to English
Build & Deploy (develop) / build-and-deploy (push) Successful in 2m23s
Build & Deploy (develop) / build-and-deploy (push) Successful in 2m23s
This commit is contained in:
@@ -4,3 +4,4 @@ out/
|
|||||||
.env*.local
|
.env*.local
|
||||||
.DS_Store
|
.DS_Store
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
*.tsbuildinfo
|
||||||
|
|||||||
Generated
+77
-33
@@ -4,8 +4,38 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="bb7b50a5-be9c-4cd7-a05b-979f0e91b173" name="Changes" comment="feat: add landing page device images and fix machine branding">
|
<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" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/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" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -93,36 +123,36 @@
|
|||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent">{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true",
|
"ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true",
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
||||||
"git-widget-placeholder": "develop",
|
"git-widget-placeholder": "develop",
|
||||||
"javascript.nodejs.core.library.configured.version": "24.20.0",
|
"javascript.nodejs.core.library.configured.version": "24.20.0",
|
||||||
"javascript.nodejs.core.library.typings.version": "24.13.3",
|
"javascript.nodejs.core.library.typings.version": "24.13.3",
|
||||||
"javascript.preferred.runtime.type.id": "node",
|
"javascript.preferred.runtime.type.id": "node",
|
||||||
"js.debugger.nextJs.config.created.client": "true",
|
"js.debugger.nextJs.config.created.client": "true",
|
||||||
"js.debugger.nextJs.config.created.server": "true",
|
"js.debugger.nextJs.config.created.server": "true",
|
||||||
"junie.onboarding.icon.badge.shown": "true",
|
"junie.onboarding.icon.badge.shown": "true",
|
||||||
"kronk.content.tree.expanded.paths": "",
|
"kronk.content.tree.expanded.paths": "",
|
||||||
"last_opened_file_path": "/home/hamed/Documents/Projects/GItLabRepo/device_web",
|
"last_opened_file_path": "/home/hamed/Documents/Projects/GItLabRepo/device_web",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"settings.editor.selected.configurable": "editor.preferences.fonts.default",
|
"settings.editor.selected.configurable": "editor.preferences.fonts.default",
|
||||||
"to.speed.mode.migration.done": "true",
|
"to.speed.mode.migration.done": "true",
|
||||||
"ts.external.directory.path": "C:\\Program Files\\JetBrains\\WebStorm 2026.1.1\\plugins\\javascript-plugin\\jsLanguageServicesImpl\\external",
|
"ts.external.directory.path": "/home/hamed/Documents/Projects/GIthubRepo/robot-market-landing/node_modules/typescript/lib",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
}</component>
|
}]]></component>
|
||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
<recent name="D:\Project\GitHubRepo\robot-market-landing\public\images\icons" />
|
<recent name="D:\Project\GitHubRepo\robot-market-landing\public\images\icons" />
|
||||||
@@ -204,7 +234,7 @@
|
|||||||
<workItem from="1789362025574" duration="23000" />
|
<workItem from="1789362025574" duration="23000" />
|
||||||
<workItem from="1789362137185" duration="377000" />
|
<workItem from="1789362137185" duration="377000" />
|
||||||
<workItem from="1789362644534" duration="6512000" />
|
<workItem from="1789362644534" duration="6512000" />
|
||||||
<workItem from="1789383269194" duration="679000" />
|
<workItem from="1789383269194" duration="1732000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="update">
|
<task id="LOCAL-00001" summary="update">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -470,11 +500,24 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1789369799782</updated>
|
<updated>1789369799782</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="34" />
|
<task id="LOCAL-00034" summary="refactor: remove disabled home sections and their unused assets">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1789384147873</created>
|
||||||
|
<option name="number" value="00034" />
|
||||||
|
<option name="presentableId" value="LOCAL-00034" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1789384147873</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="35" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
<option name="version" value="3" />
|
<option name="version" value="3" />
|
||||||
|
<option name="exactExcludedFiles">
|
||||||
|
<list>
|
||||||
|
<option value="$PROJECT_DIR$/components/AboutSection.jsx" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="UnknownFeatures">
|
<component name="UnknownFeatures">
|
||||||
<option featureType="com.intellij.fileTypeFactory" implementationName="next.config.js" />
|
<option featureType="com.intellij.fileTypeFactory" implementationName="next.config.js" />
|
||||||
@@ -486,6 +529,7 @@
|
|||||||
<MESSAGE value="initial new branch" />
|
<MESSAGE value="initial new branch" />
|
||||||
<MESSAGE value="update" />
|
<MESSAGE value="update" />
|
||||||
<MESSAGE value="feat: add landing page device images and fix machine branding" />
|
<MESSAGE value="feat: add landing page device images and fix machine branding" />
|
||||||
<option name="LAST_COMMIT_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" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# روبات مارکت — Vending Machine Showcase
|
# روبات مارکت — Vending Machine Showcase
|
||||||
|
|
||||||
صفحه معرفی دستگاههای وندینگ روبات مارکت، ساختهشده با Next.js، React و Ant Design.
|
صفحه معرفی دستگاههای وندینگ روبات مارکت، ساختهشده با Next.js، TypeScript، React و Ant Design.
|
||||||
|
|
||||||
## اجرا
|
## اجرا
|
||||||
|
|
||||||
@@ -11,6 +11,15 @@ npm run dev
|
|||||||
|
|
||||||
سایت در http://localhost:3000 در دسترس است.
|
سایت در http://localhost:3000 در دسترس است.
|
||||||
|
|
||||||
|
## بررسی نوعها
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
کد برنامه با TypeScript و حالت strict بررسی میشود. نوعهای مشترک در `types/domain.ts` و هوکهای Redux در `store/hooks.ts` قرار دارند.
|
||||||
|
تنظیمات Next.js و PostCSS برای سازگاری با ابزارهای فعلی به JavaScript باقی ماندهاند؛ تنظیمات Tailwind در `tailwind.config.ts` است.
|
||||||
|
|
||||||
## ساخت نسخه نهایی
|
## ساخت نسخه نهایی
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -22,15 +31,15 @@ npm start
|
|||||||
|
|
||||||
## ساختار و بخشهای فعال
|
## ساختار و بخشهای فعال
|
||||||
|
|
||||||
- `app/page.jsx`: صفحه اصلی شامل هدر، سبد خرید، هیرو، ویترین دستگاهها، مزایا، درباره ما، محلهای مناسب نصب و فوتر.
|
- `app/page.tsx`: صفحه اصلی شامل هدر، سبد خرید، هیرو، ویترین دستگاهها، مزایا، درباره ما، محلهای مناسب نصب و فوتر.
|
||||||
- `components/HeroSection.jsx`: اسلایدر تصاویر دستگاهها.
|
- `components/HeroSection.tsx`: اسلایدر تصاویر دستگاهها.
|
||||||
- `components/ShowcaseSection.jsx`: انتخاب نمایش موبایل یا ویترین افقی دسکتاپ.
|
- `components/ShowcaseSection.tsx`: انتخاب نمایش موبایل یا ویترین افقی دسکتاپ.
|
||||||
- `components/HorizontalShowcase.jsx`: ویترین افقی با GSAP و ویدیوی پسزمینه وابسته به اسکرول.
|
- `components/HorizontalShowcase.tsx`: ویترین افقی با GSAP و ویدیوی پسزمینه وابسته به اسکرول.
|
||||||
- `components/MobileShowcase.jsx`: فهرست دستگاهها در موبایل.
|
- `components/MobileShowcase.tsx`: فهرست دستگاهها در موبایل.
|
||||||
- `components/VendingCard.jsx` و `components/FloatingItems.jsx`: نمایش دستگاه و جلوههای تصویری.
|
- `components/VendingCard.tsx` و `components/FloatingItems.tsx`: نمایش دستگاه و جلوههای تصویری.
|
||||||
- `components/CartDrawer.jsx` و `store/`: سبد خرید و مدیریت تعداد دستگاهها با Redux.
|
- `components/CartDrawer.tsx` و `store/`: سبد خرید و مدیریت تعداد دستگاهها با Redux.
|
||||||
- `data/content.js`: محتوای فارسی و انگلیسی، مشخصات و قیمت دستگاهها و مسیر رسانهها.
|
- `data/content.ts`: محتوای فارسی و انگلیسی، مشخصات و قیمت دستگاهها و مسیر رسانهها.
|
||||||
- `app/context/` و `app/providers.jsx`: زبان، جهت صفحه، تم و تنظیمات Ant Design.
|
- `app/context/` و `app/providers.tsx`: زبان، جهت صفحه، تم و تنظیمات Ant Design.
|
||||||
- `app/globals.css`: رنگها و استایلهای مشترک.
|
- `app/globals.css`: رنگها و استایلهای مشترک.
|
||||||
- `public/`: تصاویر دستگاهها، اقلام شناور، گیفها و ویدیوی ویترین.
|
- `public/`: تصاویر دستگاهها، اقلام شناور، گیفها و ویدیوی ویترین.
|
||||||
|
|
||||||
@@ -43,14 +52,14 @@ npm start
|
|||||||
|
|
||||||
دکمه ورود به https://panel.my-rm.com متصل است.
|
دکمه ورود به https://panel.my-rm.com متصل است.
|
||||||
سبد خرید در حافظه نگهداری میشود و با بارگذاری مجدد صفحه پاک میشود.
|
سبد خرید در حافظه نگهداری میشود و با بارگذاری مجدد صفحه پاک میشود.
|
||||||
پرداخت هنوز متصل نیست؛ دکمه پرداخت در `CartDrawer.jsx` پیام نمایشی نشان میدهد.
|
پرداخت هنوز متصل نیست؛ دکمه پرداخت در `CartDrawer.tsx` پیام نمایشی نشان میدهد.
|
||||||
|
|
||||||
## تنظیم API
|
## تنظیم API
|
||||||
|
|
||||||
برای تعیین آدرس بکاند، از `.env.local.example` یک فایل `.env.local` بسازید و
|
برای تعیین آدرس بکاند، از `.env.local.example` یک فایل `.env.local` بسازید و
|
||||||
`NEXT_PUBLIC_API_BASE_URL` را تنظیم کنید. کلاینت مشترک API در `lib/axios.js` قرار دارد.
|
`NEXT_PUBLIC_API_BASE_URL` را تنظیم کنید. کلاینت مشترک API در `lib/axios.ts` قرار دارد.
|
||||||
|
|
||||||
## جایگزینی رسانهها
|
## جایگزینی رسانهها
|
||||||
|
|
||||||
فایلهای واقعی را با مسیرهای موجود در `data/content.js` جایگزین کنید یا مسیرها را در همان فایل تغییر دهید.
|
فایلهای واقعی را با مسیرهای موجود در `data/content.ts` جایگزین کنید یا مسیرها را در همان فایل تغییر دهید.
|
||||||
ویدیوی پسزمینه ویترین از `public/video/showcase-placeholder.mp4` خوانده میشود.
|
ویدیوی پسزمینه ویترین از `public/video/showcase-placeholder.mp4` خوانده میشود.
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import type {Dispatch, SetStateAction, ReactNode} from 'react';
|
||||||
|
import type {Locale, SiteContent, Direction} from '@/types/domain';
|
||||||
import {createContext, useContext, useEffect, useState} from 'react';
|
import {createContext, useContext, useEffect, useState} from 'react';
|
||||||
import {content} from '@/data/content';
|
import {content} from '@/data/content';
|
||||||
|
|
||||||
const LanguageContext = createContext(null);
|
interface LanguageContextValue {
|
||||||
|
locale: Locale;
|
||||||
|
setLocale: Dispatch<SetStateAction<Locale>>;
|
||||||
|
toggleLocale: () => void;
|
||||||
|
t: SiteContent;
|
||||||
|
dir: Direction;
|
||||||
|
mounted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function LanguageProvider({children}) {
|
const LanguageContext = createContext<LanguageContextValue | null>(null);
|
||||||
const [locale, setLocale] = useState('fa');
|
|
||||||
|
export function LanguageProvider({children}: {children: ReactNode}) {
|
||||||
|
const [locale, setLocale] = useState<Locale>('fa');
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1,12 +1,21 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import type {Dispatch, SetStateAction, ReactNode} from 'react';
|
||||||
|
import type {Theme} from '@/types/domain';
|
||||||
import {createContext, useContext, useEffect, useState} from 'react';
|
import {createContext, useContext, useEffect, useState} from 'react';
|
||||||
|
|
||||||
const ThemeContext = createContext(null);
|
interface ThemeContextValue {
|
||||||
|
theme: Theme;
|
||||||
|
setTheme: Dispatch<SetStateAction<Theme>>;
|
||||||
|
toggleTheme: () => void;
|
||||||
|
mounted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function ThemeProvider({children}) {
|
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||||
|
|
||||||
const [theme, setTheme] = useState('light');
|
export function ThemeProvider({children}: {children: ReactNode}) {
|
||||||
|
|
||||||
|
const [theme, setTheme] = useState<Theme>('light');
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
@@ -42,4 +51,3 @@ export function useTheme() {
|
|||||||
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
|
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
|
||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type {Metadata, Viewport} from 'next';
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
import {Vazirmatn} from 'next/font/google';
|
import {Vazirmatn} from 'next/font/google';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
import Providers from './providers';
|
import Providers from './providers';
|
||||||
@@ -9,19 +11,19 @@ const vazirmatn = Vazirmatn({
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'روبات مارکت | دستگاههای وندینگ هوشمند',
|
title: 'روبات مارکت | دستگاههای وندینگ هوشمند',
|
||||||
description:
|
description:
|
||||||
'روبات مارکت؛ دستگاههای فروش خودکار هوشمند با مدیریت آنلاین، تحویل نرم و طراحی مدرن.',
|
'روبات مارکت؛ دستگاههای فروش خودکار هوشمند با مدیریت آنلاین، تحویل نرم و طراحی مدرن.',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const viewport = {
|
export const viewport: Viewport = {
|
||||||
themeColor: '#00a693',
|
themeColor: '#00a693',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default function RootLayout({children}) {
|
export default function RootLayout({children}: {children: ReactNode}) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="fa" dir="rtl" className={`${vazirmatn.variable} light`} suppressHydrationWarning>
|
<html lang="fa" dir="rtl" className={`${vazirmatn.variable} light`} suppressHydrationWarning>
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// پل ارتباطی بین Redux، حالت تیره/روشن، و زبان فا/en با تنظیمات ConfigProvider آنتدیزاین.
|
import type {ReactNode} from 'react';
|
||||||
|
// Connect Redux, language, and theme settings to Ant Design.
|
||||||
|
|
||||||
import {Provider as ReduxProvider} from 'react-redux';
|
import {Provider as ReduxProvider} from 'react-redux';
|
||||||
|
import type {ThemeConfig} from 'antd';
|
||||||
import {ConfigProvider, theme as antdThemeApi} from 'antd';
|
import {ConfigProvider, theme as antdThemeApi} from 'antd';
|
||||||
import faIR from 'antd/locale/fa_IR';
|
import faIR from 'antd/locale/fa_IR';
|
||||||
import enUS from 'antd/locale/en_US';
|
import enUS from 'antd/locale/en_US';
|
||||||
@@ -10,11 +12,11 @@ import {store} from '@/store/store';
|
|||||||
import {ThemeProvider, useTheme} from './context/ThemeContext';
|
import {ThemeProvider, useTheme} from './context/ThemeContext';
|
||||||
import {LanguageProvider, useLanguage} from './context/LanguageContext';
|
import {LanguageProvider, useLanguage} from './context/LanguageContext';
|
||||||
|
|
||||||
function AntdBridge({children}) {
|
function AntdBridge({children}: {children: ReactNode}) {
|
||||||
const {theme} = useTheme();
|
const {theme} = useTheme();
|
||||||
const {locale, dir} = useLanguage();
|
const {locale, dir} = useLanguage();
|
||||||
|
|
||||||
const antdTheme = {
|
const antdTheme: ThemeConfig = {
|
||||||
algorithm: theme === 'dark' ? antdThemeApi.darkAlgorithm : antdThemeApi.defaultAlgorithm,
|
algorithm: theme === 'dark' ? antdThemeApi.darkAlgorithm : antdThemeApi.defaultAlgorithm,
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: '#00a693',
|
colorPrimary: '#00a693',
|
||||||
@@ -48,7 +50,7 @@ function AntdBridge({children}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Providers({children}) {
|
export default function Providers({children}: {children: ReactNode}) {
|
||||||
return (
|
return (
|
||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// سکشن معرفی شرکت با متن بلند؛ بهخاطر طولانی بودن متن، یک دکمه «ادامه مطلب» برای باز/بسته کردن دارد.
|
// Company introduction with optional expandable paragraphs.
|
||||||
|
|
||||||
import {useState} from 'react';
|
import {useState} from 'react';
|
||||||
import {useLanguage} from '@/app/context/LanguageContext';
|
import {useLanguage} from '@/app/context/LanguageContext';
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// کشوی (Drawer) سبد خرید: نمایش آیتمهای انتخابی، تغییر تعداد، جمع کل، و دکمه پرداخت (placeholder).
|
// Cart drawer with quantity editing, totals, and placeholder checkout.
|
||||||
|
|
||||||
import {Drawer, Button, Empty, InputNumber, message} from 'antd';
|
import {Drawer, Button, Empty, InputNumber, message} from 'antd';
|
||||||
import {DeleteOutlined} from '@ant-design/icons';
|
import {DeleteOutlined} from '@ant-design/icons';
|
||||||
import {useSelector, useDispatch} from 'react-redux';
|
import {useAppSelector, useAppDispatch} from '@/store/hooks';
|
||||||
import {
|
import {
|
||||||
selectCartItems,
|
selectCartItems,
|
||||||
selectCartTotal,
|
selectCartTotal,
|
||||||
@@ -14,17 +14,18 @@ import {
|
|||||||
setQuantity,
|
setQuantity,
|
||||||
} from '@/store/cartSlice';
|
} from '@/store/cartSlice';
|
||||||
import {useLanguage} from '@/app/context/LanguageContext';
|
import {useLanguage} from '@/app/context/LanguageContext';
|
||||||
|
import type {Locale} from '@/types/domain';
|
||||||
import api from "@/lib/axios";
|
import api from "@/lib/axios";
|
||||||
|
|
||||||
function toLocaleNumber(n, locale) {
|
function toLocaleNumber(n: number, locale: Locale) {
|
||||||
return n.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US');
|
return n.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CartDrawer() {
|
export default function CartDrawer() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isOpen = useSelector(selectIsDrawerOpen);
|
const isOpen = useAppSelector(selectIsDrawerOpen);
|
||||||
const items = useSelector(selectCartItems);
|
const items = useAppSelector(selectCartItems);
|
||||||
const total = useSelector(selectCartTotal);
|
const total = useAppSelector(selectCartTotal);
|
||||||
const {t, locale, dir} = useLanguage();
|
const {t, locale, dir} = useLanguage();
|
||||||
|
|
||||||
const currencySuffix = locale === 'fa' ? ' ریال' : ' Rials';
|
const currencySuffix = locale === 'fa' ? ' ریال' : ' Rials';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// نوار مزایا: ضمانت کالا، مشاوره رایگان، خرید اقساطی، بستهبندی ایمن.
|
// Service benefits: warranty, consultation, installments, and packaging.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SafetyCertificateOutlined,
|
SafetyCertificateOutlined,
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// رندر آیتمهای شناور (چیپس/نوشابه) دور دستگاه با حرکت تصادفی و ملایم GSAP.
|
// Animate decorative snack images with gentle randomized motion.
|
||||||
|
|
||||||
|
import type {FloatingSnack} from '@/types/domain';
|
||||||
import {useEffect, useRef} from 'react';
|
import {useEffect, useRef} from 'react';
|
||||||
import {gsap} from 'gsap';
|
import {gsap} from 'gsap';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the centralized `floatingSnacks` config as absolutely-positioned
|
* Renders the centralized `floatingSnacks` config as absolutely-positioned
|
||||||
* images and gives each one a gentle, randomized floating drift via GSAP.
|
* images and gives each one a gentle, randomized floating drift via GSAP.
|
||||||
* Positions/sizes/images all come from data/content.js — edit there only.
|
* Positions/sizes/images all come from data/content.ts — edit there only.
|
||||||
*/
|
*/
|
||||||
export default function FloatingItems({items}) {
|
export default function FloatingItems({items}: {items: FloatingSnack[]}) {
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nodes = containerRef.current?.querySelectorAll('[data-float-item]');
|
const nodes = containerRef.current?.querySelectorAll('[data-float-item]');
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// فوتر سایت: درباره ما کوتاه، لینکهای شبکه اجتماعی، دسترسی سریع، و اطلاعات تماس/آدرس.
|
// Company information, quick links, social links, and contact details.
|
||||||
|
|
||||||
import { PhoneOutlined, EnvironmentOutlined } from '@ant-design/icons';
|
import { PhoneOutlined, EnvironmentOutlined } from '@ant-design/icons';
|
||||||
import { useLanguage } from '@/app/context/LanguageContext';
|
import { useLanguage } from '@/app/context/LanguageContext';
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// هدر ثابت بالای صفحه: لوگو، منوی ناوبری، سوییچ زبان/تم، دکمه ورود به پنل کاربری و آیکون سبد خرید.
|
// Fixed navigation with language, theme, account, and cart controls.
|
||||||
|
|
||||||
import {useState, useEffect} from 'react';
|
import {useState, useEffect} from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import {Badge, Button} from 'antd';
|
import {Badge, Button} from 'antd';
|
||||||
import {ShoppingCartOutlined, MenuOutlined, CloseOutlined, UserOutlined} from '@ant-design/icons';
|
import {ShoppingCartOutlined, MenuOutlined, CloseOutlined, UserOutlined} from '@ant-design/icons';
|
||||||
import {useSelector, useDispatch} from 'react-redux';
|
import {useAppSelector, useAppDispatch} from '@/store/hooks';
|
||||||
import {selectCartCount, openDrawer} from '@/store/cartSlice';
|
import {selectCartCount, openDrawer} from '@/store/cartSlice';
|
||||||
import {useLanguage} from '@/app/context/LanguageContext';
|
import {useLanguage} from '@/app/context/LanguageContext';
|
||||||
import ThemeToggle from './ThemeToggle';
|
import ThemeToggle from './ThemeToggle';
|
||||||
import LanguageToggle from './LanguageToggle';
|
import LanguageToggle from './LanguageToggle';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const cartCount = useSelector(selectCartCount);
|
const cartCount = useAppSelector(selectCartCount);
|
||||||
const {t} = useLanguage();
|
const {t} = useLanguage();
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
@@ -1,25 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
/**
|
/** Full-width product carousel with autoplay and manual navigation. */
|
||||||
* سکشن اول سایت (Hero) — ایده کلی از صفحه وندینگ سایت Sielaff گرفته شده:
|
|
||||||
* یک اسلایدر تمامعرض که هر ۵ ثانیه بهصورت خودکار به اسلاید بعدی میرود،
|
|
||||||
* همراه با یک شکل مورب رنگی (بهجای قرمز آنها، رنگ سبز برند خودمان #00a693)،
|
|
||||||
* عنوان بزرگ دو خطی، و دکمههای فلش قبلی/بعدی برای ناوبری دستی.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||||
import {LeftOutlined, RightOutlined} from '@ant-design/icons';
|
import {LeftOutlined, RightOutlined} from '@ant-design/icons';
|
||||||
import {useLanguage} from '@/app/context/LanguageContext';
|
import {useLanguage} from '@/app/context/LanguageContext';
|
||||||
|
|
||||||
const AUTOPLAY_DELAY = 5000; // هر ۵ ثانیه اسلاید عوض میشود
|
const AUTOPLAY_DELAY = 5000; // Advance every five seconds.
|
||||||
|
|
||||||
export default function HeroSection() {
|
export default function HeroSection() {
|
||||||
const {t, dir} = useLanguage();
|
const {t, dir} = useLanguage();
|
||||||
const slides = t.hero.slides;
|
const slides = t.hero.slides;
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
const timerRef = useRef(null);
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
// رفتن به اسلاید بعدی (با چرخش به اول لیست وقتی به آخر رسید)
|
// Wrap around after the last slide.
|
||||||
const goToNext = useCallback(() => {
|
const goToNext = useCallback(() => {
|
||||||
setActiveIndex((prev) => (prev + 1) % slides.length);
|
setActiveIndex((prev) => (prev + 1) % slides.length);
|
||||||
}, [slides.length]);
|
}, [slides.length]);
|
||||||
@@ -28,9 +23,7 @@ export default function HeroSection() {
|
|||||||
setActiveIndex((prev) => (prev - 1 + slides.length) % slides.length);
|
setActiveIndex((prev) => (prev - 1 + slides.length) % slides.length);
|
||||||
}, [slides.length]);
|
}, [slides.length]);
|
||||||
|
|
||||||
// تایمر پخش خودکار: هر بار که اسلاید عوض میشود، تایمر قبلی پاک و یک تایمر
|
// Restart the autoplay timer after manual navigation.
|
||||||
// جدید ۵ ثانیهای ست میشود. با کلیک دستی روی فلشها هم همین تابع دوباره
|
|
||||||
// صدا زده میشود تا شمارش از نو شروع شود (تجربه کاربری بهتر).
|
|
||||||
const restartAutoplay = useCallback(() => {
|
const restartAutoplay = useCallback(() => {
|
||||||
if (timerRef.current) clearInterval(timerRef.current);
|
if (timerRef.current) clearInterval(timerRef.current);
|
||||||
timerRef.current = setInterval(goToNext, AUTOPLAY_DELAY);
|
timerRef.current = setInterval(goToNext, AUTOPLAY_DELAY);
|
||||||
@@ -43,10 +36,10 @@ export default function HeroSection() {
|
|||||||
};
|
};
|
||||||
}, [restartAutoplay]);
|
}, [restartAutoplay]);
|
||||||
|
|
||||||
const handleManualNav = (direction) => {
|
const handleManualNav = (direction: 'next' | 'prev') => {
|
||||||
if (direction === 'next') goToNext();
|
if (direction === 'next') goToNext();
|
||||||
else goToPrev();
|
else goToPrev();
|
||||||
restartAutoplay(); // شمارش ۵ ثانیه بعد از کلیک دستی از نو شروع شود
|
restartAutoplay(); // Reset the delay after a manual click.
|
||||||
};
|
};
|
||||||
|
|
||||||
const slide = slides[activeIndex];
|
const slide = slides[activeIndex];
|
||||||
@@ -54,7 +47,7 @@ export default function HeroSection() {
|
|||||||
return (
|
return (
|
||||||
<section id="hero" className="relative h-screen w-full overflow-hidden bg-page">
|
<section id="hero" className="relative h-screen w-full overflow-hidden bg-page">
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
{/* شکل مورب تزئینی سمت راست/چپ — نسخه سبز برند بهجای قرمز Sielaff */}
|
{/* Decorative brand-colored shape. */}
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-y-0 w-1/3 hidden md:block"
|
className="absolute inset-y-0 w-1/3 hidden md:block"
|
||||||
@@ -68,8 +61,8 @@ export default function HeroSection() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
{/* اسلایدهای عکس — همه اسلایدها روی هم قرار دارند و فقط با opacity */}
|
{/* Fade between overlapping product images. */}
|
||||||
{/* محو/نمایان میشوند (transition نرم بهجای پرش ناگهانی) */}
|
{/* Use opacity transitions for smooth slide changes. */}
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
{slides.map((s, i) => (
|
{slides.map((s, i) => (
|
||||||
@@ -88,7 +81,7 @@ export default function HeroSection() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* لایهی نیمهشفاف پشت متن، برای خوانایی روی هر عکسی */}
|
{/* Overlay for text readability. */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0"
|
className="absolute inset-0"
|
||||||
style={{
|
style={{
|
||||||
@@ -98,7 +91,7 @@ export default function HeroSection() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
{/* متن روی اسلاید: کیکر کوچک + عنوان بزرگ دو خطی + توضیح + دکمه CTA */}
|
{/* Active slide headline, description, and call to action. */}
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
<div
|
<div
|
||||||
className="relative z-10 h-full flex flex-col justify-end md:justify-end px-6 md:px-16 pb-28 md:pb-72">
|
className="relative z-10 h-full flex flex-col justify-end md:justify-end px-6 md:px-16 pb-28 md:pb-72">
|
||||||
@@ -124,7 +117,7 @@ export default function HeroSection() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
{/* فلشهای ناوبری دستی (قبلی / بعدی) — وسط دو طرف */}
|
{/* Previous and next slide controls. */}
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleManualNav('prev')}
|
onClick={() => handleManualNav('prev')}
|
||||||
@@ -141,7 +134,7 @@ export default function HeroSection() {
|
|||||||
<LeftOutlined/>
|
<LeftOutlined/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* نقطههای شمارشگر اسلاید پایین صفحه — کلیک روی هرکدام مستقیم به همان اسلاید میرود */}
|
{/* Jump directly to a slide using the indicators. */}
|
||||||
<div className="absolute bottom-8 inset-x-0 z-20 flex justify-center gap-2">
|
<div className="absolute bottom-8 inset-x-0 z-20 flex justify-center gap-2">
|
||||||
{slides.map((s, i) => (
|
{slides.map((s, i) => (
|
||||||
<button
|
<button
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import type {Machine} from '@/types/domain';
|
||||||
import {useEffect, useRef, useState} from 'react';
|
import {useEffect, useRef, useState} from 'react';
|
||||||
import {gsap} from 'gsap';
|
import {gsap} from 'gsap';
|
||||||
import {ScrollTrigger} from 'gsap/ScrollTrigger';
|
import {ScrollTrigger} from 'gsap/ScrollTrigger';
|
||||||
import {Modal, Button, message} from 'antd';
|
import {Modal, Button, message} from 'antd';
|
||||||
import {ShoppingCartOutlined} from '@ant-design/icons';
|
import {ShoppingCartOutlined} from '@ant-design/icons';
|
||||||
import {useDispatch} from 'react-redux';
|
import {useAppDispatch} from '@/store/hooks';
|
||||||
import VendingCard from './VendingCard';
|
import VendingCard from './VendingCard';
|
||||||
import {showcaseVideoSrc} from '@/data/content';
|
import {showcaseVideoSrc} from '@/data/content';
|
||||||
import {useLanguage} from '@/app/context/LanguageContext';
|
import {useLanguage} from '@/app/context/LanguageContext';
|
||||||
@@ -14,12 +15,12 @@ import {addToCart} from '@/store/cartSlice';
|
|||||||
gsap.registerPlugin(ScrollTrigger);
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
|
|
||||||
export default function HorizontalShowcase() {
|
export default function HorizontalShowcase() {
|
||||||
const sectionRef = useRef(null);
|
const sectionRef = useRef<HTMLElement>(null);
|
||||||
const trackRef = useRef(null);
|
const trackRef = useRef<HTMLDivElement>(null);
|
||||||
const videoRef = useRef(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const {t} = useLanguage();
|
const {t} = useLanguage();
|
||||||
const [quickViewMachine, setQuickViewMachine] = useState(null);
|
const [quickViewMachine, setQuickViewMachine] = useState<Machine | null>(null);
|
||||||
const [videoAvailable, setVideoAvailable] = useState(true);
|
const [videoAvailable, setVideoAvailable] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -33,10 +34,10 @@ export default function HorizontalShowcase() {
|
|||||||
Math.max(track.scrollWidth - window.innerWidth, 0)
|
Math.max(track.scrollWidth - window.innerWidth, 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
let animationFrameId = null;
|
let animationFrameId: number | null = null;
|
||||||
let latestProgress = 0;
|
let latestProgress = 0;
|
||||||
|
|
||||||
const syncVideo = (progress) => {
|
const syncVideo = (progress: number) => {
|
||||||
latestProgress = progress;
|
latestProgress = progress;
|
||||||
if (animationFrameId !== null) return;
|
if (animationFrameId !== null) return;
|
||||||
|
|
||||||
@@ -148,4 +149,4 @@ export default function HorizontalShowcase() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// دکمه سوییچ زبان فارسی/انگلیسی.
|
// Switch between Persian and English.
|
||||||
|
|
||||||
import { Button, Tooltip } from 'antd';
|
import { Button, Tooltip } from 'antd';
|
||||||
import { useLanguage } from '@/app/context/LanguageContext';
|
import { useLanguage } from '@/app/context/LanguageContext';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// گرید «مکان مناسب برای نصب» — هتلها، مدارس، داروخانهها، بیمارستانها، شیفت شب، محصولات تخصصی.
|
// Suggested installation locations for vending machines.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HomeOutlined,
|
HomeOutlined,
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import type {Machine} from '@/types/domain';
|
||||||
import {useState} from 'react';
|
import {useState} from 'react';
|
||||||
import {Modal, Button, message} from 'antd';
|
import {Modal, Button, message} from 'antd';
|
||||||
import {ShoppingCartOutlined, EyeOutlined} from '@ant-design/icons';
|
import {ShoppingCartOutlined, EyeOutlined} from '@ant-design/icons';
|
||||||
import {useDispatch} from 'react-redux';
|
import {useAppDispatch} from '@/store/hooks';
|
||||||
import {useLanguage} from '@/app/context/LanguageContext';
|
import {useLanguage} from '@/app/context/LanguageContext';
|
||||||
import {addToCart} from '@/store/cartSlice';
|
import {addToCart} from '@/store/cartSlice';
|
||||||
|
|
||||||
export default function MobileShowcase() {
|
export default function MobileShowcase() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const {t, dir} = useLanguage();
|
const {t, dir} = useLanguage();
|
||||||
const [quickViewMachine, setQuickViewMachine] = useState(null);
|
const [quickViewMachine, setQuickViewMachine] = useState<Machine | null>(null);
|
||||||
|
|
||||||
const handleAddToCart = (machine) => {
|
const handleAddToCart = (machine: Machine) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
addToCart({
|
addToCart({
|
||||||
id: machine.id,
|
id: machine.id,
|
||||||
@@ -104,4 +105,4 @@ export default function MobileShowcase() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -7,14 +7,14 @@ import MobileShowcase from './MobileShowcase';
|
|||||||
const MOBILE_BREAKPOINT = 768;
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
export default function ShowcaseSection() {
|
export default function ShowcaseSection() {
|
||||||
const [isMobile, setIsMobile] = useState(null);
|
const [isMobile, setIsMobile] = useState<boolean | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mediaQuery = window.matchMedia(
|
const mediaQuery = window.matchMedia(
|
||||||
`(max-width: ${MOBILE_BREAKPOINT - 1}px)`
|
`(max-width: ${MOBILE_BREAKPOINT - 1}px)`
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = (event) => setIsMobile(event.matches);
|
const handleChange = (event: MediaQueryListEvent) => setIsMobile(event.matches);
|
||||||
|
|
||||||
setIsMobile(mediaQuery.matches);
|
setIsMobile(mediaQuery.matches);
|
||||||
mediaQuery.addEventListener('change', handleChange);
|
mediaQuery.addEventListener('change', handleChange);
|
||||||
@@ -25,4 +25,4 @@ export default function ShowcaseSection() {
|
|||||||
if (isMobile === null) return null;
|
if (isMobile === null) return null;
|
||||||
|
|
||||||
return isMobile ? <MobileShowcase/> : <HorizontalShowcase/>;
|
return isMobile ? <MobileShowcase/> : <HorizontalShowcase/>;
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,25 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
|
||||||
|
import type {Machine} from '@/types/domain';
|
||||||
|
import type {MouseEvent} from 'react';
|
||||||
import {useRef} from 'react';
|
import {useRef} from 'react';
|
||||||
import {Button, message} from 'antd';
|
import {Button, message} from 'antd';
|
||||||
import {ShoppingCartOutlined, EyeOutlined} from '@ant-design/icons';
|
import {ShoppingCartOutlined, EyeOutlined} from '@ant-design/icons';
|
||||||
import {useDispatch} from 'react-redux';
|
import {useAppDispatch} from '@/store/hooks';
|
||||||
import {addToCart} from '@/store/cartSlice';
|
import {addToCart} from '@/store/cartSlice';
|
||||||
import {useLanguage} from '@/app/context/LanguageContext';
|
import {useLanguage} from '@/app/context/LanguageContext';
|
||||||
import FloatingItems from './FloatingItems';
|
import FloatingItems from './FloatingItems';
|
||||||
import {floatingSnacks} from '@/data/content';
|
import {floatingSnacks} from '@/data/content';
|
||||||
|
|
||||||
export default function VendingCard({machine, onQuickView}) {
|
export default function VendingCard({machine, onQuickView}: {machine: Machine; onQuickView?: (machine: Machine) => void}) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const {t, dir} = useLanguage();
|
const {t, dir} = useLanguage();
|
||||||
const stageRef = useRef(null);
|
const stageRef = useRef<HTMLDivElement>(null);
|
||||||
const lensRef = useRef(null);
|
const lensRef = useRef<HTMLDivElement>(null);
|
||||||
const revealRef = useRef(null);
|
const revealRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
const handleMouseMove = (e) => {
|
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||||
const stage = stageRef.current;
|
const stage = stageRef.current;
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
const rect = stage.getBoundingClientRect();
|
const rect = stage.getBoundingClientRect();
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import type {FloatingSnack, Locale, Machine, SiteContent} from '@/types/domain';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Single bilingual content source (fa/en). Every UI string and every piece of
|
// Single bilingual content source (fa/en). Every UI string and every piece of
|
||||||
// business copy lives here, per locale. Components read via useLanguage().
|
// business copy lives here, per locale. Components read via useLanguage().
|
||||||
// Asset paths (images/gifs/video) are locale-independent and stay the same.
|
// Asset paths (images/gifs/video) are locale-independent and stay the same.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const machinesFa = [
|
const machinesFa: Machine[] = [
|
||||||
{
|
{
|
||||||
id: 'rm-35',
|
id: 'rm-35',
|
||||||
name: 'روبات مارکت ۳۵ کانال',
|
name: 'روبات مارکت ۳۵ کانال',
|
||||||
@@ -43,7 +45,7 @@ const machinesFa = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const machinesEn = [
|
const machinesEn: Machine[] = [
|
||||||
{
|
{
|
||||||
id: 'rm-35',
|
id: 'rm-35',
|
||||||
name: 'Robot Market RM-35',
|
name: 'Robot Market RM-35',
|
||||||
@@ -82,7 +84,7 @@ const machinesEn = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const content = {
|
export const content: Record<Locale, SiteContent> = {
|
||||||
fa: {
|
fa: {
|
||||||
dir: 'rtl',
|
dir: 'rtl',
|
||||||
lang: 'fa',
|
lang: 'fa',
|
||||||
@@ -417,7 +419,7 @@ export const content = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Locale-independent asset config (positions/paths only — same in both languages).
|
// Locale-independent asset config (positions/paths only — same in both languages).
|
||||||
export const floatingSnacks = [
|
export const floatingSnacks: FloatingSnack[] = [
|
||||||
{id: 'chips-1', image: '/images/snacks/item-1.webp', size: 60, top: '5%', left: '-5%', duration: 5.5, delay: 0},
|
{id: 'chips-1', image: '/images/snacks/item-1.webp', size: 60, top: '5%', left: '-5%', duration: 5.5, delay: 0},
|
||||||
{id: 'soda-1', image: '/images/snacks/item-2.webp', size: 55, top: '68%', left: '2%', duration: 6.2, delay: 0.6},
|
{id: 'soda-1', image: '/images/snacks/item-2.webp', size: 55, top: '68%', left: '2%', duration: 6.2, delay: 0.6},
|
||||||
{id: 'chips-2', image: '/images/snacks/item-3.webp', size: 65, top: '10%', left: '85%', duration: 4.8, delay: 0.3},
|
{id: 'chips-2', image: '/images/snacks/item-3.webp', size: 65, top: '10%', left: '85%', duration: 4.8, delay: 0.3},
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./*"]
|
|
||||||
},
|
|
||||||
"jsx": "react"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -43,11 +43,11 @@ api.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status >= 500) {
|
if (status >= 500) {
|
||||||
// message.error('خطایی در سرور رخ داد. لطفاً بعداً تلاش کنید.');
|
// TODO: Display a server error message.
|
||||||
}
|
}
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
// درخواست ارسال شد ولی هیچ جوابی نیومد (قطعی اینترنت، سرور خاموش و ...)
|
// The request was sent, but no response arrived.
|
||||||
// TODO: message.error('اتصال با سرور برقرار نشد.');
|
// TODO: Display a network connection error.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -56,4 +56,4 @@ api.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
|
||||||
|
|
||||||
|
// Next.js 14 loads its configuration as JavaScript.
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
|
||||||
@@ -19,4 +21,4 @@ const nextConfig = {
|
|||||||
assetPrefix: basePath || undefined,
|
assetPrefix: basePath || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
module.exports = nextConfig;
|
||||||
|
|||||||
Generated
+64
-1
@@ -19,9 +19,13 @@
|
|||||||
"react-redux": "9.1.2"
|
"react-redux": "9.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "20.19.43",
|
||||||
|
"@types/react": "18.3.31",
|
||||||
|
"@types/react-dom": "18.3.7",
|
||||||
"autoprefixer": "10.4.19",
|
"autoprefixer": "10.4.19",
|
||||||
"postcss": "8.4.39",
|
"postcss": "8.4.39",
|
||||||
"tailwindcss": "3.4.6"
|
"tailwindcss": "3.4.6",
|
||||||
|
"typescript": "5.9.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
@@ -576,6 +580,44 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.43",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
|
||||||
|
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/prop-types": {
|
||||||
|
"version": "15.7.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
|
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "18.3.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
|
||||||
|
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/prop-types": "*",
|
||||||
|
"csstype": "^3.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react-dom": {
|
||||||
|
"version": "18.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||||
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/use-sync-external-store": {
|
"node_modules/@types/use-sync-external-store": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
|
||||||
@@ -2917,6 +2959,27 @@
|
|||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||||
|
|||||||
+13
-8
@@ -6,22 +6,27 @@
|
|||||||
"dev": "next dev -H 0.0.0.0",
|
"dev": "next dev -H 0.0.0.0",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "5.3.7",
|
||||||
|
"@reduxjs/toolkit": "2.2.7",
|
||||||
|
"antd": "5.19.1",
|
||||||
|
"axios": "1.7.2",
|
||||||
|
"gsap": "3.12.5",
|
||||||
"next": "14.2.5",
|
"next": "14.2.5",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"antd": "5.19.1",
|
"react-redux": "9.1.2"
|
||||||
"@ant-design/icons": "5.3.7",
|
|
||||||
"@reduxjs/toolkit": "2.2.7",
|
|
||||||
"react-redux": "9.1.2",
|
|
||||||
"axios": "1.7.2",
|
|
||||||
"gsap": "3.12.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "20.19.43",
|
||||||
|
"@types/react": "18.3.31",
|
||||||
|
"@types/react-dom": "18.3.7",
|
||||||
"autoprefixer": "10.4.19",
|
"autoprefixer": "10.4.19",
|
||||||
"postcss": "8.4.39",
|
"postcss": "8.4.39",
|
||||||
"tailwindcss": "3.4.6"
|
"tailwindcss": "3.4.6",
|
||||||
|
"typescript": "5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import type {PayloadAction} from '@reduxjs/toolkit';
|
||||||
|
import type {CartProduct, CartState} from '@/types/domain';
|
||||||
|
import type {RootState} from './store';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
const initialState = {
|
const initialState: CartState = {
|
||||||
items: [], // { id, name, priceNumeric, priceLabel, qty, image }
|
items: [], // { id, name, priceNumeric, priceLabel, qty, image }
|
||||||
isDrawerOpen: false,
|
isDrawerOpen: false,
|
||||||
};
|
};
|
||||||
@@ -9,7 +12,7 @@ const cartSlice = createSlice({
|
|||||||
name: 'cart',
|
name: 'cart',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
addToCart: (state, action) => {
|
addToCart: (state, action: PayloadAction<CartProduct>) => {
|
||||||
const product = action.payload;
|
const product = action.payload;
|
||||||
const existing = state.items.find((item) => item.id === product.id);
|
const existing = state.items.find((item) => item.id === product.id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -19,20 +22,20 @@ const cartSlice = createSlice({
|
|||||||
}
|
}
|
||||||
state.isDrawerOpen = true;
|
state.isDrawerOpen = true;
|
||||||
},
|
},
|
||||||
removeFromCart: (state, action) => {
|
removeFromCart: (state, action: PayloadAction<string>) => {
|
||||||
state.items = state.items.filter((item) => item.id !== action.payload);
|
state.items = state.items.filter((item) => item.id !== action.payload);
|
||||||
},
|
},
|
||||||
setQuantity: (state, action) => {
|
setQuantity: (state, action: PayloadAction<{id: string; qty: number}>) => {
|
||||||
const { id, qty } = action.payload;
|
const { id, qty } = action.payload;
|
||||||
if (!Number.isSafeInteger(qty) || qty < 1) return;
|
if (!Number.isSafeInteger(qty) || qty < 1) return;
|
||||||
const item = state.items.find((item) => item.id === id);
|
const item = state.items.find((item) => item.id === id);
|
||||||
if (item) item.qty = qty;
|
if (item) item.qty = qty;
|
||||||
},
|
},
|
||||||
incrementQty: (state, action) => {
|
incrementQty: (state, action: PayloadAction<string>) => {
|
||||||
const item = state.items.find((item) => item.id === action.payload);
|
const item = state.items.find((item) => item.id === action.payload);
|
||||||
if (item) item.qty += 1;
|
if (item) item.qty += 1;
|
||||||
},
|
},
|
||||||
decrementQty: (state, action) => {
|
decrementQty: (state, action: PayloadAction<string>) => {
|
||||||
const item = state.items.find((item) => item.id === action.payload);
|
const item = state.items.find((item) => item.id === action.payload);
|
||||||
if (item && item.qty > 1) {
|
if (item && item.qty > 1) {
|
||||||
item.qty -= 1;
|
item.qty -= 1;
|
||||||
@@ -68,11 +71,11 @@ export const {
|
|||||||
} = cartSlice.actions;
|
} = cartSlice.actions;
|
||||||
|
|
||||||
// Selectors
|
// Selectors
|
||||||
export const selectCartItems = (state) => state.cart.items;
|
export const selectCartItems = (state: RootState) => state.cart.items;
|
||||||
export const selectCartCount = (state) =>
|
export const selectCartCount = (state: RootState) =>
|
||||||
state.cart.items.reduce((sum, item) => sum + item.qty, 0);
|
state.cart.items.reduce((sum, item) => sum + item.qty, 0);
|
||||||
export const selectCartTotal = (state) =>
|
export const selectCartTotal = (state: RootState) =>
|
||||||
state.cart.items.reduce((sum, item) => sum + item.qty * item.priceNumeric, 0);
|
state.cart.items.reduce((sum, item) => sum + item.qty * item.priceNumeric, 0);
|
||||||
export const selectIsDrawerOpen = (state) => state.cart.isDrawerOpen;
|
export const selectIsDrawerOpen = (state: RootState) => state.cart.isDrawerOpen;
|
||||||
|
|
||||||
export default cartSlice.reducer;
|
export default cartSlice.reducer;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import type { AppDispatch, RootState } from './store';
|
||||||
|
|
||||||
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||||
|
export const useAppSelector = useSelector.withTypes<RootState>();
|
||||||
@@ -10,3 +10,7 @@ export const makeStore = () =>
|
|||||||
|
|
||||||
// Singleton store for the client (App Router renders providers on the client tree).
|
// Singleton store for the client (App Router renders providers on the client tree).
|
||||||
export const store = makeStore();
|
export const store = makeStore();
|
||||||
|
|
||||||
|
export type AppStore = ReturnType<typeof makeStore>;
|
||||||
|
export type RootState = ReturnType<AppStore["getState"]>;
|
||||||
|
export type AppDispatch = AppStore["dispatch"];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
import type {Config} from 'tailwindcss';
|
||||||
module.exports = {
|
const config: Config = {
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
content: [
|
content: [
|
||||||
'./app/**/*.{js,jsx,ts,tsx}',
|
'./app/**/*.{js,jsx,ts,tsx}',
|
||||||
@@ -55,3 +55,5 @@ module.exports = {
|
|||||||
preflight: true,
|
preflight: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": { "@/*": ["./*"] }
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "app/**/*.ts", "app/**/*.tsx", "components/**/*.tsx", "data/**/*.ts", "lib/**/*.ts", "store/**/*.ts", "types/**/*.ts", "tailwind.config.ts", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "docs", "out"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
export type Locale = 'fa' | 'en';
|
||||||
|
export type Direction = 'rtl' | 'ltr';
|
||||||
|
export type Theme = 'light' | 'dark';
|
||||||
|
|
||||||
|
export interface Machine {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
shortLabel: string;
|
||||||
|
priceLabel: string;
|
||||||
|
priceNumeric: number;
|
||||||
|
description: string;
|
||||||
|
highlights: string[];
|
||||||
|
image: string;
|
||||||
|
imageAlt: string;
|
||||||
|
gif: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 FloatingSnack {
|
||||||
|
id: string;
|
||||||
|
image: string;
|
||||||
|
size: number;
|
||||||
|
top: string;
|
||||||
|
left: string;
|
||||||
|
duration: number;
|
||||||
|
delay: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FeatureIcon = 'safety' | 'consulting' | 'installment' | 'packaging';
|
||||||
|
export type LocationIcon = 'hotel' | 'school' | 'pharmacy' | 'hospital' | 'night' | 'specialty';
|
||||||
|
|
||||||
|
interface InfoCard<Icon extends string> {
|
||||||
|
id: string;
|
||||||
|
icon: Icon;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SiteContent {
|
||||||
|
dir: Direction;
|
||||||
|
lang: Locale;
|
||||||
|
meta: { title: string; description: string };
|
||||||
|
nav: Record<'home' | 'showcase' | 'about' | 'locations' | 'contact' | 'login', string>;
|
||||||
|
hero: {
|
||||||
|
loading: string;
|
||||||
|
cta: string;
|
||||||
|
slides: { id: string; kicker: string; titleLines: string[]; sub: string; image: string }[];
|
||||||
|
};
|
||||||
|
cart: {
|
||||||
|
title: string;
|
||||||
|
empty: string;
|
||||||
|
total: string;
|
||||||
|
checkout: string;
|
||||||
|
checkoutToast: string;
|
||||||
|
addToast: (name: string) => string;
|
||||||
|
};
|
||||||
|
machineActions: {
|
||||||
|
addToCart: string;
|
||||||
|
quickView: string;
|
||||||
|
wishlistAdd: string;
|
||||||
|
wishlistRemove: string;
|
||||||
|
hoverHint: string;
|
||||||
|
altVariant: string;
|
||||||
|
gifAlt: (name: string) => string;
|
||||||
|
};
|
||||||
|
machines: Machine[];
|
||||||
|
features: InfoCard<FeatureIcon>[];
|
||||||
|
about: { eyebrow: string; legalName: string; more: string; less: string; paragraphs: string[] };
|
||||||
|
locations: { title: string; items: InfoCard<LocationIcon>[] };
|
||||||
|
footer: Record<'aboutLabel' | 'aboutText' | 'quickLinks' | 'contactTitle' | 'factoryLabel' | 'salesLabel' | 'rights', string>;
|
||||||
|
contactInfo: {
|
||||||
|
orderPhone: string;
|
||||||
|
orderPhoneHref: string;
|
||||||
|
factoryAddress: string;
|
||||||
|
salesOfficeAddress: string;
|
||||||
|
socialLinks: { id: string; label: string; href: string }[];
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user