steelforesight/src/app/layout/Header.tsx

553 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
}