diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 83fad6e..70d6fcc 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -3,22 +3,15 @@ - - - - - - - - - - - - - - - - + + + + + + + + + @@ -51,6 +44,7 @@ "RunOnceActivity.typescript.service.memoryLimit.init": "true", "codeWithMe.voiceChat.enabledByDefault": "false", "git-widget-placeholder": "main", + "javascript.preferred.runtime.type.id": "node", "js.debugger.nextJs.config.created.client": "true", "js.debugger.nextJs.config.created.server": "true", "last_opened_file_path": "D:/Project/GitHubRepo/robot-market-landing", @@ -99,7 +93,7 @@ - + @@ -141,7 +135,23 @@ 1785646373594 - + + + 1785661529816 + + + + 1785661529816 + + + + 1785661545228 + + + + 1785661545228 + + diff --git a/app/context/LanguageContext.jsx b/app/context/LanguageContext.jsx index 53a5eb2..5705795 100644 --- a/app/context/LanguageContext.jsx +++ b/app/context/LanguageContext.jsx @@ -1,42 +1,41 @@ 'use client'; -// context زبان سایت: نگهداری زبان فعلی (fa/en)، ذخیره در localStorage، و تنظیم جهت صفحه (rtl/ltr). -import { createContext, useContext, useEffect, useState } from 'react'; -import { content } from '@/data/content'; +import {createContext, useContext, useEffect, useState} from 'react'; +import {content} from '@/data/content'; const LanguageContext = createContext(null); -export function LanguageProvider({ children }) { - const [locale, setLocale] = useState('fa'); - const [mounted, setMounted] = useState(false); +export function LanguageProvider({children}) { + const [locale, setLocale] = useState('fa'); + const [mounted, setMounted] = useState(false); - useEffect(() => { - const saved = window.localStorage.getItem('rm-locale'); - if (saved === 'fa' || saved === 'en') setLocale(saved); - setMounted(true); - }, []); + useEffect(() => { + const saved = window.localStorage.getItem('rm-locale'); + if (saved === 'fa' || saved === 'en') setLocale(saved); + setMounted(true); + }, []); - useEffect(() => { - const root = document.documentElement; - root.lang = locale; - root.dir = content[locale].dir; - window.localStorage.setItem('rm-locale', locale); - }, [locale]); + useEffect(() => { + const root = document.documentElement; + root.lang = locale; + root.dir = content[locale].dir; + window.localStorage.setItem('rm-locale', locale); + }, [locale]); - const toggleLocale = () => setLocale((l) => (l === 'fa' ? 'en' : 'fa')); + const toggleLocale = () => setLocale((l) => (l === 'fa' ? 'en' : 'fa')); - return ( - - {children} - - ); + return ( + + {children} + + ); } export function useLanguage() { - const ctx = useContext(LanguageContext); - if (!ctx) throw new Error('useLanguage must be used within LanguageProvider'); - return ctx; + const ctx = useContext(LanguageContext); + if (!ctx) throw new Error('useLanguage must be used within LanguageProvider'); + return ctx; } diff --git a/app/context/ThemeContext.jsx b/app/context/ThemeContext.jsx index 1a15af8..2cb271d 100644 --- a/app/context/ThemeContext.jsx +++ b/app/context/ThemeContext.jsx @@ -1,47 +1,44 @@ 'use client'; -import { createContext, useContext, useEffect, useState } from 'react'; +import {createContext, useContext, useEffect, useState} from 'react'; const ThemeContext = createContext(null); -export function ThemeProvider({ children }) { - // پیشفرض سایت حالا «روشن» (light) است. اگر کاربر قبلاً تم را عوض کرده باشد - // (ذخیرهشده در localStorage) همان ترجیح رو حفظ میکنیم؛ در غیر این صورت فقط - // اگر سیستم کاربر صراحتاً روی حالت تیره تنظیم شده باشد، تیره را انتخاب میکنیم. - const [theme, setTheme] = useState('light'); - const [mounted, setMounted] = useState(false); +export function ThemeProvider({children}) { - // On mount: read saved preference, else fall back to system preference - // (but default stays light unless the OS explicitly prefers dark). - useEffect(() => { - const saved = window.localStorage.getItem('rm-theme'); - if (saved === 'light' || saved === 'dark') { - setTheme(saved); - } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) { - setTheme('dark'); - } - setMounted(true); - }, []); + const [theme, setTheme] = useState('light'); + const [mounted, setMounted] = useState(false); - // Reflect theme on class + persist. - useEffect(() => { - const root = document.documentElement; - root.classList.remove('light', 'dark'); - root.classList.add(theme); - window.localStorage.setItem('rm-theme', theme); - }, [theme]); - const toggleTheme = () => setTheme((t) => (t === 'dark' ? 'light' : 'dark')); + useEffect(() => { + const saved = window.localStorage.getItem('rm-theme'); + if (saved === 'light' || saved === 'dark') { + setTheme(saved); + } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + setTheme('dark'); + } + setMounted(true); + }, []); - return ( - - {children} - - ); + + useEffect(() => { + const root = document.documentElement; + root.classList.remove('light', 'dark'); + root.classList.add(theme); + window.localStorage.setItem('rm-theme', theme); + }, [theme]); + + const toggleTheme = () => setTheme((t) => (t === 'dark' ? 'light' : 'dark')); + + return ( + + {children} + + ); } export function useTheme() { - const ctx = useContext(ThemeContext); - if (!ctx) throw new Error('useTheme must be used within ThemeProvider'); - return ctx; + const ctx = useContext(ThemeContext); + if (!ctx) throw new Error('useTheme must be used within ThemeProvider'); + return ctx; } diff --git a/app/layout.jsx b/app/layout.jsx index 7087432..7e49162 100644 --- a/app/layout.jsx +++ b/app/layout.jsx @@ -1,29 +1,26 @@ -import { Vazirmatn } from 'next/font/google'; +import {Vazirmatn} from 'next/font/google'; import './globals.css'; import Providers from './providers'; const vazirmatn = Vazirmatn({ - subsets: ['arabic'], - variable: '--font-vazirmatn', - display: 'swap', + subsets: ['arabic'], + variable: '--font-vazirmatn', + display: 'swap', }); export const metadata = { - title: 'روبات مارکت | دستگاههای وندینگ هوشمند', - description: - 'روبات مارکت؛ دستگاههای فروش خودکار هوشمند با مدیریت آنلاین، تحویل نرم و طراحی مدرن.', + title: 'روبات مارکت | دستگاههای وندینگ هوشمند', + description: + 'روبات مارکت؛ دستگاههای فروش خودکار هوشمند با مدیریت آنلاین، تحویل نرم و طراحی مدرن.', }; -export default function RootLayout({ children }) { - // پوسته پیشفرض سمت سرور: فارسی + حالت روشن (Light). - // ThemeProvider/LanguageProvider بعد از mount شدن، اگر کاربر قبلاً تم/زبان - // دیگری را انتخاب کرده بود (ذخیرهشده در localStorage)، آن را روی اعمال - // میکنند؛ suppressHydrationWarning جلوی هشدار بیضرر عدمتطابق را میگیرد. - return ( - - +export default function RootLayout({children}) { + + return ( + + {children} - - - ); + + + ); } diff --git a/app/providers.jsx b/app/providers.jsx index f1a5178..a4a5fe2 100644 --- a/app/providers.jsx +++ b/app/providers.jsx @@ -2,60 +2,60 @@ // پل ارتباطی بین Redux، حالت تیره/روشن، و زبان فا/en با تنظیمات ConfigProvider آنتدیزاین. -import { Provider as ReduxProvider } from 'react-redux'; -import { ConfigProvider, theme as antdThemeApi } from 'antd'; +import {Provider as ReduxProvider} from 'react-redux'; +import {ConfigProvider, theme as antdThemeApi} from 'antd'; import faIR from 'antd/locale/fa_IR'; import enUS from 'antd/locale/en_US'; -import { store } from '@/store/store'; -import { ThemeProvider, useTheme } from './context/ThemeContext'; -import { LanguageProvider, useLanguage } from './context/LanguageContext'; +import {store} from '@/store/store'; +import {ThemeProvider, useTheme} from './context/ThemeContext'; +import {LanguageProvider, useLanguage} from './context/LanguageContext'; -function AntdBridge({ children }) { - const { theme } = useTheme(); - const { locale, dir } = useLanguage(); +function AntdBridge({children}) { + const {theme} = useTheme(); + const {locale, dir} = useLanguage(); - const antdTheme = { - algorithm: theme === 'dark' ? antdThemeApi.darkAlgorithm : antdThemeApi.defaultAlgorithm, - token: { - colorPrimary: '#00a693', - colorInfo: '#00a693', - borderRadius: 10, - fontFamily: 'var(--font-vazirmatn), Tahoma, sans-serif', - colorBgContainer: theme === 'dark' ? '#0c2b24' : '#ffffff', - colorBgElevated: theme === 'dark' ? '#0f332b' : '#ffffff', - colorBorder: theme === 'dark' ? '#0b3d38' : '#c0ebe3', - colorText: theme === 'dark' ? '#e6f7f4' : '#04231f', - colorTextSecondary: theme === 'dark' ? 'rgba(230,247,244,0.7)' : 'rgba(4,35,31,0.65)', - }, - components: { - Button: { controlHeight: 44, fontWeight: 600 }, - Badge: { colorError: '#ff6b57' }, - Modal: { - contentBg: theme === 'dark' ? '#0c2b24' : '#ffffff', - headerBg: theme === 'dark' ? '#0c2b24' : '#ffffff', - titleColor: theme === 'dark' ? '#e6f7f4' : '#04231f', - }, - Drawer: { - colorBgElevated: theme === 'dark' ? '#0c2b24' : '#ffffff', - }, - }, - }; + const antdTheme = { + algorithm: theme === 'dark' ? antdThemeApi.darkAlgorithm : antdThemeApi.defaultAlgorithm, + token: { + colorPrimary: '#00a693', + colorInfo: '#00a693', + borderRadius: 10, + fontFamily: 'var(--font-vazirmatn), Tahoma, sans-serif', + colorBgContainer: theme === 'dark' ? '#0c2b24' : '#ffffff', + colorBgElevated: theme === 'dark' ? '#0f332b' : '#ffffff', + colorBorder: theme === 'dark' ? '#0b3d38' : '#c0ebe3', + colorText: theme === 'dark' ? '#e6f7f4' : '#04231f', + colorTextSecondary: theme === 'dark' ? 'rgba(230,247,244,0.7)' : 'rgba(4,35,31,0.65)', + }, + components: { + Button: {controlHeight: 44, fontWeight: 600}, + Badge: {colorError: '#ff6b57'}, + Modal: { + contentBg: theme === 'dark' ? '#0c2b24' : '#ffffff', + headerBg: theme === 'dark' ? '#0c2b24' : '#ffffff', + titleColor: theme === 'dark' ? '#e6f7f4' : '#04231f', + }, + Drawer: { + colorBgElevated: theme === 'dark' ? '#0c2b24' : '#ffffff', + }, + }, + }; - return ( - - {children} - - ); + return ( + + {children} + + ); } -export default function Providers({ children }) { - return ( - - - - {children} - - - - ); +export default function Providers({children}) { + return ( + + + + {children} + + + + ); } diff --git a/components/HorizontalShowcase.jsx b/components/HorizontalShowcase.jsx index 1545662..4a4b7aa 100644 --- a/components/HorizontalShowcase.jsx +++ b/components/HorizontalShowcase.jsx @@ -4,126 +4,128 @@ // و یک ویدیوی پسزمینه دارد که با پیشرفت اسکرول جلو/عقب میرود. طبق درخواست کاربر، این سکشن // و هیرو از چرخهی رنگبندی متناوب بقیه سکشنها مستثنا هستند. -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 {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 VendingCard from './VendingCard'; -import { showcaseVideoSrc } from '@/data/content'; -import { useLanguage } from '@/app/context/LanguageContext'; -import { addToCart } from '@/store/cartSlice'; +import {showcaseVideoSrc} from '@/data/content'; +import {useLanguage} from '@/app/context/LanguageContext'; +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 { t } = useLanguage(); - const [quickViewMachine, setQuickViewMachine] = useState(null); - const [videoAvailable, setVideoAvailable] = useState(true); + const sectionRef = useRef(null); + const trackRef = useRef(null); + const videoRef = useRef(null); + const dispatch = useDispatch(); + const {t} = useLanguage(); + const [quickViewMachine, setQuickViewMachine] = useState(null); + const [videoAvailable, setVideoAvailable] = useState(true); - useEffect(() => { - const ctx = gsap.context(() => { - const track = trackRef.current; - const section = sectionRef.current; - if (!track || !section) return; + useEffect(() => { + const ctx = gsap.context(() => { + const track = trackRef.current; + const section = sectionRef.current; + if (!track || !section) return; - const scrollDistance = () => track.scrollWidth - window.innerWidth; + const scrollDistance = () => track.scrollWidth - window.innerWidth; - const horizontalTween = gsap.to(track, { - x: () => -scrollDistance(), - ease: 'none', - scrollTrigger: { - trigger: section, - start: 'top top', - end: () => `+=${scrollDistance()}`, - scrub: 1, - pin: true, - invalidateOnRefresh: true, - onUpdate: (self) => { - const video = videoRef.current; - if (video && video.duration) { - video.currentTime = self.progress * video.duration; - } - }, - }, - }); + const horizontalTween = gsap.to(track, { + x: () => -scrollDistance(), + ease: 'none', + scrollTrigger: { + trigger: section, + start: 'top top', + end: () => `+=${scrollDistance()}`, + scrub: 1, + pin: true, + invalidateOnRefresh: true, + onUpdate: (self) => { + const video = videoRef.current; + if (video && video.duration) { + video.currentTime = self.progress * video.duration; + } + }, + }, + }); - return () => { - horizontalTween.scrollTrigger?.kill(); - horizontalTween.kill(); - }; - }, sectionRef); + return () => { + horizontalTween.scrollTrigger?.kill(); + horizontalTween.kill(); + }; + }, sectionRef); - return () => ctx.revert(); - }, []); + return () => ctx.revert(); + }, []); - const handleModalAddToCart = () => { - if (!quickViewMachine) return; - dispatch( - addToCart({ - id: quickViewMachine.id, - name: quickViewMachine.name, - priceNumeric: quickViewMachine.priceNumeric, - priceLabel: quickViewMachine.priceLabel, - image: quickViewMachine.image, - }) + const handleModalAddToCart = () => { + if (!quickViewMachine) return; + dispatch( + addToCart({ + id: quickViewMachine.id, + name: quickViewMachine.name, + priceNumeric: quickViewMachine.priceNumeric, + priceLabel: quickViewMachine.priceLabel, + image: quickViewMachine.image, + }) + ); + message.success(t.cart.addToast(quickViewMachine.name)); + setQuickViewMachine(null); + }; + + return ( + + + {videoAvailable && ( + setVideoAvailable(false)} + /> + )} + + + + + {t.machines.map((machine) => ( + + ))} + + + setQuickViewMachine(null)} + footer={null} + centered + title={quickViewMachine?.name} + > + {quickViewMachine && ( + + + {quickViewMachine.description} + {quickViewMachine.priceLabel} + } + onClick={handleModalAddToCart}> + {t.machineActions.addToCart} + + + )} + + ); - message.success(t.cart.addToast(quickViewMachine.name)); - setQuickViewMachine(null); - }; - - return ( - - - {videoAvailable && ( - setVideoAvailable(false)} - /> - )} - - - - - {t.machines.map((machine) => ( - - ))} - - - setQuickViewMachine(null)} - footer={null} - centered - title={quickViewMachine?.name} - > - {quickViewMachine && ( - - - {quickViewMachine.description} - {quickViewMachine.priceLabel} - } onClick={handleModalAddToCart}> - {t.machineActions.addToCart} - - - )} - - - ); } diff --git a/public/images/machine-slide-0.png b/public/images/machine-slide-0.png index 80ef2b1..a5aa443 100644 Binary files a/public/images/machine-slide-0.png and b/public/images/machine-slide-0.png differ diff --git a/public/images/machine-slide-1.png b/public/images/machine-slide-1.png index a5aa443..de979be 100644 Binary files a/public/images/machine-slide-1.png and b/public/images/machine-slide-1.png differ diff --git a/public/images/machine-slide-31.png b/public/images/machine-slide-31.png new file mode 100644 index 0000000..80ef2b1 Binary files /dev/null and b/public/images/machine-slide-31.png differ diff --git a/public/video/company-intro-placeholder.mp4 b/public/video/company-intro-placeholder.mp4 index 52bfa8b..e306389 100644 Binary files a/public/video/company-intro-placeholder.mp4 and b/public/video/company-intro-placeholder.mp4 differ diff --git a/public/video/showcase-placeholder.mp4 b/public/video/showcase-placeholder.mp4 index f0754bf..6f28b9c 100644 Binary files a/public/video/showcase-placeholder.mp4 and b/public/video/showcase-placeholder.mp4 differ
{quickViewMachine.description}
{quickViewMachine.priceLabel}