{/* ------------------------------------------------------------------ */}
- {/* شکل مورب تزئینی سمت راست/چپ — نسخه سبز برند بهجای قرمز Sielaff */}
+ {/* Decorative brand-colored shape. */}
{/* ------------------------------------------------------------------ */}
{/* ------------------------------------------------------------------ */}
- {/* اسلایدهای عکس — همه اسلایدها روی هم قرار دارند و فقط با opacity */}
- {/* محو/نمایان میشوند (transition نرم بهجای پرش ناگهانی) */}
+ {/* Fade between overlapping product images. */}
+ {/* Use opacity transitions for smooth slide changes. */}
{/* ------------------------------------------------------------------ */}
{slides.map((s, i) => (
@@ -88,7 +81,7 @@ export default function HeroSection() {
))}
- {/* لایهی نیمهشفاف پشت متن، برای خوانایی روی هر عکسی */}
+ {/* Overlay for text readability. */}
{/* ------------------------------------------------------------------ */}
- {/* متن روی اسلاید: کیکر کوچک + عنوان بزرگ دو خطی + توضیح + دکمه CTA */}
+ {/* Active slide headline, description, and call to action. */}
{/* ------------------------------------------------------------------ */}
@@ -124,7 +117,7 @@ export default function HeroSection() {
{/* ------------------------------------------------------------------ */}
- {/* فلشهای ناوبری دستی (قبلی / بعدی) — وسط دو طرف */}
+ {/* Previous and next slide controls. */}
{/* ------------------------------------------------------------------ */}
handleManualNav('prev')}
@@ -141,7 +134,7 @@ export default function HeroSection() {
- {/* نقطههای شمارشگر اسلاید پایین صفحه — کلیک روی هرکدام مستقیم به همان اسلاید میرود */}
+ {/* Jump directly to a slide using the indicators. */}
{slides.map((s, i) => (
(null);
+ const trackRef = useRef(null);
+ const videoRef = useRef(null);
+ const dispatch = useAppDispatch();
const {t} = useLanguage();
- const [quickViewMachine, setQuickViewMachine] = useState(null);
+ const [quickViewMachine, setQuickViewMachine] = useState(null);
const [videoAvailable, setVideoAvailable] = useState(true);
useEffect(() => {
@@ -33,10 +34,10 @@ export default function HorizontalShowcase() {
Math.max(track.scrollWidth - window.innerWidth, 0)
);
- let animationFrameId = null;
+ let animationFrameId: number | null = null;
let latestProgress = 0;
- const syncVideo = (progress) => {
+ const syncVideo = (progress: number) => {
latestProgress = progress;
if (animationFrameId !== null) return;
@@ -148,4 +149,4 @@ export default function HorizontalShowcase() {
);
-}
\ No newline at end of file
+}
diff --git a/components/LanguageToggle.jsx b/components/LanguageToggle.tsx
similarity index 89%
rename from components/LanguageToggle.jsx
rename to components/LanguageToggle.tsx
index 7e0a65e..a2e4e56 100644
--- a/components/LanguageToggle.jsx
+++ b/components/LanguageToggle.tsx
@@ -1,6 +1,6 @@
'use client';
-// دکمه سوییچ زبان فارسی/انگلیسی.
+// Switch between Persian and English.
import { Button, Tooltip } from 'antd';
import { useLanguage } from '@/app/context/LanguageContext';
diff --git a/components/LocationsSection.jsx b/components/LocationsSection.tsx
similarity index 90%
rename from components/LocationsSection.jsx
rename to components/LocationsSection.tsx
index bbd7e7e..5b22399 100644
--- a/components/LocationsSection.jsx
+++ b/components/LocationsSection.tsx
@@ -1,6 +1,6 @@
'use client';
-// گرید «مکان مناسب برای نصب» — هتلها، مدارس، داروخانهها، بیمارستانها، شیفت شب، محصولات تخصصی.
+// Suggested installation locations for vending machines.
import {
HomeOutlined,
diff --git a/components/MobileShowcase.jsx b/components/MobileShowcase.tsx
similarity index 93%
rename from components/MobileShowcase.jsx
rename to components/MobileShowcase.tsx
index d8658da..2e39628 100644
--- a/components/MobileShowcase.jsx
+++ b/components/MobileShowcase.tsx
@@ -1,18 +1,19 @@
'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 {useDispatch} from 'react-redux';
+import {useAppDispatch} from '@/store/hooks';
import {useLanguage} from '@/app/context/LanguageContext';
import {addToCart} from '@/store/cartSlice';
export default function MobileShowcase() {
- const dispatch = useDispatch();
+ const dispatch = useAppDispatch();
const {t, dir} = useLanguage();
- const [quickViewMachine, setQuickViewMachine] = useState(null);
+ const [quickViewMachine, setQuickViewMachine] = useState(null);
- const handleAddToCart = (machine) => {
+ const handleAddToCart = (machine: Machine) => {
dispatch(
addToCart({
id: machine.id,
@@ -104,4 +105,4 @@ export default function MobileShowcase() {
);
-}
\ No newline at end of file
+}
diff --git a/components/ShowcaseSection.jsx b/components/ShowcaseSection.tsx
similarity index 80%
rename from components/ShowcaseSection.jsx
rename to components/ShowcaseSection.tsx
index 9be9046..3abab08 100644
--- a/components/ShowcaseSection.jsx
+++ b/components/ShowcaseSection.tsx
@@ -7,14 +7,14 @@ import MobileShowcase from './MobileShowcase';
const MOBILE_BREAKPOINT = 768;
export default function ShowcaseSection() {
- const [isMobile, setIsMobile] = useState(null);
+ const [isMobile, setIsMobile] = useState(null);
useEffect(() => {
const mediaQuery = window.matchMedia(
`(max-width: ${MOBILE_BREAKPOINT - 1}px)`
);
- const handleChange = (event) => setIsMobile(event.matches);
+ const handleChange = (event: MediaQueryListEvent) => setIsMobile(event.matches);
setIsMobile(mediaQuery.matches);
mediaQuery.addEventListener('change', handleChange);
@@ -25,4 +25,4 @@ export default function ShowcaseSection() {
if (isMobile === null) return null;
return isMobile ? : ;
-}
\ No newline at end of file
+}
diff --git a/components/ThemeToggle.jsx b/components/ThemeToggle.tsx
similarity index 100%
rename from components/ThemeToggle.jsx
rename to components/ThemeToggle.tsx
diff --git a/components/VendingCard.jsx b/components/VendingCard.tsx
similarity index 90%
rename from components/VendingCard.jsx
rename to components/VendingCard.tsx
index 18679f6..b59ac06 100644
--- a/components/VendingCard.jsx
+++ b/components/VendingCard.tsx
@@ -1,23 +1,25 @@
'use client';
+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 {useDispatch} from 'react-redux';
+import {useAppDispatch} from '@/store/hooks';
import {addToCart} from '@/store/cartSlice';
import {useLanguage} from '@/app/context/LanguageContext';
import FloatingItems from './FloatingItems';
import {floatingSnacks} from '@/data/content';
-export default function VendingCard({machine, onQuickView}) {
- const dispatch = useDispatch();
+export default function VendingCard({machine, onQuickView}: {machine: Machine; onQuickView?: (machine: Machine) => void}) {
+ const dispatch = useAppDispatch();
const {t, dir} = useLanguage();
- const stageRef = useRef(null);
- const lensRef = useRef(null);
- const revealRef = useRef(null);
+ const stageRef = useRef(null);
+ const lensRef = useRef(null);
+ const revealRef = useRef(null);
- const handleMouseMove = (e) => {
+ const handleMouseMove = (e: MouseEvent) => {
const stage = stageRef.current;
if (!stage) return;
const rect = stage.getBoundingClientRect();
diff --git a/data/content.js b/data/content.ts
similarity index 99%
rename from data/content.js
rename to data/content.ts
index c282a4d..faca087 100644
--- a/data/content.js
+++ b/data/content.ts
@@ -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
// business copy lives here, per locale. Components read via useLanguage().
// Asset paths (images/gifs/video) are locale-independent and stay the same.
// ---------------------------------------------------------------------------
-const machinesFa = [
+const machinesFa: Machine[] = [
{
id: 'rm-35',
name: 'روبات مارکت ۳۵ کانال',
@@ -43,7 +45,7 @@ const machinesFa = [
},
];
-const machinesEn = [
+const machinesEn: Machine[] = [
{
id: 'rm-35',
name: 'Robot Market RM-35',
@@ -82,7 +84,7 @@ const machinesEn = [
},
];
-export const content = {
+export const content: Record = {
fa: {
dir: 'rtl',
lang: 'fa',
@@ -417,7 +419,7 @@ export const content = {
};
// 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: '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},
diff --git a/jsconfig.json b/jsconfig.json
deleted file mode 100644
index fe655bc..0000000
--- a/jsconfig.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "compilerOptions": {
- "baseUrl": ".",
- "paths": {
- "@/*": ["./*"]
- },
- "jsx": "react"
- }
-}
diff --git a/lib/axios.js b/lib/axios.ts
similarity index 75%
rename from lib/axios.js
rename to lib/axios.ts
index 32f7867..36f1bbe 100644
--- a/lib/axios.js
+++ b/lib/axios.ts
@@ -43,11 +43,11 @@ api.interceptors.response.use(
}
if (status >= 500) {
- // message.error('خطایی در سرور رخ داد. لطفاً بعداً تلاش کنید.');
+ // TODO: Display a server error message.
}
} else if (error.request) {
- // درخواست ارسال شد ولی هیچ جوابی نیومد (قطعی اینترنت، سرور خاموش و ...)
- // TODO: message.error('اتصال با سرور برقرار نشد.');
+ // The request was sent, but no response arrived.
+ // TODO: Display a network connection error.
}
@@ -56,4 +56,4 @@ api.interceptors.response.use(
}
);
-export default api;
\ No newline at end of file
+export default api;
diff --git a/next-env.d.ts b/next-env.d.ts
new file mode 100644
index 0000000..4f11a03
--- /dev/null
+++ b/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/next.config.js b/next.config.js
index e473cd6..a6d4c08 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,5 +1,7 @@
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
+// Next.js 14 loads its configuration as JavaScript.
+/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
@@ -19,4 +21,4 @@ const nextConfig = {
assetPrefix: basePath || undefined,
};
-module.exports = nextConfig;
\ No newline at end of file
+module.exports = nextConfig;
diff --git a/package-lock.json b/package-lock.json
index a5e061a..80b980a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,9 +19,13 @@
"react-redux": "9.1.2"
},
"devDependencies": {
+ "@types/node": "20.19.43",
+ "@types/react": "18.3.31",
+ "@types/react-dom": "18.3.7",
"autoprefixer": "10.4.19",
"postcss": "8.4.39",
- "tailwindcss": "3.4.6"
+ "tailwindcss": "3.4.6",
+ "typescript": "5.9.3"
}
},
"node_modules/@alloc/quick-lru": {
@@ -576,6 +580,44 @@
"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": {
"version": "0.0.3",
"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==",
"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": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
diff --git a/package.json b/package.json
index 0990f99..1ec00df 100644
--- a/package.json
+++ b/package.json
@@ -6,22 +6,27 @@
"dev": "next dev -H 0.0.0.0",
"build": "next build",
"start": "next start",
- "lint": "next lint"
+ "lint": "next lint",
+ "typecheck": "tsc --noEmit"
},
"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",
"react": "18.3.1",
"react-dom": "18.3.1",
- "antd": "5.19.1",
- "@ant-design/icons": "5.3.7",
- "@reduxjs/toolkit": "2.2.7",
- "react-redux": "9.1.2",
- "axios": "1.7.2",
- "gsap": "3.12.5"
+ "react-redux": "9.1.2"
},
"devDependencies": {
+ "@types/node": "20.19.43",
+ "@types/react": "18.3.31",
+ "@types/react-dom": "18.3.7",
"autoprefixer": "10.4.19",
"postcss": "8.4.39",
- "tailwindcss": "3.4.6"
+ "tailwindcss": "3.4.6",
+ "typescript": "5.9.3"
}
}
diff --git a/store/cartSlice.js b/store/cartSlice.ts
similarity index 69%
rename from store/cartSlice.js
rename to store/cartSlice.ts
index 8ddf3be..7e5230a 100644
--- a/store/cartSlice.js
+++ b/store/cartSlice.ts
@@ -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';
-const initialState = {
+const initialState: CartState = {
items: [], // { id, name, priceNumeric, priceLabel, qty, image }
isDrawerOpen: false,
};
@@ -9,7 +12,7 @@ const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
- addToCart: (state, action) => {
+ addToCart: (state, action: PayloadAction) => {
const product = action.payload;
const existing = state.items.find((item) => item.id === product.id);
if (existing) {
@@ -19,20 +22,20 @@ const cartSlice = createSlice({
}
state.isDrawerOpen = true;
},
- removeFromCart: (state, action) => {
+ removeFromCart: (state, action: PayloadAction) => {
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;
if (!Number.isSafeInteger(qty) || qty < 1) return;
const item = state.items.find((item) => item.id === id);
if (item) item.qty = qty;
},
- incrementQty: (state, action) => {
+ incrementQty: (state, action: PayloadAction) => {
const item = state.items.find((item) => item.id === action.payload);
if (item) item.qty += 1;
},
- decrementQty: (state, action) => {
+ decrementQty: (state, action: PayloadAction) => {
const item = state.items.find((item) => item.id === action.payload);
if (item && item.qty > 1) {
item.qty -= 1;
@@ -68,11 +71,11 @@ export const {
} = cartSlice.actions;
// Selectors
-export const selectCartItems = (state) => state.cart.items;
-export const selectCartCount = (state) =>
+export const selectCartItems = (state: RootState) => state.cart.items;
+export const selectCartCount = (state: RootState) =>
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);
-export const selectIsDrawerOpen = (state) => state.cart.isDrawerOpen;
+export const selectIsDrawerOpen = (state: RootState) => state.cart.isDrawerOpen;
export default cartSlice.reducer;
diff --git a/store/hooks.ts b/store/hooks.ts
new file mode 100644
index 0000000..4fefe95
--- /dev/null
+++ b/store/hooks.ts
@@ -0,0 +1,7 @@
+'use client';
+
+import { useDispatch, useSelector } from 'react-redux';
+import type { AppDispatch, RootState } from './store';
+
+export const useAppDispatch = useDispatch.withTypes();
+export const useAppSelector = useSelector.withTypes();
diff --git a/store/store.js b/store/store.ts
similarity index 66%
rename from store/store.js
rename to store/store.ts
index e4de347..b52e14e 100644
--- a/store/store.js
+++ b/store/store.ts
@@ -10,3 +10,7 @@ export const makeStore = () =>
// Singleton store for the client (App Router renders providers on the client tree).
export const store = makeStore();
+
+export type AppStore = ReturnType;
+export type RootState = ReturnType;
+export type AppDispatch = AppStore["dispatch"];
diff --git a/tailwind.config.js b/tailwind.config.ts
similarity index 93%
rename from tailwind.config.js
rename to tailwind.config.ts
index 91c2677..6010570 100644
--- a/tailwind.config.js
+++ b/tailwind.config.ts
@@ -1,5 +1,5 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
+import type {Config} from 'tailwindcss';
+const config: Config = {
darkMode: 'class',
content: [
'./app/**/*.{js,jsx,ts,tsx}',
@@ -55,3 +55,5 @@ module.exports = {
preflight: true,
},
};
+
+export default config;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..05cc39d
--- /dev/null
+++ b/tsconfig.json
@@ -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"]
+}
diff --git a/types/domain.ts b/types/domain.ts
new file mode 100644
index 0000000..4c26c70
--- /dev/null
+++ b/types/domain.ts
@@ -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;
+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 {
+ 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[];
+ about: { eyebrow: string; legalName: string; more: string; less: string; paragraphs: string[] };
+ locations: { title: string; items: InfoCard[] };
+ 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 }[];
+ };
+}