steelforesight/src/app/layout/Header.tsx

442 lines
19 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 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>
)
}