442 lines
19 KiB
TypeScript
442 lines
19 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react'
|
||
import type { CSSProperties } from 'react'
|
||
import { NavLink } from 'react-router-dom'
|
||
import { Search, Menu, X, ChevronDown, Radar, Leaf, Globe, Cpu, BarChart3, Video, Mic, CalendarDays, Users, Eye, Phone, type LucideIcon } from 'lucide-react'
|
||
import { motion, AnimatePresence } from 'framer-motion'
|
||
import { useLang } from '@/context/LangContext'
|
||
|
||
/* ─── 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
|
||
|
||
type Section = { fa: string; en: string; to: string; Icon: LucideIcon }
|
||
|
||
/* ─── dropdown sections under رادار آینده ─── */
|
||
const SECTIONS: Section[] = [
|
||
{ fa: 'آیندهپژوهی و رصد هوشمند', en: 'Foresight & Intelligence', to: '/radar/foresight', Icon: Radar },
|
||
{ fa: 'پایداری و فولاد سبز', en: 'Sustainability & Green Steel', to: '/radar/sustainability', Icon: Leaf },
|
||
{ fa: 'ژئوپلیتیک و اقتصاد جهانی', en: 'Geopolitics & Global Economy', to: '/radar/geopolitics', Icon: Globe },
|
||
{ fa: 'فناوری و نوآوری', en: 'Technology & Innovation', to: '/radar/technology', Icon: Cpu },
|
||
{ fa: 'بازار و زنجیره فولاد', en: 'Market & Steel Chain', to: '/radar/market', Icon: BarChart3 },
|
||
]
|
||
|
||
/* ─── dropdown sections under رسانه و رویداد ─── */
|
||
const MEDIA: Section[] = [
|
||
{ fa: 'ویدیوکست و پادکست', en: 'Videocast & Podcast', to: '#', Icon: Video },
|
||
{ fa: 'نشستهای تخصصی', en: 'Expert Sessions', to: '#', Icon: Mic },
|
||
{ fa: 'رویدادها', en: 'Events', to: '#', Icon: CalendarDays },
|
||
]
|
||
|
||
/* ─── dropdown sections under درباره ما ─── */
|
||
const ABOUT: Section[] = [
|
||
{ fa: 'تیم ما', en: 'Our Team', to: '/about#team', Icon: Users },
|
||
{ fa: 'چشمانداز ما', en: 'Our Vision', to: '/about#vision', Icon: Eye },
|
||
{ fa: 'تماس با ما', en: 'Contact Us', to: '/contact', Icon: Phone },
|
||
]
|
||
|
||
/* ─── top-level nav items ─── */
|
||
type TopNav = { fa: string; en: string; to?: string; dropdown?: 'radar' | 'media' | 'about' }
|
||
const TOP_NAV: TopNav[] = [
|
||
{ fa: 'رادار آینده', en: 'Radar', dropdown: 'radar' },
|
||
{ fa: 'آمار و داده', en: 'Stats & Data', to: '/pulse' },
|
||
{ fa: 'در یک نگاه', en: 'At a Glance', to: '/at-a-glance' },
|
||
{ fa: 'رسانه و رویداد', en: 'Media & Events', dropdown: 'media' },
|
||
{ fa: 'شبکه خبرگان', en: 'Expert Network', to: '/experts' },
|
||
{ fa: 'درباره ما', en: 'About', dropdown: 'about' },
|
||
]
|
||
|
||
const META_LEFT: { fa: string; en: string }[] = [
|
||
{ fa: 'درباره ما', en: 'About' },
|
||
{ fa: 'کارشناسان', en: 'Experts' },
|
||
{ fa: 'همکاری', en: 'Careers' },
|
||
{ fa: 'رسانه', en: 'Press' },
|
||
]
|
||
|
||
/* ─── generic nav dropdown card ─── */
|
||
function NavDropdown({ lang, label, sections }: { lang: 'fa' | 'en'; label: string; sections: Section[] }) {
|
||
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 (
|
||
<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: open ? `2px solid ${T.red}` : '2px solid transparent',
|
||
}}
|
||
>
|
||
{label}
|
||
<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={i}
|
||
to={item.to}
|
||
onClick={() => setOpen(false)}
|
||
style={(): CSSProperties => ({
|
||
display: 'flex', alignItems: 'center', gap: 12,
|
||
padding: '15px 20px', textDecoration: 'none',
|
||
color: '#3d3d3d',
|
||
background: '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' }}
|
||
>
|
||
<item.Icon size={20} color="#CD9E53" strokeWidth={1.5} style={{ flexShrink: 0 }} />
|
||
<span>{lang === 'fa' ? item.fa : item.en}</span>
|
||
</NavLink>
|
||
))}
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/* ─── 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}
|
||
</NavLink>
|
||
) : (
|
||
<a href={to} {...handlers} style={style}>
|
||
{label}
|
||
</a>
|
||
)
|
||
}
|
||
|
||
/* ─── drawer item ─── */
|
||
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 ─── */
|
||
export default function Header() {
|
||
const [mobileOpen, setMobileOpen] = useState(false)
|
||
const [isMobile, setIsMobile] = useState(false)
|
||
const [searchOpen, setSearchOpen] = useState(false)
|
||
|
||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||
const { lang, toggle: toggleLang } = useLang()
|
||
|
||
|
||
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)
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
document.body.style.overflow = mobileOpen ? 'hidden' : ''
|
||
return () => { document.body.style.overflow = '' }
|
||
}, [mobileOpen])
|
||
|
||
useEffect(() => {
|
||
if (searchOpen) searchInputRef.current?.focus()
|
||
}, [searchOpen])
|
||
|
||
return (
|
||
<header
|
||
style={{
|
||
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',
|
||
}}
|
||
>
|
||
<div>
|
||
<div
|
||
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 */}
|
||
<NavLink
|
||
to="/"
|
||
style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 12, textDecoration: 'none', flexShrink: 0, lineHeight: 1 }}
|
||
>
|
||
<img src="/Frame%202095585206%20(2).png" 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">
|
||
The Steel Futures Institute
|
||
</span>
|
||
</div>
|
||
</NavLink>
|
||
|
||
{/* Desktop Nav */}
|
||
{!isMobile && (
|
||
<nav aria-label="ناوبری اصلی"
|
||
style={{ display: 'flex', alignItems: 'center', gap: 20, flex: 1, justifyContent: 'center' }}
|
||
>
|
||
{TOP_NAV.map((item) =>
|
||
item.dropdown
|
||
? <NavDropdown key={item.fa} lang={lang} label={lang === 'fa' ? item.fa : item.en}
|
||
sections={item.dropdown === 'radar' ? SECTIONS : item.dropdown === 'about' ? ABOUT : MEDIA} />
|
||
: <PlainNavItem key={item.fa} label={lang === 'fa' ? item.fa : item.en} to={item.to!} />
|
||
)}
|
||
</nav>
|
||
)}
|
||
|
||
{/* Actions */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
|
||
<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>
|
||
|
||
{!isMobile && (
|
||
<>
|
||
<OutlineButton style={{ height: 34, padding: '0 16px', fontSize: 12 }}>
|
||
{lang === 'fa' ? 'ورود / ثبتنام' : 'Login / Register'}
|
||
</OutlineButton>
|
||
<button
|
||
onClick={toggleLang}
|
||
aria-label={lang === 'fa' ? 'Switch to English' : 'تغییر به فارسی'}
|
||
style={{
|
||
height: 34, width: 40, padding: 0, fontSize: 20, lineHeight: 1,
|
||
background: 'transparent', border: `1px solid ${T.ruleThin}`,
|
||
cursor: 'pointer', fontFamily: 'inherit',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
transition: 'border-color 120ms',
|
||
}}
|
||
onMouseEnter={e => { e.currentTarget.style.borderColor = T.ink }}
|
||
onMouseLeave={e => { e.currentTarget.style.borderColor = T.ruleThin }}
|
||
>
|
||
{/* show the flag of the language you'll switch TO */}
|
||
{lang === 'fa' ? '🇺🇸' : '🇮🇷'}
|
||
</button>
|
||
</>
|
||
)}
|
||
|
||
{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 */}
|
||
<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}`,
|
||
}}
|
||
>
|
||
<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 style={{ flex: 1, overflowY: 'auto' }}>
|
||
<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)} />
|
||
))}
|
||
</nav>
|
||
|
||
<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>
|
||
)
|
||
}
|