refactor: migrate project to TypeScript and translate comments to English
Build & Deploy (develop) / build-and-deploy (push) Successful in 2m23s

This commit is contained in:
2026-09-14 13:27:17 +02:00
parent 1ef621f832
commit 4cd34106b0
35 changed files with 427 additions and 165 deletions
@@ -1,6 +1,6 @@
'use client';
// سکشن معرفی شرکت با متن بلند؛ به‌خاطر طولانی بودن متن، یک دکمه «ادامه مطلب» برای باز/بسته کردن دارد.
// Company introduction with optional expandable paragraphs.
import {useState} from 'react';
import {useLanguage} from '@/app/context/LanguageContext';
@@ -1,10 +1,10 @@
'use client';
// کشوی (Drawer) سبد خرید: نمایش آیتم‌های انتخابی، تغییر تعداد، جمع کل، و دکمه پرداخت (placeholder).
// Cart drawer with quantity editing, totals, and placeholder checkout.
import {Drawer, Button, Empty, InputNumber, message} from 'antd';
import {DeleteOutlined} from '@ant-design/icons';
import {useSelector, useDispatch} from 'react-redux';
import {useAppSelector, useAppDispatch} from '@/store/hooks';
import {
selectCartItems,
selectCartTotal,
@@ -14,17 +14,18 @@ import {
setQuantity,
} from '@/store/cartSlice';
import {useLanguage} from '@/app/context/LanguageContext';
import type {Locale} from '@/types/domain';
import api from "@/lib/axios";
function toLocaleNumber(n, locale) {
function toLocaleNumber(n: number, locale: Locale) {
return n.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US');
}
export default function CartDrawer() {
const dispatch = useDispatch();
const isOpen = useSelector(selectIsDrawerOpen);
const items = useSelector(selectCartItems);
const total = useSelector(selectCartTotal);
const dispatch = useAppDispatch();
const isOpen = useAppSelector(selectIsDrawerOpen);
const items = useAppSelector(selectCartItems);
const total = useAppSelector(selectCartTotal);
const {t, locale, dir} = useLanguage();
const currencySuffix = locale === 'fa' ? ' ریال' : ' Rials';
@@ -1,6 +1,6 @@
'use client';
// نوار مزایا: ضمانت کالا، مشاوره رایگان، خرید اقساطی، بسته‌بندی ایمن.
// Service benefits: warranty, consultation, installments, and packaging.
import {
SafetyCertificateOutlined,
@@ -1,17 +1,18 @@
'use client';
// رندر آیتم‌های شناور (چیپس/نوشابه) دور دستگاه با حرکت تصادفی و ملایم GSAP.
// Animate decorative snack images with gentle randomized motion.
import type {FloatingSnack} from '@/types/domain';
import {useEffect, useRef} from 'react';
import {gsap} from 'gsap';
/**
* Renders the centralized `floatingSnacks` config as absolutely-positioned
* images and gives each one a gentle, randomized floating drift via GSAP.
* Positions/sizes/images all come from data/content.js — edit there only.
* Positions/sizes/images all come from data/content.ts — edit there only.
*/
export default function FloatingItems({items}) {
const containerRef = useRef(null);
export default function FloatingItems({items}: {items: FloatingSnack[]}) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const nodes = containerRef.current?.querySelectorAll('[data-float-item]');
@@ -1,6 +1,6 @@
'use client';
// فوتر سایت: درباره ما کوتاه، لینک‌های شبکه اجتماعی، دسترسی سریع، و اطلاعات تماس/آدرس.
// Company information, quick links, social links, and contact details.
import { PhoneOutlined, EnvironmentOutlined } from '@ant-design/icons';
import { useLanguage } from '@/app/context/LanguageContext';
@@ -1,20 +1,20 @@
'use client';
// هدر ثابت بالای صفحه: لوگو، منوی ناوبری، سوییچ زبان/تم، دکمه ورود به پنل کاربری و آیکون سبد خرید.
// Fixed navigation with language, theme, account, and cart controls.
import {useState, useEffect} from 'react';
import Image from 'next/image';
import {Badge, Button} from 'antd';
import {ShoppingCartOutlined, MenuOutlined, CloseOutlined, UserOutlined} from '@ant-design/icons';
import {useSelector, useDispatch} from 'react-redux';
import {useAppSelector, useAppDispatch} from '@/store/hooks';
import {selectCartCount, openDrawer} from '@/store/cartSlice';
import {useLanguage} from '@/app/context/LanguageContext';
import ThemeToggle from './ThemeToggle';
import LanguageToggle from './LanguageToggle';
export default function Header() {
const dispatch = useDispatch();
const cartCount = useSelector(selectCartCount);
const dispatch = useAppDispatch();
const cartCount = useAppSelector(selectCartCount);
const {t} = useLanguage();
const [scrolled, setScrolled] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
@@ -1,25 +1,20 @@
'use client';
/**
* سکشن اول سایت (Hero) — ایده کلی از صفحه وندینگ سایت Sielaff گرفته شده:
* یک اسلایدر تمام‌عرض که هر ۵ ثانیه به‌صورت خودکار به اسلاید بعدی می‌رود،
* همراه با یک شکل مورب رنگی (به‌جای قرمز آن‌ها، رنگ سبز برند خودمان #00a693)،
* عنوان بزرگ دو خطی، و دکمه‌های فلش قبلی/بعدی برای ناوبری دستی.
*/
/** Full-width product carousel with autoplay and manual navigation. */
import {useCallback, useEffect, useRef, useState} from 'react';
import {LeftOutlined, RightOutlined} from '@ant-design/icons';
import {useLanguage} from '@/app/context/LanguageContext';
const AUTOPLAY_DELAY = 5000; // هر ۵ ثانیه اسلاید عوض می‌شود
const AUTOPLAY_DELAY = 5000; // Advance every five seconds.
export default function HeroSection() {
const {t, dir} = useLanguage();
const slides = t.hero.slides;
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(() => {
setActiveIndex((prev) => (prev + 1) % slides.length);
}, [slides.length]);
@@ -28,9 +23,7 @@ export default function HeroSection() {
setActiveIndex((prev) => (prev - 1 + slides.length) % slides.length);
}, [slides.length]);
// تایمر پخش خودکار: هر بار که اسلاید عوض می‌شود، تایمر قبلی پاک و یک تایمر
// جدید ۵ ثانیه‌ای ست می‌شود. با کلیک دستی روی فلش‌ها هم همین تابع دوباره
// صدا زده می‌شود تا شمارش از نو شروع شود (تجربه کاربری بهتر).
// Restart the autoplay timer after manual navigation.
const restartAutoplay = useCallback(() => {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(goToNext, AUTOPLAY_DELAY);
@@ -43,10 +36,10 @@ export default function HeroSection() {
};
}, [restartAutoplay]);
const handleManualNav = (direction) => {
const handleManualNav = (direction: 'next' | 'prev') => {
if (direction === 'next') goToNext();
else goToPrev();
restartAutoplay(); // شمارش ۵ ثانیه بعد از کلیک دستی از نو شروع شود
restartAutoplay(); // Reset the delay after a manual click.
};
const slide = slides[activeIndex];
@@ -54,7 +47,7 @@ export default function HeroSection() {
return (
<section id="hero" className="relative h-screen w-full overflow-hidden bg-page">
{/* ------------------------------------------------------------------ */}
{/* شکل مورب تزئینی سمت راست/چپ — نسخه سبز برند به‌جای قرمز Sielaff */}
{/* Decorative brand-colored shape. */}
{/* ------------------------------------------------------------------ */}
<div
className="absolute inset-y-0 w-1/3 hidden md:block"
@@ -68,8 +61,8 @@ export default function HeroSection() {
/>
{/* ------------------------------------------------------------------ */}
{/* اسلایدهای عکس — همه اسلایدها روی هم قرار دارند و فقط با opacity */}
{/* محو/نمایان می‌شوند (transition نرم به‌جای پرش ناگهانی) */}
{/* Fade between overlapping product images. */}
{/* Use opacity transitions for smooth slide changes. */}
{/* ------------------------------------------------------------------ */}
<div className="absolute inset-0">
{slides.map((s, i) => (
@@ -88,7 +81,7 @@ export default function HeroSection() {
))}
</div>
{/* لایه‌ی نیمه‌شفاف پشت متن، برای خوانایی روی هر عکسی */}
{/* Overlay for text readability. */}
<div
className="absolute inset-0"
style={{
@@ -98,7 +91,7 @@ export default function HeroSection() {
/>
{/* ------------------------------------------------------------------ */}
{/* متن روی اسلاید: کیکر کوچک + عنوان بزرگ دو خطی + توضیح + دکمه CTA */}
{/* Active slide headline, description, and call to action. */}
{/* ------------------------------------------------------------------ */}
<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">
@@ -124,7 +117,7 @@ export default function HeroSection() {
</div>
{/* ------------------------------------------------------------------ */}
{/* فلش‌های ناوبری دستی (قبلی / بعدی) — وسط دو طرف */}
{/* Previous and next slide controls. */}
{/* ------------------------------------------------------------------ */}
<button
onClick={() => handleManualNav('prev')}
@@ -141,7 +134,7 @@ export default function HeroSection() {
<LeftOutlined/>
</button>
{/* نقطه‌های شمارشگر اسلاید پایین صفحه — کلیک روی هرکدام مستقیم به همان اسلاید می‌رود */}
{/* Jump directly to a slide using the indicators. */}
<div className="absolute bottom-8 inset-x-0 z-20 flex justify-center gap-2">
{slides.map((s, i) => (
<button
@@ -1,11 +1,12 @@
'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 {useDispatch} from 'react-redux';
import {useAppDispatch} from '@/store/hooks';
import VendingCard from './VendingCard';
import {showcaseVideoSrc} from '@/data/content';
import {useLanguage} from '@/app/context/LanguageContext';
@@ -14,12 +15,12 @@ import {addToCart} from '@/store/cartSlice';
gsap.registerPlugin(ScrollTrigger);
export default function HorizontalShowcase() {
const sectionRef = useRef(null);
const trackRef = useRef(null);
const videoRef = useRef(null);
const dispatch = useDispatch();
const sectionRef = useRef<HTMLElement>(null);
const trackRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const dispatch = useAppDispatch();
const {t} = useLanguage();
const [quickViewMachine, setQuickViewMachine] = useState(null);
const [quickViewMachine, setQuickViewMachine] = useState<Machine | null>(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() {
</Modal>
</section>
);
}
}
@@ -1,6 +1,6 @@
'use client';
// دکمه سوییچ زبان فارسی/انگلیسی.
// Switch between Persian and English.
import { Button, Tooltip } from 'antd';
import { useLanguage } from '@/app/context/LanguageContext';
@@ -1,6 +1,6 @@
'use client';
// گرید «مکان مناسب برای نصب» — هتل‌ها، مدارس، داروخانه‌ها، بیمارستان‌ها، شیفت شب، محصولات تخصصی.
// Suggested installation locations for vending machines.
import {
HomeOutlined,
@@ -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<Machine | null>(null);
const handleAddToCart = (machine) => {
const handleAddToCart = (machine: Machine) => {
dispatch(
addToCart({
id: machine.id,
@@ -104,4 +105,4 @@ export default function MobileShowcase() {
</Modal>
</section>
);
}
}
@@ -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<boolean | null>(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 ? <MobileShowcase/> : <HorizontalShowcase/>;
}
}
@@ -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<HTMLDivElement>(null);
const lensRef = useRef<HTMLDivElement>(null);
const revealRef = useRef<HTMLImageElement>(null);
const handleMouseMove = (e) => {
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
const stage = stageRef.current;
if (!stage) return;
const rect = stage.getBoundingClientRect();