feat: implement application router and dynamic feature section on the home page
This commit is contained in:
parent
896cc662ab
commit
12326898b3
|
|
@ -42,7 +42,7 @@ const TOP_NAV: TopNav[] = [
|
||||||
{ fa: 'نبض صنعت', en: 'Industry Pulse', to: '/pulse' },
|
{ fa: 'نبض صنعت', en: 'Industry Pulse', to: '/pulse' },
|
||||||
{ fa: 'رسانه و رویداد', en: 'Media & Events', dropdown: 'media' },
|
{ fa: 'رسانه و رویداد', en: 'Media & Events', dropdown: 'media' },
|
||||||
{ fa: 'در یک نگاه', en: 'At a Glance', to: '/at-a-glance' },
|
{ fa: 'در یک نگاه', en: 'At a Glance', to: '/at-a-glance' },
|
||||||
{ fa: 'شبکه خبرگان', en: 'Expert Network', to: '#' },
|
{ fa: 'شبکه خبرگان', en: 'Expert Network', to: '/experts' },
|
||||||
{ fa: 'درباره ما', en: 'About', to: '#' },
|
{ fa: 'درباره ما', en: 'About', to: '#' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import Membership from '@/pages/Membership/Membership'
|
||||||
import Sustainability from '@/pages/Sustainability/Sustainability'
|
import Sustainability from '@/pages/Sustainability/Sustainability'
|
||||||
import Geopolitics from '@/pages/Geopolitics/Geopolitics'
|
import Geopolitics from '@/pages/Geopolitics/Geopolitics'
|
||||||
import AtAGlance from '@/pages/AtAGlance/AtAGlance'
|
import AtAGlance from '@/pages/AtAGlance/AtAGlance'
|
||||||
|
import Experts from '@/pages/Experts/Experts'
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
|
|
@ -42,6 +43,7 @@ export const router = createBrowserRouter([
|
||||||
{ path: 'geopolitics', element: <Geopolitics /> },
|
{ path: 'geopolitics', element: <Geopolitics /> },
|
||||||
{ path: 'membership', element: <Membership /> },
|
{ path: 'membership', element: <Membership /> },
|
||||||
{ path: 'at-a-glance', element: <AtAGlance /> },
|
{ path: 'at-a-glance', element: <AtAGlance /> },
|
||||||
|
{ path: 'experts', element: <Experts /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,508 @@
|
||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
import { useLang } from '@/context/LangContext'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { Globe, Cpu, BarChart3, Leaf, Quote } from 'lucide-react'
|
||||||
|
|
||||||
|
// --- Types ---
|
||||||
|
interface Expert {
|
||||||
|
id: string
|
||||||
|
nameFa: string
|
||||||
|
nameEn: string
|
||||||
|
roleFa: string
|
||||||
|
roleEn: string
|
||||||
|
bioFa: string
|
||||||
|
bioEn: string
|
||||||
|
photo: string
|
||||||
|
telegram?: string
|
||||||
|
linkedin?: string
|
||||||
|
twitter?: string
|
||||||
|
website?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Data ---
|
||||||
|
const FEATURED_EXPERTS: Expert[] = [
|
||||||
|
{
|
||||||
|
id: 'saeed-zarandi',
|
||||||
|
nameFa: 'دکتر سعید زرندی',
|
||||||
|
nameEn: 'Dr. Saeed Zarandi',
|
||||||
|
roleFa: 'مدیرعامل و عضو هیئتمدیره شرکت سرمایهگذاری صنایع شیمیایی ایران و مشاور صنایع معدنی',
|
||||||
|
roleEn: 'CEO & Board Member of ICIIC, Senior Mining & Metallurgy Consultant',
|
||||||
|
bioFa: 'سعید زرندی یکی از مدیران ارشد صنعت و اقتصاد ایران است که با تحصیلات در حوزه مهندسی معدن و مدیریت دولتی، در سمتهای مختلفی از جمله معاون طرح و برنامه وزارت صنعت، معدن و تجارت و مدیرعامل هلدینگهای صنعتی و معدنی فعالیت داشته است. وی در بهمن ۱۴۰۳ بهعنوان عضو هیئتمدیره فولاد مبارکه منصوب شد.',
|
||||||
|
bioEn: 'Saeed Zarandi is a senior executive in Iran\'s industrial and economic sectors. With a background in mining engineering and public administration, he has served as Deputy Minister of Industry, Mine and Trade, and CEO of major industrial holdings. He was appointed to the board of Mobarakeh Steel in early 2025.',
|
||||||
|
photo: '/1.jpg',
|
||||||
|
telegram: 'https://t.me',
|
||||||
|
linkedin: 'https://linkedin.com',
|
||||||
|
twitter: 'https://x.com',
|
||||||
|
website: 'https://google.com'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fateme-ahmadi',
|
||||||
|
nameFa: 'دکتر فاطمه احمدی',
|
||||||
|
nameEn: 'Dr. Fatemeh Ahmadi',
|
||||||
|
roleFa: 'مدیر تحقیقات ریسک ژئوپلیتیک و عضو اتاق فکر توسعه پایدار',
|
||||||
|
roleEn: 'Geopolitical Risk Director & Member of Sustainable Development Think Tank',
|
||||||
|
bioFa: 'فاطمه احمدی فارغالتحصیل دکتری مدیریت ریسک از دانشگاه علم و صنعت ایران با بیش از ۱۵ سال تجربه پژوهشی در حوزه زنجیره تأمین صنایع معدنی و فلزی است. وی مشاور ارشد چندین طرح کربنزدایی در صنعت فولاد است.',
|
||||||
|
bioEn: 'Fatemeh Ahmadi holds a Ph.D. in Risk Management from Iran University of Science and Technology. She has over 15 years of research experience in mineral and metal supply chains and is a senior consultant on carbon border regulations.',
|
||||||
|
photo: '/2.jpg',
|
||||||
|
telegram: 'https://t.me',
|
||||||
|
linkedin: 'https://linkedin.com',
|
||||||
|
twitter: 'https://x.com'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hosein-karimi',
|
||||||
|
nameFa: 'دکتر حسین کریمی',
|
||||||
|
nameEn: 'Dr. Hossein Karimi',
|
||||||
|
roleFa: 'تحلیلگر ارشد بازار جهانی کامودیتی و عضو اتاق فکر صنعت فولاد',
|
||||||
|
roleEn: 'Senior Commodity Analyst & Member of Steel Industry Think Tank',
|
||||||
|
bioFa: 'حسین کریمی دکتری مهندسی مواد از دانشگاه صنعتی اصفهان دارد. حوزه تخصصی پژوهشهای وی متالورژی صنعتی و پیشبینی تقاضای بازارهای آسیایی است و بهطور مرتب در رسانههای بینالمللی تحلیل منتشر میکند.',
|
||||||
|
bioEn: 'Hossein Karimi received his Ph.D. in Materials Engineering from Isfahan University of Technology. His expertise covers industrial metallurgy and demand forecasting for Asian steel markets, frequently writing for global publications.',
|
||||||
|
photo: '/3.jpg',
|
||||||
|
linkedin: 'https://linkedin.com',
|
||||||
|
twitter: 'https://x.com',
|
||||||
|
website: 'https://google.com'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sara-rezaei',
|
||||||
|
nameFa: 'مهندس سارا رضایی',
|
||||||
|
nameEn: 'Ms. Sara Rezaei',
|
||||||
|
roleFa: 'پژوهشگر ارشد انرژی و پایداری و مدیر اتاق فکر فناوری و نوآوری',
|
||||||
|
roleEn: 'Senior Energy & Sustainability Researcher & Head of Technology Think Tank',
|
||||||
|
bioFa: 'سارا رضایی فارغالتحصیل کارشناسی ارشد مهندسی انرژی از دانشگاه صنعتی شریف است. پژوهشهای او در زمینه بهرهوری کوره قوس الکتریکی و کربنزدایی با هیدروژن، از مراجع علمی صنعت فولاد است.',
|
||||||
|
bioEn: 'Sara Rezaei holds an M.Sc. in Energy Engineering from Sharif University of Technology. Her research on EAF efficiency, green hydrogen, and carbon reduction roadmaps serves as a benchmark in local clean steel policymaking.',
|
||||||
|
photo: '/4.jpg',
|
||||||
|
telegram: 'https://t.me',
|
||||||
|
linkedin: 'https://linkedin.com'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ali-mohammadi',
|
||||||
|
nameFa: 'دکتر علی محمدی',
|
||||||
|
nameEn: 'Dr. Ali Mohammadi',
|
||||||
|
roleFa: 'مدیر ارشد اقتصاد صنعتی و دبیر علمی اتاق فکر اقتصادی',
|
||||||
|
roleEn: 'Industrial Economics Director & Scientific Secretary of Economic Think Tank',
|
||||||
|
bioFa: 'علی محمدی دکتری اقتصاد صنعتی از دانشگاه تهران دارد. وی پیش از این مشاور اقتصادی ایمیدرو بوده و بیش از ۵۰ گزارش تحلیلی در بازارهای فولاد و بازارهای مالی داخلی و جهانی منتشر کرده است.',
|
||||||
|
bioEn: 'Ali Mohammadi holds a Ph.D. in Industrial Economics from the University of Tehran. Previously an economic advisor to IMIDRO, he has authored over 50 reports on steel tariffs, macro trends, and financial commodities.',
|
||||||
|
photo: '/5.jpg',
|
||||||
|
telegram: 'https://t.me',
|
||||||
|
linkedin: 'https://linkedin.com',
|
||||||
|
twitter: 'https://x.com',
|
||||||
|
website: 'https://google.com'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const ROOM_EXPERTS = {
|
||||||
|
sustainability: [
|
||||||
|
{ nameFa: 'دکتر فاطمه احمدی', nameEn: 'Dr. Fatemeh Ahmadi', roleFa: 'مدیر ریسک ژئوپلیتیک', roleEn: 'Geopolitical Risk Director', photo: '/2.jpg' },
|
||||||
|
{ nameFa: 'مهندس سارا رضایی', nameEn: 'Ms. Sara Rezaei', roleFa: 'تحلیلگر پایداری و انرژی', roleEn: 'Energy & Sustainability Analyst', photo: '/4.jpg' },
|
||||||
|
{ nameFa: 'دکتر مریم حسینی', nameEn: 'Dr. Maryam Hosseini', roleFa: 'پژوهشگر بازاریابی بینالملل', roleEn: 'International Marketing Scholar', photo: '/6.jpg' },
|
||||||
|
{ nameFa: 'دکتر رضا عباسی', nameEn: 'Dr. Reza Abbasi', roleFa: 'مشاور محیطزیست صنعتی', roleEn: 'Industrial Environmental Advisor', photo: '/7.jpg' }
|
||||||
|
],
|
||||||
|
steel: [
|
||||||
|
{ nameFa: 'دکتر سعید زرندی', nameEn: 'Dr. Saeed Zarandi', roleFa: 'عضو هیئتمدیره فولاد مبارکه', roleEn: 'Board Member of MSC', photo: '/1.jpg' },
|
||||||
|
{ nameFa: 'دکتر حسین کریمی', nameEn: 'Dr. Hossein Karimi', roleFa: 'تحلیلگر متالورژی و بازار', roleEn: 'Metallurgy & Market Analyst', photo: '/3.jpg' },
|
||||||
|
{ nameFa: 'مهندس رضا توکلی', nameEn: 'Ms. Reza Tavakoli', roleFa: 'تحلیلگر بورس کالا و آتی', roleEn: 'Mercantile & Futures Analyst', photo: '/8.jpg' },
|
||||||
|
{ nameFa: 'دکتر محمود صادقی', nameEn: 'Dr. Mahmoud Sadeghi', roleFa: 'مشاور فناوری کوره قوس', roleEn: 'EAF Steelmaking Consultant', photo: '/9.jpg' }
|
||||||
|
],
|
||||||
|
economics: [
|
||||||
|
{ nameFa: 'دکتر علی محمدی', nameEn: 'Dr. Ali Mohammadi', roleFa: 'مدیر ارشد اقتصاد صنعتی', roleEn: 'Industrial Economics Director', photo: '/5.jpg' },
|
||||||
|
{ nameFa: 'مهندس رضا توکلی', nameEn: 'Ms. Reza Tavakoli', roleFa: 'تحلیلگر بازار کامودیتی', roleEn: 'Commodity Market Analyst', photo: '/8.jpg' },
|
||||||
|
{ nameFa: 'دکتر حمید علوی', nameEn: 'Dr. Hamid Alavi', roleFa: 'اقتصاددان زنجیره تأمین', roleEn: 'Supply Chain Economist', photo: '/10.jpg' },
|
||||||
|
{ nameFa: 'دکتر زهرا راد', nameEn: 'Dr. Zahra Rad', roleFa: 'تحلیلگر مالی سرمایهگذاری سبز', roleEn: 'Green Investment Financial Analyst', photo: '/11.jpg' }
|
||||||
|
],
|
||||||
|
technology: [
|
||||||
|
{ nameFa: 'مهندس سارا رضایی', nameEn: 'Ms. Sara Rezaei', roleFa: 'پژوهشگر فناوریهای نوین', roleEn: 'Emerging Technologies Researcher', photo: '/4.jpg' },
|
||||||
|
{ nameFa: 'دکتر سعید زرندی', nameEn: 'Dr. Saeed Zarandi', roleFa: 'استراتژیست تحول صنعتی', roleEn: 'Industrial Transformation Strategist', photo: '/1.jpg' },
|
||||||
|
{ nameFa: 'دکتر پیمان صالحی', nameEn: 'Dr. Peyman Salehi', roleFa: 'متخصص هوش مصنوعی صنعتی', roleEn: 'Industrial AI Specialist', photo: '/12.jpg' },
|
||||||
|
{ nameFa: 'مهندس نیما راد', nameEn: 'Mr. Nima Rad', roleFa: 'تحلیلگر طرحهای هیدروژن سبز', roleEn: 'Green Hydrogen Analyst', photo: '/13.jpg' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Icons SVG components ---
|
||||||
|
const TelegramIcon = () => (
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||||||
|
<path d="M11.94 20c.27 0 .42-.16.54-.28l2.56-2.48 5.3 3.91c.98.54 1.68.26 1.93-.91l3.39-16c.36-1.44-.55-2.09-1.49-1.66L4.01 10.37c-1.4.56-1.39 1.35-.25 1.7l5.13 1.6 11.89-7.5c.56-.34 1.07-.15.65.22L11.83 14.9l-.37 5.24c.51 0 .73-.23.98-.47v.33z"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const LinkedinIcon = () => (
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||||||
|
<path d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14m-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.32 1.3v-1.11h-2.8v8.37h2.8v-4.67c0-.25.02-.5.1-.68a1.14 1.14 0 0 1 1-.77c.76 0 1 .58 1 1.42v4.7h2.8M6.5 8.37a1.37 1.37 0 1 0-1.37-1.37A1.37 1.37 0 0 0 6.5 8.37M8 18.5V10.13H5.2v8.37H8z"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const TwitterIcon = () => (
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||||||
|
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const WebsiteIcon = () => (
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||||
|
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Local Translations ---
|
||||||
|
const TR = {
|
||||||
|
fa: {
|
||||||
|
title: 'شبکه خبرگان',
|
||||||
|
subtitle: 'جمع اندیشمندان، استراتژیستها و نخبگان اتاقهای فکر اندیشکده فولاد آینده',
|
||||||
|
roomSustainability: 'خبرگان اتاق فکر توسعه پایدار',
|
||||||
|
roomSteel: 'خبرگان اتاق فکر صنعت فولاد',
|
||||||
|
roomEconomics: 'خبرگان اتاق فکر اقتصادی',
|
||||||
|
roomTechnology: 'خبرگان اتاق فکر فناوری و نوآوری'
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
title: 'Expert Network',
|
||||||
|
subtitle: 'A collective of researchers, strategists, and thinkers across our specialized rooms',
|
||||||
|
roomSustainability: 'Sustainable Development Think Tank Experts',
|
||||||
|
roomSteel: 'Steel Industry Think Tank Experts',
|
||||||
|
roomEconomics: 'Economic Think Tank Experts',
|
||||||
|
roomTechnology: 'Technology & Innovation Think Tank Experts'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main Page Component ---
|
||||||
|
export default function Experts() {
|
||||||
|
const { lang } = useLang()
|
||||||
|
const isFa = lang === 'fa'
|
||||||
|
const t = TR[lang]
|
||||||
|
|
||||||
|
// Carousel Autoplay state
|
||||||
|
const [activeIndex, setActiveIndex] = useState(0)
|
||||||
|
const [isPaused, setIsPaused] = useState(false)
|
||||||
|
const autoPlayRef = useRef<(() => void) | null>(null)
|
||||||
|
|
||||||
|
const activeExpert = FEATURED_EXPERTS[activeIndex]
|
||||||
|
|
||||||
|
// Next slide function
|
||||||
|
const handleNext = () => {
|
||||||
|
setActiveIndex((prev) => (prev + 1) % FEATURED_EXPERTS.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the slide changer reference
|
||||||
|
useEffect(() => {
|
||||||
|
autoPlayRef.current = handleNext
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start/pause autoplay interval
|
||||||
|
useEffect(() => {
|
||||||
|
if (isPaused) return
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (autoPlayRef.current) autoPlayRef.current()
|
||||||
|
}, 5000)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [isPaused])
|
||||||
|
|
||||||
|
const renderSocialIcon = (expert: Expert) => (
|
||||||
|
<div style={{ display: 'flex', gap: 10, marginTop: 14, justifyContent: 'center' }}>
|
||||||
|
{expert.telegram && (
|
||||||
|
<a href={expert.telegram} target="_blank" rel="noreferrer"
|
||||||
|
style={{ width: 32, height: 32, borderRadius: '50%', border: '1px solid #CD9E53', color: '#CD9E53', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.2s' }}
|
||||||
|
className="hover:!bg-[#CD9E53] hover:!color-[#032340] hover:!text-[#032340]"
|
||||||
|
>
|
||||||
|
<TelegramIcon />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{expert.linkedin && (
|
||||||
|
<a href={expert.linkedin} target="_blank" rel="noreferrer"
|
||||||
|
style={{ width: 32, height: 32, borderRadius: '50%', border: '1px solid #CD9E53', color: '#CD9E53', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.2s' }}
|
||||||
|
className="hover:!bg-[#CD9E53] hover:!color-[#032340] hover:!text-[#032340]"
|
||||||
|
>
|
||||||
|
<LinkedinIcon />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{expert.twitter && (
|
||||||
|
<a href={expert.twitter} target="_blank" rel="noreferrer"
|
||||||
|
style={{ width: 32, height: 32, borderRadius: '50%', border: '1px solid #CD9E53', color: '#CD9E53', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.2s' }}
|
||||||
|
className="hover:!bg-[#CD9E53] hover:!color-[#032340] hover:!text-[#032340]"
|
||||||
|
>
|
||||||
|
<TwitterIcon />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{expert.website && (
|
||||||
|
<a href={expert.website} target="_blank" rel="noreferrer"
|
||||||
|
style={{ width: 32, height: 32, borderRadius: '50%', border: '1px solid #CD9E53', color: '#CD9E53', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.2s' }}
|
||||||
|
className="hover:!bg-[#CD9E53] hover:!color-[#032340] hover:!text-[#032340]"
|
||||||
|
>
|
||||||
|
<WebsiteIcon />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div dir={isFa ? 'rtl' : 'ltr'} style={{ background: '#faf7f4', minHeight: '100vh', paddingBottom: 80 }}>
|
||||||
|
{/* ─── Page Header ─── */}
|
||||||
|
<div style={{ background: '#032340', padding: '64px 0 56px' }}>
|
||||||
|
<div className="max-w-7xl mx-auto px-12 max-md:px-5">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'stretch', justifyContent: 'flex-start' }}>
|
||||||
|
<div style={{
|
||||||
|
[isFa ? 'borderRight' : 'borderLeft']: '4px solid #CD9E53',
|
||||||
|
[isFa ? 'paddingRight' : 'paddingLeft']: 18,
|
||||||
|
} as React.CSSProperties}>
|
||||||
|
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', color: '#CD9E53', marginBottom: 8 }}>
|
||||||
|
EXPERT NETWORK
|
||||||
|
</div>
|
||||||
|
<h1 style={{ fontSize: 'clamp(28px, 3vw, 44px)', fontWeight: 900, color: '#fff', letterSpacing: '-0.5px', margin: 0 }}>
|
||||||
|
{t.title}
|
||||||
|
</h1>
|
||||||
|
<p style={{ fontSize: 'clamp(13px, 1.1vw, 16px)', color: 'rgba(255, 255, 255, 0.7)', margin: '12px 0 0', maxWidth: 640, lineHeight: 1.8 }}>
|
||||||
|
{t.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ marginTop: 48 }}>
|
||||||
|
|
||||||
|
{/* ─── Top Featured Carousel ─── */}
|
||||||
|
<section
|
||||||
|
onMouseEnter={() => setIsPaused(true)}
|
||||||
|
onMouseLeave={() => setIsPaused(false)}
|
||||||
|
style={{
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: 20,
|
||||||
|
border: '1px solid rgba(7, 29, 73, 0.08)',
|
||||||
|
boxShadow: '0 12px 40px rgba(7, 29, 73, 0.04)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
padding: '36px 40px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 24,
|
||||||
|
marginBottom: 64
|
||||||
|
}}
|
||||||
|
className="max-md:p-5"
|
||||||
|
>
|
||||||
|
{/* Main Slide Split */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: isFa ? 'row' : 'row-reverse',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 36,
|
||||||
|
}} className="max-md:flex-col">
|
||||||
|
|
||||||
|
{/* Left/Right Photo Column */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flexShrink: 0 }} className="max-md:w-full">
|
||||||
|
<div style={{ width: 220, height: 220, borderRadius: 16, overflow: 'hidden', boxShadow: '0 8px 24px rgba(7,29,73,0.1)' }}>
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.img
|
||||||
|
key={activeIndex}
|
||||||
|
src={activeExpert.photo}
|
||||||
|
alt={isFa ? activeExpert.nameFa : activeExpert.nameEn}
|
||||||
|
initial={{ opacity: 0, scale: 0.98 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.98 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
|
/>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
{renderSocialIcon(activeExpert)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right/Left Text Details */}
|
||||||
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', position: 'relative' }}>
|
||||||
|
<div style={{ position: 'absolute', top: -32, [isFa ? 'left' : 'right']: 0, opacity: 0.1, pointerEvents: 'none' }} className="max-md:hidden">
|
||||||
|
<Quote size={64} fill="#CD9E53" color="none" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={activeIndex}
|
||||||
|
initial={{ opacity: 0, x: isFa ? 15 : -15 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: isFa ? -15 : 15 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<h2 style={{ fontSize: 24, fontWeight: 900, color: '#032340', margin: 0 }}>
|
||||||
|
{isFa ? activeExpert.nameFa : activeExpert.nameEn}
|
||||||
|
</h2>
|
||||||
|
<div style={{ fontSize: 13, fontWeight: 700, color: '#CD9E53', marginTop: 6, textTransform: 'uppercase', letterSpacing: '0.5px' }}>
|
||||||
|
{isFa ? activeExpert.roleFa : activeExpert.roleEn}
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: 15, fontWeight: 300, color: 'var(--ink-3)', lineHeight: 1.9, marginTop: 18, textAlign: 'justify' }}>
|
||||||
|
{isFa ? activeExpert.bioFa : activeExpert.bioEn}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Avatars Row at the bottom */}
|
||||||
|
<div style={{ display: 'flex', gap: 12, marginTop: 28, flexWrap: 'wrap', alignItems: 'center', justifyContent: isFa ? 'flex-end' : 'flex-start' }} className="max-md:justify-center">
|
||||||
|
{FEATURED_EXPERTS.map((exp, idx) => {
|
||||||
|
const isActive = idx === activeIndex
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={exp.id}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveIndex(idx)
|
||||||
|
setIsPaused(true) // pause autoplay when user manually interacts
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: '50%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
padding: 0,
|
||||||
|
border: isActive ? '3px solid #CD9E53' : '2px solid transparent',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transform: isActive ? 'scale(1.12)' : 'scale(1)',
|
||||||
|
transition: 'all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)',
|
||||||
|
boxShadow: isActive ? '0 4px 12px rgba(205,158,83,0.3)' : 'none',
|
||||||
|
outline: 'none',
|
||||||
|
background: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={exp.photo}
|
||||||
|
alt=""
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
filter: isActive ? 'none' : 'grayscale(30%)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ─── Grid Rooms Categories ─── */}
|
||||||
|
|
||||||
|
{/* Room 1: Sustainable Development */}
|
||||||
|
<section style={{ marginBottom: 64 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '2px solid #032340', pb: 10, marginBottom: 24, paddingBottom: 12 }}>
|
||||||
|
<Leaf size={20} color="#CD9E53" />
|
||||||
|
<h2 style={{ fontSize: 19, fontWeight: 900, color: '#032340', margin: 0 }}>{t.roomSustainability}</h2>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 24 }} className="max-lg:grid-cols-2 max-md:grid-cols-1">
|
||||||
|
{ROOM_EXPERTS.sustainability.map((exp, index) => (
|
||||||
|
<ExpertCard key={index} name={isFa ? exp.nameFa : exp.nameEn} role={isFa ? exp.roleFa : exp.roleEn} photo={exp.photo} icon={<Leaf size={14} />} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Room 2: Steel Industry */}
|
||||||
|
<section style={{ marginBottom: 64 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '2px solid #032340', pb: 10, marginBottom: 24, paddingBottom: 12 }}>
|
||||||
|
<Globe size={20} color="#CD9E53" />
|
||||||
|
<h2 style={{ fontSize: 19, fontWeight: 900, color: '#032340', margin: 0 }}>{t.roomSteel}</h2>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 24 }} className="max-lg:grid-cols-2 max-md:grid-cols-1">
|
||||||
|
{ROOM_EXPERTS.steel.map((exp, index) => (
|
||||||
|
<ExpertCard key={index} name={isFa ? exp.nameFa : exp.nameEn} role={isFa ? exp.roleFa : exp.roleEn} photo={exp.photo} icon={<Globe size={14} />} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Room 3: Economics */}
|
||||||
|
<section style={{ marginBottom: 64 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '2px solid #032340', pb: 10, marginBottom: 24, paddingBottom: 12 }}>
|
||||||
|
<BarChart3 size={20} color="#CD9E53" />
|
||||||
|
<h2 style={{ fontSize: 19, fontWeight: 900, color: '#032340', margin: 0 }}>{t.roomEconomics}</h2>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 24 }} className="max-lg:grid-cols-2 max-md:grid-cols-1">
|
||||||
|
{ROOM_EXPERTS.economics.map((exp, index) => (
|
||||||
|
<ExpertCard key={index} name={isFa ? exp.nameFa : exp.nameEn} role={isFa ? exp.roleFa : exp.roleEn} photo={exp.photo} icon={<BarChart3 size={14} />} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Room 4: Technology & Innovation */}
|
||||||
|
<section>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '2px solid #032340', pb: 10, marginBottom: 24, paddingBottom: 12 }}>
|
||||||
|
<Cpu size={20} color="#CD9E53" />
|
||||||
|
<h2 style={{ fontSize: 19, fontWeight: 900, color: '#032340', margin: 0 }}>{t.roomTechnology}</h2>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 24 }} className="max-lg:grid-cols-2 max-md:grid-cols-1">
|
||||||
|
{ROOM_EXPERTS.technology.map((exp, index) => (
|
||||||
|
<ExpertCard key={index} name={isFa ? exp.nameFa : exp.nameEn} role={isFa ? exp.roleFa : exp.roleEn} photo={exp.photo} icon={<Cpu size={14} />} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Card Component ---
|
||||||
|
function ExpertCard({ name, role, photo, icon }: { name: string; role: string; photo: string; icon: React.ReactNode }) {
|
||||||
|
const [hovered, setHovered] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
style={{
|
||||||
|
borderRadius: 14,
|
||||||
|
overflow: 'hidden',
|
||||||
|
border: '1px solid rgba(7, 29, 73, 0.07)',
|
||||||
|
background: '#fff',
|
||||||
|
boxShadow: hovered ? '0 12px 30px rgba(7, 29, 73, 0.08)' : '0 4px 12px rgba(7, 29, 73, 0.02)',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
transform: hovered ? 'translateY(-4px)' : 'translateY(0)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Image with grayscale hover */}
|
||||||
|
<div style={{ width: '100%', height: 260, overflow: 'hidden', position: 'relative', background: '#eae7e2' }}>
|
||||||
|
<img
|
||||||
|
src={photo}
|
||||||
|
alt={name}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
filter: hovered ? 'grayscale(0%)' : 'grayscale(100%)',
|
||||||
|
transform: hovered ? 'scale(1.03)' : 'scale(1)',
|
||||||
|
transition: 'all 0.4s ease'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gold details footer */}
|
||||||
|
<div style={{
|
||||||
|
background: '#CD9E53',
|
||||||
|
padding: '16px 20px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
flex: 1,
|
||||||
|
color: '#032340',
|
||||||
|
position: 'relative'
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<h3 style={{ fontSize: 15, fontWeight: 900, margin: 0, lineHeight: 1.3 }}>
|
||||||
|
{name}
|
||||||
|
</h3>
|
||||||
|
<p style={{ fontSize: 11, fontWeight: 600, opacity: 0.8, margin: '6px 0 0', lineHeight: 1.4 }}>
|
||||||
|
{role}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Themed Icon in bottom corner */}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 12,
|
||||||
|
left: 16,
|
||||||
|
opacity: 0.6,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState, useEffect } from 'react'
|
||||||
import { motion, AnimatePresence, useScroll, useMotionValueEvent } from 'framer-motion'
|
import { motion, AnimatePresence, useScroll, useMotionValueEvent } from 'framer-motion'
|
||||||
import { useLang } from '@/context/LangContext'
|
import { useLang } from '@/context/LangContext'
|
||||||
|
import { ArrowLeft, ArrowRight } from 'lucide-react'
|
||||||
|
|
||||||
const NAVY = '#032340'
|
const NAVY = '#032340'
|
||||||
const GOLD = '#CD9E53'
|
const GOLD = '#CD9E53'
|
||||||
|
|
@ -17,7 +18,7 @@ type Feat = {
|
||||||
/* Your 3 hero images. As you scroll, they replace each other in place with a fade. */
|
/* Your 3 hero images. As you scroll, they replace each other in place with a fade. */
|
||||||
const FEATURES: Feat[] = [
|
const FEATURES: Feat[] = [
|
||||||
{
|
{
|
||||||
img: '/hero%201.webp', imgSide: 'right', imgPos: 'center 35%',
|
img: '/hero%201.webp', imgSide: 'right', imgPos: 'right 35%',
|
||||||
fa: {
|
fa: {
|
||||||
kicker: 'اندیشکده فولاد آینده',
|
kicker: 'اندیشکده فولاد آینده',
|
||||||
title: 'بینش راهبردی',
|
title: 'بینش راهبردی',
|
||||||
|
|
@ -83,34 +84,111 @@ function FadeUp({ children, delay = 0 }: { children: React.ReactNode; delay?: nu
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DomeIcon = () => (
|
||||||
|
<svg viewBox="0 0 64 64" width="48" height="48" fill="none" stroke={GOLD} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginBottom: 24 }}>
|
||||||
|
<path d="M42 16 A 12 12 0 1 0 46 28 A 15 15 0 1 1 42 16 Z" fill="rgba(205,158,83,0.25)" stroke="none" />
|
||||||
|
<line x1="8" y1="48" x2="56" y2="48" />
|
||||||
|
<path d="M16 48 A 16 16 0 0 1 48 48 Z" />
|
||||||
|
<path d="M28 32 L36 32 L36 48 L28 48 Z" />
|
||||||
|
<rect x="30" y="42" width="4" height="6" rx="1" fill={GOLD} />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const GearIcon = () => (
|
||||||
|
<svg viewBox="0 0 64 64" width="48" height="48" fill="none" stroke={NAVY} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginBottom: 24 }}>
|
||||||
|
<circle cx="28" cy="36" r="10" />
|
||||||
|
<path d="M28 20 L28 26 M28 46 L28 52 M12 36 L18 36 M38 36 L44 36 M16 24 L21 29 M35 43 L40 48 M16 48 L21 43 M35 29 L40 24" />
|
||||||
|
<line x1="28" y1="36" x2="48" y2="18" />
|
||||||
|
<circle cx="48" cy="18" r="5" fill={NAVY} />
|
||||||
|
<line x1="28" y1="36" x2="16" y2="16" />
|
||||||
|
<circle cx="16" cy="16" r="4" fill={NAVY} />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const ChartIcon = () => (
|
||||||
|
<svg viewBox="0 0 64 64" width="48" height="48" fill="none" stroke={GOLD} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginBottom: 24 }}>
|
||||||
|
<circle cx="42" cy="22" r="10" fill="rgba(205,158,83,0.25)" stroke="none" />
|
||||||
|
<rect x="18" y="36" width="6" height="12" rx="1" fill={GOLD} stroke="none" />
|
||||||
|
<rect x="29" y="26" width="6" height="22" rx="1" fill={GOLD} stroke="none" />
|
||||||
|
<rect x="40" y="18" width="6" height="30" rx="1" fill={GOLD} stroke="none" />
|
||||||
|
<path d="M12 44 L24 32 L36 28 L48 14" stroke={GOLD} strokeWidth="3" />
|
||||||
|
<path d="M38 14 L48 14 L48 24" stroke={GOLD} strokeWidth="3" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
/* the text column for the active slide */
|
/* the text column for the active slide */
|
||||||
function TextPanel({ feat, lang }: { feat: Feat; lang: 'fa' | 'en' }) {
|
function TextPanel({ index, feat, lang }: { index: number; feat: Feat; lang: 'fa' | 'en' }) {
|
||||||
const t = feat[lang]
|
const t = feat[lang]
|
||||||
|
const isSlide2 = index === 1
|
||||||
|
const textColor = isSlide2 ? NAVY : GOLD
|
||||||
|
const descColor = isSlide2 ? 'rgba(3, 35, 64, 0.72)' : 'rgba(255, 255, 255, 0.78)'
|
||||||
|
|
||||||
|
const renderIcon = () => {
|
||||||
|
if (index === 0) return <DomeIcon />
|
||||||
|
if (index === 1) return <GearIcon />
|
||||||
|
return <ChartIcon />
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonText = lang === 'fa'
|
||||||
|
? (index === 0 ? 'آشنایی با مأموریت ما' : index === 1 ? 'تحلیلهای فناوری' : 'داشبوردهای پایداری')
|
||||||
|
: (index === 0 ? 'Our Mission' : index === 1 ? 'Technology Analysis' : 'Sustainability Dashboards')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ direction: 'rtl', maxWidth: 460 }} className="max-md:!text-right">
|
<div style={{
|
||||||
|
direction: lang === 'fa' ? 'rtl' : 'ltr',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 480,
|
||||||
|
textAlign: lang === 'fa' ? 'right' : 'left',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: lang === 'fa' ? 'flex-start' : 'flex-end',
|
||||||
|
}}>
|
||||||
<FadeUp>
|
<FadeUp>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 18, flexDirection: 'row-reverse', justifyContent: 'flex-end' }}>
|
<div style={{ display: 'flex', justifyContent: lang === 'fa' ? 'flex-start' : 'flex-end', width: '100%' }}>
|
||||||
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', color: GOLD, textTransform: 'uppercase' }}>{t.kicker}</span>
|
{renderIcon()}
|
||||||
<div style={{ width: 36, height: 2, background: GOLD }} />
|
|
||||||
</div>
|
</div>
|
||||||
</FadeUp>
|
</FadeUp>
|
||||||
<FadeUp delay={0.05}>
|
<FadeUp delay={0.05}>
|
||||||
<h2 style={{ fontSize: 'clamp(30px, 3.4vw, 52px)', fontWeight: 900, color: NAVY, lineHeight: 1.12, letterSpacing: '-1px', margin: '0 0 20px' }}>
|
<h2 style={{
|
||||||
|
fontSize: 'clamp(28px, 3.4vw, 50px)',
|
||||||
|
fontWeight: 900,
|
||||||
|
color: textColor,
|
||||||
|
lineHeight: 1.15,
|
||||||
|
margin: '0 0 20px',
|
||||||
|
}}>
|
||||||
{t.title}
|
{t.title}
|
||||||
</h2>
|
</h2>
|
||||||
</FadeUp>
|
</FadeUp>
|
||||||
<FadeUp delay={0.1}>
|
<FadeUp delay={0.1}>
|
||||||
<p style={{ fontSize: 'clamp(14px, 1.2vw, 17px)', lineHeight: 1.95, color: 'rgba(7,29,73,0.66)', margin: '0 0 30px' }}>
|
<p style={{
|
||||||
|
fontSize: 'clamp(14px, 1.2vw, 17px)',
|
||||||
|
lineHeight: 1.95,
|
||||||
|
color: descColor,
|
||||||
|
margin: '0 0 32px',
|
||||||
|
textAlign: 'justify'
|
||||||
|
}}>
|
||||||
{t.desc}
|
{t.desc}
|
||||||
</p>
|
</p>
|
||||||
</FadeUp>
|
</FadeUp>
|
||||||
<FadeUp delay={0.15}>
|
<FadeUp delay={0.15}>
|
||||||
<div style={{ paddingTop: 20, borderTop: '1px solid rgba(7,29,73,0.12)' }}>
|
<button style={{
|
||||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 12, flexDirection: 'row-reverse', justifyContent: 'flex-end' }}>
|
display: 'inline-flex',
|
||||||
<span style={{ fontSize: 34, fontWeight: 900, color: NAVY, letterSpacing: '-1px' }}>{t.stat}</span>
|
alignItems: 'center',
|
||||||
<span style={{ fontSize: 13, color: 'rgba(7,29,73,0.55)' }}>{t.statTail}</span>
|
gap: 12,
|
||||||
</div>
|
padding: '14px 28px',
|
||||||
</div>
|
borderRadius: 30,
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 700,
|
||||||
|
background: isSlide2 ? NAVY : GOLD,
|
||||||
|
color: isSlide2 ? '#fff' : NAVY,
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
}} className="hover:!scale-105 hover:!opacity-95">
|
||||||
|
<span>{buttonText}</span>
|
||||||
|
{lang === 'fa' ? <ArrowLeft size={16} strokeWidth={2.5} /> : <ArrowRight size={16} strokeWidth={2.5} />}
|
||||||
|
</button>
|
||||||
</FadeUp>
|
</FadeUp>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -118,12 +196,13 @@ function TextPanel({ feat, lang }: { feat: Feat; lang: 'fa' | 'en' }) {
|
||||||
|
|
||||||
/* progress rail */
|
/* progress rail */
|
||||||
function Rail({ active, total }: { active: number; total: number }) {
|
function Rail({ active, total }: { active: number; total: number }) {
|
||||||
|
const inactiveColor = active === 1 ? 'rgba(3, 35, 64, 0.22)' : 'rgba(255, 255, 255, 0.3)'
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'absolute', right: 28, top: '50%', transform: 'translateY(-50%)', display: 'flex', flexDirection: 'column', gap: 12, zIndex: 20 }} className="max-md:hidden">
|
<div style={{ position: 'absolute', right: 28, top: '50%', transform: 'translateY(-50%)', display: 'flex', flexDirection: 'column', gap: 12, zIndex: 20 }} className="max-md:hidden">
|
||||||
{Array.from({ length: total }).map((_, i) => (
|
{Array.from({ length: total }).map((_, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} style={{
|
||||||
width: 3, height: i === active ? 34 : 14, borderRadius: 2,
|
width: 3, height: i === active ? 34 : 14, borderRadius: 2,
|
||||||
background: i === active ? GOLD : 'rgba(7,29,73,0.18)',
|
background: i === active ? GOLD : inactiveColor,
|
||||||
transition: 'all 400ms cubic-bezier(0.22,1,0.36,1)',
|
transition: 'all 400ms cubic-bezier(0.22,1,0.36,1)',
|
||||||
}} />
|
}} />
|
||||||
))}
|
))}
|
||||||
|
|
@ -135,6 +214,15 @@ export default function SpreadFeatures() {
|
||||||
const { lang } = useLang()
|
const { lang } = useLang()
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const [active, setActive] = useState(0)
|
const [active, setActive] = useState(0)
|
||||||
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mq = window.matchMedia('(max-width: 767px)')
|
||||||
|
const update = () => setIsMobile(mq.matches)
|
||||||
|
update()
|
||||||
|
mq.addEventListener('change', update)
|
||||||
|
return () => mq.removeEventListener('change', update)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const { scrollYProgress } = useScroll({ target: containerRef, offset: ['start start', 'end end'] })
|
const { scrollYProgress } = useScroll({ target: containerRef, offset: ['start start', 'end end'] })
|
||||||
useMotionValueEvent(scrollYProgress, 'change', (v) => {
|
useMotionValueEvent(scrollYProgress, 'change', (v) => {
|
||||||
|
|
@ -143,15 +231,15 @@ export default function SpreadFeatures() {
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
/* 300vh tall → inner pins for the whole scroll; each ~third = one slide.
|
<div ref={containerRef} style={{ height: '300vh', position: 'relative', background: '#faf7f4', marginTop: isMobile ? -60 : -68 }}>
|
||||||
Each full slide (image + text) is stacked and crossfaded, so the layout can
|
|
||||||
alternate per slide (img 1 right · img 2 left · img 3 right) without jumping. */
|
|
||||||
<div ref={containerRef} style={{ height: '300vh', position: 'relative', background: '#faf7f4' }}>
|
|
||||||
<div style={{ position: 'sticky', top: 0, height: '100vh', overflow: 'hidden' }}>
|
<div style={{ position: 'sticky', top: 0, height: '100vh', overflow: 'hidden' }}>
|
||||||
|
|
||||||
{FEATURES.map((f, i) => {
|
{FEATURES.map((f, i) => {
|
||||||
const isActive = i === active
|
const isActive = i === active
|
||||||
const imgRight = f.imgSide === 'right'
|
const isTextOnLeft = i === 1
|
||||||
|
const leftBg = isTextOnLeft ? GOLD : '#efe9e1'
|
||||||
|
const rightBg = isTextOnLeft ? '#efe9e1' : NAVY
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={i}
|
key={i}
|
||||||
|
|
@ -159,34 +247,94 @@ export default function SpreadFeatures() {
|
||||||
animate={{ opacity: isActive ? 1 : 0 }}
|
animate={{ opacity: isActive ? 1 : 0 }}
|
||||||
transition={{ duration: 0.55, ease: EASE }}
|
transition={{ duration: 0.55, ease: EASE }}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute', inset: 0,
|
position: 'absolute',
|
||||||
display: 'flex', flexDirection: imgRight ? 'row' : 'row-reverse',
|
inset: 0,
|
||||||
alignItems: 'center', justifyContent: 'center', direction: 'ltr',
|
display: 'flex',
|
||||||
|
flexDirection: isMobile ? 'column' : 'row',
|
||||||
|
direction: 'ltr',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
justifyContent: 'stretch',
|
||||||
pointerEvents: isActive ? 'auto' : 'none',
|
pointerEvents: isActive ? 'auto' : 'none',
|
||||||
}}
|
}}
|
||||||
className="max-md:!flex-col"
|
|
||||||
>
|
>
|
||||||
{/* TEXT */}
|
{/* LEFT PANEL */}
|
||||||
<div style={{ width: '46%', display: 'flex', alignItems: 'center', padding: '0 6vw' }} className="max-md:!w-full max-md:!px-6 max-md:!pt-6 max-md:!pb-0">
|
<div style={{
|
||||||
{/* only the active slide's text animates in (re-keyed so it replays each time) */}
|
width: isMobile ? '100%' : '50%',
|
||||||
{isActive && (
|
height: isMobile ? '50vh' : '100%',
|
||||||
<AnimatePresence mode="wait">
|
background: leftBg,
|
||||||
<div key={active} style={{ width: '100%' }}>
|
display: 'flex',
|
||||||
<TextPanel feat={f} lang={lang} />
|
alignItems: 'center',
|
||||||
</div>
|
justifyContent: 'center',
|
||||||
</AnimatePresence>
|
position: 'relative',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
{isTextOnLeft ? (
|
||||||
|
isActive && (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<div key={active} style={{ width: '100%', display: 'flex', justifyContent: 'center', padding: isMobile ? '24px 20px' : '0 8vw' }}>
|
||||||
|
<TextPanel index={i} feat={f} lang={lang} />
|
||||||
|
</div>
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
animate={{ scale: isActive ? 1 : 1.04 }}
|
||||||
|
transition={{ duration: 0.6, ease: EASE }}
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={f.img}
|
||||||
|
alt=""
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
objectPosition: f.imgPos || 'center center',
|
||||||
|
display: 'block'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* IMAGE */}
|
{/* RIGHT PANEL */}
|
||||||
<div style={{ width: '54%', height: '76vh', padding: '0 3vw' }} className="max-md:!w-full max-md:!h-[42vh] max-md:!px-6 max-md:!mt-4">
|
<div style={{
|
||||||
<motion.div
|
width: isMobile ? '100%' : '50%',
|
||||||
animate={{ scale: isActive ? 1 : 1.04 }}
|
height: isMobile ? '50vh' : '100%',
|
||||||
transition={{ duration: 0.6, ease: EASE }}
|
background: rightBg,
|
||||||
style={{ width: '100%', height: '100%', borderRadius: 20, overflow: 'hidden', boxShadow: '0 40px 100px -30px rgba(7,29,73,0.40)' }}
|
display: 'flex',
|
||||||
>
|
alignItems: 'center',
|
||||||
<img src={f.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: f.imgPos ?? 'center center', display: 'block' }} />
|
justifyContent: 'center',
|
||||||
</motion.div>
|
position: 'relative',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
{isTextOnLeft ? (
|
||||||
|
<motion.div
|
||||||
|
animate={{ scale: isActive ? 1 : 1.04 }}
|
||||||
|
transition={{ duration: 0.6, ease: EASE }}
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={f.img}
|
||||||
|
alt=""
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
objectPosition: f.imgPos || 'center center',
|
||||||
|
display: 'block'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
isActive && (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<div key={active} style={{ width: '100%', display: 'flex', justifyContent: 'center', padding: isMobile ? '24px 20px' : '0 8vw' }}>
|
||||||
|
<TextPanel index={i} feat={f} lang={lang} />
|
||||||
|
</div>
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue