553 lines
20 KiB
TypeScript
553 lines
20 KiB
TypeScript
import { useState, useEffect, useRef } from 'react'
|
||
import type { CSSProperties } from 'react'
|
||
import { NavLink } from 'react-router-dom'
|
||
import { Search, Menu, X } from 'lucide-react'
|
||
import { motion, AnimatePresence } from 'framer-motion'
|
||
import { useLang } from '@/context/LangContext'
|
||
|
||
/* ─────────────────────────────────────────────────────────
|
||
DESIGN TOKENS
|
||
───────────────────────────────────────────────────────── */
|
||
const T = {
|
||
paper: 'var(--paper)',
|
||
paper2: 'var(--paper-2)',
|
||
ink: 'var(--ink)',
|
||
ink3: 'var(--ink-3)',
|
||
ink4: 'var(--ink-4)',
|
||
ink5: 'var(--ink-5)',
|
||
ruleThin: 'var(--rule-thin)',
|
||
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' },
|
||
]
|
||
|
||
const META_LEFT: { fa: string; en: string }[] = [
|
||
{ fa: 'درباره ما', en: 'About' },
|
||
{ fa: 'کارشناسان', en: 'Experts' },
|
||
{ fa: 'همکاری', en: 'Careers' },
|
||
{ fa: 'رسانه', en: 'Press' },
|
||
]
|
||
|
||
/* ─────────────────────────────────────────────────────────
|
||
NAV LINK (used in main + drawer)
|
||
───────────────────────────────────────────────────────── */
|
||
function MainNavItem({ to, label }: { to: string; label: string }) {
|
||
const [hovered, setHovered] = useState(false)
|
||
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',
|
||
transition: 'color 120ms',
|
||
borderBottom: isActive ? `2px solid ${T.red}` : '2px solid transparent',
|
||
letterSpacing: '0.1px',
|
||
})}
|
||
>
|
||
{label}
|
||
</NavLink>
|
||
)
|
||
}
|
||
|
||
function DrawerNavItem({
|
||
to,
|
||
label,
|
||
onClose,
|
||
}: {
|
||
to: string
|
||
label: string
|
||
onClose: () => void
|
||
}) {
|
||
const [hovered, setHovered] = useState(false)
|
||
return (
|
||
<NavLink
|
||
to={to}
|
||
onClick={onClose}
|
||
onMouseEnter={() => setHovered(true)}
|
||
onMouseLeave={() => setHovered(false)}
|
||
style={({ isActive }: { isActive: boolean }): CSSProperties => ({
|
||
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}`,
|
||
transition: 'background 120ms, color 120ms',
|
||
})}
|
||
>
|
||
{label}
|
||
</NavLink>
|
||
)
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────
|
||
MAIN COMPONENT
|
||
───────────────────────────────────────────────────────── */
|
||
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)
|
||
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])
|
||
|
||
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',
|
||
direction: lang === 'fa' ? 'rtl' : 'ltr',
|
||
transition: 'background 220ms ease, backdrop-filter 220ms ease, border-color 220ms ease',
|
||
}}
|
||
>
|
||
{/* ═══════════════════════════════════════════
|
||
MAIN BAR — brand | nav | actions
|
||
═══════════════════════════════════════════ */}
|
||
<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,
|
||
}}
|
||
className="max-w-7xl mx-auto px-12 max-md:px-5 max-md:h-[60px]"
|
||
>
|
||
{/* ── Brand ── */}
|
||
<NavLink
|
||
to="/"
|
||
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"
|
||
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>
|
||
<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) ── */}
|
||
{!isMobile && (
|
||
<nav
|
||
aria-label="ناوبری اصلی"
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 22,
|
||
flex: 1,
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
{NAV_ITEMS.map((item) => (
|
||
<MainNavItem key={item.to} to={item.to} label={lang === 'fa' ? item.fa : item.en} />
|
||
))}
|
||
</nav>
|
||
)}
|
||
|
||
{/* ── Left: 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}`,
|
||
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,
|
||
}}
|
||
>
|
||
<Search size={14} aria-hidden="true" />
|
||
</button>
|
||
|
||
{/* Login + lang toggle — desktop only */}
|
||
{!isMobile && (
|
||
<>
|
||
<OutlineButton style={{ height: 34, padding: '0 16px', fontSize: 12 }}>
|
||
{lang === 'fa' ? 'ورود / ثبتنام' : 'Login / Register'}
|
||
</OutlineButton>
|
||
<button
|
||
onClick={toggleLang}
|
||
style={{
|
||
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',
|
||
}}
|
||
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 }}
|
||
>
|
||
{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,
|
||
}}
|
||
>
|
||
<Menu size={18} />
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ═══════════════════════════════════════════
|
||
SEARCH DROPDOWN
|
||
═══════════════════════════════════════════ */}
|
||
<AnimatePresence>
|
||
{searchOpen && (
|
||
<motion.div
|
||
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"
|
||
>
|
||
<input
|
||
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',
|
||
}}
|
||
/>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* ═══════════════════════════════════════════
|
||
MOBILE DRAWER
|
||
═══════════════════════════════════════════ */}
|
||
<AnimatePresence>
|
||
{mobileOpen && (
|
||
<>
|
||
<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,
|
||
}}
|
||
/>
|
||
<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',
|
||
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',
|
||
}}
|
||
>
|
||
<X size={20} />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Nav */}
|
||
<nav style={{ flex: 1, overflowY: 'auto' }}>
|
||
{NAV_ITEMS.map((item) => (
|
||
<DrawerNavItem
|
||
key={item.to}
|
||
to={item.to}
|
||
label={lang === 'fa' ? item.fa : item.en}
|
||
onClose={() => setMobileOpen(false)}
|
||
/>
|
||
))}
|
||
</nav>
|
||
|
||
{/* Footer */}
|
||
<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' }}
|
||
>
|
||
{lang === 'fa' ? item.fa : item.en}
|
||
</a>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
</header>
|
||
)
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────
|
||
BUTTONS
|
||
───────────────────────────────────────────────────────── */
|
||
function RedCtaButton({
|
||
children,
|
||
style: extraStyle,
|
||
}: {
|
||
children: React.ReactNode
|
||
style?: CSSProperties
|
||
}) {
|
||
const [hovered, setHovered] = useState(false)
|
||
return (
|
||
<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,
|
||
}}
|
||
>
|
||
{children}
|
||
</button>
|
||
)
|
||
}
|
||
|
||
function OutlineButton({
|
||
children,
|
||
style: extraStyle,
|
||
}: {
|
||
children: React.ReactNode
|
||
style?: CSSProperties
|
||
}) {
|
||
const [hovered, setHovered] = useState(false)
|
||
return (
|
||
<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',
|
||
...extraStyle,
|
||
}}
|
||
>
|
||
{children}
|
||
</button>
|
||
)
|
||
}
|