update
This commit is contained in:
Generated
+28
-18
@@ -3,22 +3,15 @@
|
|||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="bb7b50a5-be9c-4cd7-a05b-979f0e91b173" name="Changes" comment="update">
|
<list default="true" id="bb7b50a5-be9c-4cd7-a05b-979f0e91b173" name="Changes" comment="update">
|
||||||
<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$/app/globals.css" beforeDir="false" afterPath="$PROJECT_DIR$/app/globals.css" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/app/context/LanguageContext.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/app/context/LanguageContext.jsx" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/components/Header.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/components/Header.jsx" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/app/context/ThemeContext.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/app/context/ThemeContext.jsx" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/components/HeroSection.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/components/HeroSection.jsx" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/app/layout.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/app/layout.jsx" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/data/content.js" beforeDir="false" afterPath="$PROJECT_DIR$/data/content.js" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/app/providers.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/app/providers.jsx" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/jsconfig.json" beforeDir="false" afterPath="$PROJECT_DIR$/jsconfig.json" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/components/HorizontalShowcase.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/components/HorizontalShowcase.jsx" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/public/images/logo.svg" beforeDir="false" />
|
<change beforePath="$PROJECT_DIR$/public/images/machine-slide-0.png" beforeDir="false" afterPath="$PROJECT_DIR$/public/images/machine-slide-0.png" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/public/images/machine-35-base.svg" beforeDir="false" />
|
<change beforePath="$PROJECT_DIR$/public/images/machine-slide-1.png" beforeDir="false" afterPath="$PROJECT_DIR$/public/images/machine-slide-1.png" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/public/images/machine-35-white.svg" beforeDir="false" />
|
<change beforePath="$PROJECT_DIR$/public/video/company-intro-placeholder.mp4" beforeDir="false" afterPath="$PROJECT_DIR$/public/video/company-intro-placeholder.mp4" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/public/images/machine-48-base.svg" beforeDir="false" />
|
<change beforePath="$PROJECT_DIR$/public/video/showcase-placeholder.mp4" beforeDir="false" afterPath="$PROJECT_DIR$/public/video/showcase-placeholder.mp4" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/public/images/machine-48-white.svg" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/public/images/machine-60-base.svg" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/public/images/machine-60-white.svg" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/public/images/snacks/chips-1.svg" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/public/images/snacks/chips-2.svg" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/public/images/snacks/soda-1.svg" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/public/images/snacks/soda-2.svg" 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" />
|
||||||
@@ -51,6 +44,7 @@
|
|||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||||
"git-widget-placeholder": "main",
|
"git-widget-placeholder": "main",
|
||||||
|
"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",
|
||||||
"last_opened_file_path": "D:/Project/GitHubRepo/robot-market-landing",
|
"last_opened_file_path": "D:/Project/GitHubRepo/robot-market-landing",
|
||||||
@@ -99,7 +93,7 @@
|
|||||||
<workItem from="1785583486761" duration="9000" />
|
<workItem from="1785583486761" duration="9000" />
|
||||||
<workItem from="1785583517424" duration="145000" />
|
<workItem from="1785583517424" duration="145000" />
|
||||||
<workItem from="1785645096071" duration="26000" />
|
<workItem from="1785645096071" duration="26000" />
|
||||||
<workItem from="1785646314677" duration="10534000" />
|
<workItem from="1785646314677" duration="16547000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="update">
|
<task id="LOCAL-00001" summary="update">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -141,7 +135,23 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1785646373594</updated>
|
<updated>1785646373594</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="6" />
|
<task id="LOCAL-00006" summary="update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1785661529816</created>
|
||||||
|
<option name="number" value="00006" />
|
||||||
|
<option name="presentableId" value="LOCAL-00006" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1785661529816</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00007" summary="update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1785661545228</created>
|
||||||
|
<option name="number" value="00007" />
|
||||||
|
<option name="presentableId" value="LOCAL-00007" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1785661545228</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="8" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
|||||||
@@ -1,42 +1,41 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// context زبان سایت: نگهداری زبان فعلی (fa/en)، ذخیره در localStorage، و تنظیم جهت صفحه (rtl/ltr).
|
|
||||||
|
|
||||||
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);
|
const LanguageContext = createContext(null);
|
||||||
|
|
||||||
export function LanguageProvider({ children }) {
|
export function LanguageProvider({children}) {
|
||||||
const [locale, setLocale] = useState('fa');
|
const [locale, setLocale] = useState('fa');
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const saved = window.localStorage.getItem('rm-locale');
|
const saved = window.localStorage.getItem('rm-locale');
|
||||||
if (saved === 'fa' || saved === 'en') setLocale(saved);
|
if (saved === 'fa' || saved === 'en') setLocale(saved);
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
root.lang = locale;
|
root.lang = locale;
|
||||||
root.dir = content[locale].dir;
|
root.dir = content[locale].dir;
|
||||||
window.localStorage.setItem('rm-locale', locale);
|
window.localStorage.setItem('rm-locale', locale);
|
||||||
}, [locale]);
|
}, [locale]);
|
||||||
|
|
||||||
const toggleLocale = () => setLocale((l) => (l === 'fa' ? 'en' : 'fa'));
|
const toggleLocale = () => setLocale((l) => (l === 'fa' ? 'en' : 'fa'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LanguageContext.Provider
|
<LanguageContext.Provider
|
||||||
value={{ locale, setLocale, toggleLocale, t: content[locale], dir: content[locale].dir, mounted }}
|
value={{locale, setLocale, toggleLocale, t: content[locale], dir: content[locale].dir, mounted}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</LanguageContext.Provider>
|
</LanguageContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLanguage() {
|
export function useLanguage() {
|
||||||
const ctx = useContext(LanguageContext);
|
const ctx = useContext(LanguageContext);
|
||||||
if (!ctx) throw new Error('useLanguage must be used within LanguageProvider');
|
if (!ctx) throw new Error('useLanguage must be used within LanguageProvider');
|
||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,44 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useContext, useEffect, useState } from 'react';
|
import {createContext, useContext, useEffect, useState} from 'react';
|
||||||
|
|
||||||
const ThemeContext = createContext(null);
|
const ThemeContext = createContext(null);
|
||||||
|
|
||||||
export function ThemeProvider({ children }) {
|
export function ThemeProvider({children}) {
|
||||||
// پیشفرض سایت حالا «روشن» (light) است. اگر کاربر قبلاً تم را عوض کرده باشد
|
|
||||||
// (ذخیرهشده در localStorage) همان ترجیح رو حفظ میکنیم؛ در غیر این صورت فقط
|
|
||||||
// اگر سیستم کاربر صراحتاً روی حالت تیره تنظیم شده باشد، تیره را انتخاب میکنیم.
|
|
||||||
const [theme, setTheme] = useState('light');
|
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
|
|
||||||
// On mount: read saved preference, else fall back to system preference
|
const [theme, setTheme] = useState('light');
|
||||||
// (but default stays light unless the OS explicitly prefers dark).
|
const [mounted, setMounted] = useState(false);
|
||||||
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);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Reflect theme on <html> 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 (
|
|
||||||
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme, mounted }}>
|
useEffect(() => {
|
||||||
{children}
|
const root = document.documentElement;
|
||||||
</ThemeContext.Provider>
|
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 (
|
||||||
|
<ThemeContext.Provider value={{theme, setTheme, toggleTheme, mounted}}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTheme() {
|
export function useTheme() {
|
||||||
const ctx = useContext(ThemeContext);
|
const ctx = useContext(ThemeContext);
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-18
@@ -1,29 +1,26 @@
|
|||||||
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';
|
||||||
|
|
||||||
const vazirmatn = Vazirmatn({
|
const vazirmatn = Vazirmatn({
|
||||||
subsets: ['arabic'],
|
subsets: ['arabic'],
|
||||||
variable: '--font-vazirmatn',
|
variable: '--font-vazirmatn',
|
||||||
display: 'swap',
|
display: 'swap',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'روبات مارکت | دستگاههای وندینگ هوشمند',
|
title: 'روبات مارکت | دستگاههای وندینگ هوشمند',
|
||||||
description:
|
description:
|
||||||
'روبات مارکت؛ دستگاههای فروش خودکار هوشمند با مدیریت آنلاین، تحویل نرم و طراحی مدرن.',
|
'روبات مارکت؛ دستگاههای فروش خودکار هوشمند با مدیریت آنلاین، تحویل نرم و طراحی مدرن.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }) {
|
export default function RootLayout({children}) {
|
||||||
// پوسته پیشفرض سمت سرور: فارسی + حالت روشن (Light).
|
|
||||||
// ThemeProvider/LanguageProvider بعد از mount شدن، اگر کاربر قبلاً تم/زبان
|
return (
|
||||||
// دیگری را انتخاب کرده بود (ذخیرهشده در localStorage)، آن را روی <html> اعمال
|
<html lang="fa" dir="rtl" className={`${vazirmatn.variable} light`} suppressHydrationWarning>
|
||||||
// میکنند؛ suppressHydrationWarning جلوی هشدار بیضرر عدمتطابق را میگیرد.
|
<body className="bg-page text-primary antialiased transition-colors duration-300">
|
||||||
return (
|
|
||||||
<html lang="fa" dir="rtl" className={`${vazirmatn.variable} light`} suppressHydrationWarning>
|
|
||||||
<body className="bg-page text-primary antialiased transition-colors duration-300">
|
|
||||||
<Providers>{children}</Providers>
|
<Providers>{children}</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-49
@@ -2,60 +2,60 @@
|
|||||||
|
|
||||||
// پل ارتباطی بین Redux، حالت تیره/روشن، و زبان فا/en با تنظیمات ConfigProvider آنتدیزاین.
|
// پل ارتباطی بین Redux، حالت تیره/روشن، و زبان فا/en با تنظیمات ConfigProvider آنتدیزاین.
|
||||||
|
|
||||||
import { Provider as ReduxProvider } from 'react-redux';
|
import {Provider as ReduxProvider} from 'react-redux';
|
||||||
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';
|
||||||
import { store } from '@/store/store';
|
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}) {
|
||||||
const { theme } = useTheme();
|
const {theme} = useTheme();
|
||||||
const { locale, dir } = useLanguage();
|
const {locale, dir} = useLanguage();
|
||||||
|
|
||||||
const antdTheme = {
|
const antdTheme = {
|
||||||
algorithm: theme === 'dark' ? antdThemeApi.darkAlgorithm : antdThemeApi.defaultAlgorithm,
|
algorithm: theme === 'dark' ? antdThemeApi.darkAlgorithm : antdThemeApi.defaultAlgorithm,
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: '#00a693',
|
colorPrimary: '#00a693',
|
||||||
colorInfo: '#00a693',
|
colorInfo: '#00a693',
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
fontFamily: 'var(--font-vazirmatn), Tahoma, sans-serif',
|
fontFamily: 'var(--font-vazirmatn), Tahoma, sans-serif',
|
||||||
colorBgContainer: theme === 'dark' ? '#0c2b24' : '#ffffff',
|
colorBgContainer: theme === 'dark' ? '#0c2b24' : '#ffffff',
|
||||||
colorBgElevated: theme === 'dark' ? '#0f332b' : '#ffffff',
|
colorBgElevated: theme === 'dark' ? '#0f332b' : '#ffffff',
|
||||||
colorBorder: theme === 'dark' ? '#0b3d38' : '#c0ebe3',
|
colorBorder: theme === 'dark' ? '#0b3d38' : '#c0ebe3',
|
||||||
colorText: theme === 'dark' ? '#e6f7f4' : '#04231f',
|
colorText: theme === 'dark' ? '#e6f7f4' : '#04231f',
|
||||||
colorTextSecondary: theme === 'dark' ? 'rgba(230,247,244,0.7)' : 'rgba(4,35,31,0.65)',
|
colorTextSecondary: theme === 'dark' ? 'rgba(230,247,244,0.7)' : 'rgba(4,35,31,0.65)',
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Button: { controlHeight: 44, fontWeight: 600 },
|
Button: {controlHeight: 44, fontWeight: 600},
|
||||||
Badge: { colorError: '#ff6b57' },
|
Badge: {colorError: '#ff6b57'},
|
||||||
Modal: {
|
Modal: {
|
||||||
contentBg: theme === 'dark' ? '#0c2b24' : '#ffffff',
|
contentBg: theme === 'dark' ? '#0c2b24' : '#ffffff',
|
||||||
headerBg: theme === 'dark' ? '#0c2b24' : '#ffffff',
|
headerBg: theme === 'dark' ? '#0c2b24' : '#ffffff',
|
||||||
titleColor: theme === 'dark' ? '#e6f7f4' : '#04231f',
|
titleColor: theme === 'dark' ? '#e6f7f4' : '#04231f',
|
||||||
},
|
},
|
||||||
Drawer: {
|
Drawer: {
|
||||||
colorBgElevated: theme === 'dark' ? '#0c2b24' : '#ffffff',
|
colorBgElevated: theme === 'dark' ? '#0c2b24' : '#ffffff',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider direction={dir} locale={locale === 'fa' ? faIR : enUS} theme={antdTheme}>
|
<ConfigProvider direction={dir} locale={locale === 'fa' ? faIR : enUS} theme={antdTheme}>
|
||||||
{children}
|
{children}
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Providers({ children }) {
|
export default function Providers({children}) {
|
||||||
return (
|
return (
|
||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<LanguageProvider>
|
<LanguageProvider>
|
||||||
<AntdBridge>{children}</AntdBridge>
|
<AntdBridge>{children}</AntdBridge>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</ReduxProvider>
|
</ReduxProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+112
-110
@@ -4,126 +4,128 @@
|
|||||||
// و یک ویدیوی پسزمینه دارد که با پیشرفت اسکرول جلو/عقب میرود. طبق درخواست کاربر، این سکشن
|
// و یک ویدیوی پسزمینه دارد که با پیشرفت اسکرول جلو/عقب میرود. طبق درخواست کاربر، این سکشن
|
||||||
// و هیرو از چرخهی رنگبندی متناوب بقیه سکشنها مستثنا هستند.
|
// و هیرو از چرخهی رنگبندی متناوب بقیه سکشنها مستثنا هستند.
|
||||||
|
|
||||||
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 {useDispatch} from 'react-redux';
|
||||||
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';
|
||||||
import { addToCart } from '@/store/cartSlice';
|
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(null);
|
||||||
const trackRef = useRef(null);
|
const trackRef = useRef(null);
|
||||||
const videoRef = useRef(null);
|
const videoRef = useRef(null);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { t } = useLanguage();
|
const {t} = useLanguage();
|
||||||
const [quickViewMachine, setQuickViewMachine] = useState(null);
|
const [quickViewMachine, setQuickViewMachine] = useState(null);
|
||||||
const [videoAvailable, setVideoAvailable] = useState(true);
|
const [videoAvailable, setVideoAvailable] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ctx = gsap.context(() => {
|
const ctx = gsap.context(() => {
|
||||||
const track = trackRef.current;
|
const track = trackRef.current;
|
||||||
const section = sectionRef.current;
|
const section = sectionRef.current;
|
||||||
if (!track || !section) return;
|
if (!track || !section) return;
|
||||||
|
|
||||||
const scrollDistance = () => track.scrollWidth - window.innerWidth;
|
const scrollDistance = () => track.scrollWidth - window.innerWidth;
|
||||||
|
|
||||||
const horizontalTween = gsap.to(track, {
|
const horizontalTween = gsap.to(track, {
|
||||||
x: () => -scrollDistance(),
|
x: () => -scrollDistance(),
|
||||||
ease: 'none',
|
ease: 'none',
|
||||||
scrollTrigger: {
|
scrollTrigger: {
|
||||||
trigger: section,
|
trigger: section,
|
||||||
start: 'top top',
|
start: 'top top',
|
||||||
end: () => `+=${scrollDistance()}`,
|
end: () => `+=${scrollDistance()}`,
|
||||||
scrub: 1,
|
scrub: 1,
|
||||||
pin: true,
|
pin: true,
|
||||||
invalidateOnRefresh: true,
|
invalidateOnRefresh: true,
|
||||||
onUpdate: (self) => {
|
onUpdate: (self) => {
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
if (video && video.duration) {
|
if (video && video.duration) {
|
||||||
video.currentTime = self.progress * video.duration;
|
video.currentTime = self.progress * video.duration;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
horizontalTween.scrollTrigger?.kill();
|
horizontalTween.scrollTrigger?.kill();
|
||||||
horizontalTween.kill();
|
horizontalTween.kill();
|
||||||
};
|
};
|
||||||
}, sectionRef);
|
}, sectionRef);
|
||||||
|
|
||||||
return () => ctx.revert();
|
return () => ctx.revert();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleModalAddToCart = () => {
|
const handleModalAddToCart = () => {
|
||||||
if (!quickViewMachine) return;
|
if (!quickViewMachine) return;
|
||||||
dispatch(
|
dispatch(
|
||||||
addToCart({
|
addToCart({
|
||||||
id: quickViewMachine.id,
|
id: quickViewMachine.id,
|
||||||
name: quickViewMachine.name,
|
name: quickViewMachine.name,
|
||||||
priceNumeric: quickViewMachine.priceNumeric,
|
priceNumeric: quickViewMachine.priceNumeric,
|
||||||
priceLabel: quickViewMachine.priceLabel,
|
priceLabel: quickViewMachine.priceLabel,
|
||||||
image: quickViewMachine.image,
|
image: quickViewMachine.image,
|
||||||
})
|
})
|
||||||
|
);
|
||||||
|
message.success(t.cart.addToast(quickViewMachine.name));
|
||||||
|
setQuickViewMachine(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="showcase" ref={sectionRef} className="relative w-full overflow-hidden">
|
||||||
|
<div className="absolute inset-0 z-0">
|
||||||
|
{videoAvailable && (
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
src={showcaseVideoSrc}
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
preload="auto"
|
||||||
|
className="w-full h-full object-cover opacity-60"
|
||||||
|
onError={() => setVideoAvailable(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(180deg, rgba(4,18,15,0.55) 0%, rgba(7,30,25,0.3) 50%, rgba(4,18,15,0.6) 100%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={trackRef} className="horizontal-track relative z-10" dir="ltr">
|
||||||
|
{t.machines.map((machine) => (
|
||||||
|
<VendingCard key={machine.id} machine={machine} onQuickView={setQuickViewMachine}/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={!!quickViewMachine}
|
||||||
|
onCancel={() => setQuickViewMachine(null)}
|
||||||
|
footer={null}
|
||||||
|
centered
|
||||||
|
title={quickViewMachine?.name}
|
||||||
|
>
|
||||||
|
{quickViewMachine && (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<img src={quickViewMachine.image} alt={quickViewMachine.name}
|
||||||
|
className="w-full h-64 object-contain"/>
|
||||||
|
<p className="text-secondary leading-7">{quickViewMachine.description}</p>
|
||||||
|
<p className="text-xl font-bold text-primary">{quickViewMachine.priceLabel}</p>
|
||||||
|
<Button type="primary" size="large" icon={<ShoppingCartOutlined/>}
|
||||||
|
onClick={handleModalAddToCart}>
|
||||||
|
{t.machineActions.addToCart}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
message.success(t.cart.addToast(quickViewMachine.name));
|
|
||||||
setQuickViewMachine(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section id="showcase" ref={sectionRef} className="relative w-full overflow-hidden">
|
|
||||||
<div className="absolute inset-0 z-0">
|
|
||||||
{videoAvailable && (
|
|
||||||
<video
|
|
||||||
ref={videoRef}
|
|
||||||
src={showcaseVideoSrc}
|
|
||||||
muted
|
|
||||||
playsInline
|
|
||||||
preload="auto"
|
|
||||||
className="w-full h-full object-cover opacity-60"
|
|
||||||
onError={() => setVideoAvailable(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
'linear-gradient(180deg, rgba(4,18,15,0.55) 0%, rgba(7,30,25,0.3) 50%, rgba(4,18,15,0.6) 100%)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ref={trackRef} className="horizontal-track relative z-10" dir="ltr">
|
|
||||||
{t.machines.map((machine) => (
|
|
||||||
<VendingCard key={machine.id} machine={machine} onQuickView={setQuickViewMachine} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={!!quickViewMachine}
|
|
||||||
onCancel={() => setQuickViewMachine(null)}
|
|
||||||
footer={null}
|
|
||||||
centered
|
|
||||||
title={quickViewMachine?.name}
|
|
||||||
>
|
|
||||||
{quickViewMachine && (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<img src={quickViewMachine.image} alt={quickViewMachine.name} className="w-full h-64 object-contain" />
|
|
||||||
<p className="text-secondary leading-7">{quickViewMachine.description}</p>
|
|
||||||
<p className="text-xl font-bold text-primary">{quickViewMachine.priceLabel}</p>
|
|
||||||
<Button type="primary" size="large" icon={<ShoppingCartOutlined />} onClick={handleModalAddToCart}>
|
|
||||||
{t.machineActions.addToCart}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.7 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 290 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user