Home revamp: Spread-style hero, sections, and Lenis+GSAP smooth scroll - Hero: GSAP ScrollTrigger de-zoom (full-bleed image -> centered card + satellite scatter), pinned scrub timeline - Lenis smooth scroll synced to GSAP ticker (desktop only; native momentum on touch to avoid iOS double-smoothing); removed conflicting scroll-behavior:smooth - SpreadFeatures: 3 alternating image/text slides with crossfade + blur fade-up - Radar page + home: report cards with hover author-reveal; data in src/content - AtAGlance: standalone page + slider; IntegrationsSection data-source grid - NewsletterMarquee: looping clickable steel-newsletter ticker with live dots - FactoryReportsScroll, Videocast/Podcast sections - Remove dead FactoryNewsletterLayout + SteelNewsletterSection - Content layer under src/content for panel-ready data Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
parent
269d6c7374
commit
74a8bf567e
|
|
@ -16,6 +16,7 @@
|
|||
"framer-motion": "^12.40.0",
|
||||
"gsap": "^3.15.0",
|
||||
"jalaali-js": "^1.2.8",
|
||||
"lenis": "^1.3.23",
|
||||
"lucide-react": "^1.16.0",
|
||||
"react": "^19.2.6",
|
||||
"react-apexcharts": "^2.1.0",
|
||||
|
|
@ -2354,6 +2355,37 @@
|
|||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lenis": {
|
||||
"version": "1.3.23",
|
||||
"resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.23.tgz",
|
||||
"integrity": "sha512-YxYq3TJqj9sJNv0V9SkyQHejt14xwyIwgDaaMK89Uf9SxQfIszu+gTQSSphh6BWlLTNVKvvXAGkg+Zf+oFIevg==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"playground",
|
||||
"playground/*"
|
||||
],
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/darkroomengineering"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nuxt/kit": ">=3.0.0",
|
||||
"react": ">=17.0.0",
|
||||
"vue": ">=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nuxt/kit": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"framer-motion": "^12.40.0",
|
||||
"gsap": "^3.15.0",
|
||||
"jalaali-js": "^1.2.8",
|
||||
"lenis": "^1.3.23",
|
||||
"lucide-react": "^1.16.0",
|
||||
"react": "^19.2.6",
|
||||
"react-apexcharts": "^2.1.0",
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 276 KiB |
|
|
@ -55,7 +55,7 @@ function NewsletterBox() {
|
|||
letterSpacing: '2px', textTransform: 'uppercase',
|
||||
marginBottom: 12,
|
||||
}}>
|
||||
خبرنامه فولاد
|
||||
خبرنامه فولاد آینده
|
||||
</div>
|
||||
<div style={{ display: 'flex', border: '1px solid rgba(255,255,255,0.18)' }}>
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import { useState, useEffect, useRef } from 'react'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import type { CSSProperties } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { Search, Menu, X } from 'lucide-react'
|
||||
import { Search, Menu, X, ChevronDown, Radar, Leaf, Globe, Cpu, BarChart3 } from 'lucide-react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
|
||||
/* ─────────────────────────────────────────────────────────
|
||||
DESIGN TOKENS
|
||||
───────────────────────────────────────────────────────── */
|
||||
/* ─── tokens ─── */
|
||||
const T = {
|
||||
paper: 'var(--paper)',
|
||||
paper2: 'var(--paper-2)',
|
||||
|
|
@ -19,16 +17,23 @@ const T = {
|
|||
red: 'var(--red)',
|
||||
} as const
|
||||
|
||||
/* ─────────────────────────────────────────────────────────
|
||||
DATA
|
||||
───────────────────────────────────────────────────────── */
|
||||
const NAV_ITEMS: { fa: string; en: string; to: string }[] = [
|
||||
{ fa: 'خانه', en: 'Home', to: '/' },
|
||||
{ fa: 'آیندهپژوهی و رصد هوشمند', en: 'Foresight & Intelligence', to: '/radar' },
|
||||
{ fa: 'پایداری و فولاد سبز', en: 'Sustainability & Green Steel', to: '/sustainability' },
|
||||
{ fa: 'ژئوپلیتیک و اقتصاد جهانی', en: 'Geopolitics & Global Economy', to: '/geopolitics' },
|
||||
{ fa: 'فناوری و نوآوری', en: 'Technology & Innovation', to: '/technology' },
|
||||
{ fa: 'بازار و زنجیره فولاد', en: 'Market & Steel Chain', to: '/pulse' },
|
||||
/* ─── dropdown sections under رادار آینده ─── */
|
||||
const SECTIONS: { fa: string; en: string; to: string; Icon: React.ElementType }[] = [
|
||||
{ fa: 'آیندهپژوهی و رصد هوشمند', en: 'Foresight & Intelligence', to: '/radar', Icon: Radar },
|
||||
{ fa: 'پایداری و فولاد سبز', en: 'Sustainability & Green Steel', to: '/sustainability', Icon: Leaf },
|
||||
{ fa: 'ژئوپلیتیک و اقتصاد جهانی', en: 'Geopolitics & Global Economy', to: '/geopolitics', Icon: Globe },
|
||||
{ fa: 'فناوری و نوآوری', en: 'Technology & Innovation', to: '/technology', Icon: Cpu },
|
||||
{ fa: 'بازار و زنجیره فولاد', en: 'Market & Steel Chain', to: '/pulse', Icon: BarChart3 },
|
||||
]
|
||||
|
||||
/* ─── top-level nav items ─── */
|
||||
const TOP_NAV: { fa: string; en: string; to?: string; dropdown?: boolean }[] = [
|
||||
{ fa: 'رادار آینده', en: 'Radar', dropdown: true },
|
||||
{ fa: 'تحلیلهای استراتژیک', en: 'Strategic Analysis', to: '#' },
|
||||
{ fa: 'چندرسانهای', en: 'Multimedia', to: '#' },
|
||||
{ fa: 'در یک نگاه', en: 'At a Glance', to: '/at-a-glance' },
|
||||
{ fa: 'شبکه خبرگان', en: 'Expert Network', to: '#' },
|
||||
{ fa: 'درباره ما', en: 'About', to: '#' },
|
||||
]
|
||||
|
||||
const META_LEFT: { fa: string; en: string }[] = [
|
||||
|
|
@ -38,43 +43,114 @@ const META_LEFT: { fa: string; en: string }[] = [
|
|||
{ fa: 'رسانه', en: 'Press' },
|
||||
]
|
||||
|
||||
/* ─────────────────────────────────────────────────────────
|
||||
NAV LINK (used in main + drawer)
|
||||
───────────────────────────────────────────────────────── */
|
||||
function MainNavItem({ to, label }: { to: string; label: string }) {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
/* ─── رادار آینده dropdown card ─── */
|
||||
function RadarDropdown({ lang }: { lang: 'fa' | 'en' }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', handler)
|
||||
return () => document.removeEventListener('mousedown', handler)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
style={({ isActive }: { isActive: boolean }): CSSProperties => ({
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: isActive ? T.red : hovered ? T.ink : T.ink3,
|
||||
textDecoration: 'none',
|
||||
padding: '6px 0',
|
||||
position: 'relative',
|
||||
whiteSpace: 'nowrap',
|
||||
<div ref={ref} style={{ position: 'relative' }}>
|
||||
<button
|
||||
onClick={() => setOpen(o => !o)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 4,
|
||||
fontSize: 13, fontWeight: 600,
|
||||
color: open ? T.red : T.ink3,
|
||||
background: 'none', border: 'none', padding: '6px 0',
|
||||
cursor: 'pointer', fontFamily: 'inherit', whiteSpace: 'nowrap',
|
||||
transition: 'color 120ms',
|
||||
borderBottom: isActive ? `2px solid ${T.red}` : '2px solid transparent',
|
||||
letterSpacing: '0.1px',
|
||||
})}
|
||||
borderBottom: open ? `2px solid ${T.red}` : '2px solid transparent',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{lang === 'fa' ? 'رادار آینده' : 'Radar'}
|
||||
<ChevronDown size={12} style={{ transform: open ? 'rotate(180deg)' : 'none', transition: 'transform 200ms' }} />
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -6, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -6, scale: 0.98 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
style={{
|
||||
position: 'absolute', top: 'calc(100% + 14px)',
|
||||
right: lang === 'fa' ? 0 : 'auto',
|
||||
left: lang === 'fa' ? 'auto' : 0,
|
||||
background: '#fff', borderRadius: 12,
|
||||
boxShadow: '0 8px 48px rgba(7,29,73,0.14), 0 2px 8px rgba(7,29,73,0.06)',
|
||||
border: '1px solid rgba(7,29,73,0.08)',
|
||||
minWidth: 300, overflow: 'hidden', zIndex: 200,
|
||||
direction: lang === 'fa' ? 'rtl' : 'ltr',
|
||||
}}
|
||||
>
|
||||
{SECTIONS.map((item, i) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
onClick={() => setOpen(false)}
|
||||
style={({ isActive }: { isActive: boolean }): CSSProperties => ({
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '15px 20px', textDecoration: 'none',
|
||||
color: isActive ? T.red : '#3d3d3d',
|
||||
background: isActive ? 'rgba(155,28,28,0.04)' : 'transparent',
|
||||
borderBottom: i < SECTIONS.length - 1 ? '1px solid rgba(7,29,73,0.07)' : 'none',
|
||||
transition: 'background 100ms', fontSize: 14, fontWeight: 600,
|
||||
})}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = 'rgba(7,29,73,0.03)' }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = 'transparent' }}
|
||||
>
|
||||
<span>{lang === 'fa' ? item.fa : item.en}</span>
|
||||
<item.Icon size={28} color="#e85c0d" strokeWidth={1.5} style={{ flexShrink: 0 }} />
|
||||
</NavLink>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerNavItem({
|
||||
to,
|
||||
label,
|
||||
onClose,
|
||||
}: {
|
||||
to: string
|
||||
label: string
|
||||
onClose: () => void
|
||||
}) {
|
||||
/* ─── plain nav item with chevron ─── */
|
||||
function PlainNavItem({ label, to }: { label: string; to: string }) {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const style: CSSProperties = {
|
||||
display: 'flex', alignItems: 'center', gap: 4,
|
||||
fontSize: 13, fontWeight: 600,
|
||||
color: hovered ? T.ink : T.ink3,
|
||||
textDecoration: 'none', padding: '6px 0', whiteSpace: 'nowrap',
|
||||
transition: 'color 120ms',
|
||||
borderBottom: '2px solid transparent',
|
||||
}
|
||||
const handlers = {
|
||||
onMouseEnter: () => setHovered(true),
|
||||
onMouseLeave: () => setHovered(false),
|
||||
}
|
||||
// internal app route → client-side nav; otherwise plain anchor (hash / external)
|
||||
const isInternal = to.startsWith('/')
|
||||
return isInternal ? (
|
||||
<NavLink to={to} {...handlers} style={style}>
|
||||
{label}
|
||||
<ChevronDown size={12} />
|
||||
</NavLink>
|
||||
) : (
|
||||
<a href={to} {...handlers} style={style}>
|
||||
{label}
|
||||
<ChevronDown size={12} />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── drawer item ─── */
|
||||
function DrawerNavItem({ to, label, onClose }: { to: string; label: string; onClose: () => void }) {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
return (
|
||||
<NavLink
|
||||
|
|
@ -83,14 +159,10 @@ function DrawerNavItem({
|
|||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
style={({ isActive }: { isActive: boolean }): CSSProperties => ({
|
||||
display: 'block',
|
||||
padding: '13px 20px',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
display: 'block', padding: '13px 20px', fontSize: 14, fontWeight: 600,
|
||||
color: isActive || hovered ? T.paper : T.ink,
|
||||
background: isActive ? T.ink : hovered ? T.paper2 : 'transparent',
|
||||
textDecoration: 'none',
|
||||
borderBottom: `1px solid ${T.ruleThin}`,
|
||||
textDecoration: 'none', borderBottom: `1px solid ${T.ruleThin}`,
|
||||
transition: 'background 120ms, color 120ms',
|
||||
})}
|
||||
>
|
||||
|
|
@ -99,41 +171,28 @@ function DrawerNavItem({
|
|||
)
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────
|
||||
MAIN COMPONENT
|
||||
───────────────────────────────────────────────────────── */
|
||||
/* ─── MAIN ─── */
|
||||
export default function Header() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
const { lang, toggle: toggleLang } = useLang()
|
||||
|
||||
/* scroll listener — toggles glass + collapses meta strip */
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 8)
|
||||
onScroll()
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
return () => window.removeEventListener('scroll', onScroll)
|
||||
}, [])
|
||||
|
||||
/* viewport listener */
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia('(max-width: 1023px)')
|
||||
const handler = (e: MediaQueryListEvent | MediaQueryList) => setIsMobile(e.matches)
|
||||
handler(mq)
|
||||
mq.addEventListener('change', handler)
|
||||
handler(mq); mq.addEventListener('change', handler)
|
||||
return () => mq.removeEventListener('change', handler)
|
||||
}, [])
|
||||
|
||||
/* body scroll lock */
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = mobileOpen ? 'hidden' : ''
|
||||
return () => { document.body.style.overflow = '' }
|
||||
}, [mobileOpen])
|
||||
|
||||
/* focus search on open */
|
||||
useEffect(() => {
|
||||
if (searchOpen) searchInputRef.current?.focus()
|
||||
}, [searchOpen])
|
||||
|
|
@ -141,127 +200,70 @@ export default function Header() {
|
|||
return (
|
||||
<header
|
||||
style={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 50,
|
||||
background: scrolled ? 'rgba(255,255,255,0.88)' : T.paper,
|
||||
backdropFilter: scrolled ? 'blur(18px) saturate(160%)' : 'none',
|
||||
WebkitBackdropFilter: scrolled ? 'blur(18px) saturate(160%)' : 'none',
|
||||
borderBottom: scrolled ? '1px solid rgba(7,29,73,0.10)' : 'none',
|
||||
position: 'sticky', top: 0, zIndex: 50,
|
||||
background: 'rgba(255,255,255,0.72)',
|
||||
backdropFilter: 'blur(24px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(24px) saturate(180%)',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.35)',
|
||||
boxShadow: '0 1px 32px rgba(7,29,73,0.10)',
|
||||
direction: lang === 'fa' ? 'rtl' : 'ltr',
|
||||
transition: 'background 220ms ease, backdrop-filter 220ms ease, border-color 220ms ease',
|
||||
}}
|
||||
>
|
||||
{/* ═══════════════════════════════════════════
|
||||
MAIN BAR — brand | nav | actions
|
||||
═══════════════════════════════════════════ */}
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
borderBottom: scrolled ? 'none' : `2px solid ${T.ink}`,
|
||||
transition: 'border-color 220ms ease',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
height: 68,
|
||||
gap: 24,
|
||||
}}
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', height: 68, gap: 20 }}
|
||||
className="max-w-7xl mx-auto px-12 max-md:px-5 max-md:h-[60px]"
|
||||
>
|
||||
{/* ── Brand ── */}
|
||||
{/* Brand */}
|
||||
<NavLink
|
||||
to="/"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
textDecoration: 'none',
|
||||
flexShrink: 0,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 12, textDecoration: 'none', flexShrink: 0, lineHeight: 1 }}
|
||||
>
|
||||
<img
|
||||
src="/logo.png?v=2"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
<img src="/logo.png?v=2" alt="" aria-hidden="true"
|
||||
style={{ height: 52, width: 'auto', display: 'block', flexShrink: 0, marginTop: -8 }}
|
||||
className="max-md:h-[40px]"
|
||||
/>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 19,
|
||||
fontWeight: 900,
|
||||
color: T.ink,
|
||||
letterSpacing: '-0.5px',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
className="max-md:text-[16px]"
|
||||
>
|
||||
<span style={{ fontSize: 19, fontWeight: 900, color: T.ink, letterSpacing: '-0.5px', whiteSpace: 'nowrap' }}
|
||||
className="max-md:text-[16px]">
|
||||
اندیشکده فولاد آینده
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 8,
|
||||
letterSpacing: '2.5px',
|
||||
color: T.ink5,
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
className="max-md:hidden"
|
||||
>
|
||||
<span style={{ fontSize: 8, letterSpacing: '2.5px', color: T.ink5, fontWeight: 600, textTransform: 'uppercase', whiteSpace: 'nowrap' }}
|
||||
className="max-md:hidden">
|
||||
FUTURE STEEL POLICY INSTITUTE
|
||||
</span>
|
||||
</div>
|
||||
</NavLink>
|
||||
|
||||
{/* ── Center: Nav (desktop only) ── */}
|
||||
{/* Desktop Nav */}
|
||||
{!isMobile && (
|
||||
<nav
|
||||
aria-label="ناوبری اصلی"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 22,
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
<nav aria-label="ناوبری اصلی"
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 20, flex: 1, justifyContent: 'center' }}
|
||||
>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<MainNavItem key={item.to} to={item.to} label={lang === 'fa' ? item.fa : item.en} />
|
||||
))}
|
||||
{TOP_NAV.map((item) =>
|
||||
item.dropdown
|
||||
? <RadarDropdown key="radar" lang={lang} />
|
||||
: <PlainNavItem key={item.fa} label={lang === 'fa' ? item.fa : item.en} to={item.to!} />
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
{/* ── Left: Actions ── */}
|
||||
{/* Actions */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
|
||||
{/* Search icon button */}
|
||||
<button
|
||||
aria-label="جستجو"
|
||||
onClick={() => setSearchOpen(s => !s)}
|
||||
style={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
border: `1px solid ${T.ruleThin}`,
|
||||
width: 34, height: 34, border: `1px solid ${T.ruleThin}`,
|
||||
background: searchOpen ? T.ink : 'transparent',
|
||||
color: searchOpen ? T.paper : T.ink,
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'background 120ms, color 120ms',
|
||||
borderRadius: 0,
|
||||
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
transition: 'background 120ms, color 120ms', borderRadius: 0,
|
||||
}}
|
||||
>
|
||||
<Search size={14} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
{/* Login + lang toggle — desktop only */}
|
||||
{!isMobile && (
|
||||
<>
|
||||
<OutlineButton style={{ height: 34, padding: '0 16px', fontSize: 12 }}>
|
||||
|
|
@ -273,32 +275,24 @@ export default function Header() {
|
|||
height: 34, padding: '0 12px', fontSize: 12, fontWeight: 700,
|
||||
background: 'transparent', border: `1px solid ${T.ruleThin}`,
|
||||
color: T.ink, cursor: 'pointer', fontFamily: 'inherit',
|
||||
letterSpacing: '0.5px', transition: 'border-color 120ms, color 120ms',
|
||||
letterSpacing: '0.5px', transition: 'border-color 120ms',
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.borderColor = T.ink; e.currentTarget.style.color = T.ink }}
|
||||
onMouseLeave={e => { e.currentTarget.style.borderColor = T.ruleThin; e.currentTarget.style.color = T.ink }}
|
||||
onMouseEnter={e => { e.currentTarget.style.borderColor = T.ink }}
|
||||
onMouseLeave={e => { e.currentTarget.style.borderColor = T.ruleThin }}
|
||||
>
|
||||
{lang === 'fa' ? 'EN' : 'FA'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Mobile menu */}
|
||||
{isMobile && (
|
||||
<button
|
||||
aria-label="باز کردن منو"
|
||||
onClick={() => setMobileOpen(true)}
|
||||
style={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
border: `1px solid ${T.ruleThin}`,
|
||||
background: 'transparent',
|
||||
color: T.ink,
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 0,
|
||||
width: 34, height: 34, border: `1px solid ${T.ruleThin}`,
|
||||
background: 'transparent', color: T.ink, cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 0,
|
||||
}}
|
||||
>
|
||||
<Menu size={18} />
|
||||
|
|
@ -308,43 +302,24 @@ export default function Header() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* ═══════════════════════════════════════════
|
||||
SEARCH DROPDOWN
|
||||
═══════════════════════════════════════════ */}
|
||||
{/* Search */}
|
||||
<AnimatePresence>
|
||||
{searchOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
borderBottom: `1px solid ${T.ruleThin}`,
|
||||
background: T.paper2,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ padding: '20px 0' }}
|
||||
className="max-w-7xl mx-auto px-12 max-md:px-5"
|
||||
style={{ overflow: 'hidden', borderBottom: `1px solid ${T.ruleThin}`, background: T.paper2 }}
|
||||
>
|
||||
<div style={{ padding: '20px 0' }} className="max-w-7xl mx-auto px-12 max-md:px-5">
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="search"
|
||||
ref={searchInputRef} type="search"
|
||||
placeholder="جستجو در گزارشها، تحلیلها، رویدادها..."
|
||||
aria-label="جستجو"
|
||||
onKeyDown={e => e.key === 'Escape' && setSearchOpen(false)}
|
||||
style={{
|
||||
width: '100%',
|
||||
border: `1px solid ${T.ink}`,
|
||||
padding: '12px 16px',
|
||||
fontSize: 14,
|
||||
background: T.paper,
|
||||
color: T.ink,
|
||||
outline: 'none',
|
||||
borderRadius: 0,
|
||||
fontFamily: 'inherit',
|
||||
direction: 'rtl',
|
||||
width: '100%', border: `1px solid ${T.ink}`, padding: '12px 16px',
|
||||
fontSize: 14, background: T.paper, color: T.ink, outline: 'none',
|
||||
borderRadius: 0, fontFamily: 'inherit', direction: 'rtl',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -352,119 +327,56 @@ export default function Header() {
|
|||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* ═══════════════════════════════════════════
|
||||
MOBILE DRAWER
|
||||
═══════════════════════════════════════════ */}
|
||||
{/* Mobile Drawer */}
|
||||
<AnimatePresence>
|
||||
{mobileOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
key="overlay"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
<motion.div key="overlay"
|
||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(7,29,73,0.55)',
|
||||
zIndex: 100,
|
||||
}}
|
||||
style={{ position: 'fixed', inset: 0, background: 'rgba(7,29,73,0.55)', zIndex: 100 }}
|
||||
/>
|
||||
<motion.div
|
||||
key="drawer"
|
||||
initial={{ x: 300 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: 300 }}
|
||||
<motion.div key="drawer"
|
||||
initial={{ x: 300 }} animate={{ x: 0 }} exit={{ x: 300 }}
|
||||
transition={{ duration: 0.25, ease: [0.32, 0, 0.18, 1] }}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
height: '100dvh',
|
||||
width: 300,
|
||||
background: T.paper,
|
||||
zIndex: 101,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'fixed', top: 0, right: 0, height: '100dvh', width: 300,
|
||||
background: T.paper, zIndex: 101, display: 'flex', flexDirection: 'column',
|
||||
borderLeft: `2px solid ${T.ink}`,
|
||||
}}
|
||||
>
|
||||
{/* Drawer header */}
|
||||
<div
|
||||
style={{
|
||||
padding: '16px 20px',
|
||||
borderBottom: `1px solid ${T.ruleThin}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
background: T.ink,
|
||||
color: T.paper,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 14, fontWeight: 800 }}>
|
||||
اندیشکده فولاد آینده
|
||||
</span>
|
||||
<button
|
||||
aria-label="بستن منو"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: T.paper,
|
||||
padding: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
padding: '16px 20px', borderBottom: `1px solid ${T.ruleThin}`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
background: T.ink, color: T.paper,
|
||||
}}>
|
||||
<span style={{ fontSize: 14, fontWeight: 800 }}>اندیشکده فولاد آینده</span>
|
||||
<button aria-label="بستن منو" onClick={() => setMobileOpen(false)}
|
||||
style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: T.paper, padding: 4, display: 'flex', alignItems: 'center' }}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav style={{ flex: 1, overflowY: 'auto' }}>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<DrawerNavItem
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
<DrawerNavItem to="/" label={lang === 'fa' ? 'خانه' : 'Home'} onClose={() => setMobileOpen(false)} />
|
||||
{SECTIONS.map(item => (
|
||||
<DrawerNavItem key={item.to} to={item.to}
|
||||
label={lang === 'fa' ? item.fa : item.en}
|
||||
onClose={() => setMobileOpen(false)}
|
||||
/>
|
||||
onClose={() => setMobileOpen(false)} />
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Footer */}
|
||||
<div
|
||||
style={{
|
||||
padding: '16px 20px',
|
||||
borderTop: `1px solid ${T.ruleThin}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '16px 20px', borderTop: `1px solid ${T.ruleThin}`, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<RedCtaButton style={{ width: '100%', padding: '10px 16px' }}>
|
||||
{lang === 'fa' ? 'اشتراک سازمانی' : 'Membership'}
|
||||
</RedCtaButton>
|
||||
<OutlineButton style={{ width: '100%', padding: '10px 16px' }}>
|
||||
{lang === 'fa' ? 'ورود' : 'Login'}
|
||||
</OutlineButton>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 14,
|
||||
marginTop: 6,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{META_LEFT.map((item) => (
|
||||
<a
|
||||
key={item.fa}
|
||||
href="#"
|
||||
style={{ fontSize: 11, color: T.ink4, textDecoration: 'none' }}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 14, marginTop: 6, flexWrap: 'wrap' }}>
|
||||
{META_LEFT.map(item => (
|
||||
<a key={item.fa} href="#" style={{ fontSize: 11, color: T.ink4, textDecoration: 'none' }}>
|
||||
{lang === 'fa' ? item.fa : item.en}
|
||||
</a>
|
||||
))}
|
||||
|
|
@ -478,75 +390,33 @@ export default function Header() {
|
|||
)
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────
|
||||
BUTTONS
|
||||
───────────────────────────────────────────────────────── */
|
||||
function RedCtaButton({
|
||||
children,
|
||||
style: extraStyle,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
style?: CSSProperties
|
||||
}) {
|
||||
/* ─── buttons ─── */
|
||||
function RedCtaButton({ children, style: extraStyle }: { children: React.ReactNode; style?: CSSProperties }) {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
return (
|
||||
<button
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
<button onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}
|
||||
style={{
|
||||
background: hovered ? T.ink : T.red,
|
||||
color: T.paper,
|
||||
border: 'none',
|
||||
padding: '8px 16px',
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'inherit',
|
||||
borderRadius: 0,
|
||||
letterSpacing: '0.3px',
|
||||
transition: 'background 120ms',
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center',
|
||||
flexShrink: 0,
|
||||
height: 34,
|
||||
...extraStyle,
|
||||
background: hovered ? T.ink : T.red, color: T.paper, border: 'none',
|
||||
padding: '8px 16px', fontSize: 12, fontWeight: 700, cursor: 'pointer',
|
||||
fontFamily: 'inherit', borderRadius: 0, letterSpacing: '0.3px',
|
||||
transition: 'background 120ms', whiteSpace: 'nowrap', textAlign: 'center',
|
||||
flexShrink: 0, height: 34, ...extraStyle,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
>{children}</button>
|
||||
)
|
||||
}
|
||||
|
||||
function OutlineButton({
|
||||
children,
|
||||
style: extraStyle,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
style?: CSSProperties
|
||||
}) {
|
||||
function OutlineButton({ children, style: extraStyle }: { children: React.ReactNode; style?: CSSProperties }) {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
return (
|
||||
<button
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
<button onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}
|
||||
style={{
|
||||
background: hovered ? T.ink : 'transparent',
|
||||
color: hovered ? T.paper : T.ink,
|
||||
border: `1px solid ${T.ink}`,
|
||||
padding: '8px 14px',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'inherit',
|
||||
borderRadius: 0,
|
||||
letterSpacing: '0.3px',
|
||||
transition: 'background 120ms, color 120ms',
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center',
|
||||
background: hovered ? T.ink : 'transparent', color: hovered ? T.paper : T.ink,
|
||||
border: `1px solid ${T.ink}`, padding: '8px 14px', fontSize: 12, fontWeight: 600,
|
||||
cursor: 'pointer', fontFamily: 'inherit', borderRadius: 0, letterSpacing: '0.3px',
|
||||
transition: 'background 120ms, color 120ms', whiteSpace: 'nowrap', textAlign: 'center',
|
||||
...extraStyle,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
>{children}</button>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,58 @@
|
|||
import { useEffect } from 'react'
|
||||
import { Outlet, useLocation } from 'react-router-dom'
|
||||
import Lenis from 'lenis'
|
||||
import { gsap } from 'gsap'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
import Header from './Header'
|
||||
import { Footer } from './Footer'
|
||||
import { MarketStrip } from './MarketStrip'
|
||||
import { LangProvider } from '@/context/LangContext'
|
||||
|
||||
export default function RootLayout() {
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
|
||||
/* Lenis smooth scroll synced to the GSAP ticker + ScrollTrigger (joinspread.app recipe).
|
||||
One shared clock means scroll-driven animations never stutter against the smoothing. */
|
||||
function useSmoothScroll() {
|
||||
const { pathname } = useLocation()
|
||||
|
||||
useEffect(() => {
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
||||
|
||||
// Touch devices (iOS/Android) have excellent NATIVE momentum scrolling.
|
||||
// Running Lenis JS smoothing on top of it = double-smoothing → heavy/laggy feel.
|
||||
// So: smooth only on desktop pointers; let touch use native scroll.
|
||||
const isTouch = window.matchMedia('(pointer: coarse)').matches
|
||||
|
||||
const lenis = new Lenis({
|
||||
duration: 1.2,
|
||||
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), // exponential ease-out
|
||||
smoothWheel: !isTouch, // desktop wheel: smooth
|
||||
syncTouch: false, // touch: hands off to native momentum (no JS smoothing)
|
||||
})
|
||||
|
||||
// drive ScrollTrigger from Lenis
|
||||
lenis.on('scroll', ScrollTrigger.update)
|
||||
|
||||
// run Lenis off GSAP's ticker (single RAF clock), lag smoothing off for tight sync
|
||||
const onTick = (time: number) => lenis.raf(time * 1000)
|
||||
gsap.ticker.add(onTick)
|
||||
gsap.ticker.lagSmoothing(0)
|
||||
|
||||
return () => {
|
||||
lenis.off('scroll', ScrollTrigger.update)
|
||||
gsap.ticker.remove(onTick)
|
||||
lenis.destroy()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// reset scroll on route change (Lenis owns scroll position)
|
||||
useEffect(() => {
|
||||
window.scrollTo({ top: 0, behavior: 'auto' })
|
||||
ScrollTrigger.refresh()
|
||||
}, [pathname])
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
useSmoothScroll()
|
||||
return (
|
||||
<LangProvider>
|
||||
<div style={{ minHeight: '100dvh', display: 'flex', flexDirection: 'column' }}>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import Pulse from '@/pages/Pulse/Pulse'
|
|||
import Membership from '@/pages/Membership/Membership'
|
||||
import Sustainability from '@/pages/Sustainability/Sustainability'
|
||||
import Geopolitics from '@/pages/Geopolitics/Geopolitics'
|
||||
import AtAGlance from '@/pages/AtAGlance/AtAGlance'
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
|
|
@ -40,6 +41,7 @@ export const router = createBrowserRouter([
|
|||
{ path: 'sustainability', element: <Sustainability /> },
|
||||
{ path: 'geopolitics', element: <Geopolitics /> },
|
||||
{ path: 'membership', element: <Membership /> },
|
||||
{ path: 'at-a-glance', element: <AtAGlance /> },
|
||||
],
|
||||
},
|
||||
])
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
/* ─────────────────────────────────────────────────────────────
|
||||
Factory monthly analysis reports (ماهنامه تحلیل کارخانه).
|
||||
Separate from the component so it can be fed from the panel/API
|
||||
later (replace FACTORY_REPORTS with a fetch to the same shape).
|
||||
`dir` controls the scroll-in direction: 'left' | 'right' | 'up'.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
export type ScrollDir = 'left' | 'right' | 'up'
|
||||
|
||||
export type FactoryReport = {
|
||||
id: string
|
||||
img: string
|
||||
dir: ScrollDir
|
||||
fa: { tag: string; title: string; date: string; excerpt: string }
|
||||
en: { tag: string; title: string; date: string; excerpt: string }
|
||||
}
|
||||
|
||||
/* TEMP placeholder dataset — swap for panel/API data later. */
|
||||
export const FACTORY_REPORTS: FactoryReport[] = [
|
||||
{
|
||||
id: 'fr1', img: '/news/photo-1504307651254-35680f356dfd.jpg', dir: 'left',
|
||||
fa: { tag: 'تولید و صادرات', title: 'تحلیل عملکرد تولید و صادرات', date: 'بهمن ۱۴۰۳', excerpt: 'بررسی آمار تولید ماهانه، مقایسه با بودجه سالانه و روند صادرات فولاد تخت در بازارهای منطقهای.' },
|
||||
en: { tag: 'Output & Exports', title: 'Production & Export Performance', date: 'Feb 2025', excerpt: 'Monthly output vs. annual budget and the flat-steel export trend across regional markets.' },
|
||||
},
|
||||
{
|
||||
id: 'fr2', img: '/news/photo-1581092160607-ee22731c9b4e.jpg', dir: 'left',
|
||||
fa: { tag: 'بازار و قیمت', title: 'وضعیت بازار و قیمتگذاری', date: 'دی ۱۴۰۳', excerpt: 'تحلیل نوسانات قیمت فولاد تخت در بازار داخلی و اثر نرخ ارز بر چشمانداز زمستان.' },
|
||||
en: { tag: 'Market & Price', title: 'Market & Pricing Outlook', date: 'Jan 2025', excerpt: 'Flat-steel price swings in the domestic market and the FX effect on the winter outlook.' },
|
||||
},
|
||||
{
|
||||
id: 'fr3', img: '/news/photo-1518770660439-4636190af475.jpg', dir: 'left',
|
||||
fa: { tag: 'فناوری', title: 'دیجیتالسازی خطوط تولید', date: 'آذر ۱۴۰۳', excerpt: 'گزارشی از پیادهسازی سامانههای کنترل هوشمند و اثر آن بر بهرهوری خطوط نورد.' },
|
||||
en: { tag: 'Technology', title: 'Production Line Digitalization', date: 'Dec 2024', excerpt: 'Rolling-out smart control systems and their impact on rolling-line productivity.' },
|
||||
},
|
||||
{
|
||||
id: 'fr4', img: '/news/photo-1567789884554-0b844b597180.jpg', dir: 'up',
|
||||
fa: { tag: 'نیروی انسانی', title: 'تحلیل نیروی انسانی و بهرهوری', date: 'آبان ۱۴۰۳', excerpt: 'شاخصهای بهرهوری نیروی کار، آموزشهای ماهانه و آمار ایمنی شغلی.' },
|
||||
en: { tag: 'Workforce', title: 'Workforce & Productivity', date: 'Nov 2024', excerpt: 'Labor productivity indicators, monthly training and occupational-safety statistics.' },
|
||||
},
|
||||
{
|
||||
id: 'fr5', img: '/news/photo-1466611653911-95081537e5b7.jpg', dir: 'up',
|
||||
fa: { tag: 'پایداری', title: 'گزارش پایداری و محیط زیست', date: 'آذر ۱۴۰۳', excerpt: 'شاخصهای زیستمحیطی ماهانه؛ مصرف انرژی، کاهش انتشار کربن و بازچرخانی پساب صنعتی.' },
|
||||
en: { tag: 'Sustainability', title: 'Sustainability & Environment', date: 'Dec 2024', excerpt: 'Monthly environmental metrics: energy use, carbon reduction and industrial water recycling.' },
|
||||
},
|
||||
{
|
||||
id: 'fr6', img: '/news/photo-1570168007204-dfb528c6958f.jpg', dir: 'up',
|
||||
fa: { tag: 'زنجیره تأمین', title: 'تأمین مواد اولیه و لجستیک', date: 'مهر ۱۴۰۳', excerpt: 'وضعیت تأمین سنگآهن و قراضه، موجودی انبار و تحلیل ریسک زنجیره تأمین.' },
|
||||
en: { tag: 'Supply Chain', title: 'Raw Materials & Logistics', date: 'Oct 2024', excerpt: 'Iron-ore and scrap supply, inventory levels and supply-chain risk analysis.' },
|
||||
},
|
||||
{
|
||||
id: 'fr7', img: '/news/photo-1611273426858-450d8e3c9fce.jpg', dir: 'right',
|
||||
fa: { tag: 'سرمایهگذاری', title: 'طرحهای توسعه و سرمایهگذاری', date: 'شهریور ۱۴۰۳', excerpt: 'پیشرفت طرحهای توسعه ظرفیت و تحلیل بازده سرمایهگذاری پروژههای جاری.' },
|
||||
en: { tag: 'Investment', title: 'Expansion & Investment Plans', date: 'Sep 2024', excerpt: 'Capacity-expansion progress and ROI analysis of ongoing projects.' },
|
||||
},
|
||||
{
|
||||
id: 'fr8', img: '/news/photo-1485827404703-89b55fcc595e.jpg', dir: 'right',
|
||||
fa: { tag: 'کیفیت', title: 'کنترل کیفیت با هوش مصنوعی', date: 'مرداد ۱۴۰۳', excerpt: 'کاربرد بینایی ماشین در تشخیص عیوب سطحی و کاهش نرخ ضایعات تولید.' },
|
||||
en: { tag: 'Quality', title: 'AI-Driven Quality Control', date: 'Aug 2024', excerpt: 'Machine vision for surface-defect detection and lower production scrap rates.' },
|
||||
},
|
||||
{
|
||||
id: 'fr9', img: '/news/photo-1613665813446-82a78c468a1d.jpg', dir: 'right',
|
||||
fa: { tag: 'انرژی', title: 'تحلیل مصرف انرژی و بهینهسازی', date: 'تیر ۱۴۰۳', excerpt: 'الگوی مصرف برق و گاز، فرصتهای بهینهسازی و اثر آن بر بهای تمامشده.' },
|
||||
en: { tag: 'Energy', title: 'Energy Use & Optimization', date: 'Jul 2024', excerpt: 'Electricity and gas usage patterns, optimization opportunities and cost impact.' },
|
||||
},
|
||||
]
|
||||
|
||||
export const FACTORY_REPORTS_META = {
|
||||
fa: { overline: 'MOBARAKEH STEEL', heading: 'ماهنامه تحلیل کارخانه', sub: 'گزارشهای تحلیلی ماهانه عملکرد، بازار، فناوری و پایداری صنعت فولاد.', cta: 'دریافت گزارش' },
|
||||
en: { overline: 'MOBARAKEH STEEL', heading: 'Factory Analysis Monthly', sub: 'Monthly analytical reports on performance, market, technology and sustainability.', cta: 'Get report' },
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/* ─────────────────────────────────────────────────────────────
|
||||
Integrations / data-sources showcase content.
|
||||
Separate from the component so it can be fed from the panel/API
|
||||
later (replace INTEGRATIONS with a fetch to the same shape).
|
||||
|
||||
`logo` (a path under /public) renders a real image when present.
|
||||
Otherwise the component falls back to the `icon` key (a Lucide icon).
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
export type IntegrationIcon =
|
||||
| 'exchange' | 'globe' | 'chart' | 'activity' | 'database'
|
||||
| 'newspaper' | 'building' | 'trending' | 'coins'
|
||||
|
||||
export type Integration = {
|
||||
id: string
|
||||
name: string // tooltip / aria label
|
||||
icon: IntegrationIcon // fallback Lucide icon
|
||||
logo?: string // optional real logo path under /public (e.g. '/logos/lme.svg')
|
||||
accent?: string // tile accent tint
|
||||
col: number // grid column (1-6)
|
||||
row: number // grid row (1-5)
|
||||
}
|
||||
|
||||
/* Global steel data sources & exchanges, scattered across a 6×5 grid.
|
||||
Empty cells stay as faint placeholders for the "constellation" look. */
|
||||
export const INTEGRATIONS: Integration[] = [
|
||||
{ id: 'lme', name: 'London Metal Exchange', icon: 'exchange', accent: '#c9a227', col: 6, row: 1 },
|
||||
{ id: 'shfe', name: 'Shanghai Futures Exchange', icon: 'trending', accent: '#c0392b', col: 4, row: 2 },
|
||||
{ id: 'platts', name: 'S&P Global Platts', icon: 'chart', accent: '#2471a3', col: 6, row: 2 },
|
||||
{ id: 'reuters', name: 'Reuters', icon: 'newspaper', accent: '#e67e22', col: 3, row: 3 },
|
||||
{ id: 'fastmarkets',name: 'Fastmarkets', icon: 'activity', accent: '#1e8449', col: 5, row: 3 },
|
||||
{ id: 'worldsteel', name: 'World Steel Association', icon: 'building', accent: '#7d3c98', col: 4, row: 4 },
|
||||
{ id: 'mysteel', name: 'Mysteel', icon: 'database', accent: '#16a085', col: 6, row: 4 },
|
||||
{ id: 'cme', name: 'CME Group', icon: 'coins', accent: '#2980b9', col: 5, row: 5 },
|
||||
{ id: 'dce', name: 'Dalian Commodity Exchange', icon: 'globe', accent: '#d35400', col: 3, row: 5 },
|
||||
]
|
||||
|
||||
export const INTEGRATIONS_META = {
|
||||
fa: {
|
||||
overline: 'DATA SOURCES',
|
||||
heading: 'یکپارچگی با منابع جهانی',
|
||||
sub: 'اتصال به بیش از ۱۰۰ منبع داده، بورس و پایگاه جهانی فولاد — رصد لحظهای قیمتها، صادرات و روندهای بازار در یک جا.',
|
||||
cta: 'مشاهده منابع داده',
|
||||
},
|
||||
en: {
|
||||
overline: 'DATA SOURCES',
|
||||
heading: 'Seamless Global Integration',
|
||||
sub: 'Connected to 100+ data sources, exchanges and global steel databases — real-time prices, exports and market trends in one place.',
|
||||
cta: 'View data sources',
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/* ─────────────────────────────────────────────────────────────
|
||||
Radar (رادار آینده) content layer.
|
||||
Kept SEPARATE from the component so it can later be served from
|
||||
the panel / API without touching UI code. When the backend is
|
||||
ready, replace `RADAR_ITEMS` with a fetch to the same shape.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
export type RadarCategory = 'market' | 'tech' | 'commodity' | 'geo' | 'energy'
|
||||
|
||||
export type RadarItem = {
|
||||
id: string
|
||||
category: RadarCategory
|
||||
img: string
|
||||
/** author shown on hover (replaces the category icon) */
|
||||
author: { name: string; avatar: string }
|
||||
fa: { title: string; date: string; excerpt: string }
|
||||
en: { title: string; date: string; excerpt: string }
|
||||
}
|
||||
|
||||
/* category → label (icon is chosen in the component from this key) */
|
||||
export const RADAR_CATEGORY_LABEL: Record<RadarCategory, { fa: string; en: string }> = {
|
||||
market: { fa: 'بازار', en: 'Market' },
|
||||
tech: { fa: 'فناوری', en: 'Technology' },
|
||||
commodity: { fa: 'مواد اولیه', en: 'Commodity' },
|
||||
geo: { fa: 'ژئوپلیتیک', en: 'Geopolitics' },
|
||||
energy: { fa: 'انرژی', en: 'Energy' },
|
||||
}
|
||||
|
||||
/* TEMP placeholder dataset — swap for panel/API data later. */
|
||||
export const RADAR_ITEMS: RadarItem[] = [
|
||||
{
|
||||
id: 'r1', category: 'commodity',
|
||||
img: '/news/photo-1611273426858-450d8e3c9fce.jpg',
|
||||
author: { name: 'دکتر سارا احمدی', avatar: '/news/photo-1494790108377-be9c29b29330.jpg' },
|
||||
fa: { title: 'پیشبینی بازار با هوش مصنوعی', date: '۱۴ اردیبهشت ۱۴۰۵', excerpt: 'مدلهای یادگیری ماشین تقاضای لیتیوم و فلزات باتری را در افق ۲۰۳۵ بازتعریف میکنند.' },
|
||||
en: { title: 'AI-Powered Market Forecasting', date: 'May 4, 2026', excerpt: 'Machine-learning models are redefining lithium and battery-metal demand on the 2035 horizon.' },
|
||||
},
|
||||
{
|
||||
id: 'r2', category: 'tech',
|
||||
img: '/news/photo-1581092160607-ee22731c9b4e.jpg',
|
||||
author: { name: 'مهندس رضا کریمی', avatar: '/news/photo-1500648767791-00dcc994a43e.jpg' },
|
||||
fa: { title: 'پیشبینی بازار با هوش مصنوعی', date: '۱۴ اردیبهشت ۱۴۰۵', excerpt: 'تحول دیجیتال و اتوماسیون هوشمند در حال تغییر بنیادین زنجیره ارزش فولاد هستند.' },
|
||||
en: { title: 'AI-Powered Market Forecasting', date: 'May 4, 2026', excerpt: 'Digital transformation and smart automation are fundamentally reshaping the steel value chain.' },
|
||||
},
|
||||
{
|
||||
id: 'r3', category: 'market',
|
||||
img: '/news/photo-1504307651254-35680f356dfd.jpg',
|
||||
author: { name: 'دکتر مریم رضایی', avatar: '/news/photo-1438761681033-6461ffad8d80.jpg' },
|
||||
fa: { title: 'پیشبینی بازار با هوش مصنوعی', date: '۱۴ اردیبهشت ۱۴۰۵', excerpt: 'تحلیل روند قیمت طلا و فلزات گرانبها و اثر آن بر سرمایهگذاری در زنجیره فولاد.' },
|
||||
en: { title: 'AI-Powered Market Forecasting', date: 'May 4, 2026', excerpt: 'Gold and precious-metal price trends and their effect on steel-chain investment.' },
|
||||
},
|
||||
{
|
||||
id: 'r4', category: 'geo',
|
||||
img: '/news/photo-1518770660439-4636190af475.jpg',
|
||||
author: { name: 'دکتر علی موسوی', avatar: '/news/photo-1472099645785-5658abf4ff4e.jpg' },
|
||||
fa: { title: 'پیشبینی بازار با هوش مصنوعی', date: '۱۴ اردیبهشت ۱۴۰۵', excerpt: 'هندسه قدرت جهانی و اثر تحولات ژئوپلیتیک بر بازارهای صادراتی فولاد ایران.' },
|
||||
en: { title: 'AI-Powered Market Forecasting', date: 'May 4, 2026', excerpt: 'Global power geometry and the impact of geopolitics on Iran\'s steel export markets.' },
|
||||
},
|
||||
{
|
||||
id: 'r5', category: 'energy',
|
||||
img: '/news/photo-1466611653911-95081537e5b7.jpg',
|
||||
author: { name: 'مهندس نازنین حسینی', avatar: '/news/photo-1544005313-94ddf0286df2.jpg' },
|
||||
fa: { title: 'گذار انرژی و فولاد سبز', date: '۱۲ اردیبهشت ۱۴۰۵', excerpt: 'هیدروژن سبز و برق تجدیدپذیر مسیر کربنزدایی صنعت فولاد را شتاب میدهند.' },
|
||||
en: { title: 'Energy Transition & Green Steel', date: 'May 2, 2026', excerpt: 'Green hydrogen and renewable power are accelerating steel decarbonization.' },
|
||||
},
|
||||
{
|
||||
id: 'r6', category: 'tech',
|
||||
img: '/news/photo-1567789884554-0b844b597180.jpg',
|
||||
author: { name: 'دکتر سارا احمدی', avatar: '/news/photo-1494790108377-be9c29b29330.jpg' },
|
||||
fa: { title: 'اتوماسیون خطوط نورد', date: '۱۰ اردیبهشت ۱۴۰۵', excerpt: 'سیستمهای کنترل هوشمند بهرهوری خطوط نورد را تا ۲۲٪ افزایش دادهاند.' },
|
||||
en: { title: 'Rolling Line Automation', date: 'Apr 30, 2026', excerpt: 'Smart control systems have boosted rolling-line productivity by up to 22%.' },
|
||||
},
|
||||
{
|
||||
id: 'r7', category: 'commodity',
|
||||
img: '/news/photo-1570168007204-dfb528c6958f.jpg',
|
||||
author: { name: 'مهندس رضا کریمی', avatar: '/news/photo-1500648767791-00dcc994a43e.jpg' },
|
||||
fa: { title: 'زنجیره تأمین سنگآهن', date: '۸ اردیبهشت ۱۴۰۵', excerpt: 'نوسانات عرضه سنگآهن استرالیا و برزیل و پیامد آن برای تولیدکنندگان منطقه.' },
|
||||
en: { title: 'Iron Ore Supply Chain', date: 'Apr 28, 2026', excerpt: 'Australian and Brazilian iron-ore supply swings and their regional fallout.' },
|
||||
},
|
||||
{
|
||||
id: 'r8', category: 'market',
|
||||
img: '/news/photo-1540575467063-178a50c2df87.jpg',
|
||||
author: { name: 'دکتر مریم رضایی', avatar: '/news/photo-1438761681033-6461ffad8d80.jpg' },
|
||||
fa: { title: 'تحلیل بازار جهانی فولاد', date: '۶ اردیبهشت ۱۴۰۵', excerpt: 'سیل صادرات فولاد چین و فشار قیمتی بر بازارهای هدف صادراتی ایران.' },
|
||||
en: { title: 'Global Steel Market Analysis', date: 'Apr 26, 2026', excerpt: 'China\'s steel export flood and price pressure on Iran\'s target markets.' },
|
||||
},
|
||||
]
|
||||
|
|
@ -180,7 +180,12 @@
|
|||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
html { scroll-behavior: smooth; direction: rtl; }
|
||||
/* scroll-behavior:smooth removed — it fights Lenis JS smoothing. Lenis owns scroll now. */
|
||||
html { direction: rtl; }
|
||||
html.lenis, html.lenis body { height: auto; }
|
||||
.lenis.lenis-smooth { scroll-behavior: auto !important; }
|
||||
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
|
||||
.lenis.lenis-stopped { overflow: hidden; }
|
||||
|
||||
body {
|
||||
font-family: 'Vazir', 'Inter', system-ui, sans-serif;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import { useLang } from '@/context/LangContext'
|
||||
import AtAGlanceSection from '@/pages/Home/sections/AtAGlanceSection'
|
||||
|
||||
export default function AtAGlance() {
|
||||
const { lang } = useLang()
|
||||
const isFa = lang === 'fa'
|
||||
|
||||
return (
|
||||
<div dir={isFa ? 'rtl' : 'ltr'} style={{ background: '#faf7f4', minHeight: '100vh' }}>
|
||||
{/* page header */}
|
||||
<div style={{ background: '#071d49', 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 #c9a227',
|
||||
[isFa ? 'paddingRight' : 'paddingLeft']: 18,
|
||||
} as React.CSSProperties}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', color: '#c9a227', marginBottom: 8 }}>
|
||||
AT A GLANCE
|
||||
</div>
|
||||
<h1 style={{ fontSize: 'clamp(28px, 3vw, 44px)', fontWeight: 900, color: '#fff', letterSpacing: '-0.5px', margin: 0 }}>
|
||||
{isFa ? 'در یک نگاه' : 'At a Glance'}
|
||||
</h1>
|
||||
<p style={{ fontSize: 'clamp(13px, 1.1vw, 16px)', color: 'rgba(255,255,255,0.7)', margin: '12px 0 0', maxWidth: 560, lineHeight: 1.8 }}>
|
||||
{isFa
|
||||
? 'مهمترین تحولات و رویدادهای راهبردی صنعت فولاد جهان، گزیده و تحلیلشده در یک نگاه.'
|
||||
: 'The most important strategic developments and events in the global steel industry — curated and analyzed at a glance.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* the slider (reuses the shared section component) */}
|
||||
<AtAGlanceSection embedded />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
import HeroSection from './sections/HeroSection'
|
||||
import SpreadFeatures from './sections/SpreadFeatures'
|
||||
import IntegrationsSection from './sections/IntegrationsSection'
|
||||
import InternalNewsSection from './sections/InternalNewsSection'
|
||||
import RadarTechSection from './sections/RadarTechSection'
|
||||
import MapEventsSection from './sections/MapEventsSection'
|
||||
import FactoryNewsletterLayout from './sections/FactoryNewsletterLayout'
|
||||
import FactoryReportsScroll from './sections/FactoryReportsScroll'
|
||||
import NewsletterMarquee from './sections/NewsletterMarquee'
|
||||
import VideocastPodcastSection from './sections/VideocastPodcastSection'
|
||||
import RiskSection from './sections/RiskSection'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
|
||||
|
|
@ -12,10 +16,14 @@ export default function Home() {
|
|||
return (
|
||||
<div dir={lang === 'fa' ? 'rtl' : 'ltr'}>
|
||||
<HeroSection />
|
||||
<SpreadFeatures />
|
||||
<InternalNewsSection />
|
||||
<RadarTechSection />
|
||||
<MapEventsSection />
|
||||
<FactoryNewsletterLayout />
|
||||
<NewsletterMarquee />
|
||||
<FactoryReportsScroll />
|
||||
<IntegrationsSection />
|
||||
<VideocastPodcastSection />
|
||||
<RiskSection />
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,192 @@
|
|||
import { useState } from 'react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
|
||||
const NAVY = '#071d49'
|
||||
const GOLD = '#c9a227'
|
||||
|
||||
type Glance = {
|
||||
source: string
|
||||
date: string; dateEn: string
|
||||
title: string; titleEn: string
|
||||
desc: string; descEn: string
|
||||
img: string
|
||||
}
|
||||
|
||||
const ITEMS: Glance[] = [
|
||||
{
|
||||
source: 'Financial Times',
|
||||
date: 'اردیبهشت ۱۴۰۵', dateEn: 'May 2026',
|
||||
title: 'شوک هیدروژنی به بازار جهانی؛ آغاز رسمی عصر فولادسازی بدون کربن',
|
||||
titleEn: 'Hydrogen Shock to Global Markets; the Carbon-Free Steel Era Begins',
|
||||
desc: 'پروژه عظیم «فولاد سبز H2» در سوئد با جذب بیسابقه ۶.۵ میلیارد یورو سرمایه، رسماً وارد فاز اجرایی شد تا نخستین کارخانه فولادسازی در مقیاس تجاری بر پایه هیدروژن سبز (بدون نیاز به زغالسنگ) در جهان متولد شود. این تأسیسات با هدف کاهش ۹۵ درصدی انتشار CO₂، قواعد رقابت در زنجیره ارزش اروپا را برای همیشه بازتعریف میکند.',
|
||||
descEn: 'Sweden\'s massive "Green Steel H2" project, having raised an unprecedented €6.5bn, has officially entered its execution phase to birth the world\'s first commercial-scale steel plant powered by green hydrogen — redefining Europe\'s value-chain competition for good.',
|
||||
img: '/news/photo-1504307651254-35680f356dfd.jpg',
|
||||
},
|
||||
{
|
||||
source: 'Reuters',
|
||||
date: 'اردیبهشت ۱۴۰۵', dateEn: 'May 2026',
|
||||
title: 'مکانیسم تنظیم مرزهای کربنی اروپا وارد فاز اجرای کامل شد',
|
||||
titleEn: 'EU Carbon Border Mechanism Enters Full Enforcement',
|
||||
desc: 'با اجرای کامل CBAM، صادرکنندگان فولاد به اتحادیه اروپا اکنون موظفاند هزینه کامل کربن نهفته در محصولات خود را بپردازند. این تحول، فشار بیسابقهای بر تولیدکنندگان مبتنی بر کوره بلند وارد کرده و مزیت رقابتی را به سمت فولاد کمکربن سوق میدهد.',
|
||||
descEn: 'With CBAM now fully enforced, steel exporters to the EU must pay the full cost of embedded carbon — placing unprecedented pressure on blast-furnace producers and shifting the competitive edge toward low-carbon steel.',
|
||||
img: '/news/photo-1581092160607-ee22731c9b4e.jpg',
|
||||
},
|
||||
{
|
||||
source: 'Bloomberg',
|
||||
date: 'فروردین ۱۴۰۵', dateEn: 'Apr 2026',
|
||||
title: 'هوش مصنوعی، تقاضای جهانی فولاد مراکز داده را بازتعریف میکند',
|
||||
titleEn: 'AI Is Rewriting Global Steel Demand for Data Centers',
|
||||
desc: 'موج ساخت مراکز داده هوش مصنوعی، تقاضای جدیدی برای مقاطع ساختمانی و فولاد ویژه ایجاد کرده است. تحلیلگران پیشبینی میکنند این روند تا پایان دهه، یکی از پیشرانهای اصلی رشد مصرف فولاد در اقتصادهای پیشرفته باشد.',
|
||||
descEn: 'The AI data-center construction wave is creating fresh demand for structural sections and specialty steel — projected to be a leading driver of steel consumption growth in advanced economies through the decade.',
|
||||
img: '/news/photo-1518770660439-4636190af475.jpg',
|
||||
},
|
||||
]
|
||||
|
||||
export default function AtAGlanceSection({ embedded = false }: { embedded?: boolean }) {
|
||||
const { lang } = useLang()
|
||||
const isFa = lang === 'fa'
|
||||
const [i, setI] = useState(0)
|
||||
const [dir, setDir] = useState<1 | -1>(1)
|
||||
const it = ITEMS[i]
|
||||
|
||||
const go = (d: 1 | -1) => {
|
||||
setDir(d)
|
||||
setI((prev) => (prev + d + ITEMS.length) % ITEMS.length)
|
||||
}
|
||||
|
||||
const T = {
|
||||
heading: isFa ? 'در یک نگاه' : 'At a Glance',
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="at-a-glance" style={{ background: '#faf7f4', padding: '90px 0', overflow: 'hidden', scrollMarginTop: '90px' }} dir={isFa ? 'rtl' : 'ltr'}>
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5">
|
||||
|
||||
{/* heading with gold rule (hidden when embedded in a page that has its own header) */}
|
||||
{!embedded && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, justifyContent: 'flex-start', marginBottom: 40 }}>
|
||||
<h2 style={{ fontSize: 'clamp(26px, 2.6vw, 38px)', fontWeight: 900, color: NAVY, letterSpacing: '-0.5px', margin: 0, order: isFa ? 1 : 2 }}>
|
||||
{T.heading}
|
||||
</h2>
|
||||
<div style={{ width: 4, height: 34, background: GOLD, borderRadius: 2, order: isFa ? 2 : 1 }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* image on the LEFT, text on the RIGHT in RTL (matches the reference).
|
||||
In RTL, the first flex child lands on the right — so we use row-reverse to push image left. */}
|
||||
<div
|
||||
style={{ display: 'flex', flexDirection: isFa ? 'row-reverse' : 'row', alignItems: 'center', gap: 0, minHeight: 480, direction: 'ltr' }}
|
||||
className="max-lg:!flex-col max-lg:!gap-8"
|
||||
>
|
||||
{/* ── IMAGE side (with stacked-card shadow) ── */}
|
||||
<div style={{ width: '38%', position: 'relative', flexShrink: 0 }} className="max-lg:!w-full max-lg:!max-w-[420px]">
|
||||
{/* back card (offset, behind) — offset away from the text card */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
transform: 'translate(-28px, -22px)',
|
||||
background: '#ece7e1', borderRadius: 18,
|
||||
boxShadow: '0 30px 70px -30px rgba(7,29,73,0.25)',
|
||||
}} />
|
||||
{/* front image — portrait on desktop, shorter landscape on mobile */}
|
||||
<div
|
||||
style={{ position: 'relative', borderRadius: 18, overflow: 'hidden', boxShadow: '0 40px 90px -30px rgba(7,29,73,0.45)' }}
|
||||
className="aspect-[3/3.6] max-lg:!aspect-[16/10]"
|
||||
>
|
||||
<AnimatePresence mode="wait" custom={dir}>
|
||||
<motion.img
|
||||
key={i}
|
||||
src={it.img}
|
||||
alt=""
|
||||
initial={{ opacity: 0, scale: 1.06 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 1.02 }}
|
||||
transition={{ duration: 0.55, ease: [0.22, 1, 0.36, 1] }}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── TEXT card side ── */}
|
||||
<div style={{ flex: 1, position: 'relative', zIndex: 2 }} className="max-lg:!w-full">
|
||||
<div style={{
|
||||
background: '#fff', borderRadius: 6,
|
||||
boxShadow: '0 30px 80px -40px rgba(7,29,73,0.30)',
|
||||
padding: '40px 44px 32px',
|
||||
// pull the card toward the image to overlap it (image is on the left in RTL)
|
||||
marginLeft: isFa ? '-8%' : 0,
|
||||
marginRight: isFa ? 0 : '-8%',
|
||||
direction: isFa ? 'rtl' : 'ltr',
|
||||
}} className="max-lg:!ms-0 max-lg:!me-0 max-md:!p-6">
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, x: dir * 24 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: dir * -16 }}
|
||||
transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
<h3 style={{ fontSize: 'clamp(20px, 1.9vw, 27px)', fontWeight: 900, color: NAVY, lineHeight: 1.4, letterSpacing: '-0.3px', margin: '0 0 18px' }}>
|
||||
{isFa ? it.title : it.titleEn}
|
||||
</h3>
|
||||
|
||||
{/* date (leading side) + source (trailing side) */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20, flexWrap: 'wrap', gap: 8 }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 13, color: 'rgba(7,29,73,0.55)' }}>
|
||||
<CalendarIcon />
|
||||
{isFa ? it.date : it.dateEn}
|
||||
</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 700, color: GOLD, letterSpacing: '0.3px' }}>{it.source}</span>
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: 'clamp(13px, 1.05vw, 15px)', lineHeight: 2.1, color: 'rgba(7,29,73,0.72)', margin: 0, textAlign: 'justify' }}>
|
||||
{isFa ? it.desc : it.descEn}
|
||||
</p>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* prev / next controls */}
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 28 }}>
|
||||
<NavBtn onClick={() => go(-1)} aria="prev">‹</NavBtn>
|
||||
<NavBtn onClick={() => go(1)} aria="next">›</NavBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function NavBtn({ children, onClick, aria }: { children: React.ReactNode; onClick: () => void; aria: string }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
aria-label={aria}
|
||||
style={{
|
||||
width: 38, height: 38, borderRadius: '50%',
|
||||
background: '#fff', color: GOLD, border: `1.5px solid ${GOLD}`, cursor: 'pointer',
|
||||
fontSize: 20, fontWeight: 700, lineHeight: 1,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontFamily: 'inherit', transition: 'background 180ms, color 180ms',
|
||||
}}
|
||||
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.background = GOLD; (e.currentTarget as HTMLElement).style.color = '#fff' }}
|
||||
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = '#fff'; (e.currentTarget as HTMLElement).style.color = GOLD }}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarIcon() {
|
||||
return (
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={GOLD} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
import { useLang } from '@/context/LangContext'
|
||||
|
||||
const REPORTS = {
|
||||
fa: [
|
||||
{
|
||||
title: 'تحلیل عملکرد تولید و صادرات — بهمن ۱۴۰۳',
|
||||
desc: 'بررسی آمار تولید ماهانه، مقایسه با بودجه سالانه و تحلیل روند صادرات فولاد تخت در بازارهای منطقهای',
|
||||
date: 'بهمن ۱۴۰۳',
|
||||
img: '/news/photo-1504307651254-35680f356dfd.jpg',
|
||||
},
|
||||
{
|
||||
title: 'وضعیت بازار و قیمتگذاری — دی ۱۴۰۳',
|
||||
desc: 'تحلیل نوسانات قیمت فولاد تخت در بازار داخلی، بررسی اثر نرخ ارز و چشمانداز بازار در فصل زمستان',
|
||||
date: 'دی ۱۴۰۳',
|
||||
img: '/news/photo-1587293852726-70cdb56c2866.jpg',
|
||||
},
|
||||
{
|
||||
title: 'گزارش پایداری و محیط زیست — آذر ۱۴۰۳',
|
||||
desc: 'شاخصهای زیستمحیطی ماهانه؛ مصرف انرژی، کاهش انتشار کربن و عملکرد سیستم بازیافت پساب صنعتی',
|
||||
date: 'آذر ۱۴۰۳',
|
||||
img: '/news/photo-1473341304170-971dccb5ac1e.jpg',
|
||||
},
|
||||
{
|
||||
title: 'تحلیل نیروی انسانی و بهرهوری — آبان ۱۴۰۳',
|
||||
desc: 'بررسی شاخصهای بهرهوری نیروی کار، آموزشهای ماهانه و آمار ایمنی شغلی',
|
||||
date: 'آبان ۱۴۰۳',
|
||||
img: '/news/photo-1581092160607-ee22731c9b4e.jpg',
|
||||
},
|
||||
],
|
||||
en: [
|
||||
{
|
||||
title: 'Production & Export Performance — Feb 2025',
|
||||
desc: 'Monthly production figures, annual budget comparison, and flat steel export trend analysis across regional markets.',
|
||||
date: 'Feb 2025',
|
||||
img: '/news/photo-1504307651254-35680f356dfd.jpg',
|
||||
},
|
||||
{
|
||||
title: 'Market Conditions & Pricing — Jan 2025',
|
||||
desc: 'Flat steel price fluctuations in the domestic market, exchange rate impact analysis, and winter market outlook.',
|
||||
date: 'Jan 2025',
|
||||
img: '/news/photo-1587293852726-70cdb56c2866.jpg',
|
||||
},
|
||||
{
|
||||
title: 'Sustainability & Environment Report — Dec 2024',
|
||||
desc: 'Monthly environmental KPIs: energy consumption, carbon emission reductions, and industrial wastewater recycling performance.',
|
||||
date: 'Dec 2024',
|
||||
img: '/news/photo-1473341304170-971dccb5ac1e.jpg',
|
||||
},
|
||||
{
|
||||
title: 'Workforce & Productivity Analysis — Nov 2024',
|
||||
desc: 'Workforce productivity indicators, monthly training hours, and occupational safety statistics.',
|
||||
date: 'Nov 2024',
|
||||
img: '/news/photo-1581092160607-ee22731c9b4e.jpg',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const ISSUES = {
|
||||
fa: [
|
||||
{ num: '۱۲', title: 'فولاد و تحریمهای جدید: راهکارهای صادراتی ۱۴۰۴', date: 'بهمن ۱۴۰۳' },
|
||||
{ num: '۱۱', title: 'قیمتگذاری فولاد در بازار داخلی: چالشها و فرصتها', date: 'دی ۱۴۰۳' },
|
||||
{ num: '۱۰', title: 'آمار تولید فولاد ایران در ۹ ماهه ۱۴۰۳', date: 'آذر ۱۴۰۳' },
|
||||
{ num: '۰۹', title: 'فولاد سبز؛ چشمانداز ایران در برابر رقبای منطقهای', date: 'آبان ۱۴۰۳' },
|
||||
{ num: '۰۸', title: 'تحلیل بازار عراق: فرصتها و تهدیدها برای صادرکنندگان ایرانی', date: 'مهر ۱۴۰۳' },
|
||||
{ num: '۰۷', title: 'هوش مصنوعی در کنترل کیفیت فولاد: کاربردها و چالشها', date: 'شهریور ۱۴۰۳' },
|
||||
],
|
||||
en: [
|
||||
{ num: '12', title: 'Steel & New Sanctions: Export Strategies for 1404', date: 'Feb 2025' },
|
||||
{ num: '11', title: 'Steel Pricing in the Domestic Market: Challenges & Opportunities', date: 'Jan 2025' },
|
||||
{ num: '10', title: 'Iran Steel Production Stats — First 9 Months of 1403', date: 'Dec 2024' },
|
||||
{ num: '09', title: "Green Steel: Iran's Outlook vs Regional Competitors", date: 'Nov 2024' },
|
||||
{ num: '08', title: 'Iraq Market Analysis: Opportunities & Threats for Iranian Exporters', date: 'Oct 2024' },
|
||||
{ num: '07', title: 'AI in Steel Quality Control: Applications & Challenges', date: 'Sep 2024' },
|
||||
],
|
||||
}
|
||||
|
||||
const T = {
|
||||
fa: {
|
||||
factoryOverline: 'MOBARAKEH STEEL', factoryHeading: 'ماهنامه تحلیلی کارخانه', factoryAll: 'مشاهده همه ←',
|
||||
readMore: 'دریافت گزارش ←',
|
||||
nlOverline: 'STEEL NEWSLETTER', nlHeading: 'خبرنامه فولاد', nlArchive: 'آرشیو کامل ←',
|
||||
placeholder: 'ایمیل خود را وارد کنید...', subscribe: 'عضویت', done: '✓ ثبت شد',
|
||||
nlDesc: 'هر ماه تحلیلهای تخصصی بازار فولاد، اخبار صنعت و گزارشهای اختصاصی را در ایمیل خود دریافت کنید.',
|
||||
},
|
||||
en: {
|
||||
factoryOverline: 'MOBARAKEH STEEL', factoryHeading: 'Monthly Factory Analysis', factoryAll: 'View All →',
|
||||
readMore: 'Get Report →',
|
||||
nlOverline: 'STEEL NEWSLETTER', nlHeading: 'Steel Newsletter', nlArchive: 'Full Archive →',
|
||||
placeholder: 'Enter your email...', subscribe: 'Subscribe', done: '✓ Done',
|
||||
nlDesc: 'Receive expert steel market analysis, industry news, and exclusive reports in your inbox every month.',
|
||||
},
|
||||
}
|
||||
|
||||
export default function FactoryNewsletterLayout() {
|
||||
const { lang } = useLang()
|
||||
const t = T[lang]
|
||||
|
||||
return (
|
||||
<section dir={lang === 'fa' ? 'rtl' : 'ltr'} style={{ background: 'var(--paper-2)' }}>
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 56, paddingBottom: 64 }}>
|
||||
<div className="grid grid-cols-[60%_1fr] max-md:grid-cols-1 gap-10">
|
||||
|
||||
{/* ══ LEFT · ماهنامه ══ */}
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 28, flexWrap: 'wrap', gap: 10 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)', marginBottom: 6 }}>{t.factoryOverline}</div>
|
||||
<h2 style={{ fontSize: 'clamp(20px,2vw,28px)', fontWeight: 900, color: 'var(--ink)', letterSpacing: '-0.5px' }}>{t.factoryHeading}</h2>
|
||||
</div>
|
||||
<a href="#" style={{ fontSize: 12, fontWeight: 700, color: 'var(--red)', textDecoration: 'none', whiteSpace: 'nowrap' }}>{t.factoryAll}</a>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 max-md:grid-cols-1 gap-3">
|
||||
{REPORTS[lang].map((r, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
background: '#fff', overflow: 'hidden',
|
||||
boxShadow: '0 2px 10px rgba(7,29,73,0.08)',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s', cursor: 'pointer',
|
||||
}}
|
||||
onMouseEnter={e => { const d = e.currentTarget as HTMLDivElement; d.style.transform = 'translateY(-3px)'; d.style.boxShadow = '0 8px 24px rgba(7,29,73,0.15)' }}
|
||||
onMouseLeave={e => { const d = e.currentTarget as HTMLDivElement; d.style.transform = 'translateY(0)'; d.style.boxShadow = '0 2px 10px rgba(7,29,73,0.08)' }}
|
||||
>
|
||||
<div style={{ position: 'relative', height: 110, overflow: 'hidden' }}>
|
||||
<img src={r.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.4s' }}
|
||||
onMouseEnter={e => (e.currentTarget.style.transform = 'scale(1.07)')}
|
||||
onMouseLeave={e => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
/>
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(7,29,73,0.88) 0%, rgba(7,29,73,0.15) 100%)' }} />
|
||||
<div style={{
|
||||
position: 'absolute', top: 8,
|
||||
right: lang === 'fa' ? 8 : 'auto', left: lang === 'en' ? 8 : 'auto',
|
||||
background: 'var(--color-navy)', color: '#fff',
|
||||
fontSize: 8, fontWeight: 700, padding: '2px 7px', letterSpacing: '1.5px', textTransform: 'uppercase',
|
||||
}}>
|
||||
{lang === 'fa' ? 'فولاد مبارکه' : 'Mobarakeh Steel'}
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 8,
|
||||
right: lang === 'fa' ? 8 : 'auto', left: lang === 'en' ? 8 : 'auto',
|
||||
background: 'var(--color-blue)', color: '#fff',
|
||||
fontSize: 9, fontWeight: 700, padding: '2px 8px',
|
||||
}}>
|
||||
{r.date}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '14px', flex: 1, display: 'flex', flexDirection: 'column', gap: 6, borderTop: '3px solid var(--color-navy)' }}>
|
||||
<h3 style={{ fontSize: 12, fontWeight: 800, color: 'var(--ink)', lineHeight: 1.5, flex: 1 }}>{r.title}</h3>
|
||||
<p style={{ fontSize: 11, color: 'var(--ink-4)', lineHeight: 1.65 }}>{r.desc}</p>
|
||||
<a href="#" style={{ fontSize: 10, fontWeight: 700, color: 'var(--color-blue)', textDecoration: 'none', marginTop: 4 }}>{t.readMore}</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ══ RIGHT · خبرنامه ══ */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)', marginBottom: 6 }}>{t.nlOverline}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8 }}>
|
||||
<h2 style={{ fontSize: 'clamp(20px,2vw,28px)', fontWeight: 900, color: 'var(--ink)', letterSpacing: '-0.5px' }}>{t.nlHeading}</h2>
|
||||
<a href="#" style={{ fontSize: 12, fontWeight: 700, color: 'var(--red)', textDecoration: 'none', whiteSpace: 'nowrap', flexShrink: 0 }}>{t.nlArchive}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent issues — navy number badges */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 1, background: 'var(--rule-thin)' }}>
|
||||
{ISSUES[lang].map((issue, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{ background: '#fff', padding: '14px 16px', display: 'flex', gap: 12, alignItems: 'center', cursor: 'pointer', transition: 'background 0.15s' }}
|
||||
onMouseEnter={e => ((e.currentTarget as HTMLDivElement).style.background = 'var(--paper-2)')}
|
||||
onMouseLeave={e => ((e.currentTarget as HTMLDivElement).style.background = '#fff')}
|
||||
>
|
||||
<div style={{
|
||||
background: 'var(--color-navy)', color: '#fff',
|
||||
fontSize: 12, fontWeight: 900, width: 34, height: 34,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
flexShrink: 0, direction: 'ltr',
|
||||
}}>{issue.num}</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.4, marginBottom: 2 }}>{issue.title}</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--ink-5)' }}>{issue.date}</div>
|
||||
</div>
|
||||
<div style={{ color: 'var(--color-blue)', fontSize: 14, flexShrink: 0 }}>←</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
import { FACTORY_REPORTS, FACTORY_REPORTS_META, type FactoryReport, type ScrollDir } from '@/content/factoryReports'
|
||||
|
||||
const NAVY = '#071d49'
|
||||
const GOLD = '#c9a227'
|
||||
const EASE = [0.22, 1, 0.36, 1] as const
|
||||
|
||||
/* native ScrollAnimation — slides child in from a direction when it enters view */
|
||||
function ScrollAnimation({ children, direction = 'up' }: { children: React.ReactNode; direction?: ScrollDir }) {
|
||||
const offset =
|
||||
direction === 'left' ? { x: -70, y: 0 } :
|
||||
direction === 'right' ? { x: 70, y: 0 } :
|
||||
{ x: 0, y: 70 }
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, ...offset }}
|
||||
whileInView={{ opacity: 1, x: 0, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.35 }}
|
||||
transition={{ duration: 0.7, ease: EASE }}
|
||||
style={{ breakInside: 'avoid', marginBottom: 18 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
function ReportCard({ r, lang }: { r: FactoryReport; lang: 'fa' | 'en' }) {
|
||||
const t = r[lang]
|
||||
return (
|
||||
<article
|
||||
style={{
|
||||
background: '#fff', borderRadius: 14, overflow: 'hidden', cursor: 'pointer',
|
||||
boxShadow: '0 10px 34px -20px rgba(7,29,73,0.30)',
|
||||
transition: 'transform 200ms, box-shadow 200ms',
|
||||
}}
|
||||
onMouseEnter={(e) => { const d = e.currentTarget; d.style.transform = 'translateY(-4px)'; d.style.boxShadow = '0 22px 50px -22px rgba(7,29,73,0.38)' }}
|
||||
onMouseLeave={(e) => { const d = e.currentTarget; d.style.transform = 'translateY(0)'; d.style.boxShadow = '0 10px 34px -20px rgba(7,29,73,0.30)' }}
|
||||
>
|
||||
<div style={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<img src={r.img} alt="" style={{ width: '100%', display: 'block' }} />
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(7,29,73,0.55) 0%, transparent 55%)' }} />
|
||||
<span style={{
|
||||
position: 'absolute', top: 12, insetInlineStart: 12,
|
||||
background: GOLD, color: NAVY, fontSize: 10, fontWeight: 800,
|
||||
padding: '4px 10px', borderRadius: 999, letterSpacing: '0.3px',
|
||||
}}>{t.tag}</span>
|
||||
</div>
|
||||
<div style={{ padding: '18px 18px 16px', direction: lang === 'fa' ? 'rtl' : 'ltr' }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--ink-5)', marginBottom: 8 }}>{t.date}</div>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 900, color: NAVY, lineHeight: 1.45, margin: '0 0 10px' }}>{t.title}</h3>
|
||||
<p style={{ fontSize: 12.5, lineHeight: 1.9, color: 'rgba(7,29,73,0.62)', margin: '0 0 14px' }}>{t.excerpt}</p>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, fontWeight: 800, color: GOLD }}>
|
||||
{FACTORY_REPORTS_META[lang].cta}
|
||||
<ArrowLeft size={14} style={{ transform: lang === 'fa' ? 'none' : 'rotate(180deg)' }} />
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FactoryReportsScroll() {
|
||||
const { lang } = useLang()
|
||||
const m = FACTORY_REPORTS_META[lang]
|
||||
|
||||
return (
|
||||
<section style={{ background: '#faf7f4', padding: '90px 0' }} dir={lang === 'fa' ? 'rtl' : 'ltr'}>
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5">
|
||||
|
||||
{/* heading */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.5 }}
|
||||
transition={{ duration: 0.6, ease: EASE }}
|
||||
style={{ textAlign: 'center', marginBottom: 56 }}
|
||||
>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', color: GOLD, marginBottom: 12 }}>{m.overline}</div>
|
||||
<h2 style={{ fontSize: 'clamp(28px, 3.4vw, 46px)', fontWeight: 900, color: NAVY, letterSpacing: '-1px', margin: '0 0 14px' }}>{m.heading}</h2>
|
||||
<p style={{ fontSize: 'clamp(14px, 1.2vw, 16px)', color: 'rgba(7,29,73,0.6)', lineHeight: 1.8, maxWidth: 520, margin: '0 auto' }}>{m.sub}</p>
|
||||
</motion.div>
|
||||
|
||||
{/* 3-column masonry; cards slide in from left / up / right */}
|
||||
<div style={{ columnCount: 3, columnGap: 18 }} className="max-lg:![column-count:2] max-sm:![column-count:1]">
|
||||
{FACTORY_REPORTS.map((r) => (
|
||||
<ScrollAnimation key={r.id} direction={r.dir}>
|
||||
<ReportCard r={r} lang={lang} />
|
||||
</ScrollAnimation>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,141 +1,207 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
import gsap from 'gsap'
|
||||
import { useRef, useState, useEffect, useLayoutEffect } from 'react'
|
||||
import { gsap } from 'gsap'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
|
||||
const T = {
|
||||
const NAVY = '#071d49'
|
||||
const PAPER = '#faf7f4' // cream background revealed under the de-zoom
|
||||
|
||||
const HERO = {
|
||||
fa: {
|
||||
tag: 'اندیشکده فولاد آینده',
|
||||
h1: 'جایی که آینده به تصمیم تبدیل میشود',
|
||||
sub: 'تحلیل روندها، فناوریها و سناریوهای آینده صنعت فولاد ایران و جهان',
|
||||
title: 'آینده فولاد را امروز رصد کن.',
|
||||
desc: 'هوشمندی که هر بازار را رصد میکند و راهبرد برنده را در یک نگاه به تو میدهد.',
|
||||
},
|
||||
en: {
|
||||
tag: 'Steel Policy Institute',
|
||||
h1: 'Where the Future Becomes Decision',
|
||||
sub: 'Analyzing trends, technologies and future scenarios of the global steel industry',
|
||||
title: 'See the future of steel today.',
|
||||
desc: 'Intelligence that scans every market and hands you the winning strategy at a glance.',
|
||||
},
|
||||
}
|
||||
|
||||
const CENTER_IMG = '/hero%201.webp'
|
||||
|
||||
type Sat = { img: string; x: number; y: number; w: number; depth: number; big?: boolean }
|
||||
const SATELLITES: Sat[] = [
|
||||
{ img: '/news/photo-1567789884554-0b844b597180.jpg', x: -38, y: -26, w: 200, depth: 1.0, big: true },
|
||||
{ img: '/news/photo-1570168007204-dfb528c6958f.jpg', x: 39, y: 24, w: 200, depth: 1.0, big: true },
|
||||
{ img: '/news/photo-1504307651254-35680f356dfd.jpg', x: -44, y: 20, w: 150, depth: 1.35 },
|
||||
{ img: '/news/photo-1558618666-fcd25c85cd64.jpg', x: 43, y: -28, w: 140, depth: 1.4 },
|
||||
{ img: '/news/photo-1518770660439-4636190af475.jpg', x: -26, y: 34, w: 120, depth: 1.7 },
|
||||
{ img: '/news/photo-1611273426858-450d8e3c9fce.jpg', x: 27, y: -36, w: 120, depth: 1.7 },
|
||||
{ img: '/news/photo-1540575467063-178a50c2df87.jpg', x: -30, y: -38, w: 110, depth: 1.9 },
|
||||
{ img: '/news/photo-1581092160607-ee22731c9b4e.jpg', x: 31, y: 37, w: 110, depth: 1.9 },
|
||||
]
|
||||
|
||||
/* ─── MAIN ─── */
|
||||
export default function HeroSection() {
|
||||
const { lang } = useLang()
|
||||
const t = T[lang]
|
||||
|
||||
const sectionRef = useRef<HTMLElement>(null)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const t = HERO[lang]
|
||||
const sectionRef = useRef<HTMLDivElement>(null)
|
||||
const pinRef = useRef<HTMLDivElement>(null)
|
||||
const centerRef = useRef<HTMLDivElement>(null)
|
||||
const overlayRef = useRef<HTMLDivElement>(null)
|
||||
const textRef = useRef<HTMLDivElement>(null)
|
||||
const headlineRef = useRef<HTMLDivElement>(null)
|
||||
const hintRef = useRef<HTMLDivElement>(null)
|
||||
const satRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const el = textRef.current
|
||||
const video = videoRef.current
|
||||
const overlay = overlayRef.current
|
||||
if (!el || !video || !overlay) return
|
||||
const mq = window.matchMedia('(max-width: 767px)')
|
||||
const update = () => setIsMobile(mq.matches)
|
||||
update()
|
||||
mq.addEventListener('change', update)
|
||||
return () => mq.removeEventListener('change', update)
|
||||
}, [])
|
||||
|
||||
video.playbackRate = 1.5
|
||||
// GSAP ScrollTrigger scrub timeline — drives the whole de-zoom + scatter off scroll progress.
|
||||
// Synced to Lenis via the GSAP ticker set up in RootLayout (joinspread.app recipe).
|
||||
useLayoutEffect(() => {
|
||||
const section = sectionRef.current
|
||||
if (!section) return
|
||||
|
||||
// entrance
|
||||
const tl = gsap.timeline()
|
||||
tl.fromTo(el, { opacity: 0, y: 40 }, { opacity: 1, y: 0, duration: 1, ease: 'power3.out' })
|
||||
|
||||
// scroll parallax — scrub keeps it reversible
|
||||
ScrollTrigger.create({
|
||||
trigger: sectionRef.current,
|
||||
const ctx = gsap.context(() => {
|
||||
// single scrubbed timeline pinned for the section's scroll length
|
||||
const tl = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: section,
|
||||
start: 'top top',
|
||||
end: 'bottom top',
|
||||
scrub: 1,
|
||||
onUpdate: (self) => {
|
||||
const p = self.progress
|
||||
gsap.set(video, { scale: 1 + p * 0.12 })
|
||||
gsap.set(overlay, { opacity: 0.15 + p * 0.55 })
|
||||
gsap.set(el, { y: p * -60, opacity: 1 - p * 1.6 })
|
||||
end: 'bottom bottom',
|
||||
scrub: true,
|
||||
},
|
||||
})
|
||||
|
||||
return () => {
|
||||
tl.kill()
|
||||
ScrollTrigger.getAll().forEach(t => t.kill())
|
||||
}
|
||||
}, [])
|
||||
// background morph navy → cream
|
||||
tl.fromTo(pinRef.current, { backgroundColor: NAVY }, { backgroundColor: PAPER, ease: 'none' }, 0)
|
||||
|
||||
// center image: full-bleed cover (scale 2.55) → centered rounded card
|
||||
tl.fromTo(centerRef.current,
|
||||
{ scale: 2.55, borderRadius: 0 },
|
||||
{ scale: 1, borderRadius: 18, ease: 'none' }, 0)
|
||||
|
||||
// legibility overlay fades out as the image shrinks
|
||||
tl.fromTo(overlayRef.current, { opacity: 0.42 }, { opacity: 0, ease: 'none', duration: 0.4 }, 0)
|
||||
|
||||
// headline lifts + blurs + fades out as the de-zoom begins
|
||||
tl.fromTo(headlineRef.current,
|
||||
{ yPercent: 0, opacity: 1, filter: 'blur(0px)' },
|
||||
{ yPercent: -10, opacity: 0, filter: 'blur(8px)', ease: 'none', duration: 0.35 }, 0)
|
||||
|
||||
// scroll hint fades immediately
|
||||
tl.fromTo(hintRef.current, { opacity: 1 }, { opacity: 0, ease: 'none', duration: 0.08 }, 0)
|
||||
|
||||
// satellites: clustered behind center → fan out to scattered positions with parallax
|
||||
satRefs.current.forEach((el, i) => {
|
||||
if (!el) return
|
||||
const s = SATELLITES[i]
|
||||
tl.fromTo(el,
|
||||
{ xPercent: -50, yPercent: -50, x: `${s.x * 0.12}vw`, y: `${s.y * 0.12}vh`, scale: 0.4, opacity: 0 },
|
||||
{ x: `${s.x * s.depth}vw`, y: `${s.y * s.depth}vh`, scale: 1, opacity: 1, ease: 'none' }, 0)
|
||||
})
|
||||
}, section)
|
||||
|
||||
return () => ctx.revert()
|
||||
}, [isMobile])
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
background: 'var(--ink)',
|
||||
borderBottom:'3px solid var(--red)',
|
||||
overflow: 'hidden',
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{/* Background video */}
|
||||
<video
|
||||
ref={videoRef}
|
||||
aria-hidden="true"
|
||||
autoPlay muted loop playsInline disablePictureInPicture
|
||||
controlsList="nofullscreen nodownload noremoteplayback"
|
||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', zIndex: 0, transformOrigin: 'center center' }}
|
||||
src="/hero-bg.mp4#t=2"
|
||||
/>
|
||||
{/* Dark overlay */}
|
||||
<div ref={overlayRef} aria-hidden="true" style={{ position: 'absolute', inset: 0, background: 'rgba(7,29,73,0.60)', zIndex: 1 }} />
|
||||
{/* Vignette */}
|
||||
<div aria-hidden="true" style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 90% 80% at 50% 50%, transparent 40%, rgba(7,29,73,0.75) 100%)', zIndex: 2 }} />
|
||||
<div ref={sectionRef} style={{ height: '200vh', position: 'relative' }}>
|
||||
{/* pinned stage */}
|
||||
<div ref={pinRef} style={{ position: 'sticky', top: 0, height: '100vh', overflow: 'hidden', background: NAVY }}>
|
||||
|
||||
{/* Content block */}
|
||||
{/* satellite scatter */}
|
||||
{SATELLITES.map((sat, i) => {
|
||||
const ratio = sat.big ? 1.25 : 0.82
|
||||
return (
|
||||
<div
|
||||
ref={textRef}
|
||||
className="max-md:!left-4 max-md:!right-4 max-md:!max-w-full max-md:!top-1/2 max-md:text-center"
|
||||
key={i}
|
||||
ref={(el) => { satRefs.current[i] = el }}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '38%',
|
||||
right: lang === 'fa' ? 48 : 'auto',
|
||||
left: lang === 'en' ? 48 : 'auto',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 3,
|
||||
textAlign: lang === 'fa' ? 'right' : 'left',
|
||||
direction: lang === 'fa' ? 'rtl' : 'ltr',
|
||||
maxWidth: 520,
|
||||
padding: '0 24px',
|
||||
position: 'absolute', left: '50%', top: '50%',
|
||||
width: sat.w, height: sat.w * ratio,
|
||||
marginLeft: -sat.w / 2, marginTop: -(sat.w * ratio) / 2,
|
||||
borderRadius: 14, overflow: 'hidden',
|
||||
boxShadow: '0 24px 60px -18px rgba(7,29,73,0.28), 0 4px 14px rgba(7,29,73,0.10)',
|
||||
willChange: 'transform, opacity', opacity: 0,
|
||||
}}
|
||||
className="max-md:hidden"
|
||||
>
|
||||
{/* Headline */}
|
||||
<h1
|
||||
className="max-md:!whitespace-nowrap max-md:!text-[4.5vw]"
|
||||
style={{
|
||||
fontSize: 'clamp(22px, 2.2vw, 34px)',
|
||||
fontWeight: 900,
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: '-0.5px',
|
||||
color: 'var(--paper)',
|
||||
marginBottom: 20,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{t.h1}
|
||||
</h1>
|
||||
<img src={sat.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Subtitle */}
|
||||
<p
|
||||
className="max-md:!border-0 max-md:!px-0 max-md:!text-[12px] max-md:!leading-relaxed"
|
||||
{/* center image — de-zooms from full-bleed cover to a centered card */}
|
||||
<div
|
||||
ref={centerRef}
|
||||
style={{
|
||||
fontSize: 15,
|
||||
lineHeight: 1.8,
|
||||
color: 'rgba(255,255,255,0.65)',
|
||||
borderRight: lang === 'fa' ? '2px solid var(--red)' : 'none',
|
||||
borderLeft: lang === 'en' ? '2px solid var(--red)' : 'none',
|
||||
paddingRight: lang === 'fa' ? 14 : 0,
|
||||
paddingLeft: lang === 'en' ? 14 : 0,
|
||||
position: 'absolute', left: '50%', top: '50%',
|
||||
width: isMobile ? '82vw' : 'min(56vw, 760px)',
|
||||
height: isMobile ? '62vh' : 'min(32vw, 430px)',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
borderRadius: 0,
|
||||
boxShadow: '0 40px 90px -24px rgba(7,29,73,0.40)',
|
||||
overflow: 'hidden', zIndex: 5, willChange: 'transform, border-radius',
|
||||
}}
|
||||
>
|
||||
{t.sub}
|
||||
<img
|
||||
src={CENTER_IMG}
|
||||
alt=""
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 38%', display: 'block' }}
|
||||
/>
|
||||
<div
|
||||
ref={overlayRef}
|
||||
style={{
|
||||
position: 'absolute', inset: 0, opacity: 0.42,
|
||||
background: 'linear-gradient(to top, rgba(7,29,73,0.85) 0%, rgba(7,29,73,0.25) 45%, transparent 75%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* headline */}
|
||||
<div
|
||||
ref={headlineRef}
|
||||
style={{
|
||||
position: 'absolute', inset: 0, zIndex: 10,
|
||||
display: 'flex', flexDirection: 'column', justifyContent: 'flex-end',
|
||||
padding: '0 7vw 12vh', direction: 'rtl', pointerEvents: 'none', willChange: 'transform, opacity, filter',
|
||||
}}
|
||||
className="max-md:!px-6 max-md:!pb-[14vh]"
|
||||
>
|
||||
<div style={{ pointerEvents: 'auto', maxWidth: 900 }}>
|
||||
<h1 style={{
|
||||
fontSize: 'clamp(26px, 3.4vw, 50px)', fontWeight: 900, color: '#fff',
|
||||
lineHeight: 1.15, letterSpacing: '-1px', margin: 0, whiteSpace: 'nowrap',
|
||||
textShadow: '0 2px 30px rgba(0,0,0,0.35)',
|
||||
}} className="max-md:!whitespace-normal max-md:!text-3xl">
|
||||
{t.title}
|
||||
</h1>
|
||||
<p style={{
|
||||
fontSize: 'clamp(14px, 1.4vw, 19px)', lineHeight: 1.7, color: 'rgba(255,255,255,0.78)',
|
||||
maxWidth: 480, margin: '20px 0 0', textShadow: '0 1px 12px rgba(0,0,0,0.3)',
|
||||
}}>
|
||||
{t.desc}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* scroll hint */}
|
||||
<div
|
||||
ref={hintRef}
|
||||
style={{
|
||||
position: 'absolute', bottom: 26, left: '50%', transform: 'translateX(-50%)',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6,
|
||||
zIndex: 12, pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 9, letterSpacing: '3px', color: 'rgba(255,255,255,0.55)' }}>SCROLL</span>
|
||||
<div className="hero-hint-bob" style={{ width: 1, height: 28, background: 'linear-gradient(to bottom, rgba(255,255,255,0.5), transparent)' }} />
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.hero-hint-bob { animation: heroHintBob 1.5s ease-in-out infinite; }
|
||||
@keyframes heroHintBob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(7px)} }
|
||||
`}</style>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import {
|
||||
ArrowLeft, Landmark, Globe, BarChart3, Activity, Database,
|
||||
Newspaper, Building2, TrendingUp, Coins, type LucideIcon,
|
||||
} from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
import { INTEGRATIONS, INTEGRATIONS_META, type IntegrationIcon } from '@/content/integrations'
|
||||
|
||||
const NAVY = '#071d49'
|
||||
const GOLD = '#c9a227'
|
||||
const COLS = 6
|
||||
const ROWS = 5
|
||||
const EASE = [0.22, 1, 0.36, 1] as const
|
||||
|
||||
const ICONS: Record<IntegrationIcon, LucideIcon> = {
|
||||
exchange: Landmark, globe: Globe, chart: BarChart3, activity: Activity,
|
||||
database: Database, newspaper: Newspaper, building: Building2, trending: TrendingUp, coins: Coins,
|
||||
}
|
||||
|
||||
const CELL = new Map(INTEGRATIONS.map((it) => [`${it.col}-${it.row}`, it]))
|
||||
|
||||
export default function IntegrationsSection() {
|
||||
const { lang } = useLang()
|
||||
const isFa = lang === 'fa'
|
||||
const m = INTEGRATIONS_META[lang]
|
||||
|
||||
return (
|
||||
<section style={{ background: NAVY, padding: '96px 0', overflow: 'hidden', position: 'relative' }} dir={isFa ? 'rtl' : 'ltr'}>
|
||||
{/* drifting gold glow */}
|
||||
<motion.div
|
||||
animate={{ x: [0, 26, 0], y: [0, -18, 0], opacity: [0.8, 1, 0.8] }}
|
||||
transition={{ repeat: Infinity, duration: 9, ease: 'easeInOut' }}
|
||||
style={{
|
||||
position: 'absolute', top: '50%', insetInlineEnd: '8%', transform: 'translateY(-50%)',
|
||||
width: 460, height: 460, borderRadius: '50%',
|
||||
background: `radial-gradient(circle, ${GOLD}22 0%, transparent 70%)`, pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ position: 'relative' }}>
|
||||
<div style={{ display: 'flex', flexDirection: isFa ? 'row' : 'row-reverse', alignItems: 'center', gap: 48 }} className="max-lg:!flex-col max-lg:!gap-12">
|
||||
|
||||
{/* ── TEXT side ── */}
|
||||
<div style={{ flex: 1, minWidth: 0 }} className="max-lg:!w-full max-lg:!text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.4 }}
|
||||
transition={{ duration: 0.6, ease: EASE }}
|
||||
>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', color: GOLD, marginBottom: 18 }}>{m.overline}</div>
|
||||
<h2 style={{ fontSize: 'clamp(30px, 4vw, 56px)', fontWeight: 900, color: '#fff', letterSpacing: '-1px', lineHeight: 1.1, margin: '0 0 22px' }}>
|
||||
{m.heading}
|
||||
</h2>
|
||||
<p style={{ fontSize: 'clamp(14px, 1.3vw, 18px)', lineHeight: 1.9, color: 'rgba(255,255,255,0.62)', maxWidth: 460, margin: '0 0 34px' }} className="max-lg:!mx-auto">
|
||||
{m.sub}
|
||||
</p>
|
||||
<Link
|
||||
to="/pulse"
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 10,
|
||||
background: GOLD, color: NAVY, borderRadius: 999,
|
||||
padding: '13px 28px', fontSize: 14, fontWeight: 800, textDecoration: 'none',
|
||||
transition: 'transform 180ms, opacity 180ms',
|
||||
}}
|
||||
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.opacity = '0.88'; (e.currentTarget as HTMLElement).style.transform = 'translateX(-3px)' }}
|
||||
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.opacity = '1'; (e.currentTarget as HTMLElement).style.transform = 'translateX(0)' }}
|
||||
>
|
||||
{m.cta}
|
||||
<ArrowLeft size={16} style={{ transform: isFa ? 'none' : 'rotate(180deg)' }} />
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* ── GRID side ── */}
|
||||
<div style={{ flexShrink: 0 }} className="max-lg:!w-full max-lg:!flex max-lg:!justify-center">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${COLS}, 78px)`, gridTemplateRows: `repeat(${ROWS}, 78px)`, gap: 14 }}
|
||||
className="max-sm:![grid-template-columns:repeat(6,52px)] max-sm:![grid-template-rows:repeat(5,52px)] max-sm:!gap-2">
|
||||
{Array.from({ length: COLS * ROWS }).map((_, idx) => {
|
||||
const col = (idx % COLS) + 1
|
||||
const row = Math.floor(idx / COLS) + 1
|
||||
const it = CELL.get(`${col}-${row}`)
|
||||
|
||||
if (!it) {
|
||||
// empty placeholder cell — faint outline, soft breathing
|
||||
return (
|
||||
<motion.div
|
||||
key={idx}
|
||||
animate={{ opacity: [0.4, 0.7, 0.4] }}
|
||||
transition={{ repeat: Infinity, duration: 4 + (idx % 5) * 0.4, ease: 'easeInOut' }}
|
||||
style={{ borderRadius: 16, border: '1px solid rgba(255,255,255,0.06)', background: 'rgba(255,255,255,0.015)' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Icon = ICONS[it.icon]
|
||||
const delay = (col + row) * 0.07
|
||||
return (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0, scale: 0.3, rotate: -8 }}
|
||||
whileInView={{ opacity: 1, scale: 1, rotate: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ type: 'spring', stiffness: 220, damping: 16, delay }}
|
||||
whileHover={{ scale: 1.12, y: -4 }}
|
||||
title={it.name}
|
||||
aria-label={it.name}
|
||||
style={{
|
||||
position: 'relative',
|
||||
borderRadius: 16, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.12)',
|
||||
boxShadow: `0 12px 34px -16px ${it.accent ?? GOLD}cc`,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{/* accent glow halo */}
|
||||
<motion.div
|
||||
animate={{ opacity: [0.25, 0.55, 0.25] }}
|
||||
transition={{ repeat: Infinity, duration: 3 + (delay % 2), ease: 'easeInOut' }}
|
||||
style={{
|
||||
position: 'absolute', inset: 0, borderRadius: 16, pointerEvents: 'none',
|
||||
background: `radial-gradient(circle at 50% 50%, ${it.accent ?? GOLD}26 0%, transparent 70%)`,
|
||||
}}
|
||||
/>
|
||||
{/* gentle float of the icon itself */}
|
||||
<motion.div
|
||||
animate={{ y: [0, -5, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3.2 + (delay % 1.5), ease: 'easeInOut' }}
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
{it.logo ? (
|
||||
<img src={it.logo} alt={it.name} className="max-sm:!w-6 max-sm:!h-6" style={{ width: 34, height: 34, objectFit: 'contain', display: 'block' }} />
|
||||
) : (
|
||||
<Icon className="max-sm:!w-[18px] max-sm:!h-[18px]" size={26} color={it.accent ?? '#fff'} strokeWidth={1.9} />
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import { useLang } from '@/context/LangContext'
|
||||
|
||||
const NAVY = '#071d49'
|
||||
const GOLD = '#c9a227'
|
||||
|
||||
/* خبرنامه فولاد — recent issue titles, scrolled as a continuous marquee. */
|
||||
const ISSUES = {
|
||||
fa: [
|
||||
{ num: '۱۲', title: 'فولاد و تحریمهای جدید: راهکارهای صادراتی ۱۴۰۴', to: '#newsletter-12' },
|
||||
{ num: '۱۱', title: 'قیمتگذاری فولاد در بازار داخلی: چالشها و فرصتها', to: '#newsletter-11' },
|
||||
{ num: '۱۰', title: 'آمار تولید فولاد ایران در ۹ ماهه ۱۴۰۳', to: '#newsletter-10' },
|
||||
{ num: '۰۹', title: 'فولاد سبز؛ چشمانداز ایران در برابر رقبای منطقهای', to: '#newsletter-09' },
|
||||
{ num: '۰۸', title: 'تحلیل بازار عراق: فرصتها و تهدیدها برای صادرکنندگان ایرانی', to: '#newsletter-08' },
|
||||
{ num: '۰۷', title: 'هوش مصنوعی در کنترل کیفیت فولاد: کاربردها و چالشها', to: '#newsletter-07' },
|
||||
],
|
||||
en: [
|
||||
{ num: '12', title: 'Steel & New Sanctions: Export Strategies for 1404', to: '#newsletter-12' },
|
||||
{ num: '11', title: 'Steel Pricing in the Domestic Market: Challenges & Opportunities', to: '#newsletter-11' },
|
||||
{ num: '10', title: 'Iran Steel Production Stats — First 9 Months of 1403', to: '#newsletter-10' },
|
||||
{ num: '09', title: "Green Steel: Iran's Outlook Against Regional Rivals", to: '#newsletter-09' },
|
||||
{ num: '08', title: 'Iraq Market Analysis: Opportunities & Threats for Exporters', to: '#newsletter-08' },
|
||||
{ num: '07', title: 'AI in Steel Quality Control: Applications & Challenges', to: '#newsletter-07' },
|
||||
],
|
||||
}
|
||||
|
||||
const T = {
|
||||
fa: { overline: 'STEEL NEWSLETTER', heading: 'خبرنامه فولاد', live: 'بهروزرسانی زنده' },
|
||||
en: { overline: 'STEEL NEWSLETTER', heading: 'Steel Newsletter', live: 'Live updates' },
|
||||
}
|
||||
|
||||
function Item({ num, title, to }: { num: string; title: string; to: string }) {
|
||||
return (
|
||||
<a
|
||||
href={to}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: 12, padding: '0 32px', whiteSpace: 'nowrap', textDecoration: 'none', cursor: 'pointer' }}
|
||||
onMouseEnter={(e) => { (e.currentTarget.querySelector('.nm-title') as HTMLElement).style.color = GOLD }}
|
||||
onMouseLeave={(e) => { (e.currentTarget.querySelector('.nm-title') as HTMLElement).style.color = 'rgba(255,255,255,0.9)' }}
|
||||
>
|
||||
<span style={{
|
||||
flexShrink: 0, width: 30, height: 30, borderRadius: 8,
|
||||
background: GOLD, color: NAVY, fontSize: 12, fontWeight: 900,
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
direction: 'ltr', fontVariantNumeric: 'tabular-nums',
|
||||
}}>{num}</span>
|
||||
<span className="nm-title" style={{ fontSize: 15, fontWeight: 700, color: 'rgba(255,255,255,0.9)', transition: 'color 160ms' }}>{title}</span>
|
||||
{/* blinking live dot on the (right) trailing side of each item */}
|
||||
<span className="nm-dot" style={{ flexShrink: 0, width: 8, height: 8, borderRadius: '50%', background: '#22c55e', display: 'inline-block' }} />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default function NewsletterMarquee() {
|
||||
const { lang } = useLang()
|
||||
const isFa = lang === 'fa'
|
||||
const t = T[lang]
|
||||
const items = ISSUES[lang]
|
||||
// duplicate the list so the scroll loops seamlessly
|
||||
const loop = [...items, ...items]
|
||||
|
||||
return (
|
||||
<section style={{ background: NAVY, padding: '40px 0', overflow: 'hidden' }} dir={isFa ? 'rtl' : 'ltr'}>
|
||||
{/* heading */}
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ marginBottom: 22 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, justifyContent: 'center' }}>
|
||||
<span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '3px', color: GOLD }}>{t.overline}</span>
|
||||
<span style={{ width: 4, height: 4, borderRadius: '50%', background: 'rgba(255,255,255,0.3)' }} />
|
||||
<h2 style={{ fontSize: 'clamp(18px, 1.8vw, 24px)', fontWeight: 900, color: '#fff', margin: 0 }}>{t.heading}</h2>
|
||||
{/* live pulse */}
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, marginInlineStart: 6 }}>
|
||||
<span className="nm-pulse" style={{ width: 7, height: 7, borderRadius: '50%', background: '#1e8449', display: 'inline-block' }} />
|
||||
<span style={{ fontSize: 9, fontWeight: 700, color: '#1e8449', letterSpacing: '1px' }}>{t.live}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* moving track with edge fades */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div className="nm-fade-start" />
|
||||
<div className="nm-fade-end" />
|
||||
<div className={`nm-marquee ${isFa ? 'nm-rtl' : ''}`} style={{ display: 'flex', width: 'max-content' }}>
|
||||
{loop.map((it, i) => <Item key={i} num={it.num} title={it.title} to={it.to} />)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.nm-marquee {
|
||||
animation: nm-scroll 38s linear infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
.nm-marquee:hover { animation-play-state: paused; }
|
||||
/* RTL scrolls the opposite direction so it reads naturally */
|
||||
.nm-marquee.nm-rtl { animation-name: nm-scroll-rtl; }
|
||||
@keyframes nm-scroll {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
@keyframes nm-scroll-rtl {
|
||||
from { transform: translateX(-50%); }
|
||||
to { transform: translateX(0); }
|
||||
}
|
||||
.nm-pulse, .nm-dot { animation: nm-blink 1.4s ease-in-out infinite; }
|
||||
.nm-dot { box-shadow: 0 0 8px 1px rgba(34,197,94,0.7); }
|
||||
@keyframes nm-blink { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.3;transform:scale(.85)} }
|
||||
.nm-fade-start, .nm-fade-end {
|
||||
position: absolute; top: 0; bottom: 0; width: 120px; z-index: 2; pointer-events: none;
|
||||
}
|
||||
.nm-fade-start { left: 0; background: linear-gradient(to right, ${NAVY}, transparent); }
|
||||
.nm-fade-end { right: 0; background: linear-gradient(to left, ${NAVY}, transparent); }
|
||||
@media (prefers-reduced-motion: reduce) { .nm-marquee { animation: none; } }
|
||||
`}</style>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,55 +1,7 @@
|
|||
import { Link } from 'react-router-dom'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
|
||||
/* ─── Radar data ─────────────────────────────────────── */
|
||||
const SIGNALS = {
|
||||
fa: [
|
||||
{
|
||||
tag: 'چین', level: 'critical', levelLabel: 'بحرانی',
|
||||
title: 'صادرات فولاد چین به بالاترین رکورد ۸ ساله رسید',
|
||||
desc: 'در بهمن ۱۴۰۳ صادرات فولاد چین به ۱۰.۷ میلیون تن رسید — بالاترین رقم از ۲۰۱۶ — که فشار قیمتی شدیدی بر بازارهای منطقهای از جمله ایران وارد کرد.',
|
||||
date: 'بهمن ۱۴۰۳', img: '/news/photo-1611273426858-450d8e3c9fce.jpg',
|
||||
accentColor: '#7f1d1d', levelColor: '#7f1d1d',
|
||||
},
|
||||
{
|
||||
tag: 'اروپا', level: 'high', levelLabel: 'بالا',
|
||||
title: 'CBAM اروپا از فروردین ۱۴۰۴ اجرایی میشود',
|
||||
desc: 'مکانیزم تعدیل مرزی کربن اتحادیه اروپا برای فولاد اجرایی شد. صادرکنندگان ایرانی باید الزامات جدید گزارشدهی کربن را رعایت کنند.',
|
||||
date: 'دی ۱۴۰۳', img: '/news/photo-1473341304170-971dccb5ac1e.jpg',
|
||||
accentColor: '#dc2626', levelColor: '#dc2626',
|
||||
},
|
||||
{
|
||||
tag: 'هند', level: 'medium', levelLabel: 'متوسط',
|
||||
title: 'هند ظرفیت تولید فولاد خود را تا ۲۰۳۰ دو برابر میکند',
|
||||
desc: 'با سرمایهگذاری ۱۴۲ میلیارد دلاری دولت هند، این کشور تا ۲۰۳۰ به ظرفیت ۳۰۰ میلیون تن میرسد و رقیب جدی ایران در بازارهای صادراتی میشود.',
|
||||
date: 'آذر ۱۴۰۳', img: '/news/photo-1566873535350-6586b0b7a1f2.jpg',
|
||||
accentColor: '#b45309', levelColor: '#b45309',
|
||||
},
|
||||
],
|
||||
en: [
|
||||
{
|
||||
tag: 'China', level: 'critical', levelLabel: 'Critical',
|
||||
title: "China's Steel Exports Hit an 8-Year Record High",
|
||||
desc: "In Feb 2025, China's steel exports reached 10.7 million tons — the highest since 2016 — putting severe price pressure on regional markets including Iran.",
|
||||
date: 'Feb 2025', img: '/news/photo-1611273426858-450d8e3c9fce.jpg',
|
||||
accentColor: '#7f1d1d', levelColor: '#7f1d1d',
|
||||
},
|
||||
{
|
||||
tag: 'Europe', level: 'high', levelLabel: 'High',
|
||||
title: "Europe's CBAM Mechanism Takes Effect from Spring 2025",
|
||||
desc: "The EU's Carbon Border Adjustment Mechanism for steel is now in force. Iranian exporters must comply with new carbon reporting requirements.",
|
||||
date: 'Jan 2025', img: '/news/photo-1473341304170-971dccb5ac1e.jpg',
|
||||
accentColor: '#dc2626', levelColor: '#dc2626',
|
||||
},
|
||||
{
|
||||
tag: 'India', level: 'medium', levelLabel: 'Medium',
|
||||
title: 'India to Double Steel Production Capacity by 2030',
|
||||
desc: 'With a $142 billion government investment, India targets 300 million tons capacity by 2030, becoming a serious competitor in Iran\'s export markets.',
|
||||
date: 'Dec 2024', img: '/news/photo-1566873535350-6586b0b7a1f2.jpg',
|
||||
accentColor: '#b45309', levelColor: '#b45309',
|
||||
},
|
||||
],
|
||||
}
|
||||
import { RadarCard } from '@/pages/Radar/Radar'
|
||||
import { RADAR_ITEMS } from '@/content/radar'
|
||||
|
||||
/* ─── Tech data ──────────────────────────────────────── */
|
||||
const TECHS = {
|
||||
|
|
@ -125,7 +77,6 @@ const T = {
|
|||
export default function RadarTechSection() {
|
||||
const { lang } = useLang()
|
||||
const t = T[lang]
|
||||
const signals = SIGNALS[lang]
|
||||
const techs = TECHS[lang]
|
||||
|
||||
return (
|
||||
|
|
@ -143,44 +94,9 @@ export default function RadarTechSection() {
|
|||
<Link to="/radar" style={{ fontSize: 12, fontWeight: 700, color: 'var(--red)', textDecoration: 'none', whiteSpace: 'nowrap' }}>{t.radarAll}</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 max-md:grid-cols-1" style={{ gap: 16 }}>
|
||||
{signals.map((s, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
background: '#fff', overflow: 'hidden',
|
||||
boxShadow: '0 2px 12px rgba(0,0,0,0.07)',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s', cursor: 'pointer',
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLDivElement).style.transform = 'translateY(-4px)'; (e.currentTarget as HTMLDivElement).style.boxShadow = '0 10px 30px rgba(0,0,0,0.12)' }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.transform = 'translateY(0)'; (e.currentTarget as HTMLDivElement).style.boxShadow = '0 2px 12px rgba(0,0,0,0.07)' }}
|
||||
>
|
||||
{/* Image */}
|
||||
<div style={{ position: 'relative', height: 160, overflow: 'hidden' }}>
|
||||
<img src={s.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.4s' }}
|
||||
onMouseEnter={e => (e.currentTarget.style.transform = 'scale(1.06)')}
|
||||
onMouseLeave={e => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
/>
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.45) 0%, transparent 60%)' }} />
|
||||
{/* Level badge */}
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 12,
|
||||
right: lang === 'fa' ? 12 : 'auto', left: lang === 'en' ? 12 : 'auto',
|
||||
display: 'flex', gap: 6, alignItems: 'center',
|
||||
}}>
|
||||
<span style={{ background: s.levelColor, color: '#fff', fontSize: 9, fontWeight: 700, padding: '2px 8px', letterSpacing: '1px' }}>{s.levelLabel}</span>
|
||||
<span style={{ background: 'rgba(0,0,0,0.6)', color: '#fff', fontSize: 9, fontWeight: 700, padding: '2px 8px' }}>{s.tag}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ padding: '20px', flex: 1, display: 'flex', flexDirection: 'column', gap: 10, borderTop: `3px solid ${s.accentColor}` }}>
|
||||
<h3 style={{ fontSize: 14, fontWeight: 800, color: 'var(--ink)', lineHeight: 1.5 }}>{s.title}</h3>
|
||||
<p style={{ fontSize: 12, color: 'var(--ink-4)', lineHeight: 1.75, flex: 1 }}>{s.desc}</p>
|
||||
<div style={{ fontSize: 10, color: 'var(--ink-5)', fontWeight: 600 }}>{s.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 22 }} className="max-lg:!grid-cols-2 max-sm:!grid-cols-1">
|
||||
{RADAR_ITEMS.slice(0, 4).map((item) => (
|
||||
<RadarCard key={item.id} item={item} lang={lang} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,199 @@
|
|||
import { useRef, useState } from 'react'
|
||||
import { motion, AnimatePresence, useScroll, useMotionValueEvent } from 'framer-motion'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
|
||||
const NAVY = '#071d49'
|
||||
const GOLD = '#c9a227'
|
||||
const EASE = [0.22, 1, 0.36, 1] as const
|
||||
|
||||
type Feat = {
|
||||
img: string
|
||||
imgSide: 'left' | 'right'
|
||||
imgPos?: string
|
||||
fa: { kicker: string; title: string; desc: string; stat: string; statTail: string }
|
||||
en: { kicker: string; title: string; desc: string; stat: string; statTail: string }
|
||||
}
|
||||
|
||||
/* Your 3 hero images. As you scroll, they replace each other in place with a fade. */
|
||||
const FEATURES: Feat[] = [
|
||||
{
|
||||
img: '/hero%201.webp', imgSide: 'right', imgPos: 'center 35%',
|
||||
fa: {
|
||||
kicker: 'اندیشکده فولاد آینده',
|
||||
title: 'بینش راهبردی',
|
||||
desc: 'عصرِ رقابت بر سر تناژ به پایان رسیده است. در جهان پرشتاب فردا، هژمونی صنعتی از «عمق تحلیل»، «تسلط بر شبکههای داده» و «جسارت در طراحی آینده» خلق میشود.',
|
||||
stat: '۶ بازار',
|
||||
statTail: 'رصد ۲۴ ساعته. هشدار در کمتر از ۱۲ ثانیه.',
|
||||
},
|
||||
en: {
|
||||
kicker: 'Future Steel Institute',
|
||||
title: 'Strategic Vision',
|
||||
desc: 'The era of tonnage competition is over. Industrial leadership is built on analytical depth, data mastery, and the courage to design the future.',
|
||||
stat: '6 markets',
|
||||
statTail: 'Scanned 24/7. Alerts in under 12 seconds.',
|
||||
},
|
||||
},
|
||||
{
|
||||
img: '/hero%202.webp', imgSide: 'left', imgPos: 'center center',
|
||||
fa: {
|
||||
kicker: 'فناوری صنعتی',
|
||||
title: 'رادار فناوری',
|
||||
desc: 'انقلاب هوش مصنوعی در حال بازنویسی DNA زنجیره ارزش است. ما سیگنالهای ضعیف فناوری را شکار کرده و آنها را به استراتژیهای برنده برای رهبران صنعتی تبدیل میکنیم.',
|
||||
stat: '٪۳۸',
|
||||
statTail: 'میانگین حاشیه سود در هر معامله.',
|
||||
},
|
||||
en: {
|
||||
kicker: 'Industrial Technology',
|
||||
title: 'Technology Radar',
|
||||
desc: 'AI is rewriting the DNA of the value chain. We hunt weak technology signals and transform them into winning strategies for industry leaders.',
|
||||
stat: '38%',
|
||||
statTail: 'Average margin per deal.',
|
||||
},
|
||||
},
|
||||
{
|
||||
img: '/hero%203.webp', imgSide: 'right', imgPos: 'center center',
|
||||
fa: {
|
||||
kicker: 'ژئواکونومی',
|
||||
title: 'کلانروندها',
|
||||
desc: 'هندسه قدرت در صنعت در حال تغییر است. رمزگشایی از تقاطع پیچیده ژئواکونومی، مقررات کربنی و گذار انرژی؛ قطبنمای شما برای عبور ایمن از طوفانهای آینده.',
|
||||
stat: '۲۰ ساعت',
|
||||
statTail: 'صرفهجویی هفتگی نسبت به روش دستی.',
|
||||
},
|
||||
en: {
|
||||
kicker: 'Geo-Economics',
|
||||
title: 'Macro Trends',
|
||||
desc: 'The geometry of power is shifting. Decoding the complex intersection of geo-economics, carbon regulations and energy transition — your compass for navigating the storms ahead.',
|
||||
stat: '20 hrs',
|
||||
statTail: 'Saved per week vs. manual.',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/* fade-up + blur wrapper — fires fast on mount (re-keyed per active slide) */
|
||||
function FadeUp({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ y: 22, opacity: 0, filter: 'blur(10px)' }}
|
||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||
exit={{ y: -10, opacity: 0, filter: 'blur(6px)' }}
|
||||
transition={{ duration: 0.42, ease: EASE, delay }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
/* the text column for the active slide */
|
||||
function TextPanel({ feat, lang }: { feat: Feat; lang: 'fa' | 'en' }) {
|
||||
const t = feat[lang]
|
||||
return (
|
||||
<div style={{ direction: 'rtl', maxWidth: 460 }} className="max-md:!text-right">
|
||||
<FadeUp>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 18, flexDirection: 'row-reverse', justifyContent: 'flex-end' }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', color: GOLD, textTransform: 'uppercase' }}>{t.kicker}</span>
|
||||
<div style={{ width: 36, height: 2, background: GOLD }} />
|
||||
</div>
|
||||
</FadeUp>
|
||||
<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' }}>
|
||||
{t.title}
|
||||
</h2>
|
||||
</FadeUp>
|
||||
<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' }}>
|
||||
{t.desc}
|
||||
</p>
|
||||
</FadeUp>
|
||||
<FadeUp delay={0.15}>
|
||||
<div style={{ paddingTop: 20, borderTop: '1px solid rgba(7,29,73,0.12)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 12, flexDirection: 'row-reverse', justifyContent: 'flex-end' }}>
|
||||
<span style={{ fontSize: 34, fontWeight: 900, color: NAVY, letterSpacing: '-1px' }}>{t.stat}</span>
|
||||
<span style={{ fontSize: 13, color: 'rgba(7,29,73,0.55)' }}>{t.statTail}</span>
|
||||
</div>
|
||||
</div>
|
||||
</FadeUp>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* progress rail */
|
||||
function Rail({ active, total }: { active: number; total: number }) {
|
||||
return (
|
||||
<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) => (
|
||||
<div key={i} style={{
|
||||
width: 3, height: i === active ? 34 : 14, borderRadius: 2,
|
||||
background: i === active ? GOLD : 'rgba(7,29,73,0.18)',
|
||||
transition: 'all 400ms cubic-bezier(0.22,1,0.36,1)',
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SpreadFeatures() {
|
||||
const { lang } = useLang()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [active, setActive] = useState(0)
|
||||
|
||||
const { scrollYProgress } = useScroll({ target: containerRef, offset: ['start start', 'end end'] })
|
||||
useMotionValueEvent(scrollYProgress, 'change', (v) => {
|
||||
const next = v < 0.34 ? 0 : v < 0.67 ? 1 : 2
|
||||
if (next !== active) setActive(next)
|
||||
})
|
||||
|
||||
return (
|
||||
/* 300vh tall → inner pins for the whole scroll; each ~third = one slide.
|
||||
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' }}>
|
||||
|
||||
{FEATURES.map((f, i) => {
|
||||
const isActive = i === active
|
||||
const imgRight = f.imgSide === 'right'
|
||||
return (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={false}
|
||||
animate={{ opacity: isActive ? 1 : 0 }}
|
||||
transition={{ duration: 0.55, ease: EASE }}
|
||||
style={{
|
||||
position: 'absolute', inset: 0,
|
||||
display: 'flex', flexDirection: imgRight ? 'row' : 'row-reverse',
|
||||
alignItems: 'center', justifyContent: 'center', direction: 'ltr',
|
||||
pointerEvents: isActive ? 'auto' : 'none',
|
||||
}}
|
||||
className="max-md:!flex-col"
|
||||
>
|
||||
{/* TEXT */}
|
||||
<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">
|
||||
{/* only the active slide's text animates in (re-keyed so it replays each time) */}
|
||||
{isActive && (
|
||||
<AnimatePresence mode="wait">
|
||||
<div key={active} style={{ width: '100%' }}>
|
||||
<TextPanel feat={f} lang={lang} />
|
||||
</div>
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* IMAGE */}
|
||||
<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">
|
||||
<motion.div
|
||||
animate={{ scale: isActive ? 1 : 1.04 }}
|
||||
transition={{ duration: 0.6, ease: EASE }}
|
||||
style={{ width: '100%', height: '100%', borderRadius: 20, overflow: 'hidden', boxShadow: '0 40px 100px -30px rgba(7,29,73,0.40)' }}
|
||||
>
|
||||
<img src={f.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: f.imgPos ?? 'center center', display: 'block' }} />
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
|
||||
<Rail active={active} total={FEATURES.length} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
|
||||
const ISSUES = {
|
||||
fa: [
|
||||
{ num: '۱۲', title: 'فولاد و تحریمهای جدید: راهکارهای صادراتی ۱۴۰۴', date: 'بهمن ۱۴۰۳' },
|
||||
{ num: '۱۱', title: 'قیمتگذاری فولاد در بازار داخلی: چالشها و فرصتها', date: 'دی ۱۴۰۳' },
|
||||
{ num: '۱۰', title: 'آمار تولید فولاد ایران در ۹ ماهه ۱۴۰۳', date: 'آذر ۱۴۰۳' },
|
||||
],
|
||||
en: [
|
||||
{ num: '12', title: 'Steel & New Sanctions: Export Strategies for 1404', date: 'Feb 2025' },
|
||||
{ num: '11', title: 'Steel Pricing in the Domestic Market: Challenges & Opportunities', date: 'Jan 2025' },
|
||||
{ num: '10', title: "Iran Steel Production Stats — First 9 Months of 1403", date: 'Dec 2024' },
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
const T = {
|
||||
fa: {
|
||||
overline: 'STEEL NEWSLETTER', heading: 'خبرنامه فولاد', archive: 'آرشیو کامل ←',
|
||||
issueLabel: 'خبرنامه ماهانه', placeholder: 'ایمیل خود را وارد کنید...',
|
||||
subscribe: 'عضویت در خبرنامه', done: '✓ ثبت شد',
|
||||
overlinePlants: 'MAJOR PLANTS', headingPlants: 'کارخانههای بزرگ فولاد ایران',
|
||||
allPlants: 'مشاهده همه ←', capacity: 'ظرفیت سالانه',
|
||||
},
|
||||
en: {
|
||||
overline: 'STEEL NEWSLETTER', heading: 'Steel Newsletter', archive: 'Full Archive →',
|
||||
issueLabel: 'Monthly Issue', placeholder: 'Enter your email...',
|
||||
subscribe: 'Subscribe', done: '✓ Subscribed',
|
||||
overlinePlants: 'MAJOR PLANTS', headingPlants: "Iran's Major Steel Plants",
|
||||
allPlants: 'View All →', capacity: 'Annual capacity',
|
||||
},
|
||||
}
|
||||
|
||||
export default function SteelNewsletterSection() {
|
||||
const { lang } = useLang()
|
||||
const t = T[lang]
|
||||
const [email, setEmail] = useState('')
|
||||
const [done, setDone] = useState(false)
|
||||
|
||||
return (
|
||||
<section dir={lang === 'fa' ? 'rtl' : 'ltr'} style={{ background: 'var(--paper-2)' }}>
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 56, paddingBottom: 64 }}>
|
||||
|
||||
{/* Newsletter issues */}
|
||||
<div style={{ marginBottom: 64 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 24, flexWrap: 'wrap', gap: 12 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)', marginBottom: 6 }}>{t.overline}</div>
|
||||
<h2 style={{ fontSize: 'clamp(20px,2.5vw,28px)', fontWeight: 900, color: 'var(--ink)', letterSpacing: '-0.5px' }}>{t.heading}</h2>
|
||||
</div>
|
||||
<a href="#" style={{ fontSize: 12, fontWeight: 700, color: 'var(--red)', textDecoration: 'none' }}>{t.archive}</a>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px,1fr))', gap: 1, background: 'var(--rule-thin)', marginBottom: 24 }}>
|
||||
{ISSUES[lang].map((issue, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.35, delay: i * 0.07 }}
|
||||
style={{ background: 'var(--paper)', padding: '22px 20px', cursor: 'pointer', display: 'flex', gap: 16, alignItems: 'flex-start' }}
|
||||
>
|
||||
<div style={{ fontSize: 36, fontWeight: 900, color: 'var(--red)', letterSpacing: '-2px', lineHeight: 1, flexShrink: 0, direction: 'ltr', fontVariantNumeric: 'tabular-nums' }}>
|
||||
{issue.num}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 9, fontWeight: 700, color: 'var(--ink-4)', letterSpacing: '1.5px', marginBottom: 6, textTransform: 'uppercase' }}>{t.issueLabel}</div>
|
||||
<h3 style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.5, marginBottom: 8 }}>{issue.title}</h3>
|
||||
<div style={{ fontSize: 10, color: 'var(--ink-5)' }}>{issue.date}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Subscribe bar */}
|
||||
<div style={{ display: 'flex', maxWidth: 480, border: '1px solid var(--ink)' }}>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
placeholder={t.placeholder}
|
||||
disabled={done}
|
||||
style={{ flex: 1, border: 'none', padding: '11px 14px', fontSize: 13, fontFamily: 'Vazir, sans-serif', background: 'var(--paper)', color: 'var(--ink)', outline: 'none', direction: lang === 'fa' ? 'rtl' : 'ltr' }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => { if (email) setDone(true) }}
|
||||
disabled={done}
|
||||
style={{ background: done ? 'var(--ink)' : 'var(--red)', color: 'var(--paper)', border: 'none', padding: '11px 20px', fontSize: 12, fontWeight: 700, fontFamily: 'Vazir, sans-serif', cursor: done ? 'default' : 'pointer', whiteSpace: 'nowrap', flexShrink: 0 }}
|
||||
>
|
||||
{done ? t.done : t.subscribe}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
import { useState } from 'react'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
|
||||
const VIDEOS = [
|
||||
{
|
||||
id: 1,
|
||||
fa: { title: 'چشمانداز فولاد ایران در ۱۴۰۴', desc: 'بررسی روندهای تولید، صادرات و سیاستگذاری صنعت فولاد ایران در سال پیش رو' },
|
||||
en: { title: 'Iran Steel Outlook 1404', desc: 'Production trends, exports and policy analysis for Iran\'s steel industry' },
|
||||
ep: 'EP.12', duration: '۴۲ دقیقه', thumb: '/news/photo-1504307651254-35680f356dfd.jpg',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
fa: { title: 'تحلیل بازار جهانی فلزات پایه', desc: 'نوسانات قیمت مس، آلومینیوم و روی و تأثیر آن بر زنجیره فولاد' },
|
||||
en: { title: 'Global Base Metals Analysis', desc: 'Price volatility in copper, aluminum and zinc and its steel chain impact' },
|
||||
ep: 'EP.11', duration: '۳۸ دقیقه', thumb: '/news/photo-1558618666-fcd25c85cd64.jpg',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
fa: { title: 'ژئوپلیتیک و صادرات فولاد', desc: 'تأثیر تحولات منطقهای بر بازارهای صادراتی فولاد ایران و فرصتهای پیش رو' },
|
||||
en: { title: 'Geopolitics & Steel Exports', desc: 'How regional developments shape Iran\'s steel export markets' },
|
||||
ep: 'EP.10', duration: '۵۱ دقیقه', thumb: '/news/photo-1504328345606-18bbc8c9d7d1.jpg',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
fa: { title: 'سرمایهگذاری در صنعت فولاد', desc: 'فرصتها و چالشهای سرمایهگذاری در بخشهای مختلف زنجیره ارزش فولاد' },
|
||||
en: { title: 'Steel Industry Investment', desc: 'Opportunities and challenges across the steel value chain' },
|
||||
ep: 'EP.09', duration: '۴۵ دقیقه', thumb: '/news/photo-1611273426858-450d8e3c9fce.jpg',
|
||||
},
|
||||
]
|
||||
|
||||
const PODCASTS = [
|
||||
{
|
||||
id: 1,
|
||||
fa: { title: 'نبض صنعت — فصل سوم', ep: 'قسمت ۱۲', desc: 'گفتگو با مدیران ارشد صنعت فولاد درباره چالشهای تامین مالی و توسعه ظرفیت' },
|
||||
en: { title: 'Industry Pulse — Season 3', ep: 'Episode 12', desc: 'Steel executives on financing challenges and capacity expansion' },
|
||||
duration: '۵۸ دقیقه', cover: '/news/photo-1540575467063-178a50c2df87.jpg',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
fa: { title: 'صدای بازار', ep: 'قسمت ۸', desc: 'تحلیل هفتگی قیمتهای شمش، میلگرد و مقاطع با کارشناسان بازار' },
|
||||
en: { title: 'Market Voice', ep: 'Episode 8', desc: 'Weekly analysis of billet, rebar and section prices with market experts' },
|
||||
duration: '۳۲ دقیقه', cover: '/news/photo-1566873535350-6586b0b7a1f2.jpg',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
fa: { title: 'آینده سبز فولاد', ep: 'قسمت ۵', desc: 'مسیر صنعت فولاد ایران به سمت کاهش کربن و پایداری' },
|
||||
en: { title: 'Green Steel Future', ep: 'Episode 5', desc: 'Iran\'s steel sector path toward carbon reduction and sustainability' },
|
||||
duration: '۴۴ دقیقه', cover: '/news/photo-1578662996442-48f60103fc96.jpg',
|
||||
},
|
||||
]
|
||||
|
||||
const T = {
|
||||
fa: {
|
||||
videoLabel: 'ویدیوکست', videoSub: 'آخرین گفتگوهای تصویری اندیشکده',
|
||||
podLabel: 'پادکست', podSub: 'شنیدنیهای اندیشکده فولاد آینده',
|
||||
watchAll: 'همه ویدیوها ←', listenAll: 'همه قسمتها ←',
|
||||
},
|
||||
en: {
|
||||
videoLabel: 'Videocast', videoSub: 'Latest video conversations from the Institute',
|
||||
podLabel: 'Podcast', podSub: 'Audio episodes from Future Steel Institute',
|
||||
watchAll: 'All videos →', listenAll: 'All episodes →',
|
||||
},
|
||||
}
|
||||
|
||||
export default function VideocastPodcastSection() {
|
||||
const { lang } = useLang()
|
||||
const t = T[lang]
|
||||
const [activeVideo, setActiveVideo] = useState(0)
|
||||
const [activePod, setActivePod] = useState(0)
|
||||
const pod = PODCASTS[activePod]
|
||||
|
||||
return (
|
||||
<section style={{ direction: lang === 'fa' ? 'rtl' : 'ltr', background: 'var(--paper)' }}>
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '60% 40%' }} className="max-lg:!grid-cols-1">
|
||||
|
||||
{/* ── VIDEOCAST 70% ── */}
|
||||
<div style={{ padding: '48px 0 80px 40px', display: 'flex', flexDirection: 'column' }} className="max-md:!p-5">
|
||||
|
||||
{/* heading */}
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', marginBottom: 28, flexWrap: 'wrap', gap: 12 }}>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6 }}>
|
||||
<div style={{ width: 28, height: 2, background: 'var(--red)' }} />
|
||||
<span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '3px', color: 'var(--ink-4)' }}>VIDEOCAST</span>
|
||||
</div>
|
||||
<h2 style={{ fontSize: 22, fontWeight: 900, letterSpacing: '-0.5px', color: 'var(--ink)', margin: 0 }}>{t.videoLabel}</h2>
|
||||
<p style={{ fontSize: 12, color: 'var(--ink-4)', margin: '4px 0 0' }}>{t.videoSub}</p>
|
||||
</div>
|
||||
<button style={{ fontSize: 11, fontWeight: 700, color: 'var(--ink)', background: 'none', border: '1px solid var(--ink)', padding: '6px 16px', cursor: 'pointer', fontFamily: 'inherit' }}>
|
||||
{t.watchAll}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* featured */}
|
||||
<div style={{ marginBottom: 16, position: 'relative', overflow: 'hidden', background: '#000', flex: 1, minHeight: 200 }}>
|
||||
<img src={VIDEOS[activeVideo].thumb} alt="" style={{ width: '100%', height: '100%', minHeight: 220, objectFit: 'cover', display: 'block', opacity: 0.82, position: 'absolute', inset: 0 }} />
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(7,29,73,0.88) 0%, transparent 55%)' }} />
|
||||
{/* play */}
|
||||
<div style={{
|
||||
position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)',
|
||||
width: 54, height: 54, borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.12)', backdropFilter: 'blur(4px)',
|
||||
border: '2px solid rgba(255,255,255,0.45)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
|
||||
}}>
|
||||
<div style={{ width: 0, height: 0, borderTop: '10px solid transparent', borderBottom: '10px solid transparent', borderLeft: '18px solid #fff', marginLeft: 4 }} />
|
||||
</div>
|
||||
{/* meta */}
|
||||
<div style={{ position: 'absolute', bottom: 0, right: 0, left: 0, padding: '16px 20px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
||||
<span style={{ fontSize: 9, fontWeight: 700, background: 'var(--red)', color: '#fff', padding: '2px 8px', letterSpacing: '1px' }}>{VIDEOS[activeVideo].ep}</span>
|
||||
<span style={{ fontSize: 10, color: 'rgba(255,255,255,0.55)' }}>{VIDEOS[activeVideo].duration}</span>
|
||||
</div>
|
||||
<h3 style={{ fontSize: 17, fontWeight: 900, color: '#fff', margin: '0 0 5px', lineHeight: 1.3 }}>{VIDEOS[activeVideo][lang].title}</h3>
|
||||
<p style={{ fontSize: 11, color: 'rgba(255,255,255,0.6)', margin: 0, lineHeight: 1.65 }}>{VIDEOS[activeVideo][lang].desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* episode cards */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 12 }} className="max-sm:!grid-cols-2">
|
||||
{VIDEOS.map((v, i) => (
|
||||
<button key={v.id} onClick={() => setActiveVideo(i)} style={{
|
||||
cursor: 'pointer', padding: 0, background: 'none', textAlign: lang === 'fa' ? 'right' : 'left',
|
||||
border: i === activeVideo ? '2px solid var(--ink)' : '2px solid var(--rule-thin)',
|
||||
transition: 'border-color 150ms', display: 'flex', flexDirection: 'column',
|
||||
}}>
|
||||
<div style={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<img src={v.thumb} alt="" style={{ width: '100%', height: 80, objectFit: 'cover', display: 'block', opacity: i === activeVideo ? 1 : 0.6, transition: 'opacity 150ms' }} />
|
||||
<span style={{ position: 'absolute', top: 6, right: 6, fontSize: 9, fontWeight: 700, color: '#fff', background: i === activeVideo ? 'var(--red)' : 'rgba(0,0,0,0.65)', padding: '2px 6px', letterSpacing: '0.5px' }}>{v.ep}</span>
|
||||
</div>
|
||||
<div style={{ padding: '8px 10px 10px' }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.35, marginBottom: 4, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
||||
{v[lang].title}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--ink-5)' }}>{v.duration}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── PODCAST 30% ── */}
|
||||
<div style={{ padding: '48px 40px 80px 0', background: 'var(--paper)', display: 'flex', flexDirection: 'column', borderRight: 'none' }} className="max-md:!p-5 max-lg:!border-r-0 max-lg:!border-t-2 max-lg:!border-t-[var(--ink)] max-lg:!pt-10">
|
||||
|
||||
{/* heading */}
|
||||
<div style={{ marginBottom: 28 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6 }}>
|
||||
<div style={{ width: 28, height: 2, background: 'var(--red)' }} />
|
||||
<span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '3px', color: 'var(--ink-4)' }}>PODCAST</span>
|
||||
</div>
|
||||
<h2 style={{ fontSize: 22, fontWeight: 900, letterSpacing: '-0.5px', color: 'var(--ink)', margin: 0 }}>{t.podLabel}</h2>
|
||||
<p style={{ fontSize: 12, color: 'var(--ink-4)', margin: '4px 0 0' }}>{t.podSub}</p>
|
||||
</div>
|
||||
|
||||
{/* circular cover + info */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: 24 }}>
|
||||
<div style={{ position: 'relative', marginBottom: 20 }}>
|
||||
{/* spinning gradient ring */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: -4,
|
||||
borderRadius: '50%',
|
||||
background: 'conic-gradient(from 0deg, #07468a, #0ea5e9, #14b8a6, #9b1c1c, #07468a)',
|
||||
animation: 'spinRing 3s linear infinite',
|
||||
zIndex: 0,
|
||||
}} />
|
||||
{/* white gap ring */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: -1,
|
||||
borderRadius: '50%',
|
||||
background: 'var(--paper)',
|
||||
zIndex: 1,
|
||||
}} />
|
||||
<img src={pod.cover} alt="" style={{ position: 'relative', width: 180, height: 180, borderRadius: '50%', objectFit: 'cover', display: 'block', zIndex: 2 }} />
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '2px', color: 'var(--red)', marginBottom: 6 }}>{pod[lang].ep}</div>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 900, color: 'var(--ink)', margin: '0 0 8px', lineHeight: 1.3 }}>{pod[lang].title}</h3>
|
||||
<p style={{ fontSize: 11, color: 'var(--ink-3)', margin: '0 0 12px', lineHeight: 1.7 }}>{pod[lang].desc}</p>
|
||||
<span style={{ fontSize: 10, color: 'var(--ink-5)' }}>{pod.duration}</span>
|
||||
</div>
|
||||
|
||||
{/* progress */}
|
||||
<div style={{ width: '100%', marginTop: 16 }}>
|
||||
<div style={{ height: 3, background: 'var(--rule-thin)', borderRadius: 2, marginBottom: 6 }}>
|
||||
<div style={{ width: '35%', height: '100%', background: 'var(--ink)', borderRadius: 2 }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 9, color: 'var(--ink-5)' }}>
|
||||
<span>۲۰:۱۲</span><span>{pod.duration}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* episode list */}
|
||||
<div style={{ borderTop: '1px solid var(--rule-thin)', paddingTop: 12, flex: 1 }}>
|
||||
{PODCASTS.map((p, i) => (
|
||||
<button key={p.id} onClick={() => setActivePod(i)} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10, width: '100%',
|
||||
padding: '12px 0', background: 'none', border: 'none', cursor: 'pointer',
|
||||
borderBottom: i < PODCASTS.length - 1 ? '1px solid var(--rule-thin)' : 'none',
|
||||
textAlign: lang === 'fa' ? 'right' : 'left',
|
||||
}}>
|
||||
<img src={p.cover} alt="" style={{ width: 52, height: 52, borderRadius: '50%', objectFit: 'cover', flexShrink: 0, opacity: i === activePod ? 1 : 0.55, border: i === activePod ? '2px solid var(--ink)' : '2px solid transparent' }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--ink)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p[lang].title}</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--ink-4)' }}>{p[lang].ep} · {p.duration}</div>
|
||||
</div>
|
||||
{i === activePod && <div style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--red)', flexShrink: 0 }} />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button style={{ marginTop: 20, fontSize: 11, fontWeight: 700, color: 'var(--ink)', background: 'none', border: '1px solid var(--ink)', padding: '8px 16px', cursor: 'pointer', fontFamily: 'inherit', width: '100%' }}>
|
||||
{t.listenAll}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<style>{`
|
||||
@keyframes spinRing {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -349,10 +349,6 @@ const PRODUCT_CATEGORIES = [
|
|||
},
|
||||
]
|
||||
|
||||
const CONTENT_SECTIONS = [
|
||||
'صنعت فولاد', 'بازار سرمایه', 'فلزات و کامودیتی',
|
||||
'ارزهای دیجیتال', 'بازار ارز و طلا', 'اقتصاد کلان',
|
||||
]
|
||||
const DATA_SOURCES = ['Platts', 'Fastmarkets', 'Bainfo China', 'Dlpibica']
|
||||
|
||||
function CategoryColumn({ cat }: { cat: typeof PRODUCT_CATEGORIES[0] }) {
|
||||
|
|
|
|||
|
|
@ -1,260 +1,207 @@
|
|||
import { motion } from 'framer-motion'
|
||||
import { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { BarChart3, Cpu, Mountain, Globe, Zap, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { useLang } from '@/context/LangContext'
|
||||
import { RADAR_ITEMS, type RadarItem, type RadarCategory } from '@/content/radar'
|
||||
|
||||
/* ─── Global signals (critical/high/medium) ─────────────── */
|
||||
const SIGNALS = {
|
||||
fa: [
|
||||
{ tag: 'چین', title: 'صادرات فولاد چین به ۱۱۲ میلیون تن رسید — رکورد ۸ ساله و سقوط قیمتهای جهانی', date: 'اسفند ۱۴۰۳', level: 'critical' },
|
||||
{ tag: 'اروپا', title: 'CBAM اروپا از فروردین ۱۴۰۴ اجرایی شد — مالیات کربن بر واردات فولاد', date: 'فروردین ۱۴۰۴', level: 'critical' },
|
||||
{ tag: 'هند', title: 'هند ظرفیت تولید فولاد را تا ۲۰۳۰ به ۳۰۰ میلیون تن میرساند', date: 'بهمن ۱۴۰۳', level: 'high' },
|
||||
{ tag: 'سنگآهن', title: 'قیمت سنگآهن دالیان ۱۸٪ جهش کرد — کاهش ذخایر استرالیا', date: 'دی ۱۴۰۳', level: 'high' },
|
||||
{ tag: 'انرژی', title: 'هزینه برق صنایع فولاد ایران ۳۲٪ افزایش یافت — بحران تابستانه شبکه', date: 'آذر ۱۴۰۳', level: 'high' },
|
||||
{ tag: 'فناوری', title: 'اولین کارخانه فولاد هیدروژنی اروپا در سوئد وارد مرحله تولید تجاری شد', date: 'آبان ۱۴۰۳', level: 'medium' },
|
||||
{ tag: 'روسیه', title: 'تغییر مسیر صادرات فولاد روسیه به آسیا — فشار قیمتی بر بازارهای هدف ایران', date: 'مهر ۱۴۰۳', level: 'high' },
|
||||
{ tag: 'AI', title: 'مایکروسافت و گوگل رویههای فولاد صنعتی را با مدلهای پیشبینی AI بهینه کردند', date: 'شهریور ۱۴۰۳', level: 'medium' },
|
||||
{ tag: 'خاورمیانه', title: 'Vision 2030 عربستان: ۱۴۰ میلیارد دلار پروژه زیرساختی تا ۲۰۳۰ — تقاضای بالقوه ۸ میلیون تن', date: 'مرداد ۱۴۰۳', level: 'medium' },
|
||||
{ tag: 'نیکل', title: 'فولاد ضدزنگ: کمبود نیکل اندونزی پس از محدودیت صادرات — رشد قیمت ۲۴٪', date: 'تیر ۱۴۰۳', level: 'high' },
|
||||
{ tag: 'آمریکا', title: 'IRA آمریکا: ۳ میلیارد دلار یارانه برای فولاد کمکربن — رقابتپذیری ایران زیر فشار', date: 'خرداد ۱۴۰۳', level: 'high' },
|
||||
{ tag: 'ترکیه', title: 'ترکیه بزرگترین تولیدکننده فولاد اروپاست — EAF ۷۳٪ ظرفیت', date: 'اردیبهشت ۱۴۰۳', level: 'medium' },
|
||||
{ tag: 'لیتیوم', title: 'تقاضای فولاد در زنجیره خودروهای برقی: ۱۸ میلیون تن اضافی تا ۲۰۳۵', date: 'فروردین ۱۴۰۳', level: 'medium' },
|
||||
{ tag: 'پاکستان', title: 'بحران ارزی پاکستان — توقف LC و اختلال در ۱.۲ میلیون تن صادرات ایران', date: 'اسفند ۱۴۰۲', level: 'critical' },
|
||||
],
|
||||
en: [
|
||||
{ tag: 'China', title: "China's steel exports reach 112Mt — 8-year record and global price collapse", date: 'Mar 2025', level: 'critical' },
|
||||
{ tag: 'Europe', title: 'EU CBAM takes effect Spring 2025 — carbon tax on steel imports', date: 'Apr 2025', level: 'critical' },
|
||||
{ tag: 'India', title: 'India to reach 300Mt steel capacity by 2030', date: 'Feb 2025', level: 'high' },
|
||||
{ tag: 'Iron Ore', title: 'Dalian iron ore price surges 18% — Australian reserves declining', date: 'Jan 2025', level: 'high' },
|
||||
{ tag: 'Energy', title: "Iran steel industry electricity costs up 32% — summer grid crisis", date: 'Dec 2024', level: 'high' },
|
||||
{ tag: 'Technology', title: "Europe's first hydrogen steel plant in Sweden enters commercial production", date: 'Nov 2024', level: 'medium' },
|
||||
{ tag: 'Russia', title: "Russia reroutes steel exports to Asia — price pressure on Iran's target markets", date: 'Oct 2024', level: 'high' },
|
||||
{ tag: 'AI', title: 'Microsoft and Google optimize industrial steel processes with AI prediction models', date: 'Sep 2024', level: 'medium' },
|
||||
{ tag: 'Middle East', title: 'Saudi Vision 2030: $140B in infrastructure — 8Mt potential steel demand', date: 'Aug 2024', level: 'medium' },
|
||||
{ tag: 'Nickel', title: 'Stainless steel: Indonesian nickel shortage after export ban — 24% price spike', date: 'Jul 2024', level: 'high' },
|
||||
{ tag: 'USA', title: 'US IRA: $3B subsidy for low-carbon steel — Iran competitiveness under pressure', date: 'Jun 2024', level: 'high' },
|
||||
{ tag: 'Turkey', title: "Turkey is Europe's largest steel producer — EAF at 73% of capacity", date: 'May 2024', level: 'medium' },
|
||||
{ tag: 'Lithium', title: 'Steel demand in EV supply chain: 18Mt additional by 2035', date: 'Apr 2024', level: 'medium' },
|
||||
{ tag: 'Pakistan', title: "Pakistan currency crisis — LC halt disrupts 1.2Mt of Iran's exports", date: 'Mar 2024', level: 'critical' },
|
||||
],
|
||||
}
|
||||
const NAVY = '#071d49'
|
||||
const GOLD = '#c9a227'
|
||||
const PER_PAGE = 8
|
||||
|
||||
/* ─── Weak signals / emerging trends ────────────────────── */
|
||||
const WEAK_SIGNALS = {
|
||||
fa: [
|
||||
{ tag: 'هوش مصنوعی', title: 'LLMهای تخصصی برای پیشبینی قیمت فولاد — دقت ۸۷٪ در افق ۳۰ روزه', date: 'بهمن ۱۴۰۳', level: 'medium' },
|
||||
{ tag: 'بلاکچین', title: 'پلتفرمهای ردیابی زنجیره تأمین فولاد با بلاکچین — آزمایش در ۵ شرکت بزرگ', date: 'دی ۱۴۰۳', level: 'medium' },
|
||||
{ tag: 'ژئوتکنیک', title: 'کاهش ذخایر ککپزی با کیفیت — بحران پنهان تأمین مواد اولیه تا ۲۰۳۵', date: 'آذر ۱۴۰۳', level: 'high' },
|
||||
{ tag: 'تغییر اقلیم', title: 'تندبادهای بیشتر در خلیج فارس — خطر بندری و هزینه حملونقل بالاتر', date: 'آبان ۱۴۰۳', level: 'medium' },
|
||||
{ tag: 'ساخت افزودنی', title: 'چاپ سهبعدی قطعات فولادی در هوافضا — جایگزینی نورد در برخی محصولات تا ۲۰۳۵', date: 'مهر ۱۴۰۳', level: 'medium' },
|
||||
{ tag: 'نانوفولاد', title: 'فولاد با ساختار نانو — مقاومت ۳ برابر بیشتر با همان وزن، تهدید برای فولاد معمول', date: 'شهریور ۱۴۰۳', level: 'medium' },
|
||||
{ tag: 'جمعیت', title: 'کاهش جمعیت چین — کاهش تقاضای داخلی فولاد و افزایش فشار صادراتی تا ۲۰۴۰', date: 'مرداد ۱۴۰۳', level: 'high' },
|
||||
{ tag: 'فضا', title: 'صنعت فضایی تجاری: آلیاژهای فولادی خاص با تقاضای ۵۰۰ هزار تن تا ۲۰۳۵', date: 'تیر ۱۴۰۳', level: 'medium' },
|
||||
],
|
||||
en: [
|
||||
{ tag: 'AI', title: 'Specialized LLMs for steel price forecasting — 87% accuracy on 30-day horizon', date: 'Feb 2025', level: 'medium' },
|
||||
{ tag: 'Blockchain', title: 'Steel supply chain tracking platforms on blockchain — pilots at 5 major companies', date: 'Jan 2025', level: 'medium' },
|
||||
{ tag: 'Geotechnics', title: 'Declining quality coking coal reserves — hidden raw material crisis by 2035', date: 'Dec 2024', level: 'high' },
|
||||
{ tag: 'Climate', title: 'More frequent Gulf storms — port risk and higher shipping costs', date: 'Nov 2024', level: 'medium' },
|
||||
{ tag: 'Additive Mfg', title: '3D printing of steel components in aerospace — replacing rolling for some products by 2035', date: 'Oct 2024', level: 'medium' },
|
||||
{ tag: 'Nanosteel', title: 'Nano-structured steel — 3× strength at same weight, threatening conventional steel', date: 'Sep 2024', level: 'medium' },
|
||||
{ tag: 'Demographics', title: "China's declining population — falling domestic steel demand and rising export pressure by 2040", date: 'Aug 2024', level: 'high' },
|
||||
{ tag: 'Space', title: 'Commercial space industry: specialty steel alloys with 500kt demand by 2035', date: 'Jul 2024', level: 'medium' },
|
||||
],
|
||||
}
|
||||
|
||||
/* ─── Foresight horizons ─────────────────────────────────── */
|
||||
const HORIZONS = {
|
||||
fa: [
|
||||
{ num: '۱۴۰۵', tag: 'کوتاهمدت', title: 'رکود صادراتی ناشی از سیل فولاد چین — فشار قیمتی ۱۵–۲۰٪', summary: 'با احتمال ۷۵٪: قیمتهای جهانی فولاد در محدوده ۴۵۰–۵۰۰ دلار باقی میمانند. ایران باید بر کاهش هزینه تولید و تمایز کیفی تمرکز کند.' },
|
||||
{ num: '۱۴۰۶', tag: 'میانمدت', title: 'اثر کامل CBAM — بازتوزیع جریانهای تجاری فولاد جهانی', summary: 'صادرکنندگان بدون برنامه کربنی از بازار اروپا خارج میشوند. فرصت برای ایران: تأمین بازار جنوب جهانی که از اروپا منحرف شدهاند.' },
|
||||
{ num: '۱۴۰۷', tag: 'میانمدت', title: 'هند به دومین مصرفکننده فولاد جهان میرسد — فرصت بازار ۱۵ میلیون تن', summary: 'رشد زیرساختی هند تقاضای ۸۵ میلیون تن فولاد اضافی ایجاد میکند. ایران میتواند ۱۵–۲۰ میلیون تن از این بازار را جذب کند.' },
|
||||
{ num: '۱۴۱۰', tag: 'بلندمدت', title: 'فولاد سبز به معیار پذیرش در بازارهای پریمیوم تبدیل میشود', summary: 'صادرکنندگانی که تا ۱۴۱۰ برنامه کربنی ندارند از ۶۰٪ بازارهای جهانی محروم میشوند. نقطه بازگشتناپذیر برای ایران: ۱۴۰۷.' },
|
||||
{ num: '۱۴۱۵', tag: 'بلندمدت', title: 'هیدروژن سبز ارزانتر از گاز طبیعی — تحول کامل DRI', summary: 'با احتمال ۵۵٪: هزینه هیدروژن سبز تا ۱۴۱۵ به زیر ۲ دلار/کیلوگرم میرسد. ایران با منابع خورشیدی میتواند مزیت رقابتی بزرگ در DRI-H₂ کسب کند.' },
|
||||
{ num: '۱۴۲۰', tag: 'افق دور', title: 'ظرفیت جهانی فولاد ۲۵٪ مازاد — موج ادغام و یکپارچهسازی', summary: 'کارخانههای غیررقابتی در آسیا و خاورمیانه بسته میشوند. برندگان: تولیدکنندگان با هزینه پایین، برنامه سبز و دسترسی به بازارهای رو به رشد.' },
|
||||
],
|
||||
en: [
|
||||
{ num: '2026', tag: 'Short-term', title: 'Export recession from Chinese steel flood — 15–20% price pressure', summary: '75% probability: global steel prices stay in $450–500 range. Iran must focus on production cost reduction and quality differentiation.' },
|
||||
{ num: '2027', tag: 'Medium-term', title: 'Full CBAM effect — redistribution of global steel trade flows', summary: 'Exporters without carbon plans are pushed out of Europe. Opportunity for Iran: supply the Global South markets diverted from Europe.' },
|
||||
{ num: '2028', tag: 'Medium-term', title: 'India becomes world\'s 2nd steel consumer — 15Mt market opportunity', summary: "India's infrastructure growth creates 85Mt of additional steel demand. Iran can capture 15–20Mt of this market." },
|
||||
{ num: '2031', tag: 'Long-term', title: 'Green steel becomes the admission standard for premium markets', summary: 'Exporters without a carbon roadmap by 2031 are locked out of 60% of global markets. Point of no return for Iran: 2028.' },
|
||||
{ num: '2036', tag: 'Long-term', title: 'Green hydrogen cheaper than natural gas — full DRI transformation', summary: '55% probability: green hydrogen cost falls below $2/kg by 2036. Iran\'s solar resources could give it a major competitive edge in DRI-H₂.' },
|
||||
{ num: '2041', tag: 'Far horizon', title: '25% global steel overcapacity — wave of mergers and consolidation', summary: 'Non-competitive plants in Asia and the Middle East close. Winners: low-cost producers with green plans and access to growing markets.' },
|
||||
],
|
||||
}
|
||||
|
||||
/* ─── KPIs ──────────────────────────────────────────────── */
|
||||
const KPIS = {
|
||||
fa: [
|
||||
{ value: '۱۱۲', unit: 'میلیون تن', label: 'صادرات فولاد چین ۱۴۰۳', delta: 'رکورد ۸ ساله' },
|
||||
{ value: '۴۸۵', unit: 'دلار/تن', label: 'قیمت جهانی HRC فعلی', delta: '-۱۲٪ از اوج ۱۴۰۳' },
|
||||
{ value: '۱۸۸۵', unit: 'میلیون تن', label: 'تولید جهانی فولاد ۱۴۰۲', delta: '+۱.۸٪ رشد سالانه' },
|
||||
{ value: '۵۵٪', unit: '', label: 'سهم چین از تولید جهانی', delta: 'پایدار' },
|
||||
{ value: '۸.۷', unit: 'میلیون تن', label: 'صادرات فولاد ایران ۱۴۰۳', delta: '+۱.۸٪ رشد' },
|
||||
],
|
||||
en: [
|
||||
{ value: '112', unit: 'Mt', label: 'China steel exports 1403', delta: '8-year record' },
|
||||
{ value: '$485', unit: '/t', label: 'Current global HRC price', delta: '-12% from 1403 peak' },
|
||||
{ value: '1885', unit: 'Mt', label: 'Global steel production 2023', delta: '+1.8% annual growth' },
|
||||
{ value: '55%', unit: '', label: "China's share of global output", delta: 'Stable' },
|
||||
{ value: '8.7', unit: 'Mt', label: 'Iran steel exports 1403', delta: '+1.8% growth' },
|
||||
],
|
||||
}
|
||||
|
||||
const LEVEL_COLOR: Record<string, string> = {
|
||||
critical: '#7f1d1d',
|
||||
high: 'var(--red)',
|
||||
medium: '#b45309',
|
||||
}
|
||||
|
||||
const LEVEL_LABEL = {
|
||||
fa: { critical: 'بحرانی', high: 'بالا', medium: 'متوسط' },
|
||||
en: { critical: 'Critical', high: 'High', medium: 'Medium' },
|
||||
const CATEGORY_ICON: Record<RadarCategory, typeof BarChart3> = {
|
||||
market: BarChart3,
|
||||
tech: Cpu,
|
||||
commodity: Mountain,
|
||||
geo: Globe,
|
||||
energy: Zap,
|
||||
}
|
||||
|
||||
const T = {
|
||||
fa: {
|
||||
overline: 'FORESIGHT & INTELLIGENCE',
|
||||
heading: 'آیندهپژوهی و رصد هوشمند',
|
||||
sub: 'رصد سیگنالهای قوی و ضعیف جهانی، تحلیل روندهای نوظهور و سناریوپردازی افقهای آینده صنعت فولاد',
|
||||
kpiHeading: 'شاخصهای کلیدی بازار جهانی فولاد',
|
||||
sigHeading: 'سیگنالهای قوی — رویدادهای جاری',
|
||||
weakHeading: 'سیگنالهای ضعیف — روندهای نوظهور',
|
||||
horizHeading: 'افقهای آیندهپژوهی — سناریوهای استراتژیک',
|
||||
},
|
||||
en: {
|
||||
overline: 'FORESIGHT & INTELLIGENCE',
|
||||
heading: 'Foresight & Intelligence',
|
||||
sub: 'Monitoring strong and weak signals, emerging trend analysis, and scenario planning for the steel industry',
|
||||
kpiHeading: 'Key Global Steel Market Indicators',
|
||||
sigHeading: 'Strong Signals — Current Events',
|
||||
weakHeading: 'Weak Signals — Emerging Trends',
|
||||
horizHeading: 'Foresight Horizons — Strategic Scenarios',
|
||||
},
|
||||
fa: { overline: 'FORESIGHT & INTELLIGENCE', heading: 'رادار آینده', sub: 'رصد سیگنالهای راهبردی و روندهای نوظهور صنعت فولاد جهان', readMore: 'ادامه مطلب' },
|
||||
en: { overline: 'FORESIGHT & INTELLIGENCE', heading: 'Future Radar', sub: 'Monitoring strategic signals and emerging trends in the global steel industry', readMore: 'Read more' },
|
||||
}
|
||||
|
||||
/* ─── one card: gold bar (default) → navy author panel (hover) ─── */
|
||||
export function RadarCard({ item, lang }: { item: RadarItem; lang: 'fa' | 'en' }) {
|
||||
const [hover, setHover] = useState(false)
|
||||
const t = item[lang]
|
||||
const Icon = CATEGORY_ICON[item.category]
|
||||
|
||||
return (
|
||||
<motion.article
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
position: 'relative', borderRadius: 16, overflow: 'hidden', cursor: 'pointer',
|
||||
aspectRatio: '3 / 3.5', background: NAVY,
|
||||
boxShadow: hover ? '0 30px 70px -30px rgba(7,29,73,0.45)' : '0 16px 40px -24px rgba(7,29,73,0.30)',
|
||||
}}
|
||||
animate={{ y: hover ? -4 : 0 }}
|
||||
transition={{ duration: 0.35, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
{/* background image */}
|
||||
<motion.img
|
||||
src={item.img}
|
||||
alt=""
|
||||
animate={{ scale: hover ? 1.06 : 1 }}
|
||||
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
|
||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
||||
/>
|
||||
{/* hover dim so the navy panel reads */}
|
||||
<motion.div
|
||||
animate={{ opacity: hover ? 0.55 : 0 }}
|
||||
transition={{ duration: 0.35 }}
|
||||
style={{ position: 'absolute', inset: 0, background: NAVY }}
|
||||
/>
|
||||
|
||||
{/* ── DEFAULT: gold bar at the bottom ── */}
|
||||
<AnimatePresence>
|
||||
{!hover && (
|
||||
<motion.div
|
||||
initial={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 16, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
||||
style={{
|
||||
position: 'absolute', left: 12, right: 12, bottom: 12,
|
||||
background: GOLD, borderRadius: 12, padding: '14px 16px',
|
||||
display: 'flex', alignItems: 'center', gap: 12, direction: lang === 'fa' ? 'rtl' : 'ltr',
|
||||
}}
|
||||
>
|
||||
{/* icon on the leading (right in RTL) side of the bar */}
|
||||
<div style={{ flexShrink: 0, width: 40, height: 40, borderRadius: '50%', background: 'rgba(7,29,73,0.10)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Icon size={20} color={NAVY} strokeWidth={2} />
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 800, color: NAVY, lineHeight: 1.4, marginBottom: 4, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{t.title}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: 'rgba(7,29,73,0.65)' }}>{t.date}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* ── HOVER: navy author panel slides up ── */}
|
||||
<AnimatePresence>
|
||||
{hover && (
|
||||
<motion.div
|
||||
initial={{ y: 24, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 24, opacity: 0 }}
|
||||
transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }}
|
||||
style={{
|
||||
position: 'absolute', left: 12, right: 12, bottom: 12,
|
||||
background: 'rgba(7,29,73,0.92)', backdropFilter: 'blur(6px)',
|
||||
borderRadius: 12, padding: '16px 18px',
|
||||
direction: lang === 'fa' ? 'rtl' : 'ltr',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||
{/* author avatar replaces the category icon */}
|
||||
<motion.img
|
||||
src={item.author.avatar}
|
||||
alt={item.author.name}
|
||||
initial={{ scale: 0.6, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1], delay: 0.08 }}
|
||||
style={{ width: 40, height: 40, borderRadius: '50%', objectFit: 'cover', border: `2px solid ${GOLD}`, flexShrink: 0 }}
|
||||
/>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 800, color: '#fff', lineHeight: 1.4, marginBottom: 2 }}>{t.title}</div>
|
||||
<div style={{ fontSize: 10, color: GOLD, fontWeight: 600 }}>{item.author.name} · {t.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style={{ fontSize: 12, lineHeight: 1.8, color: 'rgba(255,255,255,0.78)', margin: 0 }}>
|
||||
{t.excerpt}
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.article>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── pagination ─── */
|
||||
function Pagination({ page, pages, onGo }: { page: number; pages: number; onGo: (p: number) => void }) {
|
||||
if (pages <= 1) return null
|
||||
// show: first, current-ish, last, with ellipsis
|
||||
const nums: (number | '…')[] = []
|
||||
for (let i = 1; i <= pages; i++) {
|
||||
if (i === 1 || i === pages || Math.abs(i - page) <= 1) nums.push(i)
|
||||
else if (nums[nums.length - 1] !== '…') nums.push('…')
|
||||
}
|
||||
|
||||
const Btn = ({ children, active, onClick, disabled }: { children: React.ReactNode; active?: boolean; onClick?: () => void; disabled?: boolean }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
minWidth: 38, height: 38, borderRadius: 10, border: 'none', padding: '0 10px',
|
||||
fontFamily: 'inherit', fontSize: 14, fontWeight: 700, cursor: disabled ? 'default' : 'pointer',
|
||||
background: active ? NAVY : '#eee9e3', color: active ? '#fff' : NAVY,
|
||||
opacity: disabled ? 0.4 : 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
transition: 'background 160ms, color 160ms',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, marginTop: 44, direction: 'ltr' }}>
|
||||
<Btn onClick={() => onGo(page - 1)} disabled={page === 1}><ChevronLeft size={18} /></Btn>
|
||||
{nums.map((n, i) =>
|
||||
n === '…'
|
||||
? <span key={`e${i}`} style={{ color: NAVY, opacity: 0.5, padding: '0 4px' }}>…</span>
|
||||
: <Btn key={n} active={n === page} onClick={() => onGo(n)}>{n}</Btn>
|
||||
)}
|
||||
<Btn onClick={() => onGo(page + 1)} disabled={page === pages}><ChevronRight size={18} /></Btn>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Radar() {
|
||||
const { lang } = useLang()
|
||||
const t = T[lang]
|
||||
const signals = SIGNALS[lang]
|
||||
const weakSignals = WEAK_SIGNALS[lang]
|
||||
const horizons = HORIZONS[lang]
|
||||
const kpis = KPIS[lang]
|
||||
const levelLabel = LEVEL_LABEL[lang]
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const pages = Math.ceil(RADAR_ITEMS.length / PER_PAGE)
|
||||
const start = (page - 1) * PER_PAGE
|
||||
const visible = RADAR_ITEMS.slice(start, start + PER_PAGE)
|
||||
|
||||
const go = (p: number) => {
|
||||
if (p < 1 || p > pages) return
|
||||
setPage(p)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div dir={lang === 'fa' ? 'rtl' : 'ltr'} style={{ background: 'var(--paper)', minHeight: '80vh' }}>
|
||||
<div dir={lang === 'fa' ? 'rtl' : 'ltr'} style={{ background: '#faf7f4', minHeight: '80vh' }}>
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 56, paddingBottom: 72 }}>
|
||||
|
||||
{/* ── Hero ── */}
|
||||
<div style={{ borderBottom: '3px solid var(--ink)' }}>
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 48, paddingBottom: 40 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
|
||||
<div style={{ width: 36, height: 2, background: 'var(--red)' }} />
|
||||
<span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>{t.overline}</span>
|
||||
</div>
|
||||
<h1 style={{ fontSize: 'clamp(28px,4vw,52px)', fontWeight: 900, letterSpacing: '-1.5px', color: 'var(--ink)', marginBottom: 12 }}>{t.heading}</h1>
|
||||
<p style={{ fontSize: 15, color: 'var(--ink-3)', lineHeight: 1.7, maxWidth: 560 }}>{t.sub}</p>
|
||||
{/* heading with gold rule on the leading side */}
|
||||
<div style={{ display: 'flex', alignItems: 'stretch', justifyContent: 'flex-start', marginBottom: 40 }}>
|
||||
<div style={{
|
||||
borderRight: lang === 'fa' ? `4px solid ${GOLD}` : undefined,
|
||||
borderLeft: lang === 'fa' ? undefined : `4px solid ${GOLD}`,
|
||||
paddingRight: lang === 'fa' ? 18 : 0,
|
||||
paddingLeft: lang === 'fa' ? 0 : 18,
|
||||
}}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', color: GOLD, marginBottom: 8 }}>{t.overline}</div>
|
||||
<h1 style={{ fontSize: 'clamp(26px, 2.8vw, 40px)', fontWeight: 900, color: NAVY, letterSpacing: '-0.5px', margin: 0 }}>{t.heading}</h1>
|
||||
<p style={{ fontSize: 'clamp(13px, 1.1vw, 15px)', color: 'rgba(7,29,73,0.6)', margin: '10px 0 0', maxWidth: 560, lineHeight: 1.8 }}>{t.sub}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── KPI Strip ── */}
|
||||
<div style={{ borderBottom: '1px solid var(--rule-thin)', background: 'var(--paper-2)' }}>
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 36, paddingBottom: 36 }}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '2px', color: 'var(--ink-4)', marginBottom: 20, textTransform: 'uppercase' }}>{t.kpiHeading}</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 24 }}>
|
||||
{kpis.map((k, i) => (
|
||||
<motion.div key={i} initial={{ opacity: 0, y: 8 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: i * 0.07 }}>
|
||||
<div style={{ fontSize: 'clamp(26px,3vw,38px)', fontWeight: 900, letterSpacing: '-1px', color: 'var(--ink)', lineHeight: 1 }}>
|
||||
{k.value}<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink-3)', marginRight: 4, marginLeft: 4 }}>{k.unit}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 6, lineHeight: 1.5 }}>{k.label}</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--red)', fontWeight: 700, marginTop: 4 }}>{k.delta}</div>
|
||||
</motion.div>
|
||||
{/* card grid */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 22 }} className="max-lg:!grid-cols-2 max-sm:!grid-cols-1">
|
||||
{visible.map((item) => (
|
||||
<RadarCard key={item.id} item={item} lang={lang} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Strong Signals ── */}
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 48, paddingBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 28 }}>
|
||||
<div style={{ width: 36, height: 2, background: 'var(--red)' }} />
|
||||
<span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '2px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>{t.sigHeading}</span>
|
||||
<Pagination page={page} pages={pages} onGo={go} />
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 1, background: 'var(--rule-thin)' }}>
|
||||
{signals.map((s, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: i * 0.04 }}
|
||||
style={{ background: 'var(--paper)', padding: '28px 24px', position: 'relative' }}
|
||||
>
|
||||
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, height: 3, background: LEVEL_COLOR[s.level] }} />
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>
|
||||
<span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '1.5px', background: 'var(--ink)', color: 'var(--paper)', padding: '3px 8px' }}>{s.tag}</span>
|
||||
<span style={{ fontSize: 9, fontWeight: 700, color: LEVEL_COLOR[s.level], letterSpacing: '1px' }}>{levelLabel[s.level as keyof typeof levelLabel]}</span>
|
||||
</div>
|
||||
<h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.55, marginBottom: 16 }}>{s.title}</h3>
|
||||
<div style={{ fontSize: 10, color: 'var(--ink-5)', fontWeight: 600 }}>{s.date}</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Weak Signals ── */}
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 48, paddingBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 28 }}>
|
||||
<div style={{ width: 36, height: 2, background: '#b45309' }} />
|
||||
<span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '2px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>{t.weakHeading}</span>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 1, background: 'var(--rule-thin)' }}>
|
||||
{weakSignals.map((s, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: i * 0.05 }}
|
||||
style={{ background: 'var(--paper)', padding: '28px 24px', position: 'relative' }}
|
||||
>
|
||||
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, height: 3, background: '#b45309' }} />
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>
|
||||
<span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '1.5px', background: '#b45309', color: '#fff', padding: '3px 8px' }}>{s.tag}</span>
|
||||
<span style={{ fontSize: 9, fontWeight: 700, color: '#b45309', letterSpacing: '1px' }}>EMERGING</span>
|
||||
</div>
|
||||
<h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.55, marginBottom: 16 }}>{s.title}</h3>
|
||||
<div style={{ fontSize: 10, color: 'var(--ink-5)', fontWeight: 600 }}>{s.date}</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Foresight Horizons ── */}
|
||||
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 48, paddingBottom: 64 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 28 }}>
|
||||
<div style={{ width: 36, height: 2, background: 'var(--ink)' }} />
|
||||
<span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '2px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>{t.horizHeading}</span>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 1, background: 'var(--rule-thin)' }}>
|
||||
{horizons.map((h, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: i * 0.06 }}
|
||||
style={{ background: 'var(--paper)', padding: '28px 24px', position: 'relative' }}
|
||||
>
|
||||
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, height: 3, background: 'var(--ink)' }} />
|
||||
<div style={{ fontSize: 48, fontWeight: 900, letterSpacing: '-3px', color: 'rgba(26,23,18,0.06)', lineHeight: 1, marginBottom: 10, direction: 'ltr' }}>{h.num}</div>
|
||||
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '1.5px', color: 'var(--red)', marginBottom: 10, textTransform: 'uppercase' }}>{h.tag}</div>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 800, color: 'var(--ink)', lineHeight: 1.45, marginBottom: 12 }}>{h.title}</h3>
|
||||
<p style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.7 }}>{h.summary}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import react from '@vitejs/plugin-react'
|
|||
import tailwindcss from '@tailwindcss/vite'
|
||||
import path from 'path'
|
||||
|
||||
// hosts allowed through Vite's host check (for tunnels: tunnelmole, localtunnel, cloudflare)
|
||||
const ALLOWED_TUNNEL_HOSTS = ['.tunnelmole.net', '.loca.lt', '.trycloudflare.com']
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
|
|
@ -10,4 +13,10 @@ export default defineConfig({
|
|||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
allowedHosts: ALLOWED_TUNNEL_HOSTS,
|
||||
},
|
||||
preview: {
|
||||
allowedHosts: ALLOWED_TUNNEL_HOSTS,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue