Initial commit

This commit is contained in:
alireza 2026-05-25 16:07:58 +03:30
parent 4456563c0e
commit 2dd08650cf
69 changed files with 14647 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -72,3 +72,5 @@ export default defineConfig([
]) ])
``` ```
"# thinktank" "# thinktank"
"# thinktank" git init git add git commit -m "first commit" git branch -M main git remote add origin https://github.com/alirezaameria2-dev/thinktank.git git push -u origin main
"# thinktank" git init git add git commit -m "first commit" git branch -M main git remote add origin https://github.com/alirezaameria2-dev/thinktank.git git push -u origin main

22
eslint.config.js Normal file
View File

@ -0,0 +1,22 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
},
},
])

19
index.html Normal file
View File

@ -0,0 +1,19 @@
<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="اندیشکده فولاد ایران — مرکز مستقل تحلیل راهبردی و سیاست‌گذاری صنعت فولاد" />
<!-- Vazir font preloads (critical weights) -->
<link rel="preload" href="/fonts/Vazir-Regular.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/fonts/Vazir-Bold.woff2" as="font" type="font/woff2" crossorigin />
<title>اندیشکده فولاد ایران — تحلیل راهبردی صنعت فولاد</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3448
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "andishkade-foolad",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.100.14",
"apexcharts": "^5.13.0",
"clsx": "^2.1.1",
"dotted-map": "^3.1.0",
"framer-motion": "^12.40.0",
"jalaali-js": "^1.2.8",
"lucide-react": "^1.16.0",
"react": "^19.2.6",
"react-apexcharts": "^2.1.0",
"react-dom": "^19.2.6",
"react-router-dom": "^7.15.1",
"tailwind-merge": "^3.6.0",
"vazir-font": "^30.1.0",
"zustand": "^5.0.13"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.3.0",
"@types/jalaali-js": "^1.2.0",
"@types/node": "^24.12.4",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"tailwindcss": "^4.3.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12"
}
}

6
public/favicon.svg Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" fill="#1a1712"/>
<rect x="6" y="8" width="20" height="3" fill="#f4efe7"/>
<rect x="6" y="14.5" width="13" height="3" fill="#9b1c1c"/>
<rect x="6" y="21" width="20" height="3" fill="#f4efe7"/>
</svg>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

24
public/icons.svg Normal file
View File

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

4
src/App.tsx Normal file
View File

@ -0,0 +1,4 @@
// App.tsx is no longer the entry point.
// Routing is handled by src/app/router.tsx → src/main.tsx
// This file is kept as a placeholder.
export default function App() { return null }

196
src/app/layout/Footer.tsx Normal file
View File

@ -0,0 +1,196 @@
import { useState } from 'react'
function SocialIcon({ label }: { label: string }) {
const [hovered, setHovered] = useState(false)
return (
<div
role="button"
tabIndex={0}
aria-label={label}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
width: 28, height: 28,
border: `1px solid ${hovered ? 'rgba(255,255,255,0.4)' : 'rgba(255,255,255,0.12)'}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 10, fontWeight: 700,
color: hovered ? '#fff' : 'rgba(255,255,255,0.4)',
cursor: 'pointer',
transition: 'border-color 150ms, color 150ms',
flexShrink: 0,
}}
>
{label}
</div>
)
}
function FooterLink({ href, label }: { href: string; label: string }) {
const [hovered, setHovered] = useState(false)
return (
<li style={{ marginBottom: 10 }}>
<a
href={href}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
fontSize: 13,
color: hovered ? '#fff' : 'rgba(255,255,255,0.55)',
textDecoration: 'none',
cursor: 'pointer',
transition: 'color 150ms',
display: 'inline-block',
lineHeight: 1.4,
}}
>
{label}
</a>
</li>
)
}
const COLUMNS = [
{
heading: 'گزارش‌ها',
links: [
{ label: 'گزارش‌های فصلی', href: '#' },
{ label: 'Flash Reports', href: '#' },
{ label: 'گزارش‌های ریسک', href: '#' },
{ label: 'گزارش رایگان', href: '#' },
{ label: 'آرشیو کامل', href: '#' },
],
},
{
heading: 'اندیشکده',
links: [
{ label: 'درباره ما', href: '#' },
{ label: 'تیم تحریریه', href: '#' },
{ label: 'اشتراک سازمانی', href: '#' },
{ label: 'همکاری با ما', href: '#' },
{ label: 'تماس', href: '#' },
],
},
{
heading: 'منابع',
links: [
{ label: 'داشبورد بازار', href: '#' },
{ label: 'رویدادها', href: '#' },
{ label: 'اسکنر جهانی', href: '#' },
{ label: 'خبرنامه', href: '#' },
{ label: 'درگاه پرداخت', href: '#' },
],
},
]
export function Footer() {
return (
<footer
dir="rtl"
style={{ backgroundColor: 'var(--ink)', color: 'rgba(255,255,255,0.6)' }}
>
{/* TOP GRID */}
<div style={{ padding: '64px 48px' }}>
<div
style={{
display: 'grid',
gridTemplateColumns: '2fr 1px 1fr 1px 1fr 1px 1fr',
gap: 0,
}}
className="max-md:grid-cols-1 max-md:gap-10"
>
{/* Brand */}
<div style={{ paddingLeft: 0, paddingRight: 48 }} className="max-md:pr-0">
<div
style={{
fontSize: 22, fontWeight: 900, color: '#fff',
lineHeight: 1.2, marginBottom: 4,
}}
>
اندیشکده فولاد ایران
</div>
<div
style={{
fontSize: 10, letterSpacing: 3,
color: 'rgba(255,255,255,0.3)', textTransform: 'uppercase',
marginBottom: 20, direction: 'ltr', textAlign: 'right',
}}
>
IRAN STEEL POLICY INSTITUTE
</div>
<p style={{ fontSize: 13, lineHeight: 1.75, color: 'rgba(255,255,255,0.5)', marginBottom: 20, maxWidth: 300 }}>
مرکز مستقل تحلیل راهبردی و سیاستگذاری صنعت فولاد و فلزات کشور
</p>
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.4)', lineHeight: 2, marginBottom: 20 }}>
<div>تهران · خیابان ولیعصر</div>
<div>info@iransteel.ir</div>
<div dir="ltr" style={{ textAlign: 'right' }}>۰۲۱۸۸۱۲۳۴۵۶</div>
</div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
<SocialIcon label="in" />
<SocialIcon label="tg" />
<SocialIcon label="tw" />
<SocialIcon label="@" />
</div>
</div>
{/* Dividers + link columns */}
{COLUMNS.map((col, i) => (
<>
<div
key={`div-${i}`}
aria-hidden="true"
style={{ background: 'rgba(255,255,255,0.08)', width: 1 }}
className="max-md:hidden"
/>
<div key={col.heading} style={{ padding: '0 36px' }} className="max-md:p-0">
<h5
style={{
fontSize: 9, fontWeight: 700, letterSpacing: 3,
textTransform: 'uppercase', color: 'rgba(255,255,255,0.35)',
margin: '0 0 16px', paddingBottom: 10,
borderBottom: '1px solid rgba(255,255,255,0.1)',
}}
>
{col.heading}
</h5>
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
{col.links.map(link => (
<FooterLink key={link.label} href={link.href} label={link.label} />
))}
</ul>
</div>
</>
))}
</div>
</div>
{/* BOTTOM ROW */}
<div
style={{
padding: '20px 48px',
borderTop: '1px solid rgba(255,255,255,0.08)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
fontSize: 10,
color: 'rgba(255,255,255,0.25)',
flexWrap: 'wrap',
gap: 12,
}}
>
<span>© ۱۴۰۳ اندیشکده فولاد ایران تمام حقوق محفوظ است</span>
<div style={{ display: 'flex', gap: 18 }}>
{['حریم خصوصی', 'شرایط استفاده', 'درگاه پرداخت', 'نقشه سایت'].map(l => (
<a
key={l}
href="#"
style={{ color: 'rgba(255,255,255,0.25)', textDecoration: 'none' }}
>
{l}
</a>
))}
</div>
</div>
</footer>
)
}

815
src/app/layout/Header.tsx Normal file
View File

@ -0,0 +1,815 @@
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'
/*
DESIGN TOKENS (mirrors CSS variables for inline styles)
*/
const T = {
paper: 'var(--paper)',
paper2: 'var(--paper-2)',
ink: 'var(--ink)',
ink2: 'var(--ink-2)',
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 = [
{ label: 'گزارش‌ها', to: '/reports' },
{ label: 'گزارش ویژه', to: '/special' },
{ label: 'ریسک‌ها', to: '/risks' },
{ label: 'رویدادها', to: '/events' },
{ label: 'اسکنر جهانی', to: '/scanner' },
{ label: 'تیم تحریریه', to: '/team' },
{ label: 'داشبورد بازار', to: '/market' },
{ label: 'آرشیو', to: '/archive' },
]
const COMPACT_NAV = [
{ label: 'گزارش‌ها', to: '/reports' },
{ label: 'بازار', to: '/market' },
{ label: 'رویدادها', to: '/events' },
{ label: 'ریسک', to: '/risks' },
]
const META_LEFT = ['درباره ما', 'کارشناسان', 'همکاری', 'رسانه']
/*
HELPERS
*/
/** Social icon box */
function SocialIcon({ label, href }: { label: string; href: string }) {
const [hovered, setHovered] = useState(false)
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={label}
style={{
width: 28,
height: 28,
border: `1px solid ${T.ruleThin}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 10,
fontWeight: 700,
color: hovered ? T.paper : T.ink3,
background: hovered ? T.ink : 'transparent',
cursor: 'pointer',
textDecoration: 'none',
transition: 'background 120ms, color 120ms',
flexShrink: 0,
lineHeight: 1,
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{label}
</a>
)
}
/** Full nav link (expanded navbar) */
function FullNavItem({
to,
label,
isLast,
}: {
to: string
label: string
isLast: boolean
}) {
const [hovered, setHovered] = useState(false)
return (
<NavLink
to={to}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={({ isActive }: { isActive: boolean }): CSSProperties => ({
padding: '12px 20px',
fontSize: 12,
fontWeight: 600,
color: isActive || hovered ? T.paper : T.ink,
background: isActive ? T.red : hovered ? T.ink : 'transparent',
textDecoration: 'none',
borderRight: !isLast ? `1px solid ${T.ruleThin}` : 'none',
display: 'flex',
alignItems: 'center',
transition: 'background 150ms, color 150ms',
whiteSpace: 'nowrap',
letterSpacing: '0.2px',
})}
>
{label}
</NavLink>
)
}
/** Compact nav link */
function CompactNavItem({
to,
label,
isFirst,
}: {
to: string
label: string
isFirst: boolean
}) {
const [hovered, setHovered] = useState(false)
return (
<NavLink
to={to}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={({ isActive }: { isActive: boolean }): CSSProperties => ({
fontSize: 12,
fontWeight: 600,
color: isActive || hovered ? T.paper : T.ink,
background: isActive ? T.ink : hovered ? T.paper2 : 'transparent',
textDecoration: 'none',
padding: '0 14px',
height: 56,
display: 'flex',
alignItems: 'center',
borderRight: `1px solid ${T.ruleThin}`,
borderLeft: isFirst ? `1px solid ${T.ruleThin}` : 'none',
whiteSpace: 'nowrap',
transition: 'background 120ms, color 120ms',
})}
>
{label}
</NavLink>
)
}
/** Drawer nav link */
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 [compact, setCompact] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false)
const [isMobile, setIsMobile] = useState(false)
const searchRef = useRef<HTMLInputElement>(null)
/* scroll listener */
useEffect(() => {
const onScroll = () => setCompact(window.scrollY > 80)
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
/* viewport listener */
useEffect(() => {
const mq = window.matchMedia('(max-width: 767px)')
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])
const showCompact = compact || isMobile
return (
<header
style={{
position: 'sticky',
top: 0,
zIndex: 50,
background: T.paper,
direction: 'rtl',
}}
>
{/*
EXPANDED STATE
*/}
<AnimatePresence initial={false}>
{!showCompact && (
<motion.div
key="expanded"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.22, ease: 'easeInOut' }}
style={{ overflow: 'hidden' }}
>
{/* ── TOP META BAR ─────────────────────────── */}
<div
style={{
borderBottom: `1px solid ${T.ruleThin}`,
padding: '8px 48px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 16,
}}
>
{/* Right: login / language */}
<ul
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
listStyle: 'none',
}}
>
<li>
<a
href="/login"
style={{
fontSize: 11,
fontWeight: 500,
color: T.ink3,
textDecoration: 'none',
}}
>
ورود
</a>
</li>
<li style={{ fontSize: 11, color: T.ruleThin, userSelect: 'none' }}>|</li>
<li>
<a
href="#"
style={{ fontSize: 11, color: T.ink4, textDecoration: 'none' }}
>
فارسی
</a>
</li>
</ul>
{/* Center: date */}
<span
style={{
fontSize: 11,
color: T.ink4,
fontWeight: 400,
letterSpacing: '0.3px',
whiteSpace: 'nowrap',
}}
>
شنبه ۲۱ بهمن ۱۴۰۳ · دوره دوازدهم
</span>
{/* Left: secondary nav */}
<ul
style={{
display: 'flex',
alignItems: 'center',
gap: 20,
listStyle: 'none',
}}
>
{META_LEFT.map((item) => (
<li key={item}>
<a
href="#"
style={{ fontSize: 11, color: T.ink4, textDecoration: 'none' }}
>
{item}
</a>
</li>
))}
</ul>
</div>
{/* ── MASTHEAD MAIN ────────────────────────── */}
<div
style={{
padding: '28px 48px',
display: 'grid',
gridTemplateColumns: '1fr auto 1fr',
alignItems: 'center',
gap: 24,
}}
>
{/* Left col */}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 10,
}}
>
<div>
<p
style={{
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '2.5px',
color: T.ink5,
lineHeight: 1.4,
}}
>
تحلیل مستقل · سیاستگذاری
</p>
<p
style={{
fontSize: 11,
color: T.ink5,
lineHeight: 1.6,
marginTop: 4,
}}
>
۲۸۰ گزارش منتشرشده · ۴۵ متخصص
</p>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<SocialIcon label="in" href="https://linkedin.com" />
<SocialIcon label="tg" href="https://t.me" />
<SocialIcon label="tw" href="https://x.com" />
</div>
</div>
{/* Center col */}
<div
style={{
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<h1
style={{
fontSize: 'clamp(28px, 3.5vw, 48px)',
fontWeight: 900,
lineHeight: 1,
letterSpacing: '-1px',
color: T.ink,
whiteSpace: 'nowrap',
}}
>
اندیشکده فولاد ایران
</h1>
<hr
aria-hidden="true"
style={{
width: 60,
height: 2,
background: T.red,
border: 'none',
margin: '8px 0',
flexShrink: 0,
}}
/>
<p
style={{
fontSize: 11,
letterSpacing: '5px',
color: T.ink4,
fontWeight: 400,
textTransform: 'uppercase',
}}
>
IRAN STEEL POLICY INSTITUTE
</p>
</div>
{/* Right col */}
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
gap: 8,
}}
>
<div style={{ position: 'relative' }}>
<input
ref={searchRef}
type="search"
placeholder="جستجو در گزارش‌ها..."
aria-label="جستجو"
style={{
border: `1px solid ${T.ink}`,
padding: '8px 12px 8px 32px',
fontSize: 12,
background: 'transparent',
width: 200,
color: T.ink,
outline: 'none',
borderRadius: 0,
fontFamily: 'inherit',
direction: 'rtl',
}}
/>
<Search
size={14}
aria-hidden="true"
style={{
position: 'absolute',
left: 10,
top: '50%',
transform: 'translateY(-50%)',
color: T.ink4,
pointerEvents: 'none',
}}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<FilledButton>اشتراک سازمانی</FilledButton>
<OutlineButton>ورود</OutlineButton>
</div>
</div>
</div>
{/* ── NAV BAR ──────────────────────────────── */}
<nav
aria-label="ناوبری اصلی"
style={{
borderTop: `1px solid ${T.ruleThin}`,
borderBottom: `3px solid ${T.ink}`,
display: 'flex',
flexDirection: 'row',
overflowX: 'auto',
}}
>
{NAV_ITEMS.map((item, i) => (
<FullNavItem
key={item.to}
to={item.to}
label={item.label}
isLast={i === NAV_ITEMS.length - 1}
/>
))}
</nav>
</motion.div>
)}
</AnimatePresence>
{/*
COMPACT STATE
*/}
<AnimatePresence initial={false}>
{showCompact && (
<motion.div
key="compact"
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.18, ease: 'easeOut' }}
>
<div
style={{
height: 56,
borderBottom: `2px solid ${T.ink}`,
padding: '0 48px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 24,
}}
>
{/* Brand */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 1, flexShrink: 0 }}>
<span
style={{
fontSize: 16,
fontWeight: 900,
lineHeight: 1,
color: T.ink,
whiteSpace: 'nowrap',
}}
>
اندیشکده فولاد ایران
</span>
<span
style={{
fontSize: 9,
letterSpacing: '3px',
color: T.ink5,
fontWeight: 400,
textTransform: 'uppercase',
lineHeight: 1,
whiteSpace: 'nowrap',
}}
>
IRAN STEEL POLICY INSTITUTE
</span>
</div>
{/* Desktop compact nav */}
{!isMobile ? (
<nav
aria-label="ناوبری فشرده"
style={{
display: 'flex',
alignItems: 'center',
flex: 1,
justifyContent: 'flex-end',
}}
>
{COMPACT_NAV.map((item, i) => (
<CompactNavItem
key={item.to}
to={item.to}
label={item.label}
isFirst={i === 0}
/>
))}
<RedCtaButton style={{ marginRight: 16 }}>اشتراک</RedCtaButton>
</nav>
) : (
/* Mobile hamburger */
<button
aria-label="باز کردن منو"
onClick={() => setMobileOpen(true)}
style={{
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: T.ink,
padding: 4,
display: 'flex',
alignItems: 'center',
}}
>
<Menu size={22} />
</button>
)}
</div>
</motion.div>
)}
</AnimatePresence>
{/*
MOBILE DRAWER
*/}
<AnimatePresence>
{mobileOpen && (
<>
{/* Overlay */}
<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(26,23,18,0.55)',
zIndex: 100,
}}
/>
{/* Drawer panel */}
<motion.div
key="drawer"
initial={{ x: 280 }}
animate={{ x: 0 }}
exit={{ x: 280 }}
transition={{ duration: 0.25, ease: [0.32, 0, 0.18, 1] }}
style={{
position: 'fixed',
top: 0,
right: 0,
height: '100dvh',
width: 280,
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',
}}
>
<span
style={{ fontSize: 14, fontWeight: 700, color: T.ink }}
>
اندیشکده فولاد ایران
</span>
<button
aria-label="بستن منو"
onClick={() => setMobileOpen(false)}
style={{
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: T.ink,
padding: 4,
display: 'flex',
alignItems: 'center',
}}
>
<X size={20} />
</button>
</div>
{/* Nav list */}
<nav style={{ flex: 1, overflowY: 'auto' }}>
{NAV_ITEMS.map((item) => (
<DrawerNavItem
key={item.to}
to={item.to}
label={item.label}
onClose={() => setMobileOpen(false)}
/>
))}
</nav>
{/* Footer CTAs */}
<div
style={{
padding: '16px 20px',
borderTop: `1px solid ${T.ruleThin}`,
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
<RedCtaButton style={{ width: '100%' }}>اشتراک سازمانی</RedCtaButton>
<OutlineButton style={{ width: '100%' }}>ورود</OutlineButton>
<div
style={{
display: 'flex',
gap: 12,
marginTop: 4,
flexWrap: 'wrap',
}}
>
{META_LEFT.map((item) => (
<a
key={item}
href="#"
style={{ fontSize: 11, color: T.ink4, textDecoration: 'none' }}
>
{item}
</a>
))}
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</header>
)
}
/*
MICRO BUTTON COMPONENTS (hover via useState clean)
*/
function FilledButton({
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.red : T.ink,
color: T.paper,
border: `1px solid ${hovered ? T.red : T.ink}`,
padding: '7px 16px',
fontSize: 12,
fontWeight: 600,
cursor: 'pointer',
fontFamily: 'inherit',
borderRadius: 0,
letterSpacing: '0.3px',
transition: 'background 120ms, border-color 120ms',
whiteSpace: 'nowrap',
...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: '7px 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>
)
}
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: '7px 16px',
fontSize: 12,
fontWeight: 700,
cursor: 'pointer',
fontFamily: 'inherit',
borderRadius: 0,
letterSpacing: '0.3px',
transition: 'background 120ms',
whiteSpace: 'nowrap',
textAlign: 'center',
flexShrink: 0,
...extraStyle,
}}
>
{children}
</button>
)
}

View File

@ -0,0 +1,135 @@
import { marketPrices } from '@/data/market';
import type { MarketPrice } from '@/data/market';
function formatValue(item: MarketPrice): string {
return item.value.toLocaleString('fa-IR');
}
function formatChange(item: MarketPrice): string {
const abs = Math.abs(item.change).toLocaleString('fa-IR');
return `${abs} (${Math.abs(item.changePercent).toFixed(1)}٪)`;
}
function TickerItem({ item }: { item: MarketPrice }) {
const changeColor =
item.trend === 'up'
? '#7dbf8a'
: item.trend === 'down'
? '#e07070'
: 'rgba(255,255,255,0.5)';
const arrow =
item.trend === 'up' ? '↑' : item.trend === 'down' ? '↓' : '—';
return (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '10px',
padding: '0 24px',
borderRight: '1px solid rgba(255,255,255,0.1)',
flexShrink: 0,
height: '40px',
}}
>
<span
style={{
color: 'rgba(255,255,255,0.5)',
fontSize: '11px',
fontWeight: 400,
}}
>
{item.name}
</span>
<span
style={{
fontWeight: 700,
fontSize: '12px',
color: '#fff',
fontVariantNumeric: 'tabular-nums',
}}
>
{formatValue(item)}{' '}
<span style={{ fontSize: '10px', color: 'rgba(255,255,255,0.4)', fontWeight: 400 }}>
{item.unit}
</span>
</span>
<span
style={{
fontSize: '11px',
fontWeight: 600,
color: changeColor,
fontVariantNumeric: 'tabular-nums',
}}
>
{arrow} {formatChange(item)}
</span>
</span>
);
}
export function MarketStrip() {
return (
<div
dir="ltr"
style={{
width: '100%',
backgroundColor: 'var(--ink)',
color: 'var(--paper)',
height: '40px',
display: 'flex',
alignItems: 'stretch',
overflow: 'hidden',
}}
>
{/* Live tag */}
<div
style={{
backgroundColor: 'var(--red)',
color: 'var(--paper)',
fontSize: '9px',
fontWeight: 700,
letterSpacing: '2.5px',
textTransform: 'uppercase',
padding: '0 20px',
display: 'flex',
alignItems: 'center',
flexShrink: 0,
whiteSpace: 'nowrap',
}}
>
بازار زنده
</div>
{/* Ticker area */}
<div
style={{
flex: 1,
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
position: 'relative',
}}
>
<div
className="ticker-track"
style={{
display: 'flex',
alignItems: 'center',
whiteSpace: 'nowrap',
}}
>
{/* Original set */}
{marketPrices.map((item, i) => (
<TickerItem key={`a-${i}`} item={item} />
))}
{/* Duplicate for seamless loop */}
{marketPrices.map((item, i) => (
<TickerItem key={`b-${i}`} item={item} />
))}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,17 @@
import { Outlet } from 'react-router-dom'
import Header from './Header'
import { Footer } from './Footer'
import { MarketStrip } from './MarketStrip'
export default function RootLayout() {
return (
<div style={{ minHeight: '100dvh', display: 'flex', flexDirection: 'column' }}>
<Header />
<MarketStrip />
<main style={{ flex: 1 }}>
<Outlet />
</main>
<Footer />
</div>
)
}

33
src/app/router.tsx Normal file
View File

@ -0,0 +1,33 @@
import { createBrowserRouter } from 'react-router-dom'
import RootLayout from './layout/RootLayout'
import Home from '@/pages/Home/Home'
import Reports from '@/pages/Reports/Reports'
import ReportDetail from '@/pages/ReportDetail/ReportDetail'
import Market from '@/pages/Market/Market'
import Subscribe from '@/pages/Subscribe/Subscribe'
import Events from '@/pages/Events/Events'
import Team from '@/pages/Team/Team'
import Archive from '@/pages/Archive/Archive'
import Special from '@/pages/Special/Special'
import Risks from '@/pages/Risks/Risks'
import Scanner from '@/pages/Scanner/Scanner'
export const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
children: [
{ index: true, element: <Home /> },
{ path: 'reports', element: <Reports /> },
{ path: 'reports/:id', element: <ReportDetail /> },
{ path: 'market', element: <Market /> },
{ path: 'subscribe', element: <Subscribe /> },
{ path: 'events', element: <Events /> },
{ path: 'team', element: <Team /> },
{ path: 'archive', element: <Archive /> },
{ path: 'special', element: <Special /> },
{ path: 'risks', element: <Risks /> },
{ path: 'scanner', element: <Scanner /> },
],
},
])

BIN
src/assets/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

1
src/assets/react.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

1
src/assets/vite.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -0,0 +1,56 @@

type BadgeVariant = 'default' | 'red' | 'amber' | 'green' | 'outline';
interface BadgeProps {
children: React.ReactNode;
variant?: BadgeVariant;
className?: string;
}
const variantStyles: Record<BadgeVariant, React.CSSProperties> = {
default: {
backgroundColor: 'var(--ink)',
color: 'var(--paper)',
border: '1px solid var(--ink)',
},
red: {
backgroundColor: 'var(--red)',
color: 'var(--paper)',
border: '1px solid var(--red)',
},
amber: {
backgroundColor: 'var(--amber)',
color: 'var(--ink)',
border: '1px solid var(--amber)',
},
green: {
backgroundColor: 'var(--green-ink)',
color: 'var(--paper)',
border: '1px solid var(--green-ink)',
},
outline: {
backgroundColor: 'transparent',
color: 'var(--ink)',
border: '1px solid var(--ink)',
},
};
export function Badge({ children, variant = 'default', className = '' }: BadgeProps) {
return (
<span
className={`inline-block ${className}`}
style={{
...variantStyles[variant],
padding: '2px 6px',
fontSize: '9px',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '1.5px',
lineHeight: 1.6,
borderRadius: 0,
}}
>
{children}
</span>
);
}

View File

@ -0,0 +1,111 @@
import { useState } from 'react'
import type { CSSProperties, ReactNode, MouseEvent } from 'react'
type ButtonVariant = 'filled' | 'outline' | 'paper' | 'ghost';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps {
children: ReactNode;
variant?: ButtonVariant;
size?: ButtonSize;
className?: string;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
type?: 'button' | 'submit' | 'reset';
disabled?: boolean;
asChild?: boolean;
}
const sizeStyles: Record<ButtonSize, CSSProperties> = {
sm: { padding: '9px 20px', fontSize: '9px' },
md: { padding: '11px 24px', fontSize: '11px' },
lg: { padding: '12px 28px', fontSize: '12px' },
};
const baseStyle: CSSProperties = {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px',
borderRadius: 0,
fontFamily: 'Vazir, sans-serif',
fontWeight: 700,
letterSpacing: '0.5px',
textTransform: 'uppercase',
cursor: 'pointer',
transition: 'background-color 200ms, color 200ms, border-color 200ms',
outline: 'none',
border: '1px solid transparent',
whiteSpace: 'nowrap',
};
const variantBase: Record<ButtonVariant, CSSProperties> = {
filled: {
backgroundColor: 'var(--ink)',
color: 'var(--paper)',
borderColor: 'var(--ink)',
},
outline: {
backgroundColor: 'transparent',
color: 'var(--ink)',
borderColor: 'var(--ink)',
},
paper: {
backgroundColor: 'var(--paper)',
color: 'var(--ink)',
borderColor: 'var(--paper)',
},
ghost: {
backgroundColor: 'transparent',
color: 'var(--ink)',
borderColor: 'transparent',
},
};
export function Button({
children,
variant = 'filled',
size = 'md',
className = '',
onClick,
type = 'button',
disabled = false,
}: ButtonProps) {
const [hovered, setHovered] = useState(false);
const getHoverStyle = (): CSSProperties => {
if (!hovered || disabled) return {};
switch (variant) {
case 'filled':
return { backgroundColor: 'var(--ink-2)' };
case 'outline':
return { backgroundColor: 'var(--ink)', color: 'var(--paper)' };
case 'paper':
return { backgroundColor: 'var(--paper-2)' };
case 'ghost':
return { backgroundColor: 'var(--paper-2)' };
default:
return {};
}
};
return (
<button
type={type}
disabled={disabled}
onClick={onClick}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className={`focus-visible:ring-2 focus-visible:ring-offset-1 ${className}`}
style={{
...baseStyle,
...variantBase[variant],
...sizeStyles[size],
...getHoverStyle(),
opacity: disabled ? 0.45 : 1,
pointerEvents: disabled ? 'none' : 'auto',
}}
>
{children}
</button>
);
}

View File

@ -0,0 +1,44 @@

interface PullQuoteProps {
text: React.ReactNode;
source?: string;
className?: string;
}
export function PullQuote({ text, source, className = '' }: PullQuoteProps) {
return (
<blockquote
className={className}
style={{
borderTop: '2px solid var(--ink)',
borderBottom: '2px solid var(--ink)',
padding: '20px 0',
margin: '24px 0',
}}
>
<p
style={{
fontSize: '20px',
fontWeight: 800,
lineHeight: 1.3,
letterSpacing: '-0.5px',
color: 'var(--ink)',
margin: 0,
}}
>
{text}
</p>
{source && (
<footer
style={{
fontSize: '11px',
color: 'var(--ink-5)',
marginTop: '10px',
}}
>
{source}
</footer>
)}
</blockquote>
);
}

View File

@ -0,0 +1,44 @@

type RuleWeight = 'heavy' | 'mid' | 'light' | 'red';
interface RuleProps {
weight?: RuleWeight;
className?: string;
}
const styles: Record<RuleWeight, React.CSSProperties> = {
heavy: {
borderTop: '3px solid var(--ink)',
borderBottom: 'none',
margin: 0,
width: '100%',
},
mid: {
borderTop: '1px solid var(--ink)',
borderBottom: 'none',
margin: 0,
width: '100%',
},
light: {
borderTop: '1px solid var(--rule-thin)',
borderBottom: 'none',
margin: 0,
width: '100%',
},
red: {
borderTop: '3px solid var(--red)',
borderBottom: 'none',
margin: 0,
width: '100%',
},
};
export function Rule({ weight = 'mid', className = '' }: RuleProps) {
return (
<hr
style={styles[weight]}
className={className}
aria-hidden="true"
/>
);
}

View File

@ -0,0 +1,91 @@
import { ArrowLeft } from 'lucide-react';
type LabelVariant = 'ink' | 'red';
interface SectionBannerLink {
text: string;
to: string;
}
interface SectionBannerProps {
label: string;
labelVariant?: LabelVariant;
link?: SectionBannerLink;
}
export function SectionBanner({
label,
labelVariant = 'ink',
link,
}: SectionBannerProps) {
const labelBg = labelVariant === 'red' ? 'var(--red)' : 'var(--ink)';
return (
<div
style={{
borderBottom: '1px solid var(--rule-thin)',
display: 'flex',
flexDirection: 'row',
alignItems: 'stretch',
gap: 0,
}}
>
{/* Label block */}
<span
style={{
backgroundColor: labelBg,
color: 'var(--paper)',
fontSize: '9px',
fontWeight: 900,
letterSpacing: '3px',
textTransform: 'uppercase',
padding: '6px 14px',
display: 'flex',
alignItems: 'center',
flexShrink: 0,
lineHeight: 1,
}}
>
{label}
</span>
{/* Rule fill */}
<div
style={{
flex: 1,
borderBottom: 'none',
alignSelf: 'center',
borderTop: '1px solid var(--rule-thin)',
marginBottom: '-1px',
}}
aria-hidden="true"
/>
{/* Optional link */}
{link && (
<a
href={link.to}
style={{
display: 'flex',
alignItems: 'center',
gap: '5px',
fontSize: '10px',
fontWeight: 700,
letterSpacing: '1px',
color: 'var(--ink)',
textDecoration: 'none',
padding: '6px 0 6px 4px',
flexShrink: 0,
cursor: 'pointer',
transition: 'color 150ms',
}}
onMouseEnter={e => (e.currentTarget.style.color = 'var(--red)')}
onMouseLeave={e => (e.currentTarget.style.color = 'var(--ink)')}
>
{link.text}
<ArrowLeft size={12} strokeWidth={2.5} />
</a>
)}
</div>
);
}

View File

@ -0,0 +1,37 @@

type StampColor = 'red' | 'ink';
interface StampProps {
children: React.ReactNode;
color?: StampColor;
tilt?: boolean;
className?: string;
}
export function Stamp({
children,
color = 'red',
tilt = true,
className = '',
}: StampProps) {
const colorValue = color === 'red' ? 'var(--red)' : 'var(--ink)';
return (
<span
className={`inline-block ${tilt ? 'stamp-tilt' : ''} ${className}`}
style={{
border: `2px solid ${colorValue}`,
color: colorValue,
padding: '3px 10px',
fontSize: '9px',
fontWeight: 900,
letterSpacing: '3px',
textTransform: 'uppercase',
lineHeight: 1.4,
display: 'inline-block',
}}
>
{children}
</span>
);
}

View File

@ -0,0 +1,40 @@
interface StatCardProps {
number: string;
label: string;
className?: string;
}
export function StatCard({ number, label, className = '' }: StatCardProps) {
return (
<div
className={className}
style={{
borderTop: '2px solid var(--ink)',
paddingTop: '16px',
}}
>
<div
style={{
fontSize: 'clamp(32px, 4vw, 44px)',
fontWeight: 900,
color: 'var(--ink)',
lineHeight: 1,
fontVariantNumeric: 'tabular-nums',
}}
>
{number}
</div>
<div
style={{
fontSize: '11px',
color: 'var(--ink-5)',
marginTop: '4px',
lineHeight: 1.4,
}}
>
{label}
</div>
</div>
);
}

View File

@ -0,0 +1,280 @@
import {
useEffect,
useRef,
useState,
createContext,
useContext,
} from 'react'
import type { ReactNode } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { X } from 'lucide-react'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
function cn(...inputs: Parameters<typeof clsx>) {
return twMerge(clsx(inputs))
}
/* ─── types ─────────────────────────────────────────── */
export interface CardData {
src: string
title: string
category: string
content: ReactNode
}
interface CarouselContextType {
onCardClose: (index: number) => void
currentIndex: number
}
/* ─── context ───────────────────────────────────────── */
const CarouselContext = createContext<CarouselContextType>({
onCardClose: () => {},
currentIndex: 0,
})
/* ─── Carousel ──────────────────────────────────────── */
export function Carousel({
items,
initialScroll = 0,
}: {
items: ReactNode[]
initialScroll?: number
}) {
const carouselRef = useRef<HTMLDivElement>(null)
const [canScrollLeft, setCanScrollLeft] = useState(false)
const [canScrollRight, setCanScrollRight] = useState(true)
const [currentIndex, setCurrentIndex] = useState(0)
useEffect(() => {
if (carouselRef.current) {
carouselRef.current.scrollLeft = initialScroll
checkScrollability()
}
}, [initialScroll])
function checkScrollability() {
if (!carouselRef.current) return
const { scrollLeft, scrollWidth, clientWidth } = carouselRef.current
setCanScrollLeft(scrollLeft > 0)
setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 1)
}
function scrollLeft() { carouselRef.current?.scrollBy({ left: -340, behavior: 'smooth' }) }
function scrollRight() { carouselRef.current?.scrollBy({ left: 340, behavior: 'smooth' }) }
function handleCardClose(index: number) {
if (!carouselRef.current) return
const cardWidth = window.innerWidth < 768 ? 230 : 350
carouselRef.current.scrollTo({ left: (cardWidth + 16) * (index + 1), behavior: 'smooth' })
setCurrentIndex(index)
}
return (
<CarouselContext.Provider value={{ onCardClose: handleCardClose, currentIndex }}>
<div className="relative w-full">
{/* Scrollable track */}
<div
ref={carouselRef}
onScroll={checkScrollability}
className="flex w-full overflow-x-scroll overscroll-x-auto scroll-smooth py-10 md:py-14 [scrollbar-width:none]"
style={{ direction: 'ltr' }}
>
<div className="flex flex-row justify-start gap-4 px-4 md:px-10">
{items.map((item, index) => (
<motion.div
key={'card' + index}
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, delay: 0.1 * index, ease: 'easeOut' }}
className="last:pl-[10%] md:last:pl-[20%]"
>
{item}
</motion.div>
))}
</div>
</div>
{/* Arrow buttons */}
<div className="flex justify-end gap-2 px-10 pb-6">
<button
onClick={scrollLeft}
disabled={!canScrollLeft}
className="h-10 w-10 rounded-full bg-[var(--paper-3)] border border-[var(--rule-thin)] flex items-center justify-center disabled:opacity-30 transition-opacity hover:bg-[var(--paper-2)]"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className="h-5 w-5 text-[var(--ink)]">
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
</button>
<button
onClick={scrollRight}
disabled={!canScrollRight}
className="h-10 w-10 rounded-full bg-[var(--paper-3)] border border-[var(--rule-thin)] flex items-center justify-center disabled:opacity-30 transition-opacity hover:bg-[var(--paper-2)]"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className="h-5 w-5 text-[var(--ink)]">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
</CarouselContext.Provider>
)
}
/* ─── Card ──────────────────────────────────────────── */
export function Card({
card,
index,
layout = false,
}: {
card: CardData
index: number
layout?: boolean
}) {
const [open, setOpen] = useState(false)
const { onCardClose } = useContext(CarouselContext)
useEffect(() => {
function onKey(e: KeyboardEvent) { if (e.key === 'Escape') handleClose() }
document.body.style.overflow = open ? 'hidden' : 'auto'
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [open])
function handleOpen() { setOpen(true) }
function handleClose() { setOpen(false); onCardClose(index) }
return (
<>
{/* ── Modal ── */}
<AnimatePresence>
{open && (
/* Outer: scroll host */
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop — click to close */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black/75 backdrop-blur-md"
onClick={handleClose}
/>
{/* Centering wrapper */}
<div className="flex min-h-full items-center justify-center p-4 md:p-8">
<motion.div
layoutId={layout ? `card-${card.title}` : undefined}
initial={{ opacity: 0, scale: 0.96, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.96, y: 20 }}
transition={{ duration: 0.3, ease: [0.32, 0.72, 0, 1] }}
className="relative z-10 w-full max-w-2xl bg-white rounded-3xl overflow-hidden shadow-2xl"
style={{ direction: 'rtl', fontFamily: 'Vazir, sans-serif', maxHeight: '88vh', overflowY: 'auto' }}
onClick={e => e.stopPropagation()}
>
{/* Close button */}
<button
onClick={handleClose}
className="absolute top-4 left-4 z-20 h-9 w-9 bg-black/10 hover:bg-black/20 rounded-full flex items-center justify-center transition-colors"
>
<X className="h-4 w-4 text-gray-700" />
</button>
{/* Hero image strip */}
<div className="relative h-52 overflow-hidden" style={{ flexShrink: 0 }}>
<img
src={card.src}
alt={card.title}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent" />
{/* inset-x-0 ensures text never overflows past modal edges */}
<div className="absolute bottom-0 inset-x-0 p-6">
<p className="text-white/70 text-xs font-bold tracking-widest uppercase mb-1">
{card.category}
</p>
<p className="text-white text-lg font-bold leading-snug line-clamp-2">
{card.title}
</p>
</div>
</div>
{/* Body */}
<div className="p-6 md:p-8">
{card.content}
</div>
</motion.div>
</div>
</div>
)}
</AnimatePresence>
{/* ── Card thumbnail ── */}
<motion.button
onClick={handleOpen}
className={cn(
'rounded-3xl overflow-hidden flex flex-col items-start justify-end relative z-10 cursor-pointer',
'h-72 w-48 md:h-[420px] md:w-80',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--red)]',
)}
style={{ direction: 'rtl' }}
whileHover={{ scale: 1.015 }}
transition={{ duration: 0.2 }}
>
{/* Bottom-to-top gradient so text is readable — tall enough to cover 2 lines */}
<div className="absolute inset-0 bg-gradient-to-t from-black/95 via-black/65 via-[35%] to-transparent z-20 pointer-events-none" />
{/* Text — bottom of card, small */}
<div className="relative z-30 p-5 w-full text-right overflow-hidden">
<p className="text-white/65 text-[10px] font-bold tracking-widest uppercase mb-1.5 truncate">
{card.category}
</p>
<p className="text-white text-sm font-bold leading-snug line-clamp-2">
{card.title}
</p>
</div>
{/* Background image */}
<BlurImage
src={card.src}
alt={card.title}
className="object-cover absolute inset-0 w-full h-full z-10"
/>
</motion.button>
</>
)
}
/* ─── BlurImage ─────────────────────────────────────── */
function BlurImage({ src, alt, className }: { src: string; alt: string; className?: string }) {
const [loaded, setLoaded] = useState(false)
return (
<img
src={src}
alt={alt}
onLoad={() => setLoaded(true)}
className={cn('transition-[filter] duration-500', loaded ? 'blur-0' : 'blur-md', className)}
/>
)
}
/* ─── useOutsideClick ───────────────────────────────── */
export function useOutsideClick(
ref: React.RefObject<HTMLDivElement | null>,
callback: () => void,
) {
useEffect(() => {
function listener(e: MouseEvent | TouchEvent) {
if (!ref.current || ref.current.contains(e.target as Node)) return
callback()
}
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
}, [ref, callback])
}

View File

@ -0,0 +1,131 @@
import type { ReactNode, CSSProperties } from 'react'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
function cn(...inputs: Parameters<typeof clsx>) {
return twMerge(clsx(inputs))
}
/* ─── BentoGrid ─────────────────────────────────────────── */
export function BentoGrid({
children,
className,
}: {
children: ReactNode
className?: string
}) {
return (
<div
className={cn(
'grid grid-cols-1 md:grid-cols-3',
className,
)}
style={{ gap: 1, background: 'var(--rule-thin)' }}
>
{children}
</div>
)
}
/* ─── BentoGridItem ──────────────────────────────────────── */
export interface BentoGridItemProps {
title: string
description?: string
header?: ReactNode /* visual block above content */
icon?: ReactNode
tag?: string
tagColor?: string /* CSS color value */
accentLine?: boolean /* left/top colored accent rule */
className?: string
style?: CSSProperties
onClick?: () => void
}
export function BentoGridItem({
title,
description,
header,
icon,
tag,
tagColor = 'var(--red)',
accentLine = false,
className,
style,
onClick,
}: BentoGridItemProps) {
return (
<div
onClick={onClick}
className={cn(
'group relative flex flex-col bg-[var(--paper)] p-6 transition-colors duration-200 hover:bg-[var(--paper-2)]',
onClick && 'cursor-pointer',
className,
)}
style={style}
>
{/* Accent bar */}
{accentLine && (
<div
style={{
position: 'absolute',
top: 0,
right: 0,
left: 0,
height: 3,
background: tagColor,
}}
/>
)}
{/* Optional visual header block */}
{header && <div className="mb-4">{header}</div>}
{/* Tag */}
{tag && (
<div
style={{
fontSize: 9,
fontWeight: 700,
letterSpacing: '2.5px',
textTransform: 'uppercase',
color: tagColor,
marginBottom: 10,
}}
>
{tag}
</div>
)}
{/* Icon + body */}
<div className="flex flex-col flex-1 gap-2">
{icon && <div className="mb-1">{icon}</div>}
<h3
style={{
fontSize: 15,
fontWeight: 700,
letterSpacing: '-0.3px',
lineHeight: 1.4,
color: 'var(--ink)',
}}
>
{title}
</h3>
{description && (
<p
style={{
fontSize: 13,
lineHeight: 1.75,
color: 'var(--ink-3)',
fontWeight: 300,
marginTop: 2,
}}
>
{description}
</p>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,7 @@
export { Rule } from './Rule';
export { Stamp } from './Stamp';
export { Badge } from './Badge';
export { Button } from './Button';
export { SectionBanner } from './SectionBanner';
export { PullQuote } from './PullQuote';
export { StatCard } from './StatCard';

View File

@ -0,0 +1,156 @@
import { useEffect, useRef, useState } from 'react'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
function cn(...inputs: Parameters<typeof clsx>) {
return twMerge(clsx(inputs))
}
export interface MovingCardItem {
quote: string
name: string
title: string
level?: 'critical' | 'high' | 'medium' | 'low' | 'opportunity'
}
interface InfiniteMovingCardsProps {
items: MovingCardItem[]
direction?: 'left' | 'right'
speed?: 'fast' | 'normal' | 'slow'
pauseOnHover?: boolean
className?: string
}
const levelConfig = {
critical: { dot: '#7f1d1d', label: 'بحرانی', text: '#7f1d1d', bg: 'rgba(127,29,29,0.08)' },
high: { dot: '#9b1c1c', label: 'بالا', text: '#9b1c1c', bg: 'rgba(155,28,28,0.07)' },
medium: { dot: '#92400e', label: 'متوسط', text: '#92400e', bg: 'rgba(146,64,14,0.07)' },
low: { dot: '#166534', label: 'پایین', text: '#166534', bg: 'rgba(22,101,52,0.07)' },
opportunity: { dot: '#1e40af', label: 'فرصت', text: '#1e40af', bg: 'rgba(30,64,175,0.07)' },
}
export function InfiniteMovingCards({
items,
direction = 'left',
speed = 'slow',
pauseOnHover = true,
className,
}: InfiniteMovingCardsProps) {
const containerRef = useRef<HTMLDivElement>(null)
const scrollerRef = useRef<HTMLUListElement>(null)
const [ready, setReady] = useState(false)
useEffect(() => {
if (!containerRef.current || !scrollerRef.current) return
/* Clone items for seamless loop */
const items = Array.from(scrollerRef.current.children)
items.forEach(item => {
const clone = item.cloneNode(true)
scrollerRef.current!.appendChild(clone)
})
/* Set CSS custom properties */
const dur = speed === 'fast' ? '25s' : speed === 'normal' ? '45s' : '70s'
containerRef.current.style.setProperty('--animation-duration', dur)
containerRef.current.style.setProperty('--animation-direction',
direction === 'right' ? 'reverse' : 'forwards')
setReady(true)
}, [direction, speed])
return (
<div
ref={containerRef}
className={cn('relative overflow-hidden', className)}
style={{
/* Fade masks on both edges */
WebkitMaskImage:
'linear-gradient(to right, transparent 0%, black 8%, black 92%, transparent 100%)',
maskImage:
'linear-gradient(to right, transparent 0%, black 8%, black 92%, transparent 100%)',
}}
>
<ul
ref={scrollerRef}
className={cn(
'flex min-w-full shrink-0 gap-5 w-max flex-nowrap',
ready && 'animate-scroll',
pauseOnHover && 'hover:[animation-play-state:paused]',
)}
style={{ direction: 'ltr' }} /* keep scroll direction LTR regardless of page RTL */
>
{items.map((item, idx) => {
const cfg = item.level ? levelConfig[item.level] : levelConfig.high
return (
<li
key={idx}
style={{
width: 340,
flexShrink: 0,
background: 'var(--paper)',
border: '1px solid var(--rule-thin)',
borderTop: `3px solid ${cfg.dot}`,
padding: '28px 28px 24px',
direction: 'rtl',
fontFamily: 'Vazir, sans-serif',
position: 'relative',
}}
>
{/* Level badge */}
<div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 14 }}>
<div style={{
width: 7, height: 7, borderRadius: '50%',
background: cfg.dot, flexShrink: 0,
}} />
<span style={{
fontSize: 9, fontWeight: 700, letterSpacing: '2px',
textTransform: 'uppercase', color: cfg.dot,
}}>
{cfg.label}
</span>
</div>
{/* Quote / description */}
<p style={{
fontSize: 13.5,
lineHeight: 1.85,
color: 'var(--ink-2)',
fontWeight: 300,
marginBottom: 20,
}}>
{item.quote}
</p>
{/* Footer */}
<div style={{
paddingTop: 14,
borderTop: '1px solid var(--rule-thin)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}>
<div>
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink)' }}>
{item.name}
</div>
<div style={{ fontSize: 10, color: 'var(--ink-5)' }}>
{item.title}
</div>
</div>
<div style={{
fontSize: 9, fontWeight: 700, letterSpacing: '1px',
textTransform: 'uppercase', color: cfg.text,
background: cfg.bg,
padding: '4px 10px',
}}>
{cfg.label}
</div>
</div>
</li>
)
})}
</ul>
</div>
)
}

View File

@ -0,0 +1,191 @@
/**
* WorldMap Aceternity-style dotted world map with animated arc routes.
*
* DottedMap (height=60, grid='diagonal') emits viewBox="0 0 119 60".
* We project lat/lng into that same coordinate space so the arcs sit
* exactly on the map dots.
*/
import { useRef, useMemo, memo } from 'react'
import { motion } from 'framer-motion'
import DottedMap from 'dotted-map'
/* ─── Public types ───────────────────────────────────── */
export interface MapDot {
start: { lat: number; lng: number; label?: string }
end: { lat: number; lng: number; label?: string }
}
interface WorldMapProps {
dots?: MapDot[]
lineColor?: string
dotColor?: string
}
/* ─── Map geometry constants ─────────────────────────── */
const VB_W = 119 // DottedMap viewBox width (height=60 → width=119)
const VB_H = 60 // DottedMap viewBox height
/* Equirectangular projection matching DottedMap's output */
function project(lat: number, lng: number) {
const x = (lng + 180) / 360 * VB_W
const y = (90 - lat) / 180 * VB_H
return { x, y }
}
/* Quadratic bezier — control point lifted proportional to distance */
function arcPath(
s: { x: number; y: number },
e: { x: number; y: number },
) {
const dx = e.x - s.x
const dy = e.y - s.y
const dist = Math.sqrt(dx * dx + dy * dy)
const mx = (s.x + e.x) / 2
const my = (s.y + e.y) / 2 - dist * 0.35 // lift proportional to length
return `M ${s.x} ${s.y} Q ${mx} ${my} ${e.x} ${e.y}`
}
/* ─── Memoised base map (expensive, only computed once) ─ */
const BaseMap = memo(function BaseMap({ dotColor }: { dotColor: string }) {
const map = new DottedMap({ height: 60, grid: 'diagonal' })
const svg = map.getSVG({
color: dotColor,
radius: 0.22,
backgroundColor: 'transparent',
})
return (
<div
className="absolute inset-0 w-full h-full"
/* eslint-disable-next-line react/no-danger */
dangerouslySetInnerHTML={{ __html: svg }}
/>
)
})
/* ─── Component ──────────────────────────────────────── */
export default function WorldMap({
dots = [],
lineColor = '#9b1c1c', // var(--red)
dotColor = '#d1cdc7', // muted cream-grey
}: WorldMapProps) {
const svgRef = useRef<SVGSVGElement>(null)
/* Pre-compute projected coordinates */
const projected = useMemo(
() =>
dots.map(d => ({
start: project(d.start.lat, d.start.lng),
end: project(d.end.lat, d.end.lng),
startLabel: d.start.label,
endLabel: d.end.label,
})),
[dots],
)
return (
/* aspect-ratio 119:60 ≈ 2:1 keeps the map undistorted */
<div className="relative w-full" style={{ aspectRatio: `${VB_W} / ${VB_H}` }}>
{/* Dotted world map base */}
<BaseMap dotColor={dotColor} />
{/* Arc + dot overlay — same viewBox as DottedMap */}
<svg
ref={svgRef}
viewBox={`0 0 ${VB_W} ${VB_H}`}
className="absolute inset-0 w-full h-full pointer-events-none"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid meet"
>
<defs>
{projected.map((_, i) => (
<linearGradient
key={`g${i}`}
id={`grd${i}`}
x1="0%" y1="0%" x2="100%" y2="0%"
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" stopColor={lineColor} stopOpacity="0" />
<stop offset="40%" stopColor={lineColor} stopOpacity="0.85" />
<stop offset="60%" stopColor={lineColor} stopOpacity="0.85" />
<stop offset="100%" stopColor={lineColor} stopOpacity="0" />
</linearGradient>
))}
</defs>
{projected.map((p, i) => {
const d = arcPath(p.start, p.end)
return (
<g key={i}>
{/* Ghost — very faint, instant, gives the "track" feel */}
<path
d={d}
fill="none"
stroke={lineColor}
strokeWidth={0.25}
strokeOpacity={0.12}
/>
{/* Animated arc */}
<motion.path
d={d}
fill="none"
stroke={`url(#grd${i})`}
strokeWidth={0.5}
strokeLinecap="round"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{
pathLength: {
duration: 1.6,
delay: i * 0.3,
ease: 'easeInOut',
repeat: Infinity,
repeatDelay: 2.5,
repeatType: 'loop',
},
opacity: { duration: 0.2, delay: i * 0.3 },
}}
/>
{/* Start endpoint */}
<PulsingDot cx={p.start.x} cy={p.start.y} color={lineColor} delay={i * 0.3} />
{/* End endpoint */}
<PulsingDot cx={p.end.x} cy={p.end.y} color={lineColor} delay={i * 0.3 + 0.8} />
</g>
)
})}
</svg>
</div>
)
}
/* ─── Pulsing dot ────────────────────────────────────── */
function PulsingDot({
cx, cy, color, delay,
}: {
cx: number; cy: number; color: string; delay: number
}) {
return (
<g>
{/* Outer ring — pulses outward */}
<motion.circle
cx={cx} cy={cy} r={0.5}
fill="none"
stroke={color}
strokeWidth={0.3}
initial={{ scale: 1, opacity: 0.8 }}
animate={{ scale: 3.5, opacity: 0 }}
transition={{
duration: 2,
repeat: Infinity,
delay,
ease: 'easeOut',
}}
style={{ transformOrigin: `${cx}px ${cy}px` }}
/>
{/* Solid centre */}
<circle cx={cx} cy={cy} r={0.6} fill={color} opacity={0.9} />
<circle cx={cx} cy={cy} r={0.3} fill="white" opacity={0.6} />
</g>
)
}

95
src/data/events.ts Normal file
View File

@ -0,0 +1,95 @@
export type EventType = 'conference' | 'exhibition' | 'seminar' | 'international';
export interface Event {
id: string;
title: string;
date: number;
month: string;
year: number;
type: EventType;
location: string;
city: string;
registrationOpen: boolean;
description: string;
}
export const events: Event[] = [
{
id: 'iran-steel-congress-1403',
title: 'کنگره بین‌المللی فولاد ایران ۱۴۰۳',
date: 12,
month: 'اسفند',
year: 1403,
type: 'conference',
location: 'مرکز همایش‌های بین‌المللی تهران',
city: 'تهران',
registrationOpen: true,
description:
'بزرگ‌ترین گردهمایی سالانه متخصصان، سیاست‌گذاران و فعالان صنعت فولاد ایران با حضور ۱۲۰۰ نفر شرکت‌کننده از ۲۵ کشور.',
},
{
id: 'middle-east-steel-metals-2024',
title: 'Middle East Steel & Metals 2024',
date: 25,
month: 'اسفند',
year: 1403,
type: 'exhibition',
location: 'مرکز نمایشگاهی دبی ورلد ترید سنتر',
city: 'دبی',
registrationOpen: true,
description:
'نمایشگاه تخصصی فولاد و فلزات خاورمیانه با حضور بیش از ۳۰۰ شرکت از ۴۰ کشور؛ فرصتی برای معرفی ظرفیت‌های صادراتی صنعت فولاد ایران.',
},
{
id: 'steel-policy-seminar-1404',
title: 'نشست تخصصی سیاست‌گذاری فولاد ۱۴۰۴',
date: 8,
month: 'فروردین',
year: 1404,
type: 'seminar',
location: 'پژوهشکده توسعه صنعتی ایران',
city: 'تهران',
registrationOpen: true,
description:
'نشست تخصصی با حضور نمایندگان وزارت صمت، سازمان ایمیدرو و انجمن‌های تخصصی برای بررسی سیاست‌های سال ۱۴۰۴ در صنعت فولاد.',
},
{
id: 'world-steel-forum-2025',
title: 'World Steel Forum 2025',
date: 20,
month: 'فروردین',
year: 1404,
type: 'international',
location: 'مرکز کنفرانس برلین',
city: 'برلین',
registrationOpen: false,
description:
'اجلاس سالانه انجمن جهانی فولاد با تمرکز بر کربن‌زدایی، تجارت جهانی و تحولات ساختاری صنعت فولاد تا افق ۲۰۳۰.',
},
{
id: 'iran-metallurgy-expo-1404',
title: 'نمایشگاه متالورژی ایران',
date: 15,
month: 'اردیبهشت',
year: 1404,
type: 'exhibition',
location: 'نمایشگاه بین‌المللی اصفهان',
city: 'اصفهان',
registrationOpen: false,
description:
'نمایشگاه تخصصی متالورژی، فولاد و فلزات غیرآهنی ایران با محوریت فناوری‌های نوین تولید و تجهیزات صنعتی.',
},
{
id: 'oecd-steel-summit-2025',
title: 'اجلاس OECD فولاد',
date: 3,
month: 'خرداد',
year: 1404,
type: 'international',
location: 'دفتر مرکزی OECD',
city: 'پاریس',
registrationOpen: false,
description:
'نشست سالانه کمیته فولاد OECD برای بررسی مازاد ظرفیت جهانی، سیاست‌های تجاری و استانداردهای پایداری در صنعت فولاد.',
},
];

179
src/data/market.ts Normal file
View File

@ -0,0 +1,179 @@
export type PriceTrend = 'up' | 'down' | 'flat';
export interface MarketPrice {
id: string;
name: string;
value: number;
unit: string;
change: number;
changePercent: number;
trend: PriceTrend;
}
export interface PriceHistoryPoint {
month: string;
price: number;
}
export interface GlobalComparison {
country: string;
flag: string;
price: number;
unit: string;
currency: string;
}
export interface ExportData {
month: string;
tonnage: number;
}
export interface InstituteStats {
totalReports: number;
years: number;
experts: number;
memberOrgs: number;
subscribers: number;
}
export const marketPrices: MarketPrice[] = [
{
id: 'steel-billet',
name: 'شمش فولاد',
value: 18400,
unit: 'تومان/کیلو',
change: 220,
changePercent: 1.2,
trend: 'up',
},
{
id: 'rebar-a3',
name: 'میلگرد A3',
value: 17800,
unit: 'تومان/کیلو',
change: -90,
changePercent: -0.5,
trend: 'down',
},
{
id: 'beam-14',
name: 'تیرآهن ۱۴',
value: 16900,
unit: 'تومان/کیلو',
change: 135,
changePercent: 0.8,
trend: 'up',
},
{
id: 'scrap-iron',
name: 'آهن قراضه',
value: 550,
unit: 'دلار/تن',
change: -11,
changePercent: -2.0,
trend: 'down',
},
{
id: 'cold-rolled-sheet',
name: 'ورق سرد',
value: 22100,
unit: 'تومان/کیلو',
change: 66,
changePercent: 0.3,
trend: 'up',
},
{
id: 'angle-bar-80',
name: 'نبشی ۸۰',
value: 15600,
unit: 'تومان/کیلو',
change: -172,
changePercent: -1.1,
trend: 'down',
},
{
id: 'mobarakeh-steel',
name: 'فولاد مبارکه',
value: 12840,
unit: 'ریال',
change: -143,
changePercent: -1.1,
trend: 'down',
},
];
export const priceHistory: PriceHistoryPoint[] = [
{ month: 'خرداد ۱۴۰۳', price: 15200 },
{ month: 'تیر ۱۴۰۳', price: 15600 },
{ month: 'مرداد ۱۴۰۳', price: 16100 },
{ month: 'شهریور ۱۴۰۳', price: 15800 },
{ month: 'مهر ۱۴۰۳', price: 16400 },
{ month: 'آبان ۱۴۰۳', price: 17000 },
{ month: 'آذر ۱۴۰۳', price: 17500 },
{ month: 'دی ۱۴۰۳', price: 17200 },
{ month: 'بهمن ۱۴۰۳', price: 17800 },
{ month: 'اسفند ۱۴۰۳', price: 18100 },
{ month: 'فروردین ۱۴۰۴', price: 18600 },
{ month: 'اردیبهشت ۱۴۰۴', price: 18400 },
];
export const globalComparison: GlobalComparison[] = [
{
country: 'ایران',
flag: '🇮🇷',
price: 18400,
unit: 'تومان/کیلو',
currency: 'IRR',
},
{
country: 'ترکیه',
flag: '🇹🇷',
price: 530,
unit: 'دلار/تن',
currency: 'USD',
},
{
country: 'هند',
flag: '🇮🇳',
price: 510,
unit: 'دلار/تن',
currency: 'USD',
},
{
country: 'چین',
flag: '🇨🇳',
price: 460,
unit: 'دلار/تن',
currency: 'USD',
},
{
country: 'روسیه',
flag: '🇷🇺',
price: 485,
unit: 'دلار/تن',
currency: 'USD',
},
];
export const monthlyExport: ExportData[] = [
{ month: 'خرداد ۱۴۰۳', tonnage: 285000 },
{ month: 'تیر ۱۴۰۳', tonnage: 310000 },
{ month: 'مرداد ۱۴۰۳', tonnage: 295000 },
{ month: 'شهریور ۱۴۰۳', tonnage: 320000 },
{ month: 'مهر ۱۴۰۳', tonnage: 350000 },
{ month: 'آبان ۱۴۰۳', tonnage: 375000 },
{ month: 'آذر ۱۴۰۳', tonnage: 390000 },
{ month: 'دی ۱۴۰۳', tonnage: 360000 },
{ month: 'بهمن ۱۴۰۳', tonnage: 415000 },
{ month: 'اسفند ۱۴۰۳', tonnage: 420000 },
{ month: 'فروردین ۱۴۰۴', tonnage: 400000 },
{ month: 'اردیبهشت ۱۴۰۴', tonnage: 410000 },
];
export const stats: InstituteStats = {
totalReports: 280,
years: 12,
experts: 45,
memberOrgs: 60,
subscribers: 2800,
};

74
src/data/plans.ts Normal file
View File

@ -0,0 +1,74 @@
export type PlanCtaVariant = 'outline' | 'filled' | 'paper';
export interface Plan {
id: string;
name: string;
price: number;
priceLabel: string;
period: string;
features: string[];
badge?: string;
featured: boolean;
ctaLabel: string;
ctaVariant: PlanCtaVariant;
}
export const plans: Plan[] = [
{
id: 'individual',
name: 'فردی',
price: 350000,
priceLabel: '۳۵۰,۰۰۰ تومان',
period: 'ماهانه',
features: [
'دسترسی به تمام گزارش‌های پایه',
'دانلود ۵ گزارش در ماه',
'دریافت خبرنامه هفتگی',
'دسترسی به داده‌های قیمت لحظه‌ای',
'آرشیو ۱۲ ماه گذشته',
],
featured: false,
ctaLabel: 'شروع اشتراک',
ctaVariant: 'outline',
},
{
id: 'organizational',
name: 'سازمانی',
price: 1200000,
priceLabel: '۱,۲۰۰,۰۰۰ تومان',
period: 'ماهانه',
features: [
'دسترسی کامل به تمام گزارش‌ها',
'دانلود نامحدود گزارش',
'دسترسی برای ۵ کاربر همزمان',
'گزارش‌های ویژه و تحلیل‌های فوری',
'آرشیو کامل از ابتدای تأسیس',
'مشاوره تلفنی ماهانه با تحلیلگران',
'داشبورد اختصاصی تحلیل بازار',
],
badge: 'پرطرفدار',
featured: true,
ctaLabel: 'شروع اشتراک',
ctaVariant: 'filled',
},
{
id: 'ministry',
name: 'وزارتخانه',
price: 0,
priceLabel: 'تماس بگیرید',
period: 'سالانه',
features: [
'دسترسی نامحدود برای تمام کارکنان',
'سفارش گزارش‌های تحلیلی اختصاصی',
'نشست‌های تخصصی حضوری با تیم پژوهشی',
'پشتیبانی اولویت‌دار ۲۴/۷',
'داشبورد تصمیم‌گیری یکپارچه',
'API اتصال به سیستم‌های داخلی',
'آموزش و کارگاه‌های تخصصی دوره‌ای',
'گزارش‌دهی سفارشی برای سیاست‌گذاران',
],
featured: false,
ctaLabel: 'درخواست مشاوره',
ctaVariant: 'paper',
},
];

335
src/data/reports.ts Normal file
View File

@ -0,0 +1,335 @@
export type ReportCategory =
| 'بازار جهانی'
| 'سیاست‌گذاری'
| 'انرژی و ESG'
| 'صادرات'
| 'تولید داخلی'
| 'ریسک و بحران';
export type ReportType = 'quarterly' | 'flash' | 'risk' | 'free' | 'special';
export interface Report {
id: string;
title: string;
category: ReportCategory;
type: ReportType;
author: string;
authorRole: string;
authorInitial: string;
publishDate: string;
pages: number;
price: number;
isFree: boolean;
summary: string;
tags: string[];
featured: boolean;
}
export const reports: Report[] = [
{
id: 'iran-steel-global-trade-scenarios-1404',
title: 'ایران در نقشه جدید تجارت فولاد جهان: چهار سناریو برای ۱۴۰۴',
category: 'بازار جهانی',
type: 'special',
author: 'دکتر علی محمدی',
authorRole: 'مدیر ارشد پژوهش',
authorInitial: 'م',
publishDate: 'بهمن ۱۴۰۳',
pages: 84,
price: 850000,
isFree: false,
summary:
'این گزارش ویژه با تحلیل روندهای کلان اقتصاد جهانی، چهار سناریوی محتمل برای جایگاه صادرات فولاد ایران در سال ۱۴۰۴ را بررسی می‌کند. مدل‌سازی کمی بر اساس داده‌های تجارت ۱۵ کشور رقیب انجام شده و توصیه‌های سیاست‌گذاری مشخص ارائه می‌دهد. این گزارش مرجع اصلی تصمیم‌گیران و مدیران ارشد صنعت فولاد کشور است.',
tags: ['تجارت جهانی', 'سناریوسازی', 'صادرات', 'سیاست‌گذاری', '۱۴۰۴'],
featured: true,
},
{
id: 'steel-energy-consumption-efficiency-1403',
title: 'مصرف انرژی در فولادسازی ایران: مقایسه با استانداردهای جهانی',
category: 'انرژی و ESG',
type: 'free',
author: 'مهندس سارا رضایی',
authorRole: 'پژوهشگر ارشد انرژی',
authorInitial: 'ر',
publishDate: 'دی ۱۴۰۳',
pages: 28,
price: 0,
isFree: true,
summary:
'این گزارش رایگان مصرف انرژی واحدهای فولادسازی ایران را با میانگین جهانی و استانداردهای IEA مقایسه می‌کند. شکاف بهره‌وری انرژی در کوره‌های قوس الکتریکی و کنورتور شناسایی شده است. راهکارهای کاهش ۱۵ تا ۲۵ درصدی مصرف انرژی نیز پیشنهاد شده است.',
tags: ['انرژی', 'بهره‌وری', 'ESG', 'کوره قوس الکتریکی', 'استاندارد جهانی'],
featured: false,
},
{
id: 'global-steel-market-q3-1403',
title: 'گزارش فصلی بازار جهانی فولاد: پاییز ۱۴۰۳',
category: 'بازار جهانی',
type: 'quarterly',
author: 'دکتر حسین کریمی',
authorRole: 'تحلیلگر ارشد بازار',
authorInitial: 'ک',
publishDate: 'آذر ۱۴۰۳',
pages: 52,
price: 450000,
isFree: false,
summary:
'تحلیل جامع قیمت‌ها، تقاضا و عرضه در بازار جهانی فولاد طی فصل پاییز ۱۴۰۳ ارائه شده است. روند کاهش تقاضای چین و افزایش ظرفیت تولید هند به‌تفصیل بررسی شده است. چشم‌انداز قیمتی برای زمستان ۱۴۰۳ نیز ارائه می‌شود.',
tags: ['بازار جهانی', 'قیمت‌گذاری', 'چین', 'هند', 'پاییز ۱۴۰۳'],
featured: false,
},
{
id: 'iran-steel-export-sanctions-impact-1403',
title: 'اثر تحریم‌های جدید بر مسیرهای صادراتی فولاد ایران',
category: 'صادرات',
type: 'risk',
author: 'دکتر فاطمه احمدی',
authorRole: 'مدیر تحقیقات ریسک',
authorInitial: 'ا',
publishDate: 'آذر ۱۴۰۳',
pages: 36,
price: 380000,
isFree: false,
summary:
'این گزارش اثرات بسته تحریمی اخیر بر کانال‌های صادرات فولاد ایران به بازارهای منطقه‌ای را ارزیابی می‌کند. ریسک‌های لجستیکی، بانکی و قراردادی مستند شده است. راهکارهای جایگزین برای حفظ جریان صادرات پیشنهاد می‌شود.',
tags: ['تحریم', 'صادرات', 'ریسک', 'لجستیک', 'بازارهای منطقه‌ای'],
featured: false,
},
{
id: 'flash-iron-ore-price-surge-1403',
title: 'فلش: جهش قیمت سنگ‌آهن و تأثیر بر هزینه تمام‌شده فولاد ایران',
category: 'بازار جهانی',
type: 'flash',
author: 'مهندس رضا توکلی',
authorRole: 'تحلیلگر بازار کامودیتی',
authorInitial: 'ت',
publishDate: 'آبان ۱۴۰۳',
pages: 12,
price: 120000,
isFree: false,
summary:
'گزارش فوری درباره افزایش ۱۸ درصدی قیمت سنگ‌آهن در بورس دالیان و اثر فوری آن بر هزینه تمام‌شده تولید فولاد در ایران. برآورد اثر بر حاشیه سود کارخانه‌های داخلی ارائه شده است. توصیه‌های پوشش ریسک قیمتی برای مدیران خرید نیز مطرح است.',
tags: ['سنگ‌آهن', 'قیمت', 'هزینه تمام‌شده', 'فوری'],
featured: false,
},
{
id: 'domestic-production-dri-outlook-1403',
title: 'چشم‌انداز تولید آهن احیای مستقیم ایران تا افق ۱۴۰۵',
category: 'تولید داخلی',
type: 'quarterly',
author: 'دکتر علی محمدی',
authorRole: 'مدیر ارشد پژوهش',
authorInitial: 'م',
publishDate: 'مهر ۱۴۰۳',
pages: 44,
price: 420000,
isFree: false,
summary:
'ایران با بیش از ۳۰ میلیون تن ظرفیت DRI، رتبه اول جهان را دارد؛ این گزارش مسیر توسعه این صنعت تا ۱۴۰۵ را مدل‌سازی می‌کند. بررسی پروژه‌های در دست احداث و موانع تأمین گاز به‌تفصیل آمده است. ریسک‌های بهره‌برداری و پیشنهادهای بهینه‌سازی نیز ارائه شده است.',
tags: ['DRI', 'آهن احیای مستقیم', 'ظرفیت', 'تولید', 'گاز طبیعی'],
featured: false,
},
{
id: 'esg-carbon-emissions-steel-iran-1403',
title: 'کربن‌زدایی در صنعت فولاد ایران: الزامات و فرصت‌ها',
category: 'انرژی و ESG',
type: 'special',
author: 'مهندس سارا رضایی',
authorRole: 'پژوهشگر ارشد انرژی',
authorInitial: 'ر',
publishDate: 'شهریور ۱۴۰۳',
pages: 58,
price: 520000,
isFree: false,
summary:
'الزامات آینده CBAM اتحادیه اروپا و اثر آن بر رقابت‌پذیری صادرات فولاد ایران تحلیل شده است. مسیرهای فناوری کربن‌زدایی شامل هیدروژن سبز و CCS برای شرایط ایران ارزیابی شده‌اند. برنامه عملیاتی ده‌ساله برای کاهش شدت کربن پیشنهاد می‌شود.',
tags: ['ESG', 'کربن', 'CBAM', 'هیدروژن سبز', 'محیط زیست'],
featured: false,
},
{
id: 'policy-steel-pricing-reform-1403',
title: 'اصلاح ساختار قیمت‌گذاری فولاد: درس‌هایی از تجربه ترکیه و هند',
category: 'سیاست‌گذاری',
type: 'quarterly',
author: 'دکتر فاطمه احمدی',
authorRole: 'مدیر تحقیقات ریسک',
authorInitial: 'ا',
publishDate: 'مرداد ۱۴۰۳',
pages: 46,
price: 390000,
isFree: false,
summary:
'مقایسه تطبیقی ساختار قیمت‌گذاری فولاد در ایران، ترکیه و هند با تمرکز بر نقش دولت و مکانیسم‌های بازار. اثربخشی سیاست‌های قیمت‌گذاری دستوری در حفظ تعادل عرضه-تقاضا ارزیابی شده است. توصیه‌های سیاستی برای کاهش اعوجاج قیمتی ارائه می‌شود.',
tags: ['سیاست‌گذاری', 'قیمت‌گذاری', 'ترکیه', 'هند', 'اصلاح ساختار'],
featured: false,
},
{
id: 'flash-china-steel-overcapacity-1403',
title: 'فلش: مازاد ظرفیت فولاد چین و موج صادراتی به آسیای غربی',
category: 'بازار جهانی',
type: 'flash',
author: 'دکتر حسین کریمی',
authorRole: 'تحلیلگر ارشد بازار',
authorInitial: 'ک',
publishDate: 'تیر ۱۴۰۳',
pages: 10,
price: 110000,
isFree: false,
summary:
'چین در نیمه اول ۲۰۲۴ رکورد صادرات فولاد را با ۵۴ میلیون تن شکست. این گزارش فوری تأثیر مستقیم این موج صادراتی بر بازارهای ایران، عراق و امارات را تحلیل می‌کند. توصیه اقدام فوری برای کنسرسیوم‌های صادراتی ایران ارائه شده است.',
tags: ['چین', 'مازاد ظرفیت', 'صادرات', 'آسیای غربی', 'رقابت'],
featured: false,
},
{
id: 'risk-geopolitical-middle-east-steel-1403',
title: 'ریسک‌های ژئوپلیتیک خاورمیانه و زنجیره تأمین فولاد ایران',
category: 'ریسک و بحران',
type: 'risk',
author: 'دکتر فاطمه احمدی',
authorRole: 'مدیر تحقیقات ریسک',
authorInitial: 'ا',
publishDate: 'خرداد ۱۴۰۳',
pages: 40,
price: 360000,
isFree: false,
summary:
'تحلیل ریسک ژئوپلیتیک منطقه خاورمیانه بر امنیت زنجیره تأمین فولاد ایران از زاویه واردات مواد اولیه و صادرات محصول نهایی. سناریوهای بحران در مسیرهای دریایی خلیج فارس و دریای سرخ مدل‌سازی شده‌اند. برنامه تداوم کسب‌وکار برای تولیدکنندگان ارائه می‌شود.',
tags: ['ژئوپلیتیک', 'خاورمیانه', 'زنجیره تأمین', 'ریسک', 'دریای سرخ'],
featured: false,
},
{
id: 'export-iraq-market-analysis-1403',
title: 'تحلیل بازار فولاد عراق: فرصت‌های صادراتی ایران',
category: 'صادرات',
type: 'quarterly',
author: 'مهندس رضا توکلی',
authorRole: 'تحلیلگر بازار کامودیتی',
authorInitial: 'ت',
publishDate: 'اردیبهشت ۱۴۰۳',
pages: 38,
price: 340000,
isFree: false,
summary:
'عراق با تقاضای سالانه بیش از ۵ میلیون تن فولاد، مهم‌ترین بازار صادراتی ایران است. این گزارش رقابت‌پذیری فولاد ایران در برابر رقبای ترکیه، چین و اوکراین در بازار عراق را ارزیابی می‌کند. استراتژی‌های بازاریابی و توسعه بازار پیشنهاد شده است.',
tags: ['عراق', 'صادرات', 'بازار', 'رقابت‌پذیری', 'میلگرد'],
featured: false,
},
{
id: 'domestic-capacity-utilization-1402',
title: 'ظرفیت بهره‌برداری واحدهای فولادسازی ایران: آسیب‌شناسی و راهکار',
category: 'تولید داخلی',
type: 'quarterly',
author: 'دکتر علی محمدی',
authorRole: 'مدیر ارشد پژوهش',
authorInitial: 'م',
publishDate: 'فروردین ۱۴۰۳',
pages: 50,
price: 410000,
isFree: false,
summary:
'میانگین ضریب بهره‌برداری کارخانه‌های فولادسازی ایران به ۶۸ درصد رسیده که پایین‌تر از میانگین جهانی ۷۶ درصد است. این گزارش موانع عملیاتی، تأمین مواد اولیه و بازار را شناسایی کرده است. برنامه‌های بهبود بهره‌برداری در کوتاه‌مدت و میان‌مدت ارائه می‌شود.',
tags: ['ظرفیت', 'بهره‌برداری', 'کارایی', 'تولید', 'کارخانه'],
featured: false,
},
{
id: 'flash-rebar-demand-construction-1402',
title: 'فلش: کاهش تقاضای میلگرد در بخش ساختمانی و اثر بر کارخانه‌ها',
category: 'تولید داخلی',
type: 'flash',
author: 'مهندس سارا رضایی',
authorRole: 'پژوهشگر ارشد انرژی',
authorInitial: 'ر',
publishDate: 'اسفند ۱۴۰۲',
pages: 14,
price: 115000,
isFree: false,
summary:
'رکود در بازار مسکن منجر به کاهش ۱۲ درصدی تقاضای میلگرد در زمستان ۱۴۰۲ شده است. این گزارش فوری اثر این کاهش بر درآمد و برنامه تولید کارخانه‌های میلگردساز را ارزیابی می‌کند. توصیه تنظیم پورتفولیوی محصول برای جبران افت تقاضا مطرح شده است.',
tags: ['میلگرد', 'ساختمان', 'تقاضا', 'رکود', 'تولید داخلی'],
featured: false,
},
{
id: 'policy-privatization-steel-companies-1402',
title: 'واگذاری شرکت‌های فولادی دولتی: ارزیابی تجربه یک دهه خصوصی‌سازی',
category: 'سیاست‌گذاری',
type: 'special',
author: 'دکتر حسین کریمی',
authorRole: 'تحلیلگر ارشد بازار',
authorInitial: 'ک',
publishDate: 'بهمن ۱۴۰۲',
pages: 62,
price: 560000,
isFree: false,
summary:
'ارزیابی جامع نتایج خصوصی‌سازی شرکت‌های فولادی بزرگ ایران در یک دهه گذشته با شاخص‌های بهره‌وری، اشتغال و سرمایه‌گذاری. تجربه‌های موفق و ناموفق واگذاری مستند شده است. الگوی بهینه مشارکت دولتی-خصوصی در صنعت فولاد پیشنهاد می‌شود.',
tags: ['خصوصی‌سازی', 'سیاست‌گذاری', 'دولت', 'مدیریت', 'بهره‌وری'],
featured: false,
},
{
id: 'risk-water-scarcity-steel-industry-1402',
title: 'بحران آب و تهدیدات پایداری صنعت فولاد ایران',
category: 'ریسک و بحران',
type: 'risk',
author: 'مهندس سارا رضایی',
authorRole: 'پژوهشگر ارشد انرژی',
authorInitial: 'ر',
publishDate: 'دی ۱۴۰۲',
pages: 42,
price: 370000,
isFree: false,
summary:
'استرس آبی در کلاستر فولادسازی اصفهان-یزد-خراسان به نقطه بحرانی رسیده است. این گزارش ریسک تعطیلی یا کاهش تولید ناشی از کمبود آب را برای ۱۴ واحد تولیدی ارزیابی می‌کند. راهکارهای فناوری و مدیریتی برای کاهش مصرف آب پیشنهاد شده است.',
tags: ['آب', 'بحران', 'پایداری', 'اصفهان', 'ریسک محیط زیستی'],
featured: false,
},
{
id: 'global-market-q1-1402',
title: 'گزارش فصلی بازار جهانی فولاد: بهار ۱۴۰۲',
category: 'بازار جهانی',
type: 'quarterly',
author: 'دکتر علی محمدی',
authorRole: 'مدیر ارشد پژوهش',
authorInitial: 'م',
publishDate: 'تیر ۱۴۰۲',
pages: 54,
price: 440000,
isFree: false,
summary:
'بررسی روندهای بازار جهانی فولاد در بهار ۱۴۰۲ با تمرکز بر بازگشایی اقتصاد چین و اثرات آن بر قیمت‌ها. تأثیر افزایش نرخ بهره فدرال رزرو بر سرمایه‌گذاری در ساختمان و زیرساخت تحلیل شده است. پیش‌بینی روند قیمتی تا پایان ۱۴۰۲ ارائه می‌شود.',
tags: ['بازار جهانی', 'چین', 'نرخ بهره', 'تقاضا', 'بهار ۱۴۰۲'],
featured: false,
},
{
id: 'export-africa-emerging-markets-1402',
title: 'فرصت‌های صادرات فولاد ایران به بازارهای نوظهور آفریقا',
category: 'صادرات',
type: 'special',
author: 'دکتر فاطمه احمدی',
authorRole: 'مدیر تحقیقات ریسک',
authorInitial: 'ا',
publishDate: 'آبان ۱۴۰۲',
pages: 48,
price: 480000,
isFree: false,
summary:
'آفریقا با رشد سالانه ۶ درصد در تقاضای فولاد، بازار استراتژیک بلندمدت برای صادرکنندگان ایران محسوب می‌شود. این گزارش ۸ بازار هدف اولویت‌دار آفریقایی را با تحلیل ریسک و فرصت معرفی می‌کند. مدل ورود به بازار و شرکای تجاری محتمل نیز پیشنهاد شده است.',
tags: ['آفریقا', 'بازارهای نوظهور', 'صادرات', 'تنوع‌سازی', 'بلندمدت'],
featured: false,
},
{
id: 'flash-gas-price-hike-steel-cost-1403',
title: 'فلش: اثر افزایش قیمت گاز بر قیمت تمام‌شده فولاد ایران',
category: 'انرژی و ESG',
type: 'flash',
author: 'مهندس رضا توکلی',
authorRole: 'تحلیلگر بازار کامودیتی',
authorInitial: 'ت',
publishDate: 'شهریور ۱۴۰۳',
pages: 11,
price: 105000,
isFree: false,
summary:
'افزایش ۳۰ درصدی قیمت گاز صنعتی در مهر ۱۴۰۳ هزینه تولید هر تن DRI را حدود ۱۲۰۰ تومان افزایش می‌دهد. این گزارش فوری اثر این شوک هزینه‌ای بر حاشیه سود تولیدکنندگان DRI و فولاد را محاسبه می‌کند. راهکارهای کوتاه‌مدت برای مدیریت این افزایش هزینه پیشنهاد شده است.',
tags: ['گاز', 'انرژی', 'هزینه تولید', 'DRI', 'قیمت‌گذاری'],
featured: false,
},
];

109
src/data/team.ts Normal file
View File

@ -0,0 +1,109 @@
export interface TeamMember {
id: string;
name: string;
role: string;
initial: string;
bio: string;
expertise: string[];
reportCount: number;
email?: string;
}
export const team: TeamMember[] = [
{
id: 'ali-mohammadi',
name: 'دکتر علی محمدی',
role: 'مدیر ارشد پژوهش',
initial: 'م',
bio: 'دکتری اقتصاد صنعتی از دانشگاه تهران با بیش از ۱۸ سال تجربه در تحلیل بازارهای فولاد و فلزات. پیش از پیوستن به اندیشکده، مشاور ارشد سازمان توسعه و نوسازی معادن و صنایع معدنی ایران (ایمیدرو) بوده است. بیش از ۵۰ گزارش تحلیلی و ۱۲ مقاله علمی در حوزه اقتصاد فولاد منتشر کرده است.',
expertise: [
'اقتصاد صنعتی',
'تحلیل بازار فولاد',
'سیاست‌گذاری صنعتی',
'مدل‌سازی اقتصادی',
'تجارت بین‌الملل',
],
reportCount: 52,
email: 'mohammadi@andishkade-foolad.ir',
},
{
id: 'fateme-ahmadi',
name: 'دکتر فاطمه احمدی',
role: 'مدیر تحقیقات ریسک',
initial: 'ا',
bio: 'دکتری مدیریت ریسک از دانشگاه علم و صنعت ایران با تخصص در ارزیابی ریسک‌های ژئوپلیتیک و زنجیره تأمین. سابقه مشاوره به چندین شرکت بزرگ فولادی کشور در حوزه مدیریت ریسک تجاری و قراردادی را دارد. نویسنده کتاب «مدیریت ریسک در صنایع معدنی و فلزی» است.',
expertise: [
'مدیریت ریسک',
'تحریم‌پژوهی',
'ژئوپلیتیک انرژی',
'تأمین مالی پروژه',
'مذاکرات تجاری',
],
reportCount: 38,
email: 'ahmadi@andishkade-foolad.ir',
},
{
id: 'hosein-karimi',
name: 'دکتر حسین کریمی',
role: 'تحلیلگر ارشد بازار',
initial: 'ک',
bio: 'دکتری مهندسی مواد از دانشگاه صنعتی اصفهان با تخصص ترکیبی در متالورژی و اقتصاد بازار کامودیتی‌ها. پنج سال سابقه کار در واحد پژوهش و توسعه فولاد مبارکه اصفهان دارد. به‌طور منظم گزارش‌های فصلی بازار جهانی فولاد را تهیه و منتشر می‌کند.',
expertise: [
'بازار جهانی فولاد',
'قیمت‌گذاری کامودیتی',
'متالورژی صنعتی',
'پیش‌بینی تقاضا',
'بازارهای آسیایی',
],
reportCount: 41,
email: 'karimi@andishkade-foolad.ir',
},
{
id: 'sara-rezaei',
name: 'مهندس سارا رضایی',
role: 'پژوهشگر ارشد انرژی',
initial: 'ر',
bio: 'کارشناسی ارشد مهندسی انرژی از دانشگاه شریف با تمرکز بر بهینه‌سازی مصرف انرژی در صنایع سنگین. تجربه عملیاتی در حسابرسی انرژی کارخانه‌های فولادی در اصفهان، خراسان و هرمزگان دارد. پژوهش‌های او در حوزه ESG و کربن‌زدایی صنعت فولاد ایران از مراجع اصلی این حوزه است.',
expertise: [
'انرژی صنعتی',
'ESG و پایداری',
'کربن‌زدایی',
'بهره‌وری انرژی',
'مقررات زیست‌محیطی',
],
reportCount: 29,
email: 'rezaei@andishkade-foolad.ir',
},
{
id: 'reza-tavakoli',
name: 'مهندس رضا توکلی',
role: 'تحلیلگر بازار کامودیتی',
initial: 'ت',
bio: 'کارشناسی ارشد اقتصاد از دانشگاه تهران با تخصص در تحلیل بنیادی و تکنیکال بازارهای کامودیتی. سابقه فعالیت در کارگزاری‌های بورس کالا و تهیه گزارش‌های فوری برای معامله‌گران فولاد را دارد. مسئول پوشش خبری و تحلیلی بازارهای بورس دالیان، LME و بازار داخلی است.',
expertise: [
'بورس کالا',
'تحلیل تکنیکال',
'بازارهای آتی',
'قیمت‌گذاری سنگ‌آهن',
'بازار داخلی فولاد',
],
reportCount: 34,
email: 'tavakoli@andishkade-foolad.ir',
},
{
id: 'maryam-hosseini',
name: 'دکتر مریم حسینی',
role: 'پژوهشگر ارشد صادرات',
initial: 'ح',
bio: 'دکتری بازاریابی بین‌الملل از دانشگاه الزهرا با تجربه گسترده در مطالعات بازارهای صادراتی آسیا، آفریقا و اروپای شرقی. پیش از این در اتاق بازرگانی ایران و کمیته فولاد آن مشغول به فعالیت بوده است. حوزه تخصصی او توسعه بازارهای صادراتی جدید و استراتژی‌های نفوذ به بازار است.',
expertise: [
'بازاریابی بین‌الملل',
'توسعه صادرات',
'بازار آفریقا و آسیا',
'استراتژی بازار',
'مذاکرات تجاری',
],
reportCount: 22,
email: 'hosseini@andishkade-foolad.ir',
},
];

239
src/index.css Normal file
View File

@ -0,0 +1,239 @@
@import "tailwindcss";
/*
VAZIR FONT FACES
*/
@font-face {
font-family: 'Vazir';
src: url('/fonts/Vazir-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Vazir';
src: url('/fonts/Vazir-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Vazir';
src: url('/fonts/Vazir-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Vazir';
src: url('/fonts/Vazir-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Vazir';
src: url('/fonts/Vazir-Black.woff2') format('woff2');
font-weight: 900;
font-style: normal;
font-display: swap;
}
/*
DESIGN TOKENS
*/
:root {
--paper: #f4efe7;
--paper-2: #ede8de;
--paper-3: #e4ddd0;
--ink: #1a1712;
--ink-2: #2e2a25;
--ink-3: #4a4540;
--ink-4: #6b6560;
--ink-5: #766e68;
--ink-6: #c8c2bc;
--rule-thin: rgba(26,23,18,0.12);
--red: #9b1c1c;
--red-2: #b52020;
--amber: #92400e;
--green-ink: #166534;
}
/*
TYPOGRAPHY SYSTEM
Scale (clamp: mobile desktop)
--fs-display hero headlines 32 72px
--fs-h1 page-level headings 26 52px
--fs-h2 section headings 20 38px
--fs-h3 card/sub headings 15 22px
--fs-h4 small headings 13 17px
--fs-body-lg lead paragraphs 14 16px
--fs-body body text 13 15px
--fs-sm secondary / meta 11 13px
--fs-xs labels / captions 9 11px
*/
:root {
/* Font sizes */
--fs-display: clamp(32px, 5.5vw, 72px);
--fs-h1: clamp(26px, 3.8vw, 52px);
--fs-h2: clamp(20px, 2.8vw, 38px);
--fs-h3: clamp(15px, 1.5vw, 22px);
--fs-h4: clamp(13px, 1.1vw, 17px);
--fs-body-lg: clamp(14px, 1.1vw, 16px);
--fs-body: clamp(13px, 0.9vw, 15px);
--fs-sm: clamp(11px, 0.75vw, 13px);
--fs-xs: clamp(9px, 0.6vw, 11px);
/* Letter spacing */
--ls-display: -2.5px;
--ls-h1: -2px;
--ls-h2: -1px;
--ls-h3: -0.3px;
--ls-body: 0px;
--ls-label: 1.5px;
--ls-overline: 2.5px;
--ls-widest: 3px;
/* Line heights */
--lh-display: 1.04;
--lh-heading: 1.12;
--lh-subhead: 1.3;
--lh-body: 1.8;
--lh-relaxed: 1.85;
--lh-label: 1.3;
}
/* ── Base element styles ──────────────────────── */
h1 {
font-size: var(--fs-h1);
font-weight: 900;
line-height: var(--lh-heading);
letter-spacing: var(--ls-h1);
color: var(--ink);
margin: 0;
}
h2 {
font-size: var(--fs-h2);
font-weight: 900;
line-height: var(--lh-heading);
letter-spacing: var(--ls-h2);
color: var(--ink);
margin: 0;
}
h3 {
font-size: var(--fs-h3);
font-weight: 700;
line-height: var(--lh-subhead);
letter-spacing: var(--ls-h3);
color: var(--ink);
margin: 0;
}
h4 {
font-size: var(--fs-h4);
font-weight: 700;
line-height: var(--lh-subhead);
color: var(--ink);
margin: 0;
}
p {
font-size: var(--fs-body);
font-weight: 400; /* was 300 — too thin on most screens */
line-height: var(--lh-body);
color: var(--ink-3);
margin: 0;
}
/*
BASE
*/
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; direction: rtl; }
body {
font-family: 'Vazir', system-ui, sans-serif;
background: var(--paper);
color: var(--ink);
font-size: var(--fs-body);
line-height: var(--lh-body);
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
#root { min-height: 100dvh; display: flex; flex-direction: column; }
::-webkit-scrollbar { width: 3px; height: 3px; }
::-webkit-scrollbar-thumb { background: var(--ink); }
:focus-visible {
outline: 2px solid var(--red);
outline-offset: 2px;
}
::selection { background: var(--ink); color: var(--paper); }
/*
TYPOGRAPHY UTILITIES
*/
/* Overline — small uppercase tracking label */
.t-overline {
font-size: var(--fs-xs);
font-weight: 700;
letter-spacing: var(--ls-overline);
text-transform: uppercase;
line-height: var(--lh-label);
}
/* Caption / meta text */
.t-caption {
font-size: var(--fs-sm);
font-weight: 500;
line-height: var(--lh-label);
color: var(--ink-4);
}
/* Lead paragraph */
.t-lead {
font-size: var(--fs-body-lg);
font-weight: 400;
line-height: var(--lh-relaxed);
color: var(--ink-3);
}
/*
TICKER ANIMATION (RTL direction)
*/
@keyframes ticker-rtl {
0% { transform: translateX(50%); }
100% { transform: translateX(-50%); }
}
.ticker-track { animation: ticker-rtl 34s linear infinite; white-space: nowrap; }
@media (prefers-reduced-motion: reduce) { .ticker-track { animation: none; } }
/*
STAMP TILT
*/
.stamp-tilt { transform: rotate(-1.5deg); }
/*
INFINITE SCROLL ANIMATION
*/
@keyframes scroll {
to { transform: translateX(calc(-50% - 0.5rem)); }
}
.animate-scroll {
animation: scroll var(--animation-duration, 60s) var(--animation-direction, forwards) linear infinite;
}
.animate-scroll-risk {
animation: scroll 72s forwards linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-scroll { animation: none; }
.animate-scroll-risk { animation: none; }
}
/*
PRINT
*/
@media print { .no-print { display: none !important; } }

96
src/lib/utils.ts Normal file
View File

@ -0,0 +1,96 @@
/**
* Shared utility functions Persian/Jalali formatting & helpers
*/
/**
* Format a number as Persian locale (e.g. 18400 "۱۸,۴۰۰")
*/
export function formatPersianNumber(n: number): string {
return n.toLocaleString('fa-IR')
}
/**
* Format a Toman price (e.g. 850000 "۸۵۰,۰۰۰ تومان")
*/
export function formatPrice(n: number): string {
if (n === 0) return 'رایگان'
return `${n.toLocaleString('fa-IR')} تومان`
}
/**
* Format a price compactly (e.g. 1200000 "۱.۲ م.ت")
*/
export function formatPriceCompact(n: number): string {
if (n === 0) return 'رایگان'
if (n >= 1_000_000) {
const m = n / 1_000_000
return `${m.toLocaleString('fa-IR')} م.ت`
}
if (n >= 1_000) {
const k = n / 1_000
return `${k.toLocaleString('fa-IR')} ه`
}
return `${n.toLocaleString('fa-IR')} تومان`
}
/**
* Get CSS color for trend direction
*/
export function trendColor(trend: 'up' | 'down' | 'flat'): string {
if (trend === 'up') return 'var(--green-ink)'
if (trend === 'down') return 'var(--red)'
return 'var(--ink-4)'
}
/**
* Trend arrow character
*/
export function trendArrow(trend: 'up' | 'down' | 'flat'): string {
if (trend === 'up') return '↑'
if (trend === 'down') return '↓'
return '—'
}
/**
* Truncate string to max length with ellipsis
*/
export function truncate(str: string, max: number): string {
if (str.length <= max) return str
return str.slice(0, max).trimEnd() + '...'
}
/**
* Get category dot color CSS variable
*/
export function categoryColor(category: string): string {
const map: Record<string, string> = {
'بازار جهانی': 'var(--ink)',
'سیاست‌گذاری': 'var(--ink-3)',
'انرژی و ESG': 'var(--amber)',
'صادرات': 'var(--ink-2)',
'تولید داخلی': 'var(--ink)',
'ریسک و بحران': 'var(--red)',
}
return map[category] ?? 'var(--ink-5)'
}
/**
* Get report type label in Persian
*/
export function reportTypeLabel(type: string): string {
const map: Record<string, string> = {
quarterly: 'گزارش فصلی',
flash: 'Flash Report',
risk: 'گزارش ریسک',
free: 'رایگان',
special: 'گزارش ویژه',
}
return map[type] ?? type
}
/**
* Clamp a value between min and max
*/
export function clamp(val: number, min: number, max: number): number {
return Math.min(Math.max(val, min), max)
}

11
src/main.tsx Normal file
View File

@ -0,0 +1,11 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import './index.css'
import { router } from './app/router'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)

View File

@ -0,0 +1,430 @@
import { useState, useMemo } from 'react'
import { Link } from 'react-router-dom'
import { reports } from '@/data/reports'
import type { ReportCategory, ReportType } from '@/data/reports'
/* ─── constants ──────────────────────────────────────────── */
const TYPE_LABELS: Record<ReportType, string> = {
quarterly: 'فصلی',
flash: 'فلش',
risk: 'ریسک',
free: 'رایگان',
special: 'ویژه',
}
const TYPE_COLORS: Record<ReportType, string> = {
quarterly: '#166534',
flash: '#9b1c1c',
risk: '#92400e',
free: '#1d4ed8',
special: '#4a1d96',
}
/* Persian month order for sorting date groups */
const MONTH_ORDER: Record<string, number> = {
'فروردین': 1, 'اردیبهشت': 2, 'خرداد': 3, 'تیر': 4,
'مرداد': 5, 'شهریور': 6, 'مهر': 7, 'آبان': 8,
'آذر': 9, 'دی': 10, 'بهمن': 11, 'اسفند': 12,
}
function parseDateGroup(dateStr: string): number {
const parts = dateStr.trim().split(' ')
if (parts.length < 2) return 0
const month = MONTH_ORDER[parts[0]] ?? 0
const year = parseInt(parts[1]) || 0
return year * 100 + month
}
/* ─── unique categories from data ───────────────────────── */
const ALL_CATEGORIES: (ReportCategory | 'همه')[] = [
'همه',
...Array.from(new Set(reports.map((r) => r.category))) as ReportCategory[],
]
/* ─── price formatter ────────────────────────────────────── */
function formatPrice(price: number): string {
if (price === 0) return 'رایگان'
return price.toLocaleString('fa-IR') + ' ت'
}
/* ─── report row ─────────────────────────────────────────── */
function ReportRow({ report }: { report: (typeof reports)[0] }) {
const [hovered, setHovered] = useState(false)
const typeColor = TYPE_COLORS[report.type]
const typeLabel = TYPE_LABELS[report.type]
return (
<div
style={{
display: 'grid',
gridTemplateColumns: '64px 1fr 140px 52px 80px',
alignItems: 'center',
borderBottom: '1px solid var(--rule-thin)',
background: hovered ? 'var(--paper-2)' : 'transparent',
transition: 'background 100ms',
minHeight: 40,
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{/* type label */}
<div
style={{
padding: '8px 12px',
borderLeft: '1px solid var(--rule-thin)',
}}
>
<span
style={{
fontSize: 9,
fontWeight: 700,
letterSpacing: '0.8px',
color: typeColor,
border: `1px solid ${typeColor}`,
padding: '2px 5px',
whiteSpace: 'nowrap',
}}
>
{typeLabel}
</span>
</div>
{/* title */}
<div style={{ padding: '8px 16px', borderLeft: '1px solid var(--rule-thin)', overflow: 'hidden' }}>
<Link
to={`/reports/${report.id}`}
style={{
fontSize: 13,
fontWeight: 700,
color: hovered ? 'var(--red)' : 'var(--ink)',
textDecoration: 'none',
transition: 'color 100ms',
display: 'block',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{report.title}
</Link>
</div>
{/* author */}
<div
style={{
padding: '8px 12px',
borderLeft: '1px solid var(--rule-thin)',
overflow: 'hidden',
}}
>
<span
style={{
fontSize: 11,
color: 'var(--ink-5)',
fontWeight: 400,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: 'block',
}}
>
{report.author}
</span>
</div>
{/* pages */}
<div
style={{
padding: '8px 10px',
borderLeft: '1px solid var(--rule-thin)',
textAlign: 'center',
}}
>
<span style={{ fontSize: 11, color: 'var(--ink-5)', fontWeight: 400 }}>
{report.pages.toLocaleString('fa-IR')} ص
</span>
</div>
{/* price */}
<div style={{ padding: '8px 12px', textAlign: 'left' }}>
<span
style={{
fontSize: 11,
fontWeight: 700,
color: report.isFree ? 'var(--green-ink)' : 'var(--ink-3)',
whiteSpace: 'nowrap',
}}
>
{formatPrice(report.price)}
</span>
</div>
</div>
)
}
/* ─── main component ─────────────────────────────────────── */
export default function Archive() {
const [search, setSearch] = useState('')
const [activeCategory, setActiveCategory] = useState<ReportCategory | 'همه'>('همه')
const filtered = useMemo(() => {
let result = [...reports]
if (activeCategory !== 'همه') {
result = result.filter((r) => r.category === activeCategory)
}
if (search.trim()) {
const q = search.trim().toLowerCase()
result = result.filter(
(r) =>
r.title.toLowerCase().includes(q) ||
r.author.toLowerCase().includes(q) ||
r.category.toLowerCase().includes(q) ||
r.tags.some((t) => t.toLowerCase().includes(q))
)
}
return result
}, [search, activeCategory])
/* group by publishDate, sorted newest first */
const groups = useMemo(() => {
const map = new Map<string, (typeof reports)[0][]>()
for (const report of filtered) {
const key = report.publishDate
if (!map.has(key)) map.set(key, [])
map.get(key)!.push(report)
}
return Array.from(map.entries()).sort(
([a], [b]) => parseDateGroup(b) - parseDateGroup(a)
)
}, [filtered])
return (
<div
style={{
maxWidth: 1280,
margin: '0 auto',
padding: '64px 48px',
direction: 'rtl',
}}
>
{/* ─── Page header ───────────────────────────────────── */}
<div
style={{
display: 'flex',
alignItems: 'stretch',
borderBottom: '3px solid var(--ink)',
marginBottom: 36,
}}
>
<div
style={{
background: 'var(--ink)',
padding: '14px 28px',
display: 'flex',
alignItems: 'center',
gap: 14,
}}
>
<h1
style={{
fontSize: 24,
fontWeight: 900,
color: 'var(--paper)',
letterSpacing: '-0.5px',
whiteSpace: 'nowrap',
}}
>
آرشیو گزارشها
</h1>
<span
style={{
fontSize: 12,
fontWeight: 700,
color: 'var(--ink)',
background: 'var(--paper)',
padding: '2px 10px',
letterSpacing: '0.3px',
}}
>
{reports.length.toLocaleString('fa-IR')} گزارش
</span>
</div>
<div style={{ flex: 1 }} />
</div>
{/* ─── Search ────────────────────────────────────────── */}
<div style={{ marginBottom: 20 }}>
<input
type="text"
placeholder="جستجو در آرشیو..."
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{
width: '100%',
border: '1px solid var(--ink)',
borderRadius: 0,
padding: '12px 18px',
fontSize: 14,
fontFamily: 'inherit',
background: 'transparent',
color: 'var(--ink)',
outline: 'none',
direction: 'rtl',
}}
/>
</div>
{/* ─── Category filter chips ─────────────────────────── */}
<div
style={{
display: 'flex',
gap: 6,
flexWrap: 'wrap',
marginBottom: 40,
paddingBottom: 20,
borderBottom: '1px solid var(--rule-thin)',
}}
>
{ALL_CATEGORIES.map((cat) => {
const isActive = activeCategory === cat
return (
<button
key={cat}
onClick={() => setActiveCategory(cat as ReportCategory | 'همه')}
style={{
padding: '6px 16px',
fontSize: 12,
fontWeight: isActive ? 700 : 500,
border: isActive ? '1px solid var(--ink)' : '1px solid var(--rule-thin)',
background: isActive ? 'var(--ink)' : 'transparent',
color: isActive ? 'var(--paper)' : 'var(--ink-4)',
cursor: 'pointer',
fontFamily: 'inherit',
whiteSpace: 'nowrap',
transition: 'all 120ms',
outline: 'none',
borderRadius: 0,
}}
>
{cat}
</button>
)
})}
</div>
{/* ─── Date groups ───────────────────────────────────── */}
{groups.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
{groups.map(([date, groupReports]) => (
<section key={date}>
{/* section divider */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 14,
marginBottom: 0,
}}
>
<span
style={{
fontSize: 9,
fontWeight: 700,
letterSpacing: '2px',
textTransform: 'uppercase',
color: 'var(--ink-5)',
whiteSpace: 'nowrap',
}}
>
{date}
</span>
<div
style={{
flex: 1,
height: 1,
background: 'var(--rule-thin)',
}}
/>
<span style={{ fontSize: 9, color: 'var(--ink-6)', whiteSpace: 'nowrap' }}>
{groupReports.length.toLocaleString('fa-IR')} گزارش
</span>
</div>
{/* column headers */}
<div
style={{
display: 'grid',
gridTemplateColumns: '64px 1fr 140px 52px 80px',
alignItems: 'center',
borderTop: '2px solid var(--ink)',
borderBottom: '1px solid var(--ink)',
marginTop: 8,
background: 'var(--paper-2)',
}}
>
{[
{ label: 'نوع', style: { padding: '6px 12px', borderLeft: '1px solid var(--rule-thin)' } },
{ label: 'عنوان گزارش', style: { padding: '6px 16px', borderLeft: '1px solid var(--rule-thin)' } },
{ label: 'نویسنده', style: { padding: '6px 12px', borderLeft: '1px solid var(--rule-thin)' } },
{ label: 'صفحه', style: { padding: '6px 10px', borderLeft: '1px solid var(--rule-thin)', textAlign: 'center' as const } },
{ label: 'قیمت', style: { padding: '6px 12px', textAlign: 'left' as const } },
].map((col) => (
<div key={col.label} style={col.style}>
<span
style={{
fontSize: 9,
fontWeight: 700,
letterSpacing: '1.2px',
color: 'var(--ink-4)',
textTransform: 'uppercase',
}}
>
{col.label}
</span>
</div>
))}
</div>
{/* report rows */}
<div
style={{
border: '1px solid var(--rule-thin)',
borderTop: 'none',
}}
>
{groupReports.map((report) => (
<ReportRow key={report.id} report={report} />
))}
</div>
</section>
))}
</div>
) : (
<div
style={{
padding: '80px 0',
textAlign: 'center',
color: 'var(--ink-5)',
}}
>
<p style={{ fontSize: 15, fontWeight: 300 }}>نتیجهای یافت نشد</p>
<p style={{ fontSize: 13, marginTop: 8, color: 'var(--ink-6)' }}>
عبارت جستجو یا فیلتر دستهبندی را تغییر دهید
</p>
</div>
)}
{/* responsive: hide author/pages columns on small screens */}
<style>{`
@media (max-width: 767px) {
.archive-row {
grid-template-columns: 56px 1fr 70px !important;
}
}
`}</style>
</div>
)
}

367
src/pages/Events/Events.tsx Normal file
View File

@ -0,0 +1,367 @@
import { useState } from 'react'
import { events } from '@/data/events'
import type { EventType } from '@/data/events'
/* ─── constants ──────────────────────────────────────────── */
type FilterValue = 'همه' | EventType
const FILTERS: { label: string; value: FilterValue }[] = [
{ label: 'همه', value: 'همه' },
{ label: 'کنگره', value: 'conference' },
{ label: 'نمایشگاه', value: 'exhibition' },
{ label: 'سمینار', value: 'seminar' },
{ label: 'جهانی', value: 'international' },
]
const TYPE_LABELS: Record<EventType, string> = {
conference: 'کنگره',
exhibition: 'نمایشگاه',
seminar: 'سمینار',
international: 'جهانی',
}
const TYPE_BORDER_COLORS: Record<EventType, string> = {
conference: 'var(--ink)',
exhibition: '#6b4a00',
seminar: '#1a4a2a',
international: 'var(--red)',
}
/* ─── event card ─────────────────────────────────────────── */
function EventCard({ event }: { event: (typeof events)[0] }) {
const [hovered, setHovered] = useState(false)
const borderColor = TYPE_BORDER_COLORS[event.type]
return (
<article
style={{
border: '1px solid var(--rule-thin)',
background: hovered ? 'var(--paper-2)' : 'var(--paper)',
transition: 'background 140ms',
display: 'flex',
flexDirection: 'column',
gap: 0,
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{/* date + type header */}
<div style={{ display: 'flex', borderBottom: '2px solid var(--ink)' }}>
{/* date block */}
<div
style={{
padding: '18px 24px',
borderLeft: '1px solid var(--rule-thin)',
minWidth: 100,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--paper)',
flexShrink: 0,
}}
>
<span
style={{
fontSize: 52,
fontWeight: 900,
lineHeight: 1,
color: 'var(--ink)',
letterSpacing: '-2px',
display: 'block',
}}
>
{event.date.toLocaleString('fa-IR')}
</span>
<span
style={{
fontSize: 11,
fontWeight: 500,
color: 'var(--ink-4)',
marginTop: 4,
display: 'block',
whiteSpace: 'nowrap',
}}
>
{event.month} {event.year.toLocaleString('fa-IR')}
</span>
</div>
{/* type badge + title area top */}
<div
style={{
flex: 1,
padding: '18px 20px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: 8,
}}
>
<span
style={{
display: 'inline-block',
alignSelf: 'flex-start',
fontSize: 10,
fontWeight: 700,
letterSpacing: '1.2px',
color: borderColor,
border: `1px solid ${borderColor}`,
padding: '2px 8px',
}}
>
{TYPE_LABELS[event.type]}
</span>
<h3
style={{
fontSize: 16,
fontWeight: 800,
lineHeight: 1.45,
color: 'var(--ink)',
letterSpacing: '-0.3px',
}}
>
{event.title}
</h3>
</div>
</div>
{/* body */}
<div style={{ padding: '16px 20px', flex: 1, display: 'flex', flexDirection: 'column', gap: 10 }}>
{/* location */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span
style={{
width: 4,
height: 4,
background: 'var(--ink-4)',
flexShrink: 0,
display: 'inline-block',
}}
/>
<span style={{ fontSize: 12, color: 'var(--ink-4)', fontWeight: 400 }}>
{event.location}،&nbsp;{event.city}
</span>
</div>
{/* description */}
<p
style={{
fontSize: 13,
fontWeight: 300,
color: 'var(--ink-3)',
lineHeight: 1.8,
}}
>
{event.description}
</p>
</div>
{/* footer */}
<div
style={{
borderTop: '1px solid var(--rule-thin)',
padding: '12px 20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
}}
>
{event.registrationOpen ? (
<a
href="#"
style={{
fontSize: 13,
fontWeight: 700,
color: 'var(--red)',
textDecoration: 'none',
letterSpacing: '-0.2px',
display: 'flex',
alignItems: 'center',
gap: 4,
}}
>
ثبتنام
<span style={{ fontSize: 15 }}></span>
</a>
) : (
<span
style={{
fontSize: 11,
fontWeight: 500,
color: 'var(--ink-5)',
letterSpacing: '0.5px',
}}
>
بهزودی
</span>
)}
</div>
</article>
)
}
/* ─── main component ─────────────────────────────────────── */
export default function Events() {
const [activeFilter, setActiveFilter] = useState<FilterValue>('همه')
const filtered = activeFilter === 'همه'
? events
: events.filter((e) => e.type === activeFilter)
return (
<div
style={{
maxWidth: 1280,
margin: '0 auto',
padding: '64px 48px',
direction: 'rtl',
}}
>
{/* ─── Page title banner ─────────────────────────────── */}
<div
style={{
display: 'flex',
alignItems: 'stretch',
borderBottom: '3px solid var(--ink)',
marginBottom: 36,
}}
>
<div
style={{
background: 'var(--ink)',
padding: '14px 28px',
display: 'flex',
alignItems: 'center',
}}
>
<h1
style={{
fontSize: 24,
fontWeight: 900,
color: 'var(--paper)',
letterSpacing: '-0.5px',
whiteSpace: 'nowrap',
}}
>
رویدادهای پیشرو
</h1>
</div>
<div style={{ flex: 1 }} />
<div
style={{
display: 'flex',
alignItems: 'center',
padding: '14px 0 14px 4px',
}}
>
<span
style={{
fontSize: 13,
fontWeight: 500,
color: 'var(--ink-4)',
letterSpacing: '0.5px',
}}
>
۱۴۰۳۱۴۰۴
</span>
</div>
</div>
{/* ─── Filter tabs ───────────────────────────────────── */}
<div
style={{
display: 'flex',
gap: 0,
borderBottom: '1px solid var(--rule-thin)',
marginBottom: 40,
overflowX: 'auto',
scrollbarWidth: 'none',
}}
>
{FILTERS.map((f) => {
const isActive = activeFilter === f.value
return (
<button
key={f.value}
onClick={() => setActiveFilter(f.value)}
style={{
padding: '10px 24px',
fontSize: 13,
fontWeight: isActive ? 700 : 500,
border: 'none',
borderBottom: isActive ? '2px solid transparent' : '2px solid transparent',
background: isActive ? 'var(--ink)' : 'transparent',
color: isActive ? 'var(--paper)' : 'var(--ink-4)',
cursor: 'pointer',
fontFamily: 'inherit',
whiteSpace: 'nowrap',
flexShrink: 0,
transition: 'all 120ms',
position: 'relative',
marginBottom: -1,
outline: 'none',
}}
>
{f.label}
{f.value !== 'همه' && (
<span
style={{
marginRight: 6,
fontSize: 10,
color: isActive ? 'rgba(244,239,231,0.6)' : 'var(--ink-6)',
}}
>
({events.filter((e) => e.type === f.value).length})
</span>
)}
</button>
)
})}
</div>
{/* ─── Events grid ───────────────────────────────────── */}
<div
className="events-grid"
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: 1,
background: 'var(--rule-thin)',
}}
>
{filtered.map((event) => (
<EventCard key={event.id} event={event} />
))}
{/* fill odd trailing cell */}
{filtered.length % 2 !== 0 && (
<div style={{ background: 'var(--paper)' }} />
)}
</div>
{filtered.length === 0 && (
<div
style={{
padding: '80px 0',
textAlign: 'center',
color: 'var(--ink-5)',
}}
>
<p style={{ fontSize: 15, fontWeight: 300 }}>رویدادی در این دستهبندی یافت نشد</p>
</div>
)}
{/* responsive */}
<style>{`
@media (max-width: 767px) {
.events-grid {
grid-template-columns: 1fr !important;
}
}
`}</style>
</div>
)
}

27
src/pages/Home/Home.tsx Normal file
View File

@ -0,0 +1,27 @@
import HeroSection from './sections/HeroSection'
import SnapshotBar from './sections/SnapshotBar'
import GlobalScannerSection from './sections/GlobalScannerSection'
import ReportsCarouselSection from './sections/ReportsCarouselSection'
import LatestNewsBentoSection from './sections/LatestNewsBentoSection'
import EventsSection from './sections/EventsSection'
import RiskSection from './sections/RiskSection'
import PlansSection from './sections/PlansSection'
import TeamSection from './sections/TeamSection'
import NewsletterSection from './sections/NewsletterSection'
export default function Home() {
return (
<div>
<HeroSection />
<SnapshotBar />
<GlobalScannerSection />
<ReportsCarouselSection />
<LatestNewsBentoSection />
<EventsSection />
<RiskSection />
<TeamSection />
<PlansSection />
<NewsletterSection />
</div>
)
}

View File

@ -0,0 +1,104 @@
import { Link } from 'react-router-dom'
import { events } from '@/data/events'
import type { Event } from '@/data/events'
const typeLabel: Record<Event['type'], string> = {
conference: 'کنگره',
exhibition: 'نمایشگاه',
seminar: 'سمینار',
international: 'جهانی',
}
const typeBorderColor: Record<Event['type'], string> = {
conference: 'var(--ink)',
exhibition: '#6b4a00',
seminar: '#1a4a2a',
international: 'var(--red)',
}
const typeTextColor: Record<Event['type'], string> = {
conference: 'var(--ink)',
exhibition: '#6b4a00',
seminar: '#1a4a2a',
international: 'var(--red)',
}
export default function EventsSection() {
const displayed = events.slice(0, 4)
return (
<section style={{ borderBottom: '2px solid var(--ink)' }}>
<div className="px-12 py-20 max-md:px-5 max-md:py-14">
{/* Banner */}
<div style={{ display: 'flex', alignItems: 'center', borderBottom: '1px solid var(--rule-thin)', marginBottom: 0 }}>
<div style={{ background: 'var(--ink)', color: 'var(--paper)', padding: '8px 20px', fontSize: '9px', fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase' }}>رویدادهای پیشرو</div>
<div style={{ flex: 1 }} />
<Link to="/events" style={{ fontSize: '10px', fontWeight: 700, letterSpacing: '1.5px', textTransform: 'uppercase', color: 'var(--ink-4)', textDecoration: 'none', padding: '8px 0' }}>تقویم کامل </Link>
</div>
{/* 4-col grid */}
<div
style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)' }}
className="max-md:grid-cols-2 max-sm:grid-cols-1"
>
{displayed.map((event, idx) => (
<div
key={event.id}
style={{
padding: '32px 28px',
borderRight: idx === 0 ? 'none' : '1px solid var(--rule-thin)',
}}
className="max-md:border-b max-md:border-[var(--rule-thin)] max-md:last:border-b-0"
>
{/* Date block */}
<div style={{ borderBottom: '2px solid var(--ink)', paddingBottom: 12, marginBottom: 16 }}>
<div style={{ fontSize: 52, fontWeight: 900, letterSpacing: '-2px', lineHeight: 1, color: 'var(--ink)' }}>
{event.date.toLocaleString('fa-IR')}
</div>
<div style={{ fontSize: 12, color: 'var(--ink-4)', marginTop: 2 }}>
{event.month} {event.year.toLocaleString('fa-IR')}
</div>
</div>
{/* Type pill */}
<div style={{ marginBottom: 12 }}>
<span
style={{
display: 'inline-block',
border: `1px solid ${typeBorderColor[event.type]}`,
color: typeTextColor[event.type],
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '1px',
padding: '3px 8px',
lineHeight: 1.4,
}}
>
{typeLabel[event.type]}
</span>
</div>
{/* Title */}
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.4, marginBottom: 6 }}>{event.title}</div>
{/* Location */}
<div style={{ fontSize: 11, color: 'var(--ink-5)', marginBottom: 14 }}>{event.location} · {event.city}</div>
{/* CTA */}
{event.registrationOpen ? (
<Link to="#" style={{ fontSize: 10, fontWeight: 700, letterSpacing: '2px', textTransform: 'uppercase', color: 'var(--red)', textDecoration: 'none', cursor: 'pointer' }}>
ثبتنام
</Link>
) : (
<span style={{ fontSize: 10, color: 'var(--ink-5)', letterSpacing: '1px', textTransform: 'uppercase', fontWeight: 700 }}>
بهزودی
</span>
)}
</div>
))}
</div>
</div>
</section>
)
}

View File

@ -0,0 +1,180 @@
import { motion } from 'framer-motion'
import WorldMap from '@/components/ui/world-map'
/* Steel trade routes
Arcs show geopolitically relevant steel flows affecting Iran's market
*/
const ROUTES = [
// ── Iran's trade flows (2 routes) ───────────────────────────────
// Iran → Iraq (main export market)
{ start: { lat: 35.69, lng: 51.39 }, end: { lat: 33.31, lng: 44.36 } },
// Iran → Central Asia (Uzbekistan)
{ start: { lat: 35.69, lng: 51.39 }, end: { lat: 41.30, lng: 69.24 } },
// ── China's global reach (3 routes) ─────────────────────────────
// Shanghai → Rotterdam (European market dumping)
{ start: { lat: 31.23, lng: 121.47 }, end: { lat: 51.92, lng: 4.47 } },
// Shanghai → Lagos (ChinaAfrica infrastructure push)
{ start: { lat: 31.23, lng: 121.47 }, end: { lat: 6.45, lng: 3.39 } },
// Shanghai → Singapore (SE Asia dominance)
{ start: { lat: 31.23, lng: 121.47 }, end: { lat: 1.35, lng: 103.82 } },
// ── Russia flows (2 routes) ──────────────────────────────────────
// Moscow → Beijing (raw material / billet supply)
{ start: { lat: 55.75, lng: 37.62 }, end: { lat: 39.91, lng: 116.39 } },
// Moscow → Tehran (billet supply to Iran)
{ start: { lat: 55.75, lng: 37.62 }, end: { lat: 35.69, lng: 51.39 } },
// ── India's competing routes (2 routes) ──────────────────────────
// Mumbai → Dubai (competing with Iran in Gulf)
{ start: { lat: 19.07, lng: 72.88 }, end: { lat: 25.20, lng: 55.27 } },
// Mumbai → Mombasa (East Africa — directly competes with Iran)
{ start: { lat: 19.07, lng: 72.88 }, end: { lat: -4.05, lng: 39.67 } },
// ── Americas → Asia (iron ore flows) ────────────────────────────
// São Paulo → Shanghai (Brazilian iron ore)
{ start: { lat: -23.55, lng: -46.63 }, end: { lat: 31.23, lng: 121.47 } },
// ── South Korea → SE Asia ───────────────────────────────────────
{ start: { lat: 37.56, lng: 126.97 }, end: { lat: 1.35, lng: 103.82 } },
// ── Turkey → Europe ──────────────────────────────────────────────
// Istanbul → Berlin (Turkey as EU steel supplier)
{ start: { lat: 41.01, lng: 28.97 }, end: { lat: 52.52, lng: 13.40 } },
// ── Australia → China (iron ore) ────────────────────────────────
{ start: { lat: -33.87, lng: 151.21 }, end: { lat: 31.23, lng: 121.47 } },
]
/* ─── Section ────────────────────────────────────────── */
export default function GlobalScannerSection() {
return (
<section
style={{
background: 'var(--paper)',
borderBottom: '2px solid var(--ink)',
overflow: 'hidden',
}}
>
{/* ── Text header ────────────────────────────────── */}
<div
style={{ textAlign: 'center' }}
className="px-12 pt-[72px] pb-12 max-md:px-5 max-md:pt-10 max-md:pb-8"
>
{/* Overline */}
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 10,
marginBottom: 20,
}}
>
<div style={{ width: 20, height: 2, background: 'var(--red)' }} />
<span style={{
fontSize: 9,
fontWeight: 700,
letterSpacing: '3px',
textTransform: 'uppercase',
color: 'var(--red)',
}}>
اسکنر جهانی · پوشش ۲۴ ساعته
</span>
<div style={{ width: 20, height: 2, background: 'var(--red)' }} />
</div>
{/* Headline */}
<h2 style={{
fontSize: 'clamp(28px, 3.5vw, 52px)',
fontWeight: 900,
lineHeight: 1.1,
letterSpacing: '-1.5px',
color: 'var(--ink)',
marginBottom: 16,
}}>
تمام رویدادهای فولادی دنیا{' '}
<motion.span
style={{ color: 'var(--ink-6)', display: 'inline' }}
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: 0.3 }}
>
را رصد میکنیم
</motion.span>
</h2>
{/* Subhead */}
<p style={{
fontSize: 15,
lineHeight: 1.85,
fontWeight: 400,
color: 'var(--ink-3)',
maxWidth: 600,
margin: '0 auto',
}}>
از بورس دالیان چین تا گمرک بصره عراق، از تصمیمات کمیته فولاد OECD
تا ظرفیتسازیهای هند هر سیگنالی که بر رقابتپذیری ایران اثر
میگذارد، پیش از بقیه به دست شما میرسد.
</p>
</div>
{/* ── World map (full width, no side padding) ─── */}
<div className="px-6 max-md:px-0">
<WorldMap
dots={ROUTES}
lineColor="#9b1c1c"
dotColor="#c8c2bc"
/>
</div>
{/* ── Stats strip ──────────────────────────────── */}
<div
style={{
borderTop: '1px solid var(--rule-thin)',
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
}}
className="max-md:grid-cols-2"
>
{[
{ n: '۱۸۰+', label: 'بازار رصد‌شده', sub: 'در ۵ قاره' },
{ n: '۷/۲۴', label: 'پایش لحظه‌ای', sub: 'قیمت و حجم' },
{ n: '۴۰+', label: 'منبع داده', sub: 'بورس‌ها و گمرک‌ها' },
{ n: '۱۵', label: 'کشور رقیب', sub: 'زیر ذره‌بین' },
].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.08 }}
style={{
padding: '28px 24px',
borderRight: i > 0 ? '1px solid var(--rule-thin)' : 'none',
textAlign: 'center',
}}
className="max-md:border-r-0 max-md:border-b max-md:border-[var(--rule-thin)]"
>
<div style={{
fontSize: 32,
fontWeight: 900,
color: 'var(--ink)',
letterSpacing: '-1px',
lineHeight: 1,
marginBottom: 4,
}}>
{s.n}
</div>
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink-2)', marginBottom: 2 }}>
{s.label}
</div>
<div style={{ fontSize: 10, color: 'var(--ink-5)' }}>
{s.sub}
</div>
</motion.div>
))}
</div>
</section>
)
}

View File

@ -0,0 +1,315 @@
import { useState } from 'react'
import { motion } from 'framer-motion'
import { reports } from '@/data/reports'
const featured = reports.find(r => r.featured)!
/* Dramatic steel mill / industrial image */
const BG_IMAGE =
'https://images.unsplash.com/photo-1504328345606-18bbc8c9d7d1?q=85&w=2400&auto=format&fit=crop'
const STATS = [
{ n: '۴', label: 'سناریو محتمل' },
{ n: '۱۵', label: 'کشور رقیب' },
{ n: '۸۴', label: 'صفحه تحلیلی' },
{ n: '۱۲', label: 'توصیه سیاستی' },
]
export default function HeroSection() {
const [buyHov, setBuyHov] = useState(false)
const [freeHov, setFreeHov] = useState(false)
return (
<section
style={{
position: 'relative',
height: 'min(88vh, 780px)',
minHeight: 540,
overflow: 'hidden',
borderBottom: '3px solid var(--ink)',
}}
>
{/* ── Background image ── */}
<img
src={BG_IMAGE}
alt=""
aria-hidden="true"
style={{
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center 40%',
}}
/>
{/* ── Gradient overlay — dark vignette from bottom-right ── */}
<div style={{
position: 'absolute',
inset: 0,
background: [
'linear-gradient(to top, rgba(10,8,6,0.96) 0%, rgba(10,8,6,0.5) 45%, transparent 70%)',
'linear-gradient(to left, rgba(10,8,6,0.7) 0%, rgba(10,8,6,0.15) 55%, transparent 80%)',
].join(', '),
}} />
{/* ── Top-right badge ── */}
<div style={{
position: 'absolute',
top: 32,
left: 48,
display: 'flex',
gap: 10,
alignItems: 'center',
}}
className="max-md:left-5 max-md:top-5"
>
<div style={{
background: 'var(--red)',
color: 'white',
fontSize: 9,
fontWeight: 700,
letterSpacing: '2.5px',
textTransform: 'uppercase',
padding: '5px 14px',
}}>
گزارش ویژه
</div>
<span style={{ fontSize: 10, color: 'rgba(255,255,255,0.45)', letterSpacing: '1px' }}>
بهمن ۱۴۰۳
</span>
</div>
{/* ── Main content — anchored bottom ── */}
<div style={{
position: 'absolute',
inset: 0,
display: 'grid',
gridTemplateColumns: '1fr 240px',
alignItems: 'flex-end',
gap: 48,
}}
className="px-14 pb-[52px] max-md:grid-cols-1 max-md:px-5 max-md:pb-8"
>
{/* ── Left: headline + meta ── */}
<div>
{/* Overline */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 20 }}
>
<div style={{ width: 24, height: 2, background: 'var(--red)', flexShrink: 0 }} />
<span style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: '2.5px',
textTransform: 'uppercase',
color: 'rgba(255,255,255,0.5)',
}}>
گزارش راهبردی · تحلیل سناریو
</span>
</motion.div>
{/* Headline */}
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
style={{
fontSize: 'clamp(36px, 5vw, 72px)',
fontWeight: 900,
lineHeight: 1.04,
letterSpacing: '-2.5px',
color: 'white',
marginBottom: 24,
maxWidth: 700,
}}
>
{featured.title.split(':').map((part, i) => (
<span key={i}>
{i === 0 ? part + ':' : (
<span style={{ color: 'rgba(255,255,255,0.45)' }}>{part}</span>
)}
{i === 0 && <br />}
</span>
))}
</motion.h1>
{/* Summary — one line */}
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.25 }}
style={{
fontSize: 14,
lineHeight: 1.8,
fontWeight: 400,
color: 'rgba(255,255,255,0.6)',
maxWidth: 580,
marginBottom: 32,
}}
>
مدلسازی کمی بر اساس دادههای تجارت ۱۵ کشور رقیب از بدبینانه تا خوشبینانه،
با توصیههای سیاستی مستقیم برای وزارت صمت و سازمان بنادر.
</motion.p>
{/* Divider */}
<div style={{ width: '100%', height: 1, background: 'rgba(255,255,255,0.12)', marginBottom: 24 }} />
{/* Author + Price + CTA in one bar */}
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.35 }}
style={{
display: 'flex',
alignItems: 'center',
gap: 24,
flexWrap: 'wrap',
}}
>
{/* Author */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 36, height: 36,
background: 'var(--red)',
color: 'white',
fontSize: 14, fontWeight: 900,
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
{featured.authorInitial}
</div>
<div>
<div style={{ fontSize: 12, fontWeight: 700, color: 'white', lineHeight: 1.2 }}>
{featured.author}
</div>
<div style={{ fontSize: 10, color: 'rgba(255,255,255,0.4)', lineHeight: 1.3 }}>
{featured.authorRole}
</div>
</div>
</div>
{/* Separator */}
<div style={{ width: 1, height: 32, background: 'rgba(255,255,255,0.15)' }} />
{/* Price */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 5 }}>
<span style={{
fontSize: 28,
fontWeight: 900,
letterSpacing: '-1px',
color: 'white',
lineHeight: 1,
}}>
{featured.price.toLocaleString('fa-IR')}
</span>
<span style={{ fontSize: 11, color: 'rgba(255,255,255,0.4)' }}>تومان</span>
</div>
{/* Buttons */}
<div style={{ display: 'flex', gap: 10 }}>
<button
onMouseEnter={() => setBuyHov(true)}
onMouseLeave={() => setBuyHov(false)}
style={{
background: buyHov ? 'var(--paper-2)' : 'var(--paper)',
color: 'var(--ink)',
border: 'none',
padding: '11px 26px',
fontSize: 12,
fontWeight: 700,
fontFamily: 'Vazir, sans-serif',
cursor: 'pointer',
letterSpacing: '0.5px',
transition: 'background 160ms',
whiteSpace: 'nowrap',
}}
>
خرید گزارش
</button>
<button
onMouseEnter={() => setFreeHov(true)}
onMouseLeave={() => setFreeHov(false)}
style={{
background: freeHov ? 'rgba(255,255,255,0.1)' : 'transparent',
color: 'white',
border: '1px solid rgba(255,255,255,0.3)',
padding: '11px 26px',
fontSize: 12,
fontWeight: 400,
fontFamily: 'Vazir, sans-serif',
cursor: 'pointer',
letterSpacing: '0.5px',
transition: 'background 160ms',
whiteSpace: 'nowrap',
}}
>
دانلود خلاصه رایگان
</button>
</div>
</motion.div>
</div>
{/* ── Right: floating stats panel ── */}
<motion.div
initial={{ opacity: 0, x: -16 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.45 }}
style={{
background: 'rgba(255,255,255,0.06)',
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)',
border: '1px solid rgba(255,255,255,0.1)',
padding: '20px',
marginBottom: 2,
}}
className="max-md:hidden"
>
<div style={{
fontSize: 9,
fontWeight: 700,
letterSpacing: '2px',
textTransform: 'uppercase',
color: 'rgba(255,255,255,0.35)',
marginBottom: 16,
paddingBottom: 10,
borderBottom: '1px solid rgba(255,255,255,0.1)',
}}>
در این گزارش
</div>
{STATS.map((s, i) => (
<div
key={i}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'baseline',
padding: '10px 0',
borderBottom: i < STATS.length - 1 ? '1px solid rgba(255,255,255,0.07)' : 'none',
}}
>
<span style={{ fontSize: 11, color: 'rgba(255,255,255,0.5)', fontWeight: 300 }}>
{s.label}
</span>
<span style={{
fontSize: 22,
fontWeight: 900,
letterSpacing: '-0.5px',
color: 'white',
lineHeight: 1,
}}>
{s.n}
</span>
</div>
))}
</motion.div>
</div>
</section>
)
}

View File

@ -0,0 +1,240 @@
import type { CSSProperties } from 'react'
import { motion } from 'framer-motion'
import { reports } from '@/data/reports'
const featured = reports.find(r => r.featured)!
const flashReport = reports.find(r => r.type === 'flash')!
/* ─── Price snapshot ────────────────────────────────────── */
const PRICES = [
{ label: 'HRC آسیا', value: '۵۹۰', delta: '+۳.۲٪', up: true },
{ label: 'میلگرد ایران', value: '۲۱,۸۰۰', delta: '+۰.۸٪', up: true },
{ label: 'سنگ‌آهن دالیان', value: '۱۰۸', delta: '+۱۸٪', up: true },
{ label: 'HRC اروپا', value: '۶۲۴', delta: '۱.۴٪', up: false },
]
/* ─── Shared card style (NO gridColumn here — set via className) */
const CARD: CSSProperties = {
background: 'var(--paper)',
padding: '28px 28px 24px',
display: 'flex',
flexDirection: 'column',
direction: 'rtl',
fontFamily: 'Vazir, sans-serif',
position: 'relative',
}
function Tag({ text, color = 'var(--red)' }: { text: string; color?: string }) {
return (
<div style={{
fontSize: 9,
fontWeight: 700,
letterSpacing: '2.5px',
textTransform: 'uppercase',
color,
marginBottom: 12,
}}>
{text}
</div>
)
}
function Accent({ color = 'var(--red)' }: { color?: string }) {
return (
<div style={{
position: 'absolute', top: 0, right: 0, left: 0,
height: 3, background: color,
}} />
)
}
/* ─── Section ────────────────────────────────────────────── */
export default function LatestNewsBentoSection() {
return (
<section style={{ background: 'var(--paper-2)', borderBottom: '2px solid var(--ink)' }}>
{/* ── Header ── */}
<div className="px-12 pt-16 max-md:px-5 max-md:pt-10">
<div style={{
display: 'flex', alignItems: 'center', gap: 16,
marginBottom: 32, paddingBottom: 20, borderBottom: '2px solid var(--ink)',
}}>
<div style={{
background: 'var(--ink)', color: 'var(--paper)',
fontSize: 9, fontWeight: 700, letterSpacing: '3px',
textTransform: 'uppercase', padding: '6px 16px',
}}>
آخرین اخبار
</div>
<h2 style={{ fontSize: 'clamp(16px,2vw,22px)', fontWeight: 900, letterSpacing: '-0.5px' }}>
رصد روزانه بازار و صنعت
</h2>
<div style={{ flex: 1 }} />
<span style={{ fontSize: 10, fontWeight: 500, color: 'var(--ink-5)' }}>بهمن ۱۴۰۳</span>
</div>
</div>
{/* ── Bento grid ── */}
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="px-12 pb-14 max-md:px-5 max-md:pb-10"
>
{/*
3-col grid on desktop, 1-col on mobile.
Items and use col-span-1 md:col-span-2.
NO gridColumn in inline style inline style overrides Tailwind.
*/}
<div
className="grid grid-cols-1 md:grid-cols-3"
style={{ gap: 1, background: 'var(--rule-thin)' }}
>
{/* ① Featured report — col-span-2 on desktop ─── */}
<div style={{ ...CARD, minHeight: 260 }} className="col-span-1 md:col-span-2">
<Accent />
<Tag text="تازه‌ترین گزارش ویژه" />
{/* Ghost number */}
<div style={{
fontSize: 96, fontWeight: 900, lineHeight: 0.8,
letterSpacing: '-5px', color: 'rgba(26,23,18,0.05)',
marginBottom: 12, userSelect: 'none', direction: 'ltr',
}}>01</div>
<h3 style={{ fontSize: 'clamp(16px,2vw,20px)', fontWeight: 900, letterSpacing: '-0.5px', lineHeight: 1.35, color: 'var(--ink)', marginBottom: 12, flex: 1 }}>
{featured.title}
</h3>
<p style={{ fontSize: 13, lineHeight: 1.75, fontWeight: 400, color: 'var(--ink-3)', marginBottom: 20,
display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
{featured.summary}
</p>
{/* Footer */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, paddingTop: 16, borderTop: '1px solid var(--rule-thin)' }}>
<div style={{ width: 30, height: 30, background: 'var(--red)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white', fontSize: 12, fontWeight: 900, flexShrink: 0 }}>
{featured.authorInitial}
</div>
<div>
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink)' }}>{featured.author}</div>
<div style={{ fontSize: 10, color: 'var(--ink-5)' }}>{featured.publishDate} · {featured.pages} صفحه</div>
</div>
<div style={{ flex: 1 }} />
<span style={{ fontSize: 12, fontWeight: 700, color: 'var(--red)' }}>خواندن </span>
</div>
</div>
{/* ② Market prices ─────────────────────────────── */}
<div style={CARD}>
<Accent color="var(--green-ink)" />
<Tag text="قیمت‌های لحظه‌ای" color="var(--green-ink)" />
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--ink)', marginBottom: 16 }}>
آخرین نرخهای بازار
</div>
<div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
{PRICES.map(p => (
<div key={p.label} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '9px 0', borderBottom: '1px solid var(--rule-thin)' }}>
<span style={{ fontSize: 11, color: 'var(--ink-4)', fontWeight: 500 }}>{p.label}</span>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink)', direction: 'ltr' }}>{p.value}</span>
<span style={{ fontSize: 10, fontWeight: 700, color: p.up ? 'var(--green-ink)' : 'var(--red)', direction: 'ltr' }}>{p.delta}</span>
</div>
</div>
))}
</div>
</div>
{/* ③ Flash report ──────────────────────────────── */}
<div style={CARD}>
<Accent color="var(--amber)" />
<Tag text={`فلش · ${flashReport.publishDate}`} color="var(--amber)" />
<div style={{ fontSize: 38, fontWeight: 900, letterSpacing: '-2px', color: 'var(--amber)', lineHeight: 1, marginBottom: 12, direction: 'ltr' }}>
+۱۸٪
</div>
<h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.4, marginBottom: 8, flex: 1 }}>
جهش قیمت سنگآهن در بورس دالیان
</h3>
<p style={{ fontSize: 12, color: 'var(--ink-4)', fontWeight: 400, lineHeight: 1.6 }}>
برآورد اثر بر هزینه تمامشده تولیدکنندگان DRI ایران
</p>
</div>
{/* ④ Event ─────────────────────────────────────── */}
<div style={CARD}>
<Accent color="var(--ink-3)" />
<Tag text="رویداد · آینده" color="var(--ink-3)" />
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 16 }}>
<div style={{ background: 'var(--ink)', color: 'var(--paper)', padding: '8px 12px', textAlign: 'center', lineHeight: 1.2, flexShrink: 0, minWidth: 48 }}>
<div style={{ fontSize: 22, fontWeight: 900 }}>۲۵</div>
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '1px', textTransform: 'uppercase' }}>بهمن</div>
</div>
<div>
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--ink)' }}>تهران</div>
<div style={{ fontSize: 10, color: 'var(--ink-5)' }}>هتل اسپیناس پالاس</div>
</div>
</div>
<h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.4, flex: 1 }}>
همایش ملی فولاد ایران ۱۴۰۳
</h3>
<p style={{ fontSize: 12, color: 'var(--ink-4)', fontWeight: 400, lineHeight: 1.6, marginTop: 6 }}>
نشست سالانه تولیدکنندگان، صادرکنندگان و سیاستگذاران
</p>
</div>
{/* ⑤ Risk alert ────────────────────────────────── */}
<div style={CARD}>
<Accent color="#7f1d1d" />
<div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 12 }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#7f1d1d', flexShrink: 0 }} />
<span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '2.5px', textTransform: 'uppercase', color: '#7f1d1d' }}>
هشدار ریسک · بحرانی
</span>
</div>
<div style={{ display: 'inline-block', background: 'rgba(127,29,29,0.08)', padding: '4px 10px', fontSize: 10, fontWeight: 700, color: '#7f1d1d', marginBottom: 14 }}>
دی ۱۴۰۳
</div>
<h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.4, flex: 1 }}>
مسدود شدن ۳ مسیر پرداخت صادراتی
</h3>
<p style={{ fontSize: 12, color: 'var(--ink-4)', fontWeight: 400, lineHeight: 1.6, marginTop: 8 }}>
بسته تحریمی جدید مسیرهای بانکی صادرات فولاد را محدود کرده است
</p>
</div>
{/* ⑥ Expert quote — col-span-2 on desktop ────── */}
<div style={CARD} className="col-span-1 md:col-span-2">
<Accent color="var(--ink-6)" />
<Tag text="یادداشت تحلیلگر" color="var(--ink-4)" />
<div style={{ fontSize: 56, lineHeight: 0.7, color: 'var(--rule-thin)', fontWeight: 900, marginBottom: 10, direction: 'ltr', userSelect: 'none' }}>"</div>
<p style={{ fontSize: 'clamp(14px,1.2vw,16px)', fontWeight: 700, lineHeight: 1.65, letterSpacing: '-0.2px', color: 'var(--ink)', marginBottom: 20, flex: 1 }}>
فولاد ایران در آستانه تصمیمی تاریخی است: یا باید بازارهای جایگزین را با
جدیت دنبال کند، یا شاهد کاهش تدریجی سهم صادراتی خود در خاورمیانه باشد.
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, paddingTop: 16, borderTop: '1px solid var(--rule-thin)' }}>
<div style={{ width: 28, height: 28, background: 'var(--red)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white', fontSize: 11, fontWeight: 900, flexShrink: 0 }}>م</div>
<div>
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink)' }}>دکتر علی محمدی</div>
<div style={{ fontSize: 10, color: 'var(--ink-5)' }}>مدیر ارشد پژوهش · اندیشکده فولاد</div>
</div>
</div>
</div>
{/* ⑦ Key stat ──────────────────────────────────── */}
<div style={CARD}>
<Accent color="var(--ink-3)" />
<Tag text="آمار کلیدی" color="var(--ink-3)" />
<div style={{ fontSize: 52, fontWeight: 900, letterSpacing: '-3px', lineHeight: 1, color: 'var(--ink)', marginBottom: 6, direction: 'ltr' }}>
۵۴M
</div>
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink-3)', marginBottom: 8 }}>
تن صادرات فولاد چین
</div>
<p style={{ fontSize: 12, color: 'var(--ink-5)', fontWeight: 400, lineHeight: 1.65, flex: 1 }}>
در ۶ ماه اول ۲۰۲۴ رکوردی که قیمتهای آسیای غربی را ۱۲٪ کاهش داد
</p>
</div>
</div>
</motion.div>
</section>
)
}

View File

@ -0,0 +1,251 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { SectionBanner } from '@/components/ui/SectionBanner'
import { reports, type Report } from '@/data/reports'
const displayReports = reports.filter(r => !r.featured).slice(0, 6)
const typeLabel: Record<string, string> = {
quarterly: 'گزارش فصلی',
flash: 'فوری',
risk: 'ریسک',
free: 'رایگان',
special: 'ویژه',
}
const dotColor: Record<string, string> = {
quarterly: 'var(--ink)',
flash: 'var(--red)',
risk: 'var(--red)',
free: 'var(--green-ink)',
special: 'var(--amber)',
}
function formatPrice(n: number) {
return n.toLocaleString('fa-IR')
}
interface ReportCardProps {
report: Report
colIndex: number
rowIndex: number
totalRows: number
}
function ReportCard({ report, colIndex, rowIndex, totalRows }: ReportCardProps) {
const [hovered, setHovered] = useState(false)
const isLastRow = rowIndex === totalRows - 1
const isRightCol = colIndex === 0 // RTL: right col is index 0
return (
<div
style={{
padding: '40px',
borderBottom: isLastRow ? 'none' : '1px solid var(--rule-thin)',
borderLeft: isRightCol ? '1px solid var(--rule-thin)' : 'none',
cursor: 'pointer',
transition: 'background-color 150ms',
backgroundColor: hovered ? 'var(--paper-2)' : 'transparent',
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{/* Type badge */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '16px',
}}
>
<div
style={{
width: '4px',
height: '4px',
borderRadius: '50%',
backgroundColor: dotColor[report.type] ?? 'var(--ink)',
flexShrink: 0,
}}
/>
<span
style={{
fontSize: '9px',
textTransform: 'uppercase',
letterSpacing: '2px',
color: 'var(--ink-5)',
fontWeight: 700,
}}
>
{typeLabel[report.type] ?? report.type} · {report.category}
</span>
</div>
{/* Title */}
<Link to={`/reports/${report.id}`} style={{ textDecoration: 'none' }}>
<h3
style={{
fontSize: 'clamp(18px, 2vw, 20px)',
fontWeight: 800,
lineHeight: 1.35,
letterSpacing: '-0.3px',
color: hovered ? 'var(--red)' : 'var(--ink)',
marginBottom: '12px',
transition: 'color 150ms',
cursor: 'pointer',
}}
>
{report.title}
</h3>
</Link>
{/* Deck */}
<p
style={{
fontSize: '13px',
fontWeight: 300,
color: 'var(--ink-3)',
lineHeight: 1.8,
marginBottom: '20px',
}}
>
{report.summary.slice(0, 140)}
{report.summary.length > 140 ? '…' : ''}
</p>
{/* Divider */}
<div
style={{
height: '1px',
backgroundColor: 'var(--rule-thin)',
marginBottom: '16px',
}}
/>
{/* Footer row */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-end',
gap: '12px',
}}
>
{/* Price block */}
<div>
{report.isFree ? (
<span
style={{
fontSize: '16px',
fontWeight: 900,
color: 'var(--green-ink)',
}}
>
دانلود رایگان
</span>
) : (
<span
style={{
fontSize: '16px',
fontWeight: 900,
color: 'var(--ink)',
}}
>
{formatPrice(report.price)}
<span
style={{
fontSize: '11px',
fontWeight: 400,
color: 'var(--ink-4)',
marginRight: '4px',
}}
>
تومان
</span>
</span>
)}
<div
style={{
fontSize: '10px',
color: 'var(--ink-5)',
marginTop: '3px',
}}
>
{report.pages} صفحه · {report.publishDate}
</div>
</div>
{/* Buy link */}
<Link
to={`/reports/${report.id}`}
style={{
fontSize: '10px',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '2px',
color: 'var(--red)',
textDecoration: 'none',
cursor: 'pointer',
transition: 'text-decoration 150ms',
whiteSpace: 'nowrap',
}}
onMouseEnter={e =>
(e.currentTarget.style.textDecoration = 'underline')
}
onMouseLeave={e => (e.currentTarget.style.textDecoration = 'none')}
>
{report.isFree ? 'دانلود' : 'خرید گزارش'}
</Link>
</div>
</div>
)
}
export default function LatestReportsSection() {
const rows = Math.ceil(displayReports.length / 2)
return (
<section
style={{
padding: '96px 48px',
borderBottom: '2px solid var(--ink)',
direction: 'rtl',
}}
className="max-md:px-6 max-md:py-16"
>
{/* Section header */}
<div style={{ marginBottom: '40px' }}>
<SectionBanner
label="آخرین گزارش‌ها"
link={{ text: 'مشاهده آرشیو کامل ←', to: '/reports' }}
/>
</div>
{/* 2-col grid */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
border: '1px solid var(--rule-thin)',
borderBottom: 'none',
borderLeft: 'none',
}}
className="max-md:grid-cols-1"
>
{displayReports.map((report, idx) => {
const colIndex = idx % 2
const rowIndex = Math.floor(idx / 2)
return (
<ReportCard
key={report.id}
report={report}
colIndex={colIndex}
rowIndex={rowIndex}
totalRows={rows}
/>
)
})}
</div>
</section>
)
}

View File

@ -0,0 +1,103 @@
import { useState } from 'react'
export default function NewsletterSection() {
const [email, setEmail] = useState('')
const [submitted, setSubmitted] = useState(false)
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (email.trim()) {
setSubmitted(true)
setEmail('')
}
}
return (
<section style={{ borderBottom: '3px solid var(--ink)' }}>
{/* Outer grid — strip inline padding, use Tailwind */}
<div
style={{ display: 'grid', gridTemplateColumns: '1fr 1px 1fr', minHeight: 180 }}
className="px-12 max-md:px-5 max-md:grid-cols-1"
>
{/* Left: description */}
<div
style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
className="py-12 pr-12 max-md:py-10 max-md:pr-0"
>
<div style={{ fontSize: 26, fontWeight: 900, color: 'var(--ink)', letterSpacing: '-0.5px', marginBottom: 8 }}>
خبرنامه اندیشکده
</div>
<p style={{ fontSize: 13, color: 'var(--ink-3)', fontWeight: 400, lineHeight: 1.75 }}>
هر هفته: خلاصه تحولات بازار فولاد، گزارشهای جدید و هشدارهای فوری مستقیم در صندوق ورودی شما
</p>
<div style={{ fontSize: 11, color: 'var(--ink-5)', marginTop: 10 }}>
بیش از ۲,۸۰۰ مدیر، مقام دولتی و کارشناس عضو
</div>
</div>
{/* Divider */}
<div style={{ background: 'var(--rule-thin)' }} className="max-md:hidden" />
{/* Right: form */}
<div
style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 10 }}
className="py-12 pl-12 max-md:pt-0 max-md:pb-10 max-md:pl-0"
>
{submitted ? (
<div style={{ fontSize: 15, color: 'var(--green-ink)', fontWeight: 600, padding: '12px 0' }}>
عضویت شما با موفقیت ثبت شد. از همراهی شما متشکریم.
</div>
) : (
<form onSubmit={handleSubmit} style={{ display: 'flex', gap: 0 }}>
<label htmlFor="nl-email" style={{ position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0,0,0,0)' }}>
ایمیل سازمانی شما
</label>
<input
id="nl-email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="ایمیل سازمانی شما"
required
autoComplete="email"
style={{
flex: 1,
padding: '12px 14px',
border: '1px solid var(--ink)',
borderLeft: 'none',
fontFamily: 'Vazir, sans-serif',
fontSize: 13,
background: 'var(--paper)',
color: 'var(--ink)',
outline: 'none',
borderRadius: 0,
minWidth: 0,
}}
/>
<button
type="submit"
style={{
background: 'var(--ink)',
color: 'var(--paper)',
border: 'none',
padding: '12px 24px',
fontFamily: 'Vazir, sans-serif',
fontSize: 12,
fontWeight: 700,
cursor: 'pointer',
whiteSpace: 'nowrap',
borderRadius: 0,
letterSpacing: '0.5px',
flexShrink: 0,
}}
>
عضویت رایگان
</button>
</form>
)}
<p style={{ fontSize: 10, color: 'var(--ink-5)' }}>اطلاعات شما محرمانه است و فروخته نمیشود</p>
</div>
</div>
</section>
)
}

View File

@ -0,0 +1,338 @@
import { useState } from 'react'
import { plans } from '@/data/plans'
/* ─── Checkmark icon ─────────────────────────────────── */
function Check({ featured }: { featured: boolean }) {
return (
<svg
width="16" height="16" viewBox="0 0 16 16" fill="none"
style={{ flexShrink: 0, marginTop: 1 }}
>
<circle
cx="8" cy="8" r="7.5"
stroke={featured ? 'rgba(255,255,255,0.35)' : 'var(--rule-thin)'}
fill={featured ? 'rgba(255,255,255,0.1)' : 'transparent'}
/>
<path
d="M5 8l2 2 4-4"
stroke={featured ? '#f4efe7' : 'var(--green-ink)'}
strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"
/>
</svg>
)
}
/* ─── Individual plan card ───────────────────────────── */
function PlanCard({ plan, isMiddle }: { plan: (typeof plans)[0]; isMiddle: boolean }) {
const [hovered, setHovered] = useState(false)
const featured = plan.featured
return (
<div
style={{
background: featured ? 'var(--ink)' : 'white',
border: featured ? 'none' : '1px solid var(--rule-thin)',
borderTop: featured ? 'none' : '1px solid var(--rule-thin)',
padding: '32px 28px',
display: 'flex',
flexDirection: 'column',
position: 'relative',
boxShadow: isMiddle ? '0 8px 40px rgba(26,23,18,0.12)' : 'none',
zIndex: isMiddle ? 1 : 0,
}}
>
{/* Badge */}
{plan.badge && (
<div style={{
position: 'absolute',
top: -12,
right: '50%',
transform: 'translateX(50%)',
background: 'var(--red)',
color: 'white',
fontSize: 9,
fontWeight: 700,
letterSpacing: '2px',
textTransform: 'uppercase',
padding: '4px 14px',
whiteSpace: 'nowrap',
}}>
{plan.badge}
</div>
)}
{/* Plan name */}
<div style={{
fontSize: 11,
fontWeight: 700,
letterSpacing: '1.5px',
textTransform: 'uppercase',
color: featured ? 'rgba(244,239,231,0.5)' : 'var(--ink-5)',
marginBottom: 8,
}}>
{plan.name}
</div>
{/* Tagline */}
<div style={{
fontSize: 13,
color: featured ? 'rgba(244,239,231,0.55)' : 'var(--ink-4)',
marginBottom: 24,
fontWeight: 300,
}}>
{plan.id === 'individual' && 'برای کارشناسان و محققان'}
{plan.id === 'organizational' && 'برای شرکت‌ها و سازمان‌ها'}
{plan.id === 'ministry' && 'برای نهادهای دولتی'}
</div>
{/* Price */}
<div style={{ marginBottom: 6 }}>
<span style={{
fontSize: 42,
fontWeight: 900,
letterSpacing: '-2px',
lineHeight: 1,
color: featured ? 'var(--paper)' : 'var(--ink)',
fontVariantNumeric: 'tabular-nums',
}}>
{plan.price === 0 ? 'تماس' : plan.price.toLocaleString('fa-IR')}
</span>
{plan.price > 0 && (
<span style={{
fontSize: 13,
fontWeight: 400,
color: featured ? 'rgba(244,239,231,0.5)' : 'var(--ink-4)',
marginRight: 6,
}}>
تومان
</span>
)}
</div>
<div style={{
fontSize: 11,
color: featured ? 'rgba(244,239,231,0.4)' : 'var(--ink-5)',
marginBottom: 28,
}}>
{plan.period}
</div>
{/* CTA button */}
<button
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
width: '100%',
padding: '12px',
border: featured
? 'none'
: hovered ? 'none' : '1px solid var(--ink)',
background: featured
? hovered ? 'var(--paper-2)' : 'var(--paper)'
: hovered ? 'var(--ink)' : 'transparent',
color: featured
? 'var(--ink)'
: hovered ? 'var(--paper)' : 'var(--ink)',
fontSize: 12,
fontWeight: 700,
fontFamily: 'Vazir, sans-serif',
cursor: 'pointer',
borderRadius: 0,
marginBottom: 28,
letterSpacing: '0.5px',
transition: 'background 180ms, color 180ms, border 180ms',
}}
>
{plan.ctaLabel}
</button>
{/* Divider */}
<div style={{
borderTop: featured ? '1px solid rgba(244,239,231,0.12)' : '1px solid var(--rule-thin)',
marginBottom: 24,
}} />
{/* Features */}
<ul style={{ listStyle: 'none', display: 'flex', flexDirection: 'column', gap: 12 }}>
{plan.features.map(f => (
<li key={f} style={{ display: 'flex', alignItems: 'flex-start', gap: 10, textAlign: 'right' }}>
<Check featured={featured} />
<span style={{
fontSize: 13,
lineHeight: 1.5,
color: featured ? 'rgba(244,239,231,0.75)' : 'var(--ink-3)',
fontWeight: 300,
}}>
{f}
</span>
</li>
))}
</ul>
</div>
)
}
/* ─── Enterprise bottom card ─────────────────────────── */
const enterpriseFeatures = [
'پروژه‌های نامحدود',
'پشتیبانی تیمی اختصاصی',
'فاکتور رسمی سازمانی',
'بدون سقف توکن یا داده',
'دعوت اعضای نامحدود',
'قرارداد SLA معتبر',
'نقش‌های سفارشی کاربری',
'امنیت پیشرفته سازمانی',
'API اتصال به سیستم داخلی',
'گزارش‌دهی اختصاصی',
'کارگاه‌های تخصصی دوره‌ای',
'مشاوره راهبردی مستقیم',
]
function EnterpriseCard() {
const [hov, setHov] = useState(false)
return (
<div style={{
border: '1px solid var(--rule-thin)',
background: 'white',
padding: '36px 40px',
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '40px',
alignItems: 'start',
}}
className="max-md:grid-cols-1 max-md:gap-6"
>
{/* Left: description */}
<div>
<div style={{
fontSize: 11, fontWeight: 700, letterSpacing: '2px',
textTransform: 'uppercase', color: 'var(--ink-5)', marginBottom: 10,
}}>
وزارتخانه / سازمان دولتی
</div>
<h3 style={{
fontSize: 22, fontWeight: 900, letterSpacing: '-0.5px',
color: 'var(--ink)', marginBottom: 12,
}}>
راهکار سازمانی کامل
</h3>
<p style={{
fontSize: 13, lineHeight: 1.8, color: 'var(--ink-3)',
fontWeight: 300, marginBottom: 24,
}}>
به راهکارهای سفارشی، پشتیبانی اختصاصی یا قیمتگذاری حجمی نیاز دارید؟
با کارشناسان ما درباره پلن منحصربهفرد سازمانتان صحبت کنید.
</p>
<button
onMouseEnter={() => setHov(true)}
onMouseLeave={() => setHov(false)}
style={{
padding: '13px 28px',
border: '1px solid var(--ink)',
background: hov ? 'var(--ink)' : 'transparent',
color: hov ? 'var(--paper)' : 'var(--ink)',
fontSize: 12, fontWeight: 700,
fontFamily: 'Vazir, sans-serif',
cursor: 'pointer',
borderRadius: 0,
transition: 'background 180ms, color 180ms',
letterSpacing: '0.5px',
}}
>
تماس با تیم فروش
</button>
</div>
{/* Right: feature grid */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '10px 20px',
}}>
{enterpriseFeatures.map(f => (
<div key={f} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Check featured={false} />
<span style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.4, fontWeight: 300 }}>
{f}
</span>
</div>
))}
</div>
</div>
)
}
/* ─── Section ────────────────────────────────────────── */
export default function PlansSection() {
const allThree = plans
return (
<section style={{
background: 'var(--paper-2)',
borderBottom: '2px solid var(--ink)',
}}>
<div className="px-12 py-20 max-md:px-5 max-md:py-14">
{/* Header */}
<div style={{ textAlign: 'center', marginBottom: 56 }}>
<div style={{
fontSize: 9, fontWeight: 700, letterSpacing: '3px',
textTransform: 'uppercase', color: 'var(--red)',
marginBottom: 14,
}}>
اشتراک و دسترسی
</div>
<h2 style={{
fontSize: 'clamp(26px, 3vw, 40px)',
fontWeight: 900,
letterSpacing: '-1.5px',
color: 'var(--ink)',
marginBottom: 12,
}}>
پلن مناسب خود را انتخاب کنید
</h2>
<p style={{
fontSize: 14, lineHeight: 1.8,
color: 'var(--ink-4)', fontWeight: 300,
maxWidth: 480, margin: '0 auto',
}}>
از دسترسی پایه برای پژوهشگران تا راهکار کامل برای وزارتخانهها
هر سطحی از نیاز را پوشش میدهیم.
</p>
</div>
{/* 3-col plan cards */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 0,
marginBottom: 32,
position: 'relative',
}}
className="max-md:grid-cols-1 max-md:gap-0"
>
{allThree.map((plan, idx) => (
<PlanCard
key={plan.id}
plan={plan}
isMiddle={idx === 1}
/>
))}
</div>
{/* Enterprise bottom card */}
<EnterpriseCard />
{/* Money-back note */}
<div style={{
textAlign: 'center',
marginTop: 20,
fontSize: 11,
color: 'var(--ink-5)',
fontWeight: 300,
}}>
&nbsp; ضمانت بازگشت وجه ۱۴ روزه &nbsp;·&nbsp; بدون قرارداد اجباری
</div>
</div>
</section>
)
}

View File

@ -0,0 +1,279 @@
import { Link } from 'react-router-dom'
import { Carousel, Card } from '@/components/ui/apple-cards-carousel'
import type { CardData } from '@/components/ui/apple-cards-carousel'
import { reports } from '@/data/reports'
import type { Report } from '@/data/reports'
/* ─── Per-report images (industrial, steel-relevant) ─ */
const reportImage: Record<string, string> = {
'iran-steel-global-trade-scenarios-1404':
'https://images.unsplash.com/photo-1578662996442-48f60103fc96?q=80&w=1600&fit=crop',
'steel-energy-consumption-efficiency-1403':
'https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?q=80&w=1600&fit=crop',
'global-steel-market-q3-1403':
'https://images.unsplash.com/photo-1504328345606-18bbc8c9d7d1?q=80&w=1600&fit=crop',
'iran-steel-export-sanctions-impact-1403':
'https://images.unsplash.com/photo-1494412519320-aa613dfb7738?q=80&w=1600&fit=crop',
'flash-iron-ore-price-surge-1403':
'https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?q=80&w=1600&fit=crop',
'domestic-production-dri-outlook-1403':
'https://images.unsplash.com/photo-1567789884554-0b844b597180?q=80&w=1600&fit=crop',
'esg-carbon-emissions-steel-iran-1403':
'https://images.unsplash.com/photo-1466611653911-95081537e5b7?q=80&w=1600&fit=crop',
'policy-steel-pricing-reform-1403':
'https://images.unsplash.com/photo-1551288049-bebda4e38f71?q=80&w=1600&fit=crop',
'flash-china-steel-overcapacity-1403':
'https://images.unsplash.com/photo-1570168007204-dfb528c6958f?q=80&w=1600&fit=crop',
}
const fallbackImage =
'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?q=80&w=1600&fit=crop'
const typeLabel: Record<string, string> = {
quarterly: 'گزارش فصلی',
flash: 'گزارش فوری',
risk: 'گزارش ریسک',
free: 'رایگان',
special: 'گزارش ویژه',
}
const typeColor: Record<string, string> = {
flash: 'var(--red)',
risk: '#92400e',
free: '#166534',
quarterly: 'var(--ink-4)',
special: 'var(--ink)',
}
/* ─── Key findings per category (mock but plausible) ─ */
const keyFindings: Record<string, string[]> = {
'بازار جهانی': [
'کاهش ۱۲٪ تقاضای چین در نیمه دوم ۲۰۲۴ فشار قیمتی جهانی ایجاد کرده',
'هند با رشد ۱۸٪ ظرفیت تولید، رقیب اصلی ایران در بازارهای منطقه شده',
'پیش‌بینی بازگشت تقاضا در Q2 2025 با ریکاوری بخش ساختمان آمریکا',
],
'انرژی و ESG': [
'شکاف مصرف انرژی واحدهای ایرانی ۲۵٪ بیشتر از میانگین جهانی است',
'اجرای CBAM از ۲۰۲۶ می‌تواند ۱۵٪ رقابت‌پذیری صادرات را کاهش دهد',
'مسیر هیدروژن سبز برای ایران تا ۱۴۱۰ اقتصادی نخواهد بود',
],
'صادرات': [
'عراق، افغانستان و پاکستان ۶۵٪ صادرات فولاد ایران را تشکیل می‌دهند',
'مسیرهای جایگزین آفریقای شرقی پتانسیل ۲ میلیون تن اضافی دارد',
'تحریم‌های جدید ۳ مسیر اصلی پرداخت را مسدود کرده است',
],
'تولید داخلی': [
'ایران با ۳۰ میلیون تن DRI رتبه اول جهانی را حفظ کرده است',
'کمبود گاز زمستانی می‌تواند تا ۲۰٪ ظرفیت تولید را کاهش دهد',
'۸ پروژه در دست احداث تا ۱۴۰۵ به ظرفیت می‌رسند',
],
'سیاست‌گذاری': [
'قیمت‌گذاری دستوری هزینه ۱.۲ میلیارد دلار در سال به صنعت تحمیل می‌کند',
'تجربه ترکیه نشان می‌دهد آزادسازی تدریجی قیمت بهترین مسیر است',
'اصلاح تعرفه‌های سنگ‌آهن می‌تواند ۸٪ به حاشیه سود اضافه کند',
],
'ریسک و بحران': [
'احتمال تشدید تحریم‌ها در ۶ ماه آینده ۶۵٪ برآورد می‌شود',
'ریسک کمبود ارز برای واردات مواد اولیه در سطح بالا قرار دارد',
'شکست مذاکرات هسته‌ای می‌تواند ۳۰٪ صادرات را تحت تأثیر قرار دهد',
],
}
/* ─── Report contents (TOC-style) ───────────────────── */
const reportContents: Record<string, string[]> = {
quarterly: ['مروری بر بازار فصل', 'تحلیل قیمت‌ها و حجم معاملات', 'رویدادهای کلیدی', 'پیش‌بینی فصل آینده', 'داده‌های خام ضمیمه'],
flash: ['خلاصه اجرایی', 'رویداد و محرک', 'تأثیر بر بازار داخلی', 'توصیه‌های فوری'],
risk: ['ارزیابی ریسک', 'تحلیل سناریو', 'احتمال‌سنجی', 'راهکارهای پوشش ریسک', 'پیوست داده'],
free: ['خلاصه یافته‌ها', 'روش‌شناسی', 'مقایسه بین‌المللی', 'توصیه‌های سیاستی'],
special: ['تحلیل جامع', 'سناریوسازی کمی', 'مقایسه تطبیقی', 'توصیه‌های راهبردی', 'مدل Excel پیوست'],
}
/* ─── Expanded card body ─────────────────────────────── */
function ReportCardContent({ report }: { report: Report }) {
const findings = keyFindings[report.category] ?? []
const contents = reportContents[report.type] ?? []
return (
<div style={{ direction: 'rtl', fontFamily: 'Vazir, sans-serif' }}>
{/* Tags */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 20 }}>
{report.tags.map(tag => (
<span key={tag} style={{
fontSize: 10, fontWeight: 600,
padding: '3px 10px', borderRadius: 999,
background: '#f1f5f9', color: '#475569',
}}>
{tag}
</span>
))}
</div>
{/* Summary */}
<p style={{
fontSize: 14, lineHeight: 1.9, fontWeight: 300,
color: '#374151', marginBottom: 24,
paddingBottom: 24, borderBottom: '1px solid #f1f5f9',
}}>
{report.summary}
</p>
{/* Key findings */}
{findings.length > 0 && (
<div style={{ marginBottom: 24 }}>
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '2px', textTransform: 'uppercase', color: '#9b1c1c', marginBottom: 12 }}>
یافتههای کلیدی
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{findings.map((f, i) => (
<div key={i} style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
<div style={{ width: 5, height: 5, borderRadius: '50%', background: '#9b1c1c', flexShrink: 0, marginTop: 6 }} />
<span style={{ fontSize: 13, lineHeight: 1.7, color: '#374151', fontWeight: 300 }}>{f}</span>
</div>
))}
</div>
</div>
)}
{/* Report contents */}
{contents.length > 0 && (
<div style={{ marginBottom: 24, padding: '16px 20px', background: '#f8fafc', borderRadius: 12 }}>
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '2px', textTransform: 'uppercase', color: '#64748b', marginBottom: 10 }}>
محتوای این گزارش
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{contents.map((item, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 10, color: '#94a3b8', fontWeight: 700, minWidth: 18 }}>
{(i + 1).toLocaleString('fa-IR')}
</span>
<span style={{ fontSize: 12, color: '#475569', lineHeight: 1.5 }}>{item}</span>
</div>
))}
</div>
</div>
)}
{/* Meta grid */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10, marginBottom: 20 }}>
{[
{ label: 'نویسنده', value: report.author },
{ label: 'صفحات', value: `${report.pages} صفحه` },
{ label: 'تاریخ انتشار', value: report.publishDate },
].map(item => (
<div key={item.label} style={{ background: '#f8fafc', borderRadius: 10, padding: '10px 14px', textAlign: 'center' }}>
<div style={{ fontSize: 9, color: '#94a3b8', marginBottom: 4, fontWeight: 700, letterSpacing: '1px', textTransform: 'uppercase' }}>
{item.label}
</div>
<div style={{ fontSize: 12, fontWeight: 700, color: '#1e293b' }}>{item.value}</div>
</div>
))}
</div>
{/* Author */}
<div style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '14px 18px', background: '#f8fafc', borderRadius: 12, marginBottom: 20,
}}>
<div style={{
width: 40, height: 40, background: '#1a1712', color: '#f4efe7',
borderRadius: 999, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 16, fontWeight: 900, flexShrink: 0,
}}>
{report.authorInitial}
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 700, color: '#1e293b' }}>{report.author}</div>
<div style={{ fontSize: 11, color: '#64748b' }}>{report.authorRole}</div>
</div>
</div>
{/* CTA */}
<div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
{report.isFree ? (
<Link to={`/reports/${report.id}`} style={{
background: '#166534', color: 'white',
padding: '13px 28px', fontSize: 13, fontWeight: 700,
fontFamily: 'Vazir, sans-serif', borderRadius: 999,
textDecoration: 'none', display: 'inline-block',
}}>
دانلود رایگان
</Link>
) : (
<>
<button style={{
background: '#1a1712', color: '#f4efe7', border: 'none',
padding: '13px 28px', fontSize: 13, fontWeight: 700,
fontFamily: 'Vazir, sans-serif', cursor: 'pointer', borderRadius: 999,
}}>
خرید {report.price.toLocaleString('fa-IR')} تومان
</button>
<Link to={`/reports/${report.id}`} style={{
color: '#9b1c1c', fontSize: 12, fontWeight: 700, textDecoration: 'none',
}}>
مشاهده جزئیات
</Link>
</>
)}
</div>
</div>
)
}
/* ─── Section ────────────────────────────────────────── */
export default function ReportsCarouselSection() {
const carouselReports = reports.filter(r => !r.featured).slice(0, 10)
const cards: CardData[] = carouselReports.map(report => ({
src: reportImage[report.id] ?? fallbackImage,
title: report.title,
category: typeLabel[report.type] ?? report.type,
content: <ReportCardContent report={report} />,
}))
const cardElements = cards.map((card, index) => (
<Card key={card.src + index} card={card} index={index} layout />
))
return (
<section style={{ borderBottom: '2px solid var(--ink)' }}>
{/* Header */}
<div className="px-12 max-md:px-5">
<div style={{
display: 'flex', alignItems: 'center',
borderBottom: '1px solid var(--rule-thin)', paddingTop: 52,
}}>
<div style={{
background: 'var(--ink)', color: 'var(--paper)',
padding: '8px 20px', fontSize: 9, fontWeight: 700,
letterSpacing: '3px', textTransform: 'uppercase',
}}>
تحلیلهای اخیر
</div>
<div style={{ flex: 1 }} />
<Link to="/reports" style={{
fontSize: 10, fontWeight: 700, letterSpacing: '1.5px',
textTransform: 'uppercase', color: 'var(--ink-4)',
textDecoration: 'none', padding: '8px 0',
}}>
همه گزارشها
</Link>
</div>
</div>
{/* Legend */}
<div className="px-12 pt-[14px] max-md:px-5">
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
{Object.entries(typeLabel).map(([key, label]) => (
<div key={key} style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: typeColor[key] }} />
<span style={{ fontSize: 10, color: 'var(--ink-4)', fontWeight: 600 }}>{label}</span>
</div>
))}
</div>
</div>
<Carousel items={cardElements} />
</section>
)
}

View File

@ -0,0 +1,264 @@
import { useEffect, useRef, useState } from 'react'
import { Link } from 'react-router-dom'
/* ─── Risk data ──────────────────────────────────────────── */
const riskItems = [
{
quote: 'چین در نیمه اول ۲۰۲۴ رکورد ۵۴ میلیون تن صادرات فولاد را شکست. این موج صادراتی قیمت بازارهای آسیای غربی را ۱۲٪ کاهش داده و مستقیماً سهم ایران در عراق را تهدید می‌کند.',
name: 'بازار جهانی — چین',
date: 'بهمن ۱۴۰۳',
level: 'high' as const,
},
{
quote: 'اجرای مکانیسم CBAM اتحادیه اروپا از ۲۰۲۶ برای صنایع فولاد آغاز می‌شود. محصولات ایرانی با ضریب کربن بالاتر از میانگین جهانی، در بازار اروپا با افزایش هزینه ۱۵ تا ۲۲ دلار به ازای هر تن مواجه خواهند شد.',
name: 'مقررات — اتحادیه اروپا',
date: 'افق: ۲۰۲۶',
level: 'medium' as const,
},
{
quote: 'بسته تحریمی جدید، ۳ مسیر اصلی پرداخت بین‌المللی شرکت‌های فولادی ایران را مسدود کرده است. ریسک تأخیر در دریافت وجوه صادراتی به بالاترین سطح در ۵ سال اخیر رسیده است.',
name: 'ریسک ژئوپلیتیک — تحریم',
date: 'دی ۱۴۰۳',
level: 'critical' as const,
},
{
quote: 'هند با افزایش ۱۸٪ ظرفیت تولید فولاد در سال ۲۰۲۴، به سومین تولیدکننده بزرگ جهان تبدیل شد. رقابت هند در بازارهای هدف ایران شامل عراق، افغانستان و آفریقای شرقی به‌شدت تشدید شده.',
name: 'رقابت — هند',
date: 'افق میان‌مدت',
level: 'high' as const,
},
{
quote: 'محدودیت‌های زمستانی گاز صنعتی در ۳ ماه گذشته باعث توقف موقت ۱۴ واحد تولیدی فولاد در اصفهان، خراسان و هرمزگان شده است. ریسک کمبود انرژی همچنان ادامه دارد.',
name: 'زیرساخت — انرژی داخلی',
date: 'پایش مستمر',
level: 'high' as const,
},
{
quote: 'نرخ برابری ریال در برابر دلار در ۳ ماه اخیر ۸٪ کاهش یافته است. این نوسان از یک سو هزینه واردات مواد اولیه را افزایش و از سوی دیگر رقابت‌پذیری صادراتی را بهبود می‌بخشد.',
name: 'ریسک مالی — ارز',
date: 'بهمن ۱۴۰۳',
level: 'medium' as const,
},
{
quote: 'کاهش واردات فولاد ترکیه از بازار عراق، فرصت افزایش سهم ایران از ۴۰٪ به ۵۵٪ را فراهم کرده است. پنجره فرصت تا اواخر سال ۱۴۰۴ باز است.',
name: 'فرصت — بازار عراق',
date: 'افق: ۱۴۰۴',
level: 'opportunity' as const,
},
{
quote: 'کشورهای آفریقای شرقی (کنیا، اتیوپی، تانزانیا) در مجموع ۸ میلیون تن واردات فولاد در سال دارند. سهم فعلی ایران تنها ۳٪ است در حالی که زیرساخت لجستیکی برای افزایش به ۱۵٪ وجود دارد.',
name: 'فرصت — آفریقای شرقی',
date: 'بررسی استراتژیک',
level: 'opportunity' as const,
},
{
quote: 'قیمت سنگ‌آهن در بورس دالیان در ماه اخیر ۱۸٪ جهش داشته است. این افزایش مستقیماً هزینه تمام‌شده واحدهای وابسته به واردات سنگ‌آهن را متأثر می‌کند.',
name: 'کامودیتی — سنگ‌آهن',
date: 'آبان ۱۴۰۳',
level: 'medium' as const,
},
{
quote: 'روسیه پس از تحریم‌های غرب، مسیرهای صادراتی خود را به آسیا و خاورمیانه تغییر داده و با قیمت‌های پایین‌تر از نرخ جهانی وارد بازارهای هدف ایران شده است.',
name: 'رقابت — روسیه',
date: 'در حال پایش',
level: 'high' as const,
},
]
const LEVEL = {
critical: { dot: '#7f1d1d', label: 'بحرانی', bg: 'rgba(127,29,29,0.08)' },
high: { dot: '#9b1c1c', label: 'بالا', bg: 'rgba(155,28,28,0.07)' },
medium: { dot: '#92400e', label: 'متوسط', bg: 'rgba(146,64,14,0.07)' },
low: { dot: '#166534', label: 'پایین', bg: 'rgba(22,101,52,0.07)' },
opportunity: { dot: '#1e40af', label: 'فرصت', bg: 'rgba(30,64,175,0.07)' },
}
/* ─── Section ────────────────────────────────────────────── */
export default function RiskSection() {
const containerRef = useRef<HTMLDivElement>(null)
const listRef = useRef<HTMLUListElement>(null)
const [ready, setReady] = useState(false)
useEffect(() => {
if (!containerRef.current || !listRef.current) return
/* Clone all items for seamless loop */
const original = Array.from(listRef.current.children)
original.forEach(item => {
listRef.current!.appendChild(item.cloneNode(true))
})
setReady(true)
}, [])
return (
<section style={{ borderBottom: '2px solid var(--ink)', background: 'var(--paper-2)' }}>
<div className="px-12 pt-14 max-md:px-5 max-md:pt-10">
{/* ── Header bar ── */}
<div style={{
display: 'flex',
alignItems: 'center',
borderBottom: '1px solid var(--rule-thin)',
}}>
<div style={{
background: 'var(--red)',
color: 'white',
padding: '8px 20px',
fontSize: 9,
fontWeight: 700,
letterSpacing: '3px',
textTransform: 'uppercase',
}}>
پایش ریسک · لحظهای
</div>
<div style={{
marginRight: 16,
fontSize: 10,
color: 'var(--ink-5)',
fontWeight: 600,
letterSpacing: '0.5px',
}}>
{riskItems.length} سیگنال فعال
</div>
<div style={{ flex: 1 }} />
<Link to="/risks" style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: '1.5px',
textTransform: 'uppercase',
color: 'var(--ink-4)',
textDecoration: 'none',
padding: '8px 0',
}}>
همه ریسکها
</Link>
</div>
{/* ── Legend ── */}
<div style={{ display: 'flex', gap: 20, flexWrap: 'wrap', padding: '14px 0 0' }}>
{[
{ color: '#7f1d1d', label: 'بحرانی' },
{ color: '#9b1c1c', label: 'بالا' },
{ color: '#92400e', label: 'متوسط' },
{ color: '#166534', label: 'پایین' },
{ color: '#1e40af', label: 'فرصت' },
].map(l => (
<div key={l.label} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: l.color }} />
<span style={{ fontSize: 10, color: 'var(--ink-4)', fontWeight: 600 }}>{l.label}</span>
</div>
))}
</div>
</div>
{/* ── Auto-scroll ticker ── */}
<div
ref={containerRef}
style={{
position: 'relative',
overflow: 'hidden',
padding: '28px 0 40px',
/* Fade masks on both edges */
WebkitMaskImage:
'linear-gradient(to right, transparent 0%, black 7%, black 93%, transparent 100%)',
maskImage:
'linear-gradient(to right, transparent 0%, black 7%, black 93%, transparent 100%)',
}}
>
<ul
ref={listRef}
className={ready ? 'animate-scroll-risk' : ''}
style={{
display: 'flex',
gap: 20,
width: 'max-content',
flexWrap: 'nowrap',
listStyle: 'none',
direction: 'ltr',
/* pause on hover */
animationPlayState: 'running',
}}
onMouseEnter={e => (e.currentTarget.style.animationPlayState = 'paused')}
onMouseLeave={e => (e.currentTarget.style.animationPlayState = 'running')}
>
{riskItems.map((item, idx) => {
const cfg = LEVEL[item.level]
return (
<li
key={idx}
style={{
width: 340,
flexShrink: 0,
background: 'var(--paper)',
border: '1px solid var(--rule-thin)',
borderTop: `3px solid ${cfg.dot}`,
padding: '24px 24px 20px',
direction: 'rtl',
fontFamily: 'Vazir, sans-serif',
display: 'flex',
flexDirection: 'column',
}}
>
{/* Level badge */}
<div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 12 }}>
<div style={{
width: 7,
height: 7,
borderRadius: '50%',
background: cfg.dot,
flexShrink: 0,
}} />
<span style={{
fontSize: 9,
fontWeight: 700,
letterSpacing: '2px',
textTransform: 'uppercase',
color: cfg.dot,
}}>
{cfg.label}
</span>
</div>
{/* Quote */}
<p style={{
fontSize: 13,
lineHeight: 1.85,
color: 'var(--ink-2)',
fontWeight: 400,
marginBottom: 16,
flex: 1,
}}>
{item.quote}
</p>
{/* Footer */}
<div style={{
paddingTop: 12,
borderTop: '1px solid var(--rule-thin)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}>
<div>
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--ink)' }}>{item.name}</div>
<div style={{ fontSize: 10, color: 'var(--ink-5)' }}>{item.date}</div>
</div>
<div style={{
fontSize: 9,
fontWeight: 700,
letterSpacing: '1px',
textTransform: 'uppercase',
color: cfg.dot,
background: cfg.bg,
padding: '4px 10px',
}}>
{cfg.label}
</div>
</div>
</li>
)
})}
</ul>
</div>
</section>
)
}

View File

@ -0,0 +1,116 @@
import { marketPrices } from '@/data/market'
const prices = marketPrices.slice(0, 4)
export default function SnapshotBar() {
return (
<div
style={{
borderBottom: '2px solid var(--ink)',
backgroundColor: 'var(--paper-2)',
direction: 'rtl',
}}
>
<div
style={{
padding: '0 48px',
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
}}
className="max-md:grid-cols-2 max-md:px-6"
>
{prices.map((price, idx) => {
const isLast = idx === prices.length - 1
const isUp = price.trend === 'up'
const isDown = price.trend === 'down'
const arrow = isUp ? '↑' : isDown ? '↓' : '—'
const changeColor = isUp
? 'var(--green-ink)'
: isDown
? 'var(--red)'
: 'var(--ink-4)'
return (
<div
key={price.id}
style={{
padding: '24px 0',
borderRight: isLast ? 'none' : '1px solid var(--rule-thin)',
paddingLeft: idx === 0 ? '0' : '28px',
paddingRight: idx === prices.length - 1 ? '0' : '28px',
}}
className={`max-md:${
idx % 2 === 1
? 'border-r-0'
: idx < 2
? 'border-b border-[var(--rule-thin)]'
: ''
}`}
>
{/* Label */}
<div
style={{
fontSize: '9px',
textTransform: 'uppercase',
letterSpacing: '2px',
color: 'var(--ink-5)',
marginBottom: '6px',
fontWeight: 600,
}}
>
{price.name}
</div>
{/* Value */}
<div
style={{
fontSize: '22px',
fontWeight: 900,
letterSpacing: '-0.5px',
color: 'var(--ink)',
lineHeight: 1.1,
marginBottom: '4px',
}}
>
{price.value.toLocaleString('fa-IR')}
</div>
{/* Change row */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<span
style={{
fontSize: '11px',
fontWeight: 700,
color: changeColor,
}}
>
{arrow}{' '}
{Math.abs(price.changePercent).toLocaleString('fa-IR', {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
})}
٪
</span>
<span
style={{
fontSize: '10px',
color: 'var(--ink-4)',
fontWeight: 400,
}}
>
{price.unit}
</span>
</div>
</div>
)
})}
</div>
</div>
)
}

View File

@ -0,0 +1,165 @@
import { useState } from 'react'
import { reports } from '@/data/reports'
const featured = reports.find(r => r.featured) ?? reports[0]
const items = [
{ num: '۱', title: 'تحلیل ۴ سناریوی اصلی', desc: 'از بدبینانه تا خوش‌بینانه با احتمال‌سنجی کمی و مدل‌سازی قیمتی' },
{ num: '۲', title: 'نقشه بازارهای هدف جدید', desc: 'جایگزین‌های اروپا: آفریقای شرقی، آسیای مرکزی، عراق و افغانستان' },
{ num: '۳', title: '۱۲ توصیه سیاستی مستقیم', desc: 'برای وزارت صمت، گمرک، سازمان بنادر و بانک مرکزی' },
{ num: '۴', title: 'مقایسه تطبیقی با رقبا', desc: 'ایران در برابر ترکیه، هند، روسیه و برزیل در هر سناریو' },
{ num: '۵', title: 'مدل Excel پیوست', desc: 'داده‌های خام و مدل قابل شخصی‌سازی برای تیم‌های تحلیلی' },
]
function BuyButton({ label, dark }: { label: string; dark?: boolean }) {
const [hov, setHov] = useState(false)
return (
<button
onMouseEnter={() => setHov(true)}
onMouseLeave={() => setHov(false)}
style={{
padding: '12px 24px',
border: dark ? 'none' : '1px solid rgba(255,255,255,0.3)',
background: dark
? hov ? 'var(--paper-2)' : 'var(--paper)'
: hov ? 'rgba(255,255,255,0.08)' : 'transparent',
color: dark ? 'var(--ink)' : 'var(--paper)',
fontFamily: 'Vazir, sans-serif',
fontSize: 12,
fontWeight: dark ? 700 : 400,
cursor: 'pointer',
borderRadius: 0,
transition: 'background 200ms',
whiteSpace: 'nowrap',
}}
>
{label}
</button>
)
}
export default function SpecialSpreadSection() {
return (
<section
style={{
background: 'var(--ink)',
color: 'var(--paper)',
borderBottom: '3px solid var(--ink)',
display: 'grid',
gridTemplateColumns: '1.2fr 1fr',
}}
className="max-md:grid-cols-1"
>
{/* LEFT */}
<div
style={{
padding: '72px 72px 72px 48px',
borderLeft: '1px solid rgba(255,255,255,0.08)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}
className="max-md:p-8"
>
<div>
{/* Overline */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 28 }}>
<div style={{ width: 28, height: 2, background: 'var(--red)', flexShrink: 0 }} />
<span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'rgba(255,255,255,0.5)' }}>
گزارش استراتژیک · زمستان ۱۴۰۳
</span>
</div>
{/* Headline */}
<h2
style={{
fontSize: 'clamp(32px,3.5vw,52px)',
fontWeight: 900,
lineHeight: 1.1,
letterSpacing: '-1.5px',
color: 'var(--paper)',
marginBottom: 24,
}}
>
ایران در نقشه<br />
جدید تجارت<br />
فولاد جهان:<br />
<span style={{ color: 'rgba(255,255,255,0.35)' }}>چهار سناریو</span><br />
برای ۱۴۰۴
</h2>
{/* Description */}
<p style={{ fontSize: 15, lineHeight: 1.85, color: 'rgba(255,255,255,0.65)', fontWeight: 300, marginBottom: 36 }}>
{featured.summary}
</p>
</div>
<div>
{/* Author */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12, paddingTop: 24, borderTop: '1px solid rgba(255,255,255,0.12)', marginBottom: 28 }}>
<div style={{ width: 38, height: 38, background: 'var(--red)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 15, fontWeight: 900, color: 'white', flexShrink: 0 }}>
{featured.authorInitial}
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--paper)' }}>{featured.author}</div>
<div style={{ fontSize: 11, color: 'rgba(255,255,255,0.45)' }}>{featured.authorRole}</div>
</div>
</div>
{/* Price + CTA */}
<div style={{ display: 'flex', alignItems: 'center', gap: 20, flexWrap: 'wrap' }}>
<div>
<div style={{ fontSize: 10, color: 'rgba(255,255,255,0.4)', letterSpacing: '2px', textTransform: 'uppercase', marginBottom: 4 }}>قیمت گزارش</div>
<div style={{ fontSize: 34, fontWeight: 900, color: 'var(--paper)', letterSpacing: '-1px' }}>
{featured.price.toLocaleString('fa-IR')} تومان
</div>
</div>
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
<BuyButton label="خرید گزارش" dark />
<BuyButton label="خلاصه رایگان" />
</div>
</div>
</div>
</div>
{/* RIGHT */}
<div style={{ padding: '72px 48px 72px 40px', display: 'flex', flexDirection: 'column' }} className="max-md:p-8 max-md:pt-0">
<div
style={{
fontSize: 9,
fontWeight: 700,
letterSpacing: '3px',
textTransform: 'uppercase',
color: 'rgba(255,255,255,0.35)',
paddingBottom: 12,
borderBottom: '1px solid rgba(255,255,255,0.12)',
marginBottom: 20,
}}
>
آنچه در این گزارش مییابید
</div>
{items.map((item, i) => (
<div
key={i}
style={{
padding: '18px 0',
borderBottom: i < items.length - 1 ? '1px solid rgba(255,255,255,0.08)' : 'none',
display: 'flex',
gap: 16,
alignItems: 'flex-start',
}}
>
<div style={{ fontSize: 32, fontWeight: 900, color: 'rgba(255,255,255,0.1)', lineHeight: 1, flexShrink: 0, width: 32, textAlign: 'left', direction: 'ltr' }}>
{item.num}
</div>
<div>
<div style={{ fontSize: 14, fontWeight: 700, color: 'rgba(255,255,255,0.85)', marginBottom: 4 }}>{item.title}</div>
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.4)', lineHeight: 1.65 }}>{item.desc}</div>
</div>
</div>
))}
</div>
</section>
)
}

View File

@ -0,0 +1,51 @@
import { Link } from 'react-router-dom'
import { team } from '@/data/team'
function MemberCard({ member, idx }: { member: (typeof team)[0]; idx: number }) {
const borderRight = idx % 2 === 0 ? '1px solid var(--rule-thin)' : 'none'
const borderTop = idx >= 2 ? '1px solid var(--rule-thin)' : 'none'
return (
<div
style={{ padding: '40px', borderRight, borderTop, cursor: 'pointer', transition: 'background 200ms' }}
onMouseEnter={e => (e.currentTarget.style.background = 'var(--paper-2)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
className="max-md:border-r-0 max-md:border-t max-md:border-[var(--rule-thin)] max-md:first:border-t-0 max-md:p-6"
>
<div style={{ width: 56, height: 56, background: 'var(--paper-3)', border: '1px solid var(--rule-thin)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 22, fontWeight: 900, color: 'var(--ink)', marginBottom: 16 }}>
{member.initial}
</div>
<div style={{ fontSize: 15, fontWeight: 800, color: 'var(--ink)', letterSpacing: '-0.3px', marginBottom: 3 }}>{member.name}</div>
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '1.5px', textTransform: 'uppercase', color: 'var(--red)', marginBottom: 12 }}>{member.role}</div>
<p style={{ fontSize: 13, color: 'var(--ink-3)', lineHeight: 1.75, fontWeight: 400, marginBottom: 14 }}>{member.bio}</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: 14 }}>
{member.expertise.slice(0, 4).map(tag => (
<span key={tag} style={{ fontSize: 9, padding: '2px 7px', background: 'var(--paper-3)', color: 'var(--ink-4)', fontWeight: 600, letterSpacing: '0.5px' }}>{tag}</span>
))}
</div>
<div style={{ paddingTop: 12, borderTop: '1px solid var(--rule-thin)', fontSize: 11, color: 'var(--ink-5)' }}>
<strong style={{ fontSize: 20, fontWeight: 900, color: 'var(--ink)', marginLeft: 4 }}>{member.reportCount}</strong>
گزارش منتشرشده
</div>
</div>
)
}
export default function TeamSection() {
return (
<section style={{ borderBottom: '2px solid var(--ink)' }}>
<div className="px-12 py-20 max-md:px-5 max-md:py-14">
<div style={{ display: 'flex', alignItems: 'center', borderBottom: '1px solid var(--rule-thin)', marginBottom: '40px' }}>
<div style={{ background: 'var(--ink)', color: 'var(--paper)', padding: '8px 20px', fontSize: '9px', fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase' }}>تیم تحریریه</div>
<div style={{ flex: 1 }} />
<Link to="/team" style={{ fontSize: '10px', fontWeight: 700, letterSpacing: '1.5px', textTransform: 'uppercase', color: 'var(--ink-4)', textDecoration: 'none', padding: '8px 0' }}>همه کارشناسان </Link>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }} className="max-md:grid-cols-1">
{team.slice(0, 4).map((member, idx) => (
<MemberCard key={member.id} member={member} idx={idx} />
))}
</div>
</div>
</section>
)
}

757
src/pages/Market/Market.tsx Normal file
View File

@ -0,0 +1,757 @@
import { useState, useMemo } from 'react'
import type React from 'react'
import ReactApexChart from 'react-apexcharts'
import type { ApexOptions } from 'apexcharts'
import { TrendingUp, TrendingDown, Minus } from 'lucide-react'
import { marketPrices, priceHistory, globalComparison, monthlyExport } from '@/data/market'
import type { MarketPrice, PriceTrend } from '@/data/market'
/* ─── chart shared theme ─────────────────────────────────── */
const CHART_FONT = 'Vazir, sans-serif'
const GRID_COLOR = 'rgba(26,23,18,0.08)'
const LABEL_COLOR = '#766e68'
/* ─── time range filter ──────────────────────────────────── */
type Range = '1M' | '3M' | '6M' | '1Y'
const RANGES: { label: string; value: Range; months: number }[] = [
{ label: '۱ ماه', value: '1M', months: 1 },
{ label: '۳ ماه', value: '3M', months: 3 },
{ label: '۶ ماه', value: '6M', months: 6 },
{ label: '۱ سال', value: '1Y', months: 12 },
]
/* ─── trend icon ─────────────────────────────────────────── */
function TrendIcon({ trend }: { trend: PriceTrend }) {
if (trend === 'up') return <TrendingUp size={12} />
if (trend === 'down') return <TrendingDown size={12} />
return <Minus size={12} />
}
/* ─── price card ─────────────────────────────────────────── */
function PriceCard({ item }: { item: MarketPrice }) {
const isUp = item.trend === 'up'
const isDown = item.trend === 'down'
const trendColor = isUp ? 'var(--green-ink)' : isDown ? 'var(--red)' : 'var(--ink-4)'
const trendBg = isUp
? 'rgba(22,101,52,0.08)'
: isDown
? 'rgba(155,28,28,0.08)'
: 'rgba(26,23,18,0.06)'
return (
<div
style={{
borderLeft: '1px solid var(--rule-thin)',
padding: 32,
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
<p style={{ fontSize: 13, color: 'var(--ink-3)', marginBottom: 4 }}>{item.name}</p>
<p
style={{
fontSize: 28,
fontWeight: 900,
color: 'var(--ink)',
lineHeight: 1,
fontVariantNumeric: 'tabular-nums',
letterSpacing: '-0.5px',
}}
>
{item.value.toLocaleString('fa-IR')}
</p>
<p style={{ fontSize: 11, color: 'var(--ink-4)' }}>{item.unit}</p>
{/* Change badge */}
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 5,
padding: '4px 10px',
background: trendBg,
color: trendColor,
fontSize: 11,
fontWeight: 700,
width: 'fit-content',
marginTop: 4,
}}
>
<TrendIcon trend={item.trend} />
<span>
{item.changePercent > 0 ? '+' : ''}
{item.changePercent.toFixed(1)}٪
</span>
<span style={{ color: trendColor, opacity: 0.7, fontSize: 10 }}>
({item.change > 0 ? '+' : ''}
{item.change.toLocaleString('fa-IR')})
</span>
</div>
</div>
)
}
/* ─── comparison tool ────────────────────────────────────── */
function ComparisonTool() {
const [productA, setProductA] = useState(marketPrices[0].id)
const [productB, setProductB] = useState(marketPrices[1].id)
const itemA = marketPrices.find((m) => m.id === productA)!
const itemB = marketPrices.find((m) => m.id === productB)!
const selectStyle: React.CSSProperties = {
border: '1px solid var(--ink)',
padding: '8px 14px',
fontSize: 12,
fontFamily: 'inherit',
background: 'var(--paper)',
color: 'var(--ink)',
borderRadius: 0,
cursor: 'pointer',
outline: 'none',
direction: 'rtl',
flex: 1,
}
return (
<div>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 16,
marginBottom: 32,
flexWrap: 'wrap',
}}
>
<select
value={productA}
onChange={(e) => setProductA(e.target.value)}
style={selectStyle}
>
{marketPrices.map((m) => (
<option key={m.id} value={m.id}>
{m.name}
</option>
))}
</select>
<span
style={{
fontSize: 16,
fontWeight: 900,
color: 'var(--ink-4)',
padding: '0 8px',
}}
>
در برابر
</span>
<select
value={productB}
onChange={(e) => setProductB(e.target.value)}
style={selectStyle}
>
{marketPrices.map((m) => (
<option key={m.id} value={m.id}>
{m.name}
</option>
))}
</select>
</div>
{/* Side-by-side comparison */}
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr auto 1fr',
gap: 0,
border: '1px solid var(--rule-thin)',
}}
>
<div style={{ padding: 32, textAlign: 'center' }}>
<p style={{ fontSize: 13, color: 'var(--ink-3)', marginBottom: 8 }}>{itemA.name}</p>
<p
style={{
fontSize: 32,
fontWeight: 900,
color: 'var(--ink)',
letterSpacing: '-1px',
lineHeight: 1,
}}
>
{itemA.value.toLocaleString('fa-IR')}
</p>
<p style={{ fontSize: 11, color: 'var(--ink-5)', marginTop: 4 }}>{itemA.unit}</p>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 4,
marginTop: 12,
padding: '4px 12px',
background:
itemA.trend === 'up'
? 'rgba(22,101,52,0.08)'
: itemA.trend === 'down'
? 'rgba(155,28,28,0.08)'
: 'rgba(26,23,18,0.06)',
color:
itemA.trend === 'up'
? 'var(--green-ink)'
: itemA.trend === 'down'
? 'var(--red)'
: 'var(--ink-4)',
fontSize: 12,
fontWeight: 700,
}}
>
<TrendIcon trend={itemA.trend} />
{itemA.changePercent > 0 ? '+' : ''}
{itemA.changePercent.toFixed(1)}٪
</div>
</div>
{/* VS divider */}
<div
style={{
borderRight: '1px solid var(--rule-thin)',
borderLeft: '1px solid var(--rule-thin)',
width: 60,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
fontSize: 16,
fontWeight: 900,
color: 'var(--ink-4)',
letterSpacing: '2px',
writingMode: 'vertical-rl',
}}
>
VS
</span>
</div>
<div style={{ padding: 32, textAlign: 'center' }}>
<p style={{ fontSize: 13, color: 'var(--ink-3)', marginBottom: 8 }}>{itemB.name}</p>
<p
style={{
fontSize: 32,
fontWeight: 900,
color: 'var(--ink)',
letterSpacing: '-1px',
lineHeight: 1,
}}
>
{itemB.value.toLocaleString('fa-IR')}
</p>
<p style={{ fontSize: 11, color: 'var(--ink-5)', marginTop: 4 }}>{itemB.unit}</p>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 4,
marginTop: 12,
padding: '4px 12px',
background:
itemB.trend === 'up'
? 'rgba(22,101,52,0.08)'
: itemB.trend === 'down'
? 'rgba(155,28,28,0.08)'
: 'rgba(26,23,18,0.06)',
color:
itemB.trend === 'up'
? 'var(--green-ink)'
: itemB.trend === 'down'
? 'var(--red)'
: 'var(--ink-4)',
fontSize: 12,
fontWeight: 700,
}}
>
<TrendIcon trend={itemB.trend} />
{itemB.changePercent > 0 ? '+' : ''}
{itemB.changePercent.toFixed(1)}٪
</div>
</div>
</div>
<p
style={{
fontSize: 11,
color: 'var(--ink-5)',
textAlign: 'center',
marginTop: 16,
fontStyle: 'italic',
}}
>
نمودار مقایسهای در نسخههای آینده تکمیل میشود
</p>
</div>
)
}
/* ─── main component ─────────────────────────────────────── */
export default function Market() {
const [priceRange, setPriceRange] = useState<Range>('1Y')
/* Filter price history based on selected range */
const filteredHistory = useMemo(() => {
const range = RANGES.find((r) => r.value === priceRange)!
return priceHistory.slice(-range.months)
}, [priceRange])
/* Line chart options */
const lineOptions: ApexOptions = {
chart: {
type: 'area',
toolbar: { show: false },
fontFamily: CHART_FONT,
background: 'transparent',
animations: { enabled: true, speed: 400 },
},
colors: ['#9b1c1c'],
stroke: { width: 2, curve: 'smooth' },
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.15,
opacityTo: 0,
stops: [0, 100],
},
},
grid: {
borderColor: GRID_COLOR,
strokeDashArray: 3,
},
xaxis: {
categories: filteredHistory.map((p) => p.month),
labels: {
style: { fontFamily: CHART_FONT, colors: LABEL_COLOR, fontSize: '11px' },
rotate: -35,
},
axisBorder: { color: GRID_COLOR },
axisTicks: { color: GRID_COLOR },
},
yaxis: {
opposite: true,
labels: {
style: { fontFamily: CHART_FONT, colors: LABEL_COLOR, fontSize: '11px' },
formatter: (val: number) => val.toLocaleString('fa-IR'),
},
},
tooltip: {
theme: 'light',
style: { fontFamily: CHART_FONT, fontSize: '12px' },
y: { formatter: (val: number) => val.toLocaleString('fa-IR') + ' تومان/کیلو' },
},
dataLabels: { enabled: false },
}
const lineSeries = [
{
name: 'شمش فولاد',
data: filteredHistory.map((p) => p.price),
},
]
/* Bar chart options */
const barOptions: ApexOptions = {
chart: {
type: 'bar',
toolbar: { show: false },
fontFamily: CHART_FONT,
background: 'transparent',
animations: { enabled: true, speed: 400 },
},
colors: ['#2e2a25'],
plotOptions: {
bar: {
borderRadius: 0,
columnWidth: '65%',
},
},
grid: {
borderColor: GRID_COLOR,
strokeDashArray: 3,
},
xaxis: {
categories: monthlyExport.map((e) => e.month),
labels: {
style: { fontFamily: CHART_FONT, colors: LABEL_COLOR, fontSize: '11px' },
rotate: -35,
},
axisBorder: { color: GRID_COLOR },
axisTicks: { color: GRID_COLOR },
},
yaxis: {
opposite: true,
labels: {
style: { fontFamily: CHART_FONT, colors: LABEL_COLOR, fontSize: '11px' },
formatter: (val: number) => (val / 1000).toFixed(0) + 'k',
},
},
tooltip: {
theme: 'light',
style: { fontFamily: CHART_FONT, fontSize: '12px' },
y: {
formatter: (val: number) =>
(val / 1000).toLocaleString('fa-IR') + ' هزار تن',
},
},
dataLabels: { enabled: false },
}
const barSeries = [
{
name: 'صادرات',
data: monthlyExport.map((e) => e.tonnage),
},
]
/* Section heading style */
const SectionHeading = ({
title,
subtitle,
children,
}: {
title: string
subtitle?: string
children?: React.ReactNode
}) => (
<div
style={{
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'space-between',
marginBottom: 32,
flexWrap: 'wrap',
gap: 12,
}}
>
<div>
<h2
style={{
fontSize: 22,
fontWeight: 900,
color: 'var(--ink)',
letterSpacing: '-0.5px',
}}
>
{title}
</h2>
{subtitle && (
<p style={{ fontSize: 12, color: 'var(--ink-5)', marginTop: 4 }}>{subtitle}</p>
)}
</div>
{children}
</div>
)
return (
<div style={{ direction: 'rtl' }}>
{/* ─── Page Header ───────────────────────────────── */}
<div
style={{
maxWidth: 1280,
margin: '0 auto',
padding: '48px 48px 32px',
borderBottom: '3px solid var(--ink)',
}}
>
<div
style={{
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 16,
}}
>
<div>
<h1
style={{
fontSize: 36,
fontWeight: 900,
letterSpacing: '-1px',
color: 'var(--ink)',
marginBottom: 6,
}}
>
داشبورد بازار
</h1>
<p style={{ fontSize: 15, fontWeight: 300, color: 'var(--ink-3)' }}>
قیمتهای لحظهای، روند تاریخی و تحلیل صادرات فولاد ایران
</p>
</div>
<div
style={{
fontSize: 11,
color: 'var(--ink-5)',
border: '1px solid var(--rule-thin)',
padding: '6px 14px',
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<span
style={{
width: 7,
height: 7,
borderRadius: '50%',
background: 'var(--green-ink)',
display: 'inline-block',
}}
/>
آخرین بروزرسانی: ۴ خرداد ۱۴۰۴
</div>
</div>
</div>
{/* ─── Section 1: Price Grid ──────────────────────── */}
<div
style={{
borderBottom: '2px solid var(--ink)',
padding: '48px 0',
}}
>
<div style={{ maxWidth: 1280, margin: '0 auto', padding: '0 48px' }}>
<SectionHeading
title="قیمت‌های امروز"
subtitle="قیمت بازار داخلی — تومان به کیلوگرم / دلار به تن"
/>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
borderTop: '1px solid var(--rule-thin)',
borderRight: '1px solid var(--rule-thin)',
}}
className="price-grid"
>
{marketPrices.map((item) => (
<PriceCard key={item.id} item={item} />
))}
</div>
</div>
</div>
{/* ─── Section 2: Price History Chart ────────────── */}
<div
style={{
borderBottom: '2px solid var(--ink)',
padding: '64px 0',
}}
>
<div style={{ maxWidth: 1280, margin: '0 auto', padding: '0 48px' }}>
<SectionHeading
title="روند قیمت شمش فولاد"
subtitle="تومان به کیلوگرم"
>
{/* Range selector */}
<div style={{ display: 'flex', gap: 0 }}>
{RANGES.map((r, rIdx) => (
<button
key={r.value}
onClick={() => setPriceRange(r.value)}
style={{
padding: '6px 16px',
fontSize: 11,
fontWeight: 700,
border: '1px solid var(--ink)',
borderLeft: rIdx === 0 ? '1px solid var(--ink)' : 'none',
background: priceRange === r.value ? 'var(--ink)' : 'transparent',
color: priceRange === r.value ? 'var(--paper)' : 'var(--ink)',
cursor: 'pointer',
fontFamily: 'inherit',
borderRadius: 0,
transition: 'all 120ms',
}}
>
{r.label}
</button>
))}
</div>
</SectionHeading>
<div
style={{
border: '1px solid var(--rule-thin)',
padding: '24px 8px 8px',
background: 'var(--paper)',
}}
>
<ReactApexChart
options={lineOptions}
series={lineSeries}
type="area"
height={320}
/>
</div>
</div>
</div>
{/* ─── Section 3: Global Comparison Table ─────────── */}
<div
style={{
borderBottom: '2px solid var(--ink)',
padding: '64px 0',
}}
>
<div style={{ maxWidth: 1280, margin: '0 auto', padding: '0 48px' }}>
<SectionHeading
title="مقایسه جهانی"
subtitle="قیمت شمش فولاد در بازارهای اصلی"
/>
<table
style={{
borderCollapse: 'collapse',
width: '100%',
fontSize: 13,
}}
>
<thead>
<tr style={{ background: 'var(--ink)', color: 'var(--paper)' }}>
{['کشور', 'قیمت شمش (دلار/تن)', 'ارز', 'مقایسه با ایران'].map((h) => (
<th
key={h}
style={{
padding: '14px 20px',
textAlign: 'right',
fontWeight: 700,
fontSize: 11,
letterSpacing: '0.5px',
textTransform: 'uppercase',
}}
>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{globalComparison.map((item, i) => {
const isIran = item.country === 'ایران'
return (
<tr
key={item.country}
style={{
background: i % 2 === 0 ? 'var(--paper)' : 'var(--paper-2)',
borderBottom: '1px solid var(--rule-thin)',
}}
>
<td style={{ padding: '14px 20px', fontWeight: isIran ? 700 : 400 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 18 }}>{item.flag}</span>
{item.country}
{isIran && (
<span
style={{
fontSize: 9,
fontWeight: 700,
color: 'var(--paper)',
background: 'var(--red)',
padding: '2px 6px',
letterSpacing: '1px',
}}
>
ایران
</span>
)}
</div>
</td>
<td style={{ padding: '14px 20px', fontWeight: 700 }}>
{item.currency === 'USD'
? `$${item.price.toLocaleString()}`
: item.price.toLocaleString('fa-IR') + ' تومان/کیلو'}
</td>
<td style={{ padding: '14px 20px', color: 'var(--ink-4)' }}>
{item.currency}
</td>
<td style={{ padding: '14px 20px' }}>
{isIran ? (
<span
style={{
fontSize: 10,
color: 'var(--ink-5)',
fontStyle: 'italic',
}}
>
مبنا
</span>
) : (
<span
style={{
fontSize: 11,
fontWeight: 600,
color: item.price > 500 ? 'var(--green-ink)' : 'var(--red)',
}}
>
{item.price > 500 ? 'بالاتر از میانگین' : 'پایین‌تر از میانگین'}
</span>
)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
{/* ─── Section 4: Export Bar Chart ─────────────────── */}
<div
style={{
borderBottom: '2px solid var(--ink)',
padding: '64px 0',
}}
>
<div style={{ maxWidth: 1280, margin: '0 auto', padding: '0 48px' }}>
<SectionHeading
title="صادرات ماهانه"
subtitle="هزار تن — بر اساس داده‌های گمرک ایران"
/>
<div
style={{
border: '1px solid var(--rule-thin)',
padding: '24px 8px 8px',
background: 'var(--paper)',
}}
>
<ReactApexChart options={barOptions} series={barSeries} type="bar" height={280} />
</div>
</div>
</div>
{/* ─── Section 5: Comparison Tool ──────────────────── */}
<div style={{ padding: '64px 0' }}>
<div style={{ maxWidth: 1280, margin: '0 auto', padding: '0 48px' }}>
<SectionHeading
title="ابزار مقایسه"
subtitle="مقایسه قیمت لحظه‌ای دو محصول"
/>
<ComparisonTool />
</div>
</div>
{/* ─── Responsive styles ─────────────────────────── */}
<style>{`
@media (max-width: 767px) {
.price-grid {
grid-template-columns: 1fr 1fr !important;
}
}
@media (max-width: 480px) {
.price-grid {
grid-template-columns: 1fr !important;
}
}
`}</style>
</div>
)
}

View File

@ -0,0 +1,721 @@
import { useState } from 'react'
import { Link, useParams, Navigate } from 'react-router-dom'
import { Lock, Download, FileText, ChevronRight, Tag, User } from 'lucide-react'
import { reports } from '@/data/reports'
import type { Report } from '@/data/reports'
/* ─── helpers ────────────────────────────────────────────── */
function formatPrice(price: number): string {
return price.toLocaleString('fa-IR') + ' تومان'
}
const TYPE_LABELS: Record<string, string> = {
quarterly: 'گزارش فصلی',
flash: 'Flash Report',
risk: 'گزارش ریسک',
free: 'گزارش رایگان',
special: 'گزارش ویژه',
}
/* ─── Persian lorem paragraphs ──────────────────────────── */
const LOREM_PARAGRAPHS = [
'صنعت فولاد ایران در دوره‌ای از تحولات عمیق ساختاری قرار دارد. ظرفیت تولیدی کشور در سال‌های اخیر به رکوردهای تاریخی دست یافته، اما همزمان چالش‌های پیچیده‌ای در حوزه صادرات، تأمین مالی و دسترسی به بازارهای جهانی در برابر صنعت قرار گرفته است. این گزارش با بهره‌گیری از داده‌های اولیه و تحلیل‌های کمی، وضعیت کنونی را از زوایای مختلف بررسی می‌کند.',
'در این مطالعه، رویکرد ترکیبی از تحلیل سری‌های زمانی قیمتی، مدل‌سازی اقتصادسنجی و تحلیل سناریو به‌کار رفته است. داده‌های مورد استفاده از منابع معتبر بین‌المللی از جمله World Steel Association، Platts و گزارش‌های رسمی وزارت صنعت، معدن و تجارت جمع‌آوری شده‌اند. دوره تحلیل پنج سال اخیر را در بر می‌گیرد.',
'یافته‌های کلیدی این گزارش نشان می‌دهد که تقاضای داخلی فولاد در بخش ساختمان و زیرساخت در میان‌مدت روند کاهشی دارد، در حالی که تقاضای صنعتی و ماشین‌سازی رشد ملایمی را تجربه خواهد کرد. این تحول در ترکیب تقاضا مستلزم بازنگری در استراتژی محصول کارخانه‌های فولادی کشور است.',
'توصیه‌های سیاست‌گذاری این گزارش در سه محور اصلی ارائه می‌شود: اول، اصلاح ساختار قیمت‌گذاری انرژی و مواد اولیه برای بهبود رقابت‌پذیری صادراتی؛ دوم، توسعه مسیرهای دسترسی به بازارهای هدف جایگزین در آفریقا و آسیای جنوب‌شرقی؛ سوم، تقویت ظرفیت مذاکراتی بخش خصوصی در قراردادهای بین‌المللی.',
]
const TOC_ITEMS = [
'خلاصه مدیریتی',
'بررسی وضعیت کنونی بازار',
'تحلیل روندهای عرضه و تقاضا',
'مدل‌سازی سناریوهای آینده',
'تأثیرات ژئوپلیتیک و محیط کسب‌وکار',
'تحلیل رقابت‌پذیری و موقعیت ایران',
'توصیه‌های سیاست‌گذاری و اجرایی',
'پیوست‌ها: داده‌ها و روش‌شناسی',
]
/* ─── related reports sidebar ────────────────────────────── */
function RelatedReports({ current }: { current: Report }) {
const related = reports
.filter((r) => r.id !== current.id && r.category === current.category)
.slice(0, 3)
if (related.length === 0) return null
return (
<div style={{ marginTop: 16 }}>
<p
style={{
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '2px',
color: 'var(--ink-5)',
marginBottom: 12,
}}
>
گزارشهای مرتبط
</p>
{related.map((r, i) => (
<div
key={r.id}
style={{
borderBottom: i < related.length - 1 ? '1px solid var(--rule-thin)' : 'none',
padding: '12px 0',
}}
>
<Link
to={`/reports/${r.id}`}
style={{
fontSize: 12,
fontWeight: 600,
color: 'var(--ink)',
textDecoration: 'none',
lineHeight: 1.4,
display: 'block',
marginBottom: 6,
}}
>
{r.title}
</Link>
<span
style={{
fontSize: 11,
fontWeight: 900,
color: r.isFree ? 'var(--green-ink)' : 'var(--ink)',
}}
>
{r.isFree ? 'رایگان' : formatPrice(r.price)}
</span>
</div>
))}
</div>
)
}
/* ─── main component ─────────────────────────────────────── */
export default function ReportDetail() {
const { id } = useParams<{ id: string }>()
const [buyHovered, setBuyHovered] = useState(false)
const [dlHovered, setDlHovered] = useState(false)
const report = reports.find((r) => r.id === id)
if (!report) return <Navigate to="/reports" replace />
return (
<div
style={{
maxWidth: 1280,
margin: '0 auto',
padding: '48px 48px 80px',
direction: 'rtl',
}}
>
{/* ─── Breadcrumb ────────────────────────────────── */}
<nav
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 40,
flexWrap: 'wrap',
}}
>
<Link
to="/reports"
style={{ fontSize: 11, color: 'var(--ink-5)', textDecoration: 'none' }}
>
گزارشها
</Link>
<ChevronRight size={10} style={{ color: 'var(--ink-5)', transform: 'scaleX(-1)' }} />
<Link
to={`/reports?category=${report.category}`}
style={{ fontSize: 11, color: 'var(--ink-5)', textDecoration: 'none' }}
>
{report.category}
</Link>
<ChevronRight size={10} style={{ color: 'var(--ink-5)', transform: 'scaleX(-1)' }} />
<span
style={{
fontSize: 11,
color: 'var(--ink)',
fontWeight: 600,
maxWidth: 300,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{report.title}
</span>
</nav>
{/* ─── Two-column layout ─────────────────────────── */}
<div
style={{
display: 'grid',
gridTemplateColumns: '2fr 1fr',
gap: 48,
alignItems: 'start',
}}
className="report-detail-grid"
>
{/* ═══ LEFT: Main content ══════════════════════════ */}
<div>
{/* Kicker */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
<span
style={{
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '2px',
color: 'var(--red)',
}}
>
{TYPE_LABELS[report.type] || report.type} · {report.category}
</span>
<div
style={{
flex: 1,
height: 1,
background: 'var(--red)',
opacity: 0.3,
}}
/>
</div>
{/* Headline */}
<h1
style={{
fontSize: 'clamp(28px, 3vw, 40px)',
fontWeight: 900,
lineHeight: 1.2,
letterSpacing: '-1px',
color: 'var(--ink)',
marginBottom: 20,
}}
>
{report.title}
</h1>
{/* Meta row */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 16,
marginBottom: 24,
flexWrap: 'wrap',
}}
>
{/* Author initial */}
<div
style={{
width: 30,
height: 30,
background: 'var(--ink)',
color: 'var(--paper)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 13,
fontWeight: 700,
flexShrink: 0,
}}
>
{report.authorInitial}
</div>
<div>
<p style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.3 }}>
{report.author}
</p>
<p style={{ fontSize: 11, color: 'var(--ink-5)', lineHeight: 1.3 }}>
{report.authorRole}
</p>
</div>
<div
style={{
width: 1,
height: 28,
background: 'var(--rule-thin)',
}}
/>
<span style={{ fontSize: 11, color: 'var(--ink-5)' }}>{report.publishDate}</span>
<div
style={{
width: 1,
height: 28,
background: 'var(--rule-thin)',
}}
/>
<span style={{ fontSize: 11, color: 'var(--ink-5)' }}>
<FileText size={11} style={{ display: 'inline', marginLeft: 4 }} />
{report.pages} صفحه
</span>
</div>
{/* Thick rule */}
<div style={{ height: 2, background: 'var(--ink)', marginBottom: 32 }} />
{/* Abstract */}
<section style={{ marginBottom: 40 }}>
<p
style={{
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '2.5px',
color: 'var(--ink-5)',
marginBottom: 12,
}}
>
چکیده
</p>
{LOREM_PARAGRAPHS.map((para, i) => (
<p
key={i}
style={{
fontSize: 15,
fontWeight: 300,
lineHeight: 1.85,
color: 'var(--ink-2)',
marginBottom: 18,
}}
>
{i === 0 ? report.summary : para}
</p>
))}
</section>
{/* Table of Contents */}
<section style={{ marginBottom: 40 }}>
<div
style={{
borderTop: '2px solid var(--ink)',
borderBottom: '1px solid var(--rule-thin)',
padding: '14px 0',
marginBottom: 16,
}}
>
<p
style={{
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '2.5px',
color: 'var(--ink-5)',
}}
>
فهرست مطالب
</p>
</div>
<ol
style={{
listStyle: 'none',
padding: 0,
margin: 0,
}}
>
{TOC_ITEMS.map((item, i) => (
<li
key={i}
style={{
display: 'flex',
alignItems: 'baseline',
gap: 12,
padding: '10px 0',
borderBottom: '1px solid var(--rule-thin)',
fontSize: 13,
color: 'var(--ink-2)',
}}
>
<span
style={{
fontSize: 10,
fontWeight: 700,
color: 'var(--ink-5)',
minWidth: 20,
textAlign: 'center',
}}
>
{(i + 1).toLocaleString('fa-IR')}
</span>
<span>{item}</span>
<span style={{ flex: 1, borderBottom: '1px dotted var(--rule-thin)', marginBottom: 3 }} />
<span style={{ fontSize: 11, color: 'var(--ink-5)' }}>
{((i + 1) * 8 + 3).toLocaleString('fa-IR')}
</span>
</li>
))}
</ol>
</section>
{/* Free preview + paywall */}
{!report.isFree && (
<section style={{ marginBottom: 40 }}>
<div
style={{
borderTop: '2px solid var(--ink)',
borderBottom: '1px solid var(--rule-thin)',
padding: '14px 0',
marginBottom: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<p
style={{
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '2.5px',
color: 'var(--ink-5)',
}}
>
پیشنمایش رایگان
</p>
<span
style={{
fontSize: 9,
fontWeight: 700,
letterSpacing: '1px',
color: 'var(--red)',
border: '1px solid var(--red)',
padding: '2px 8px',
}}
>
۳ صفحه اول
</span>
</div>
<div style={{ position: 'relative', overflow: 'hidden' }}>
{/* Fake preview content */}
<div style={{ padding: '20px 0' }}>
{[90, 100, 75, 100, 60, 100, 85, 40].map((width, i) => (
<div
key={i}
style={{
height: 12,
background: 'var(--rule-thin)',
marginBottom: 10,
width: `${width}%`,
borderRadius: 0,
}}
/>
))}
</div>
{/* Gradient overlay */}
<div
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '70%',
background:
'linear-gradient(to bottom, transparent 0%, var(--paper) 80%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-end',
paddingBottom: 20,
gap: 8,
}}
>
<Lock size={18} style={{ color: 'var(--ink-4)' }} />
<p
style={{
fontSize: 14,
fontWeight: 700,
color: 'var(--ink)',
textAlign: 'center',
}}
>
برای دسترسی کامل اشتراک بگیرید
</p>
<Link
to="/subscribe"
style={{
padding: '10px 28px',
background: 'var(--ink)',
color: 'var(--paper)',
fontSize: 12,
fontWeight: 700,
textDecoration: 'none',
letterSpacing: '0.5px',
border: '1px solid var(--ink)',
}}
>
مشاهده طرحهای اشتراک
</Link>
</div>
</div>
</section>
)}
{/* Tags */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 16 }}>
<Tag size={12} style={{ color: 'var(--ink-5)', marginTop: 2, flexShrink: 0 }} />
{report.tags.map((tag) => (
<span
key={tag}
style={{
fontSize: 11,
fontWeight: 500,
color: 'var(--ink-3)',
border: '1px solid var(--rule-thin)',
padding: '3px 10px',
}}
>
{tag}
</span>
))}
</div>
</div>
{/* ═══ RIGHT: Sidebar ══════════════════════════════ */}
<div
style={{ position: 'sticky', top: 76 }}
className="report-sidebar"
>
{/* Price card */}
<div
style={{
border: '2px solid var(--ink)',
padding: 32,
background: 'var(--paper)',
}}
>
<p
style={{
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '2px',
color: 'var(--ink-5)',
marginBottom: 12,
}}
>
قیمت گزارش
</p>
{/* Price */}
<p
style={{
fontSize: 36,
fontWeight: 900,
letterSpacing: '-1px',
color: report.isFree ? 'var(--green-ink)' : 'var(--ink)',
lineHeight: 1,
marginBottom: 16,
}}
>
{report.isFree ? 'رایگان' : formatPrice(report.price)}
</p>
{/* Meta */}
<div
style={{
borderTop: '1px solid var(--rule-thin)',
paddingTop: 12,
marginBottom: 20,
display: 'flex',
flexDirection: 'column',
gap: 6,
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 11, color: 'var(--ink-5)' }}>تعداد صفحات</span>
<span style={{ fontSize: 11, fontWeight: 600, color: 'var(--ink)' }}>
{report.pages} صفحه
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 11, color: 'var(--ink-5)' }}>تاریخ انتشار</span>
<span style={{ fontSize: 11, fontWeight: 600, color: 'var(--ink)' }}>
{report.publishDate}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 11, color: 'var(--ink-5)' }}>فرمت</span>
<span style={{ fontSize: 11, fontWeight: 600, color: 'var(--ink)' }}>PDF</span>
</div>
</div>
{/* Buy button */}
<button
onMouseEnter={() => setBuyHovered(true)}
onMouseLeave={() => setBuyHovered(false)}
style={{
width: '100%',
padding: '14px 0',
background: buyHovered ? 'var(--red)' : 'var(--ink)',
color: 'var(--paper)',
border: buyHovered ? '1px solid var(--red)' : '1px solid var(--ink)',
fontSize: 13,
fontWeight: 700,
cursor: 'pointer',
fontFamily: 'inherit',
borderRadius: 0,
transition: 'background 150ms, border-color 150ms',
letterSpacing: '0.3px',
marginBottom: 8,
}}
>
{report.isFree ? 'دانلود رایگان' : 'خرید و دانلود'}
</button>
{/* Download summary button */}
<button
onMouseEnter={() => setDlHovered(true)}
onMouseLeave={() => setDlHovered(false)}
style={{
width: '100%',
padding: '12px 0',
background: dlHovered ? 'var(--paper-2)' : 'transparent',
color: 'var(--ink)',
border: '1px solid var(--ink)',
fontSize: 12,
fontWeight: 600,
cursor: 'pointer',
fontFamily: 'inherit',
borderRadius: 0,
transition: 'background 150ms',
letterSpacing: '0.3px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
}}
>
<Download size={13} />
دانلود خلاصه
</button>
{/* Note */}
<p
style={{
fontSize: 10,
color: 'var(--ink-5)',
textAlign: 'center',
marginTop: 12,
}}
>
فاکتور رسمی صادر میشود
</p>
</div>
{/* Author card */}
<div
style={{
border: '1px solid var(--rule-thin)',
padding: 24,
marginTop: 16,
}}
>
<div
style={{
display: 'flex',
alignItems: 'flex-start',
gap: 14,
marginBottom: 12,
}}
>
<div
style={{
width: 48,
height: 48,
background: 'var(--ink)',
color: 'var(--paper)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 20,
fontWeight: 700,
flexShrink: 0,
}}
>
{report.authorInitial}
</div>
<div>
<p style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink)' }}>
{report.author}
</p>
<p style={{ fontSize: 11, color: 'var(--ink-5)', marginTop: 2 }}>
{report.authorRole}
</p>
</div>
</div>
<p
style={{
fontSize: 12,
fontWeight: 300,
lineHeight: 1.7,
color: 'var(--ink-3)',
marginBottom: 12,
}}
>
متخصص در تحلیل بازارهای فولاد با بیش از یک دهه تجربه در پژوهش و سیاستگذاری صنعتی.
</p>
<Link
to="/team"
style={{
fontSize: 10,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '1px',
color: 'var(--red)',
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
gap: 4,
}}
>
<User size={10} />
مشاهده پروفایل کامل
</Link>
</div>
{/* Related reports */}
<div
style={{
border: '1px solid var(--rule-thin)',
padding: 24,
marginTop: 16,
}}
>
<RelatedReports current={report} />
</div>
</div>
</div>
{/* ─── Responsive styles ─────────────────────────── */}
<style>{`
@media (max-width: 767px) {
.report-detail-grid {
grid-template-columns: 1fr !important;
}
.report-sidebar {
position: static !important;
}
}
@media (max-width: 767px) {
.report-sticky-buy {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 20;
padding: 16px 20px;
background: var(--paper);
border-top: 2px solid var(--ink);
}
}
`}</style>
</div>
)
}

View File

@ -0,0 +1,581 @@
import { useState, useMemo } from 'react'
import { Link } from 'react-router-dom'
import { Search, ChevronRight, ChevronLeft } from 'lucide-react'
import { reports } from '@/data/reports'
import type { ReportCategory, ReportType } from '@/data/reports'
/* ─── constants ──────────────────────────────────────────── */
const CATEGORIES: { label: string; value: ReportCategory | 'همه' }[] = [
{ label: 'همه', value: 'همه' },
{ label: 'بازار جهانی', value: 'بازار جهانی' },
{ label: 'سیاست‌گذاری', value: 'سیاست‌گذاری' },
{ label: 'انرژی و ESG', value: 'انرژی و ESG' },
{ label: 'صادرات', value: 'صادرات' },
{ label: 'تولید داخلی', value: 'تولید داخلی' },
{ label: 'ریسک و بحران', value: 'ریسک و بحران' },
]
const TYPES: { label: string; value: ReportType | 'همه' | 'رایگان' }[] = [
{ label: 'همه', value: 'همه' },
{ label: 'رایگان', value: 'رایگان' },
{ label: 'Flash Report', value: 'flash' },
{ label: 'فصلی', value: 'quarterly' },
{ label: 'گزارش ویژه', value: 'special' },
{ label: 'ریسک', value: 'risk' },
]
const SORT_OPTIONS = [
{ label: 'جدیدترین', value: 'newest' },
{ label: 'قدیمی‌ترین', value: 'oldest' },
{ label: 'گران‌ترین', value: 'price-desc' },
{ label: 'ارزان‌ترین', value: 'price-asc' },
]
const TYPE_COLORS: Record<ReportType, string> = {
quarterly: '#166534',
flash: '#9b1c1c',
risk: '#92400e',
free: '#1d4ed8',
special: '#4a1d96',
}
const TYPE_LABELS: Record<ReportType, string> = {
quarterly: 'فصلی',
flash: 'فلش',
risk: 'ریسک',
free: 'رایگان',
special: 'ویژه',
}
const PAGE_SIZE = 9
/* ─── price formatter ────────────────────────────────────── */
function formatPrice(price: number): string {
if (price === 0) return 'رایگان'
return price.toLocaleString('fa-IR') + ' تومان'
}
/* ─── report card ────────────────────────────────────────── */
function ReportCard({ report }: { report: (typeof reports)[0] }) {
const [hovered, setHovered] = useState(false)
return (
<article
style={{
borderLeft: '1px solid var(--rule-thin)',
borderBottom: '1px solid var(--rule-thin)',
padding: '32px',
display: 'flex',
flexDirection: 'column',
gap: 0,
background: 'var(--paper)',
cursor: 'default',
}}
>
{/* top meta */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<span
style={{
width: 6,
height: 6,
borderRadius: '50%',
background: TYPE_COLORS[report.type],
flexShrink: 0,
}}
/>
<span
style={{
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '1.5px',
color: 'var(--ink-5)',
}}
>
{report.category}
</span>
{report.isFree && (
<span
style={{
marginRight: 'auto',
marginLeft: 0,
fontSize: 9,
fontWeight: 700,
letterSpacing: '1px',
color: 'var(--paper)',
background: 'var(--green-ink)',
padding: '2px 6px',
textTransform: 'uppercase',
}}
>
رایگان
</span>
)}
<span
style={{
marginRight: report.isFree ? 0 : 'auto',
fontSize: 9,
fontWeight: 700,
letterSpacing: '1px',
color: TYPE_COLORS[report.type],
border: `1px solid ${TYPE_COLORS[report.type]}`,
padding: '2px 6px',
}}
>
{TYPE_LABELS[report.type]}
</span>
</div>
{/* title */}
<Link
to={`/reports/${report.id}`}
style={{
fontSize: 15,
fontWeight: 800,
lineHeight: 1.4,
letterSpacing: '-0.3px',
color: hovered ? 'var(--red)' : 'var(--ink)',
textDecoration: 'none',
marginBottom: 10,
display: 'block',
transition: 'color 120ms',
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{report.title}
</Link>
{/* meta row */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
flexWrap: 'wrap',
}}
>
<span style={{ fontSize: 11, color: 'var(--ink-5)' }}>{report.author}</span>
<span style={{ fontSize: 11, color: 'var(--ink-6)' }}>·</span>
<span style={{ fontSize: 11, color: 'var(--ink-5)' }}>{report.publishDate}</span>
<span style={{ fontSize: 11, color: 'var(--ink-6)' }}>·</span>
<span style={{ fontSize: 11, color: 'var(--ink-5)' }}>{report.pages} صفحه</span>
</div>
{/* divider */}
<div
style={{
height: 1,
background: 'var(--rule-thin)',
margin: '16px 0',
}}
/>
{/* price row */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span
style={{
fontSize: 15,
fontWeight: 900,
color: report.isFree ? 'var(--green-ink)' : 'var(--ink)',
}}
>
{formatPrice(report.price)}
</span>
<Link
to={`/reports/${report.id}`}
style={{
fontSize: 10,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '1px',
color: 'var(--red)',
textDecoration: 'none',
borderBottom: '1px solid var(--red)',
paddingBottom: 1,
transition: 'opacity 120ms',
}}
>
{report.isFree ? 'دانلود' : 'خرید'}
</Link>
</div>
</article>
)
}
/* ─── main component ─────────────────────────────────────── */
export default function Reports() {
const [activeCategory, setActiveCategory] = useState<ReportCategory | 'همه'>('همه')
const [activeType, setActiveType] = useState<ReportType | 'همه' | 'رایگان'>('همه')
const [sortBy, setSortBy] = useState('newest')
const [searchQuery, setSearchQuery] = useState('')
const [page, setPage] = useState(1)
const filtered = useMemo(() => {
let result = [...reports]
if (activeCategory !== 'همه') {
result = result.filter((r) => r.category === activeCategory)
}
if (activeType !== 'همه') {
if (activeType === 'رایگان') {
result = result.filter((r) => r.isFree)
} else {
result = result.filter((r) => r.type === activeType)
}
}
if (searchQuery.trim()) {
const q = searchQuery.trim().toLowerCase()
result = result.filter(
(r) =>
r.title.toLowerCase().includes(q) ||
r.author.toLowerCase().includes(q) ||
r.category.toLowerCase().includes(q) ||
r.tags.some((t) => t.toLowerCase().includes(q))
)
}
switch (sortBy) {
case 'oldest':
result = result.reverse()
break
case 'price-desc':
result = result.sort((a, b) => b.price - a.price)
break
case 'price-asc':
result = result.sort((a, b) => a.price - b.price)
break
}
return result
}, [activeCategory, activeType, sortBy, searchQuery])
const totalPages = Math.ceil(filtered.length / PAGE_SIZE)
const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
// Reset to page 1 on filter change
const handleCategoryChange = (cat: ReportCategory | 'همه') => {
setActiveCategory(cat)
setPage(1)
}
const handleTypeChange = (type: ReportType | 'همه' | 'رایگان') => {
setActiveType(type)
setPage(1)
}
return (
<div
style={{
maxWidth: 1280,
margin: '0 auto',
padding: '64px 48px',
direction: 'rtl',
}}
>
{/* Page header */}
<div
style={{
borderBottom: '3px solid var(--ink)',
paddingBottom: 24,
marginBottom: 48,
}}
>
<h1
style={{
fontSize: 36,
fontWeight: 900,
letterSpacing: '-1px',
marginBottom: 6,
color: 'var(--ink)',
}}
>
گزارشها
</h1>
<p style={{ fontSize: 15, fontWeight: 300, color: 'var(--ink-3)' }}>
تحلیلهای تخصصی بازار فولاد ایران و جهان گزارشهای فصلی، ریسک، و فلش
</p>
</div>
{/* ─── Filter bar ──────────────────────────────────── */}
<div
style={{
position: 'sticky',
top: 56,
background: 'var(--paper)',
borderBottom: '1px solid var(--rule-thin)',
paddingBottom: 16,
marginBottom: 48,
zIndex: 10,
}}
>
{/* Category pills */}
<div
style={{
display: 'flex',
gap: 6,
overflowX: 'auto',
paddingBottom: 10,
scrollbarWidth: 'none',
}}
>
{CATEGORIES.map((cat) => (
<button
key={cat.value}
onClick={() => handleCategoryChange(cat.value as ReportCategory | 'همه')}
style={{
padding: '6px 16px',
fontSize: 12,
fontWeight: 600,
borderRadius: 0,
border:
activeCategory === cat.value
? '1px solid var(--ink)'
: '1px solid var(--ink-4)',
background: activeCategory === cat.value ? 'var(--ink)' : 'transparent',
color: activeCategory === cat.value ? 'var(--paper)' : 'var(--ink-4)',
cursor: 'pointer',
whiteSpace: 'nowrap',
fontFamily: 'inherit',
transition: 'all 120ms',
flexShrink: 0,
}}
>
{cat.label}
</button>
))}
</div>
{/* Second row: type + sort + search */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
flexWrap: 'wrap',
paddingTop: 8,
}}
>
{/* Type filter */}
<div style={{ display: 'flex', gap: 6, overflowX: 'auto', scrollbarWidth: 'none' }}>
{TYPES.map((t) => (
<button
key={t.value}
onClick={() => handleTypeChange(t.value as ReportType | 'همه' | 'رایگان')}
style={{
padding: '5px 12px',
fontSize: 11,
fontWeight: 600,
borderRadius: 0,
border:
activeType === t.value
? '1px solid var(--ink)'
: '1px solid var(--rule-thin)',
background: activeType === t.value ? 'var(--ink)' : 'transparent',
color: activeType === t.value ? 'var(--paper)' : 'var(--ink-5)',
cursor: 'pointer',
whiteSpace: 'nowrap',
fontFamily: 'inherit',
transition: 'all 120ms',
flexShrink: 0,
}}
>
{t.label}
</button>
))}
</div>
<div style={{ flex: 1 }} />
{/* Sort */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 11, color: 'var(--ink-5)', whiteSpace: 'nowrap' }}>
مرتبسازی
</span>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
style={{
border: '1px solid var(--ink)',
padding: '6px 12px',
fontSize: 11,
fontFamily: 'inherit',
background: 'var(--paper)',
color: 'var(--ink)',
borderRadius: 0,
cursor: 'pointer',
outline: 'none',
}}
>
{SORT_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</div>
{/* Search */}
<div style={{ position: 'relative' }}>
<input
type="text"
placeholder="جستجو..."
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value)
setPage(1)
}}
style={{
border: '1px solid var(--ink)',
padding: '8px 36px 8px 12px',
fontSize: 12,
fontFamily: 'inherit',
background: 'transparent',
color: 'var(--ink)',
borderRadius: 0,
outline: 'none',
width: 200,
direction: 'rtl',
}}
/>
<Search
size={14}
style={{
position: 'absolute',
left: 10,
top: '50%',
transform: 'translateY(-50%)',
color: 'var(--ink-4)',
pointerEvents: 'none',
}}
/>
</div>
</div>
</div>
{/* ─── Report grid ─────────────────────────────────── */}
{paged.length > 0 ? (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
border: '1px solid var(--rule-thin)',
borderLeft: 'none',
borderBottom: 'none',
}}
className="reports-grid"
>
{paged.map((report) => (
<ReportCard key={report.id} report={report} />
))}
{/* Fill empty cells in last row to keep grid consistent */}
{paged.length % 3 !== 0 &&
Array.from({ length: 3 - (paged.length % 3) }).map((_, i) => (
<div
key={`empty-${i}`}
style={{
borderLeft: '1px solid var(--rule-thin)',
borderBottom: '1px solid var(--rule-thin)',
}}
/>
))}
</div>
) : (
<div
style={{
padding: '80px 0',
textAlign: 'center',
color: 'var(--ink-4)',
}}
>
<p style={{ fontSize: 18, fontWeight: 300 }}>نتیجهای یافت نشد</p>
<p style={{ fontSize: 13, marginTop: 8, color: 'var(--ink-5)' }}>
جستجو یا فیلترهای خود را تغییر دهید
</p>
</div>
)}
{/* ─── Pagination ──────────────────────────────────── */}
{totalPages > 1 && (
<div
style={{
marginTop: 48,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
borderTop: '1px solid var(--rule-thin)',
paddingTop: 32,
}}
>
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '8px 18px',
fontSize: 12,
fontWeight: 600,
border: '1px solid var(--ink)',
background: 'transparent',
color: page === 1 ? 'var(--ink-5)' : 'var(--ink)',
borderColor: page === 1 ? 'var(--rule-thin)' : 'var(--ink)',
cursor: page === 1 ? 'not-allowed' : 'pointer',
fontFamily: 'inherit',
borderRadius: 0,
opacity: page === 1 ? 0.4 : 1,
}}
>
<ChevronRight size={14} />
قبلی
</button>
<span style={{ fontSize: 12, color: 'var(--ink-5)' }}>
صفحه {page.toLocaleString('fa-IR')} از {totalPages.toLocaleString('fa-IR')}
</span>
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '8px 18px',
fontSize: 12,
fontWeight: 600,
border: '1px solid var(--ink)',
background: 'transparent',
color: page === totalPages ? 'var(--ink-5)' : 'var(--ink)',
borderColor: page === totalPages ? 'var(--rule-thin)' : 'var(--ink)',
cursor: page === totalPages ? 'not-allowed' : 'pointer',
fontFamily: 'inherit',
borderRadius: 0,
opacity: page === totalPages ? 0.4 : 1,
}}
>
بعدی
<ChevronLeft size={14} />
</button>
</div>
)}
{/* ─── Mobile responsive styles ────────────────────── */}
<style>{`
@media (max-width: 767px) {
.reports-grid {
grid-template-columns: 1fr !important;
}
}
@media (max-width: 1024px) and (min-width: 768px) {
.reports-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
}
`}</style>
</div>
)
}

340
src/pages/Risks/Risks.tsx Normal file
View File

@ -0,0 +1,340 @@
import { Link } from 'react-router-dom';
import { reports } from '@/data/reports';
const riskMatrix = [
{ id: 1, title: 'تشدید تحریم‌های صادراتی', category: 'ژئوپلیتیک', probability: 'بالا', impact: 'بسیار بالا', level: 'critical' },
{ id: 2, title: 'سقوط قیمت جهانی فولاد', category: 'بازار', probability: 'متوسط', impact: 'بالا', level: 'high' },
{ id: 3, title: 'کمبود انرژی صنعتی', category: 'زیرساخت', probability: 'بالا', impact: 'متوسط', level: 'high' },
{ id: 4, title: 'اجرای CBAM اروپا', category: 'مقررات', probability: 'قطعی', impact: 'متوسط', level: 'medium' },
{ id: 5, title: 'رقابت صادرات چین', category: 'بازار', probability: 'قطعی', impact: 'بالا', level: 'high' },
{ id: 6, title: 'نوسانات نرخ ارز', category: 'مالی', probability: 'بالا', impact: 'متوسط', level: 'medium' },
] as const;
type RiskLevel = 'critical' | 'high' | 'medium' | 'low';
const levelConfig: Record<RiskLevel, { color: string; label: string }> = {
critical: { color: '#7f1d1d', label: 'بحرانی' },
high: { color: 'var(--red)', label: 'بالا' },
medium: { color: 'var(--amber)', label: 'متوسط' },
low: { color: 'var(--green-ink)', label: 'پایین' },
};
function formatPrice(price: number): string {
if (price === 0) return 'رایگان';
return price.toLocaleString('fa-IR') + ' تومان';
}
export default function Risks() {
const riskReports = reports.filter((r) => r.type === 'risk');
return (
<div
dir="rtl"
style={{
fontFamily: 'Vazir, sans-serif',
background: 'var(--paper)',
color: 'var(--ink)',
minHeight: '100vh',
}}
>
{/* Page Header */}
<div
style={{
borderBottom: '4px double var(--ink)',
padding: '40px 48px 32px',
background: 'var(--paper)',
}}
>
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
<div
style={{
fontSize: '11px',
letterSpacing: '0.12em',
color: 'var(--ink-5)',
fontWeight: 600,
marginBottom: '12px',
}}
>
اندیشکده فولاد ایران پایش ریسک
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: '16px' }}>
<h1
style={{
fontSize: '36px',
fontWeight: 900,
margin: 0,
lineHeight: 1.3,
}}
>
پایش ریسکهای صنعت فولاد
</h1>
<span
style={{
fontSize: '12px',
color: 'var(--ink-4)',
border: '1px solid var(--rule-thin)',
padding: '4px 12px',
background: 'var(--paper-2)',
}}
>
بهروز شده: بهمن ۱۴۰۳
</span>
</div>
{/* Legend */}
<div
style={{
display: 'flex',
gap: '24px',
marginTop: '24px',
flexWrap: 'wrap',
}}
>
{(Object.keys(levelConfig) as RiskLevel[]).map((level) => (
<div key={level} style={{ display: 'flex', alignItems: 'center', gap: '7px' }}>
<div
style={{
width: '10px',
height: '10px',
borderRadius: '50%',
background: levelConfig[level].color,
flexShrink: 0,
}}
/>
<span style={{ fontSize: '13px', color: 'var(--ink-3)' }}>{levelConfig[level].label}</span>
</div>
))}
</div>
</div>
</div>
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '40px 48px 64px' }}>
{/* Risk Matrix Table */}
<div style={{ marginBottom: '56px' }}>
<h2
style={{
fontSize: '18px',
fontWeight: 800,
margin: '0 0 20px',
color: 'var(--ink)',
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<span style={{ display: 'inline-block', width: '4px', height: '20px', background: 'var(--red)' }} />
ماتریس ریسکهای کلیدی
</h2>
<div style={{ border: '1px solid var(--ink)', overflow: 'hidden' }}>
{/* Table Header */}
<div
style={{
display: 'grid',
gridTemplateColumns: '48px 1fr 100px 100px 120px 130px',
background: 'var(--ink)',
color: 'var(--paper)',
padding: '12px 16px',
gap: '0',
}}
>
{['#', 'عنوان ریسک', 'دسته', 'احتمال', 'شدت تأثیر', 'سطح ریسک'].map((col, i) => (
<div
key={i}
style={{
fontSize: '12px',
fontWeight: 700,
letterSpacing: '0.05em',
padding: '0 8px',
borderRight: i > 0 ? '1px solid rgba(255,255,255,0.12)' : 'none',
}}
>
{col}
</div>
))}
</div>
{/* Table Rows */}
{riskMatrix.map((risk, idx) => {
const cfg = levelConfig[risk.level as RiskLevel];
return (
<div
key={risk.id}
style={{
display: 'grid',
gridTemplateColumns: '48px 1fr 100px 100px 120px 130px',
background: idx % 2 === 0 ? 'var(--paper)' : 'var(--paper-2)',
borderTop: '1px solid var(--rule-thin)',
padding: '14px 16px',
alignItems: 'center',
}}
>
{/* # */}
<div style={{ fontSize: '13px', color: 'var(--ink-5)', fontWeight: 600, padding: '0 8px' }}>
{risk.id}
</div>
{/* Title */}
<div style={{ fontSize: '14px', fontWeight: 600, color: 'var(--ink)', padding: '0 8px', borderRight: '1px solid var(--rule-thin)' }}>
{risk.title}
</div>
{/* Category */}
<div style={{ fontSize: '12px', color: 'var(--ink-4)', padding: '0 8px', borderRight: '1px solid var(--rule-thin)' }}>
{risk.category}
</div>
{/* Probability */}
<div style={{ fontSize: '12px', color: 'var(--ink-3)', padding: '0 8px', borderRight: '1px solid var(--rule-thin)' }}>
{risk.probability}
</div>
{/* Impact */}
<div style={{ fontSize: '12px', color: 'var(--ink-3)', padding: '0 8px', borderRight: '1px solid var(--rule-thin)' }}>
{risk.impact}
</div>
{/* Level */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '0 8px',
borderRight: '1px solid var(--rule-thin)',
}}
>
<div
style={{
width: '9px',
height: '9px',
borderRadius: '50%',
background: cfg.color,
flexShrink: 0,
}}
/>
<span style={{ fontSize: '12px', fontWeight: 700, color: cfg.color }}>
{cfg.label}
</span>
</div>
</div>
);
})}
</div>
</div>
{/* Risk Reports Section */}
<div>
<div
style={{
borderTop: '3px solid var(--ink)',
borderBottom: '1px solid var(--rule-thin)',
padding: '18px 0',
marginBottom: '24px',
display: 'flex',
alignItems: 'baseline',
gap: '16px',
}}
>
<h2 style={{ fontSize: '20px', fontWeight: 800, margin: 0 }}>
گزارشهای ریسک اندیشکده
</h2>
<span style={{ fontSize: '13px', color: 'var(--ink-5)' }}>
{riskReports.length} گزارش تحلیلی
</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0' }}>
{riskReports.map((report, idx) => (
<Link
key={report.id}
to={`/reports/${report.id}`}
style={{
display: 'grid',
gridTemplateColumns: '90px 1fr 120px 140px 140px',
gap: '0',
alignItems: 'center',
padding: '16px 0',
borderBottom: '1px solid var(--rule-thin)',
background: idx % 2 === 0 ? 'var(--paper)' : 'var(--paper-2)',
textDecoration: 'none',
color: 'inherit',
}}
>
{/* Type badge */}
<div style={{ padding: '0 0 0 12px' }}>
<span
style={{
fontSize: '10px',
fontWeight: 700,
color: 'var(--paper)',
background: '#7f1d1d',
padding: '3px 8px',
letterSpacing: '0.06em',
}}
>
ریسک
</span>
</div>
{/* Title */}
<div
style={{
fontSize: '14px',
fontWeight: 600,
color: 'var(--ink)',
borderRight: '1px solid var(--rule-thin)',
padding: '0 16px',
}}
>
{report.title}
</div>
{/* Date */}
<div
style={{
fontSize: '12px',
color: 'var(--ink-5)',
borderRight: '1px solid var(--rule-thin)',
padding: '0 16px',
}}
>
{report.publishDate}
</div>
{/* Pages */}
<div
style={{
fontSize: '12px',
color: 'var(--ink-5)',
borderRight: '1px solid var(--rule-thin)',
padding: '0 16px',
}}
>
{report.pages} صفحه
</div>
{/* Price + link */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
gap: '12px',
borderRight: '1px solid var(--rule-thin)',
padding: '0 16px',
}}
>
<span style={{ fontSize: '13px', fontWeight: 700, color: 'var(--ink)' }}>
{formatPrice(report.price)}
</span>
<span
style={{
fontSize: '12px',
color: 'var(--red)',
fontWeight: 600,
textDecoration: 'underline',
}}
>
مشاهده
</span>
</div>
</Link>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,430 @@
import { marketPrices, globalComparison } from '@/data/market';
import type { MarketPrice } from '@/data/market';
const riskAlerts = [
{
title: 'فشار صادرات چین',
desc: 'افزایش ۱۸٪ صادرات ارزان چین فشار قیمتی روی بازار آسیا',
level: 'high',
},
{
title: 'محدودیت انرژی داخلی',
desc: 'کاهش تولید زمستانی به دلیل محدودیت گاز صنعتی',
level: 'medium',
},
{
title: 'فرصت بازار عراق',
desc: 'کاهش واردات ترکیه، فرصت افزایش سهم بازار برای ایران',
level: 'low',
},
] as const;
type AlertLevel = 'high' | 'medium' | 'low';
const alertBorderColor: Record<AlertLevel, string> = {
high: 'var(--red)',
medium: 'var(--amber)',
low: 'var(--green-ink)',
};
const alertLabelColor: Record<AlertLevel, string> = {
high: 'var(--red)',
medium: 'var(--amber)',
low: 'var(--green-ink)',
};
const alertLabel: Record<AlertLevel, string> = {
high: 'بالا',
medium: 'متوسط',
low: 'فرصت',
};
function formatNumber(n: number): string {
return n.toLocaleString('fa-IR');
}
function PriceTicker({ item, col, row, totalCols, totalRows }: { item: MarketPrice; col: number; row: number; totalCols: number; totalRows: number }) {
const isUp = item.trend === 'up';
const isDown = item.trend === 'down';
const trendArrow = isUp ? '▲' : isDown ? '▼' : '—';
const trendColor = isUp ? 'var(--green-ink)' : isDown ? 'var(--red)' : 'var(--ink-5)';
const changePrefix = item.change > 0 ? '+' : '';
return (
<div
style={{
padding: '24px 20px',
borderLeft: col < totalCols - 1 ? '1px solid var(--rule-thin)' : 'none',
borderBottom: row < totalRows - 1 ? '1px solid var(--rule-thin)' : 'none',
background: 'var(--paper)',
}}
>
{/* Product name */}
<div
style={{
fontSize: '13px',
fontWeight: 700,
color: 'var(--ink-3)',
marginBottom: '10px',
letterSpacing: '0.03em',
}}
>
{item.name}
</div>
{/* Price */}
<div
style={{
fontSize: '34px',
fontWeight: 900,
fontVariantNumeric: 'tabular-nums',
fontFeatureSettings: '"tnum"',
color: 'var(--ink)',
lineHeight: 1,
marginBottom: '6px',
fontFamily: 'monospace, Vazir, sans-serif',
}}
>
{formatNumber(item.value)}
</div>
{/* Unit */}
<div
style={{
fontSize: '11px',
color: 'var(--ink-5)',
marginBottom: '14px',
}}
>
{item.unit}
</div>
{/* Change + trend */}
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<span style={{ fontSize: '22px', color: trendColor, lineHeight: 1 }}>
{trendArrow}
</span>
<span
style={{
fontSize: '13px',
fontWeight: 700,
color: trendColor,
}}
>
{changePrefix}{formatNumber(item.change)}
</span>
<span
style={{
fontSize: '12px',
fontWeight: 600,
color: trendColor,
background: isUp
? 'rgba(34,197,94,0.08)'
: isDown
? 'rgba(220,38,38,0.08)'
: 'transparent',
padding: '2px 7px',
}}
>
{changePrefix}{item.changePercent.toFixed(1)}٪
</span>
</div>
</div>
);
}
export default function Scanner() {
// Use first 6 prices for the grid
const gridPrices = marketPrices.slice(0, 6);
const COLS = 3;
const ROWS = Math.ceil(gridPrices.length / COLS);
// For CSS bar chart: find max price in globalComparison for proportional width
// Filter to USD-only for a fair comparison (exclude IRR)
const usdPrices = globalComparison.filter((g) => g.currency === 'USD');
const maxUSD = Math.max(...usdPrices.map((g) => g.price));
return (
<div
dir="rtl"
style={{
fontFamily: 'Vazir, sans-serif',
background: 'var(--paper)',
color: 'var(--ink)',
minHeight: '100vh',
}}
>
{/* Page Header */}
<div
style={{
borderBottom: '4px double var(--ink)',
padding: '40px 48px 32px',
}}
>
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
<div
style={{
fontSize: '11px',
letterSpacing: '0.12em',
color: 'var(--ink-5)',
fontWeight: 600,
marginBottom: '12px',
}}
>
اندیشکده فولاد ایران دادههای بازار
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-end',
flexWrap: 'wrap',
gap: '16px',
}}
>
<div>
<h1
style={{
fontSize: '36px',
fontWeight: 900,
margin: '0 0 8px',
lineHeight: 1.3,
}}
>
اسکنر بازار جهانی فولاد
</h1>
<p
style={{
fontSize: '15px',
color: 'var(--ink-4)',
margin: 0,
}}
>
دادههای لحظهای قیمتهای بازار داخلی و جهانی
</p>
</div>
<span
style={{
fontSize: '12px',
color: 'var(--ink-4)',
border: '1px solid var(--rule-thin)',
padding: '4px 12px',
background: 'var(--paper-2)',
whiteSpace: 'nowrap',
}}
>
بهروز شده: بهمن ۱۴۰۳
</span>
</div>
</div>
</div>
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '40px 48px 64px' }}>
{/* Price Ticker Grid */}
<div style={{ marginBottom: '56px' }}>
<div
style={{
display: 'flex',
alignItems: 'baseline',
gap: '12px',
marginBottom: '20px',
}}
>
<span style={{ display: 'inline-block', width: '4px', height: '20px', background: 'var(--ink)' }} />
<h2 style={{ fontSize: '18px', fontWeight: 800, margin: 0 }}>قیمتهای لحظهای بازار</h2>
</div>
<div
style={{
border: '2px solid var(--ink)',
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
}}
className="max-md:grid-cols-1"
>
{gridPrices.map((item, idx) => {
const col = idx % COLS;
const row = Math.floor(idx / COLS);
return (
<PriceTicker
key={item.id}
item={item}
col={col}
row={row}
totalCols={COLS}
totalRows={ROWS}
/>
);
})}
</div>
</div>
{/* Global Comparison Bar Chart */}
<div style={{ marginBottom: '56px' }}>
<div
style={{
borderTop: '3px solid var(--ink)',
borderBottom: '1px solid var(--rule-thin)',
padding: '16px 0',
marginBottom: '28px',
display: 'flex',
alignItems: 'baseline',
gap: '16px',
}}
>
<h2 style={{ fontSize: '18px', fontWeight: 800, margin: 0 }}>مقایسه جهانی قیمت میلگرد</h2>
<span style={{ fontSize: '12px', color: 'var(--ink-5)' }}>دلار / تن بازار صادراتی</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
{usdPrices.map((entry) => {
const barPct = Math.round((entry.price / maxUSD) * 100);
return (
<div key={entry.country} style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
{/* Flag + Country */}
<div
style={{
width: '110px',
display: 'flex',
alignItems: 'center',
gap: '8px',
flexShrink: 0,
}}
>
<span style={{ fontSize: '20px', lineHeight: 1 }}>{entry.flag}</span>
<span style={{ fontSize: '14px', fontWeight: 600, color: 'var(--ink)' }}>
{entry.country}
</span>
</div>
{/* Bar */}
<div
style={{
flex: 1,
height: '28px',
background: 'var(--paper-3)',
position: 'relative',
overflow: 'hidden',
}}
>
<div
style={{
position: 'absolute',
top: 0,
right: 0,
height: '100%',
width: `${barPct}%`,
background: entry.country === 'چین' ? '#7f1d1d' : 'var(--ink)',
transition: 'width 0.4s ease',
}}
/>
</div>
{/* Value */}
<div
style={{
width: '90px',
textAlign: 'left',
flexShrink: 0,
}}
>
<span
style={{
fontSize: '16px',
fontWeight: 800,
fontVariantNumeric: 'tabular-nums',
color: 'var(--ink)',
}}
>
${entry.price}
</span>
<span style={{ fontSize: '11px', color: 'var(--ink-5)', marginRight: '4px' }}>/تن</span>
</div>
</div>
);
})}
</div>
</div>
{/* Active Risk Cards */}
<div>
<div
style={{
borderTop: '3px solid var(--red)',
borderBottom: '1px solid var(--rule-thin)',
padding: '16px 0',
marginBottom: '24px',
display: 'flex',
alignItems: 'baseline',
gap: '16px',
}}
>
<h2 style={{ fontSize: '18px', fontWeight: 800, margin: 0 }}>ریسکهای بازار فعال</h2>
<span style={{ fontSize: '12px', color: 'var(--ink-5)' }}>هشدارهای جاری</span>
</div>
<div
style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}
className="max-md:grid-cols-1"
>
{riskAlerts.map((alert) => {
const level = alert.level as AlertLevel;
return (
<div
key={alert.title}
style={{
background: 'var(--paper)',
border: '1px solid var(--rule-thin)',
borderRight: `4px solid ${alertBorderColor[level]}`,
padding: '20px',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '10px',
}}
>
<h3
style={{
fontSize: '15px',
fontWeight: 700,
margin: 0,
color: 'var(--ink)',
}}
>
{alert.title}
</h3>
<span
style={{
fontSize: '11px',
fontWeight: 700,
color: alertLabelColor[level],
border: `1px solid ${alertLabelColor[level]}`,
padding: '2px 8px',
}}
>
{alertLabel[level]}
</span>
</div>
<p
style={{
fontSize: '13px',
lineHeight: 1.8,
color: 'var(--ink-4)',
margin: 0,
}}
>
{alert.desc}
</p>
</div>
);
})}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,383 @@
import { Link } from 'react-router-dom';
import { reports } from '@/data/reports';
import type { Report } from '@/data/reports';
const typeLabel: Record<string, string> = {
special: 'ویژه',
quarterly: 'فصلی',
flash: 'فوری',
risk: 'ریسک',
free: 'رایگان',
};
function formatPrice(price: number): string {
if (price === 0) return 'رایگان';
return price.toLocaleString('fa-IR') + ' تومان';
}
function SpecialCard({ report }: { report: Report }) {
return (
<Link
to={`/reports/${report.id}`}
style={{
display: 'block',
textDecoration: 'none',
color: 'inherit',
border: '2px solid var(--ink)',
padding: '28px',
background: 'var(--paper)',
transition: 'background 0.15s',
}}
>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center', marginBottom: '14px' }}>
<span
style={{
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.08em',
color: 'var(--paper)',
background: 'var(--red)',
padding: '2px 8px',
textTransform: 'uppercase',
}}
>
{typeLabel[report.type] ?? report.type}
</span>
<span
style={{
fontSize: '12px',
color: 'var(--ink-4)',
borderRight: '1px solid var(--rule-thin)',
paddingRight: '10px',
}}
>
{report.category}
</span>
</div>
<h2
style={{
fontSize: '22px',
fontWeight: 700,
lineHeight: 1.5,
color: 'var(--ink)',
margin: '0 0 14px',
}}
>
{report.title}
</h2>
<p
style={{
fontSize: '14px',
lineHeight: 1.9,
color: 'var(--ink-3)',
margin: '0 0 20px',
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{report.summary}
</p>
<div
style={{
borderTop: '1px solid var(--rule-thin)',
paddingTop: '16px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '12px',
flexWrap: 'wrap',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div
style={{
width: '34px',
height: '34px',
background: 'var(--ink)',
color: 'var(--paper)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 700,
fontSize: '15px',
flexShrink: 0,
}}
>
{report.authorInitial}
</div>
<div>
<div style={{ fontSize: '13px', fontWeight: 600, color: 'var(--ink)' }}>{report.author}</div>
<div style={{ fontSize: '11px', color: 'var(--ink-5)' }}>{report.authorRole}</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span style={{ fontSize: '13px', color: 'var(--ink-4)' }}>{report.pages} صفحه</span>
<span style={{ fontSize: '13px', color: 'var(--ink-4)' }}>{report.publishDate}</span>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
borderRight: '1px solid var(--rule-thin)',
paddingRight: '12px',
}}
>
<span style={{ fontSize: '15px', fontWeight: 700, color: report.isFree ? 'var(--green-ink)' : 'var(--ink)' }}>
{formatPrice(report.price)}
</span>
<span
style={{
fontSize: '13px',
fontWeight: 600,
padding: '4px 14px',
background: 'var(--ink)',
color: 'var(--paper)',
cursor: 'pointer',
}}
>
{report.isFree ? 'دریافت رایگان' : 'خرید گزارش'}
</span>
</div>
</div>
</div>
</Link>
);
}
function CompactCard({ report }: { report: Report }) {
return (
<Link
to={`/reports/${report.id}`}
style={{
display: 'block',
textDecoration: 'none',
color: 'inherit',
border: '1px solid var(--rule-thin)',
borderTop: '3px solid var(--ink-3)',
padding: '18px',
background: 'var(--paper)',
}}
>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '10px' }}>
<span
style={{
fontSize: '10px',
fontWeight: 700,
color: 'var(--ink-4)',
background: 'var(--paper-2)',
padding: '2px 7px',
letterSpacing: '0.06em',
}}
>
{typeLabel[report.type] ?? report.type}
</span>
<span style={{ fontSize: '11px', color: 'var(--ink-5)' }}>{report.category}</span>
</div>
<h3
style={{
fontSize: '15px',
fontWeight: 700,
lineHeight: 1.6,
color: 'var(--ink)',
margin: '0 0 10px',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{report.title}
</h3>
<p
style={{
fontSize: '13px',
lineHeight: 1.8,
color: 'var(--ink-4)',
margin: '0 0 14px',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{report.summary}
</p>
<div
style={{
borderTop: '1px solid var(--rule-thin)',
paddingTop: '12px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span style={{ fontSize: '12px', color: 'var(--ink-5)' }}>{report.publishDate}</span>
<span style={{ fontSize: '13px', fontWeight: 700, color: report.isFree ? 'var(--green-ink)' : 'var(--red)' }}>
{formatPrice(report.price)}
</span>
</div>
</Link>
);
}
export default function Special() {
const specialReports = reports.filter((r) => r.type === 'special');
const otherReports = reports.filter((r) => r.type !== 'special');
return (
<div
dir="rtl"
style={{
fontFamily: 'Vazir, sans-serif',
background: 'var(--paper)',
color: 'var(--ink)',
minHeight: '100vh',
}}
>
{/* Hero Banner */}
<div
style={{
background: 'var(--ink)',
padding: '60px 48px',
borderBottom: '4px solid var(--red)',
}}
>
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
<div
style={{
fontSize: '11px',
letterSpacing: '0.12em',
color: 'rgba(255,255,255,0.5)',
marginBottom: '16px',
fontWeight: 600,
}}
>
اندیشکده فولاد ایران پژوهشهای تخصصی
</div>
<h1
style={{
fontSize: '48px',
fontWeight: 900,
color: 'var(--paper)',
margin: '0 0 16px',
lineHeight: 1.3,
}}
>
گزارشهای ویژه
</h1>
<p
style={{
fontSize: '17px',
color: 'rgba(255,255,255,0.7)',
margin: 0,
maxWidth: '680px',
lineHeight: 1.8,
}}
>
تحلیلهای عمیق و سناریوپردازیهای راهبردی برای تصمیمگیرندگان صنعت فولاد
</p>
<div
style={{
marginTop: '28px',
display: 'flex',
gap: '32px',
fontSize: '13px',
color: 'rgba(255,255,255,0.5)',
}}
>
<span>{specialReports.length} گزارش ویژه</span>
<span style={{ borderRight: '1px solid rgba(255,255,255,0.2)', paddingRight: '32px' }}>
{reports.length} گزارش در مجموع
</span>
</div>
</div>
</div>
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '0 24px' }}>
{/* Section A — Special Reports */}
<div style={{ padding: '48px 0 40px' }}>
<div
style={{
display: 'flex',
alignItems: 'baseline',
gap: '16px',
marginBottom: '32px',
}}
>
<div style={{ width: '4px', height: '28px', background: 'var(--red)', flexShrink: 0 }} />
<h2 style={{ fontSize: '24px', fontWeight: 800, margin: 0, color: 'var(--ink)' }}>
گزارشهای ویژه
</h2>
<span
style={{
fontSize: '13px',
color: 'var(--ink-5)',
borderRight: '1px solid var(--rule-thin)',
paddingRight: '16px',
}}
>
تحلیلهای سناریومحور و راهبردی
</span>
</div>
<div
className="max-md:grid-cols-1"
style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '24px' }}
>
{specialReports.map((report) => (
<SpecialCard key={report.id} report={report} />
))}
</div>
</div>
{/* Thick Rule Separator */}
<div style={{ borderTop: '4px double var(--ink)', margin: '0 0 48px' }} />
{/* Section B — Other Reports */}
<div style={{ paddingBottom: '64px' }}>
<div
style={{
display: 'flex',
alignItems: 'baseline',
gap: '16px',
marginBottom: '32px',
}}
>
<div style={{ width: '4px', height: '28px', background: 'var(--ink-4)', flexShrink: 0 }} />
<h2 style={{ fontSize: '22px', fontWeight: 800, margin: 0, color: 'var(--ink)' }}>
سایر گزارشهای تحلیلی
</h2>
<span
style={{
fontSize: '13px',
color: 'var(--ink-5)',
borderRight: '1px solid var(--rule-thin)',
paddingRight: '16px',
}}
>
گزارشهای فصلی، فوری و تحلیل ریسک
</span>
</div>
<div
className="max-md:grid-cols-1"
style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '20px' }}
>
{otherReports.map((report) => (
<CompactCard key={report.id} report={report} />
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,680 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { Check, X, ChevronDown, ChevronUp, Phone } from 'lucide-react'
import { plans } from '@/data/plans'
import type { Plan } from '@/data/plans'
/* ─── feature comparison data ────────────────────────────── */
const FEATURES: { label: string; basic: string; pro: string; gov: string }[] = [
{ label: 'دسترسی به گزارش‌های جدید', basic: 'check', pro: 'check', gov: 'check' },
{ label: 'Flash Reports فوری', basic: 'x', pro: 'check', gov: 'check' },
{ label: 'آرشیو کامل', basic: 'محدود (۱ سال)', pro: 'نامحدود', gov: 'نامحدود' },
{ label: 'داشبورد بازار', basic: 'check', pro: 'check', gov: 'check' },
{ label: 'جلسه با تیم تحریریه', basic: 'x', pro: 'ماهانه', gov: 'فصلی' },
{ label: 'گزارش‌های سفارشی', basic: 'x', pro: 'x', gov: 'check' },
{ label: 'ارائه حضوری', basic: 'x', pro: 'x', gov: 'check' },
{ label: 'تعداد کاربران', basic: '۱', pro: 'تا ۵', gov: 'نامحدود' },
{ label: 'فاکتور رسمی', basic: 'x', pro: 'check', gov: 'check' },
{ label: 'پشتیبانی اختصاصی', basic: 'x', pro: 'check', gov: 'check' },
]
/* ─── FAQ data ───────────────────────────────────────────── */
const FAQS = [
{
q: 'آیا می‌توانم گزارش را پیش از خرید ببینم؟',
a: 'بله، برای تمام گزارش‌ها یک پیش‌نمایش رایگان شامل چکیده، فهرست مطالب و چند صفحه اول در دسترس است. همچنین برخی گزارش‌ها به‌طور کامل رایگان منتشر می‌شوند.',
},
{
q: 'فاکتور رسمی چگونه صادر می‌شود؟',
a: 'پس از تکمیل خرید، فاکتور رسمی با مشخصات سازمانی شما حداکثر ظرف ۲۴ ساعت کاری از طریق ایمیل ارسال می‌شود. برای اشتراک‌های سازمانی، قرارداد رسمی نیز منعقد می‌شود.',
},
{
q: 'نحوه پرداخت چگونه است؟',
a: 'پرداخت از طریق درگاه بانکی اینترنتی (شبکه شتاب) و انتقال بانکی مستقیم امکان‌پذیر است. برای اشتراک‌های سازمانی پرداخت به‌صورت حواله بانکی با صدور فاکتور رسمی انجام می‌شود.',
},
{
q: 'آیا اشتراک سازمانی قابل لغو است؟',
a: 'بله، اشتراک سازمانی با اطلاع ۳۰ روز پیشین قابل فسخ است. مبالغ پرداختی به‌صورت پرو-راتا بر اساس ماه‌های باقیمانده محاسبه و بازگشت داده می‌شود.',
},
{
q: 'گزارش‌های سفارشی چه مدت طول می‌کشد؟',
a: 'بسته به پیچیدگی موضوع، گزارش‌های سفارشی معمولاً ۴ تا ۸ هفته زمان نیاز دارند. پس از جلسه اولیه تعریف دامنه کار، برنامه زمانی دقیق ارائه می‌شود.',
},
]
/* ─── plan extra features ────────────────────────────────── */
const PLAN_EXTRA_FEATURES: Record<string, string[]> = {
basic: [
'دسترسی به گزارش‌های فصلی بازار',
'خلاصه گزارش‌های تخصصی',
'خبرنامه هفتگی',
'پایگاه داده قیمت‌های بازار',
'آرشیو ۱ ساله گزارش‌ها',
'داشبورد بازار پایه',
'دانلود PDF گزارش‌ها',
'پشتیبانی ایمیلی',
],
professional: [
'تمام گزارش‌های تخصصی و ریسک',
'گزارش‌های فوری (Flash)',
'مدل‌های کمی و داده‌های خام',
'آرشیو کامل ۵ ساله',
'API دسترسی به داده قیمت',
'پشتیبانی اولویت‌دار',
'تا ۵ کاربر سازمانی',
'فاکتور رسمی خودکار',
'داشبورد پیشرفته بازار',
'خبرنامه اختصاصی پیش از انتشار',
],
government: [
'دسترسی نامحدود تیمی',
'گزارش‌های سفارشی اختصاصی',
'جلسات مشاوره ماهانه',
'ارائه حضوری نتایج تحقیقات',
'آموزش و کارگاه تخصصی',
'پشتیبانی ۲۴ ساعته اختصاصی',
'دسترسی پیش از انتشار',
'مدیر حساب اختصاصی',
'قرارداد و فاکتور سازمانی',
'گزارش وضعیت اعضاء',
'لایسنس چندکاربره نامحدود',
'پنل مدیریت تیم',
],
}
/* ─── cell renderer for feature table ───────────────────── */
function FeatureCell({ value }: { value: string }) {
if (value === 'check') {
return (
<td
style={{
padding: '13px 24px',
borderBottom: '1px solid var(--rule-thin)',
borderRight: '1px solid var(--rule-thin)',
textAlign: 'center',
}}
>
<Check size={15} strokeWidth={2.5} style={{ color: 'var(--green-ink)' }} />
</td>
)
}
if (value === 'x') {
return (
<td
style={{
padding: '13px 24px',
borderBottom: '1px solid var(--rule-thin)',
borderRight: '1px solid var(--rule-thin)',
textAlign: 'center',
opacity: 0.35,
}}
>
<X size={13} strokeWidth={2} style={{ color: 'var(--ink-4)' }} />
</td>
)
}
return (
<td
style={{
padding: '13px 24px',
borderBottom: '1px solid var(--rule-thin)',
borderRight: '1px solid var(--rule-thin)',
textAlign: 'center',
fontSize: 11,
fontWeight: 600,
color: 'var(--ink-3)',
}}
>
{value}
</td>
)
}
/* ─── plan card ──────────────────────────────────────────── */
function PlanCard({ plan }: { plan: Plan }) {
const [hovered, setHovered] = useState(false)
const features = PLAN_EXTRA_FEATURES[plan.id] || plan.features
return (
<div
style={{
background: plan.featured ? 'var(--ink)' : 'var(--paper)',
padding: 40,
display: 'flex',
flexDirection: 'column',
height: '100%',
position: 'relative',
}}
>
{/* Badge */}
{plan.badge && (
<div
style={{
position: 'absolute',
top: -1,
right: 32,
background: 'var(--red)',
color: 'var(--paper)',
fontSize: 9,
fontWeight: 700,
letterSpacing: '1.5px',
textTransform: 'uppercase',
padding: '4px 12px',
}}
>
{plan.badge}
</div>
)}
{/* Plan name */}
<p
style={{
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '2.5px',
color: plan.featured ? 'rgba(244,239,231,0.5)' : 'var(--ink-5)',
marginBottom: 8,
}}
>
طرح
</p>
<h3
style={{
fontSize: 24,
fontWeight: 900,
color: plan.featured ? 'var(--paper)' : 'var(--ink)',
letterSpacing: '-0.5px',
marginBottom: 20,
}}
>
{plan.name}
</h3>
{/* Price */}
<div style={{ marginBottom: 24 }}>
{typeof plan.price === 'number' ? (
<>
<span
style={{
fontSize: 32,
fontWeight: 900,
color: plan.featured ? 'var(--paper)' : 'var(--ink)',
letterSpacing: '-1px',
lineHeight: 1,
}}
>
{plan.price.toLocaleString('fa-IR')}
</span>
<span
style={{
fontSize: 12,
color: plan.featured ? 'rgba(244,239,231,0.6)' : 'var(--ink-4)',
marginRight: 4,
}}
>
{plan.period}
</span>
</>
) : (
<span
style={{
fontSize: 28,
fontWeight: 900,
color: plan.featured ? 'var(--paper)' : 'var(--ink)',
}}
>
تماس بگیرید
</span>
)}
<p
style={{
fontSize: 11,
color: plan.featured ? 'rgba(244,239,231,0.55)' : 'var(--ink-5)',
marginTop: 6,
}}
>
{plan.period}
</p>
</div>
{/* Divider */}
<div
style={{
height: 1,
background: plan.featured ? 'rgba(244,239,231,0.15)' : 'var(--rule-thin)',
marginBottom: 24,
}}
/>
{/* Features */}
<ul style={{ listStyle: 'none', padding: 0, margin: '0 0 32px', flex: 1 }}>
{features.map((feat, i) => (
<li
key={i}
style={{
display: 'flex',
alignItems: 'flex-start',
gap: 10,
padding: '7px 0',
borderBottom:
i < features.length - 1
? `1px solid ${plan.featured ? 'rgba(244,239,231,0.08)' : 'var(--rule-thin)'}`
: 'none',
fontSize: 12,
color: plan.featured ? 'rgba(244,239,231,0.85)' : 'var(--ink-3)',
}}
>
<Check
size={12}
style={{
marginTop: 2,
flexShrink: 0,
color: plan.featured ? 'rgba(244,239,231,0.6)' : 'var(--green-ink)',
}}
/>
{feat}
</li>
))}
</ul>
{/* CTA */}
<button
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
width: '100%',
padding: '14px 0',
fontSize: 13,
fontWeight: 700,
cursor: 'pointer',
fontFamily: 'inherit',
borderRadius: 0,
letterSpacing: '0.3px',
transition: 'all 150ms',
border: plan.featured
? hovered
? '1px solid var(--paper-2)'
: '1px solid var(--paper)'
: hovered
? '1px solid var(--ink)'
: '1px solid var(--rule-thin)',
background: plan.featured
? hovered
? 'var(--paper-2)'
: 'var(--paper)'
: hovered
? 'var(--ink)'
: 'transparent',
color: plan.featured ? 'var(--ink)' : hovered ? 'var(--paper)' : 'var(--ink)',
}}
>
{plan.ctaLabel}
</button>
</div>
)
}
/* ─── FAQ item ───────────────────────────────────────────── */
function FaqItem({ q, a, index }: { q: string; a: string; index: number }) {
const [open, setOpen] = useState(false)
return (
<div style={{ borderBottom: '1px solid var(--rule-thin)' }}>
<button
onClick={() => setOpen(!open)}
style={{
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '20px 0',
background: 'transparent',
border: 'none',
cursor: 'pointer',
fontFamily: 'inherit',
textAlign: 'right',
gap: 16,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span style={{ fontSize: 11, fontWeight: 700, color: 'var(--ink-5)', minWidth: 20 }}>
{(index + 1).toLocaleString('fa-IR')}
</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--ink)' }}>{q}</span>
</div>
<span style={{ color: 'var(--ink-4)', flexShrink: 0 }}>
{open ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</span>
</button>
{open && (
<div
style={{
padding: '0 0 20px 36px',
fontSize: 14,
fontWeight: 300,
lineHeight: 1.8,
color: 'var(--ink-3)',
}}
>
{a}
</div>
)}
</div>
)
}
/* ─── CTA button with hover state ───────────────────────── */
function CtaButton({ label }: { label: string }) {
const [hovered, setHovered] = useState(false)
return (
<button
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
padding: '14px 32px',
fontSize: 13,
fontWeight: 700,
cursor: 'pointer',
fontFamily: 'inherit',
borderRadius: 0,
letterSpacing: '0.3px',
transition: 'all 150ms',
background: hovered ? 'var(--paper-2)' : 'var(--paper)',
color: 'var(--ink)',
border: hovered ? '1px solid var(--paper-2)' : '1px solid var(--paper)',
}}
>
{label}
</button>
)
}
/* ─── main component ─────────────────────────────────────── */
export default function Subscribe() {
return (
<div style={{ direction: 'rtl' }}>
{/* ─── Page Header ───────────────────────────────── */}
<div
style={{
maxWidth: 1280,
margin: '0 auto',
padding: '64px 48px 48px',
borderBottom: '3px solid var(--ink)',
textAlign: 'center',
}}
>
<h1
style={{
fontSize: 36,
fontWeight: 900,
letterSpacing: '-1px',
color: 'var(--ink)',
marginBottom: 12,
}}
>
اشتراک و دسترسی
</h1>
<p
style={{
fontSize: 15,
fontWeight: 300,
color: 'var(--ink-3)',
maxWidth: 560,
margin: '0 auto',
lineHeight: 1.7,
}}
>
دسترسی به جامعترین تحلیلهای بازار فولاد ایران از گزارشهای فصلی تا مشاوره سازمانی
</p>
</div>
{/* ─── Section 1: Plans ──────────────────────────── */}
<div style={{ padding: '64px 0', borderBottom: '2px solid var(--ink)' }}>
<div style={{ maxWidth: 1280, margin: '0 auto', padding: '0 48px' }}>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
border: '1px solid var(--rule-thin)',
borderLeft: 'none',
}}
className="plans-grid"
>
{plans.map((plan) => (
<div
key={plan.id}
style={{ borderLeft: '1px solid var(--rule-thin)' }}
>
<PlanCard plan={plan} />
</div>
))}
</div>
</div>
</div>
{/* ─── Section 2: Feature Comparison Table ─────────── */}
<div style={{ padding: '64px 0', borderBottom: '2px solid var(--ink)' }}>
<div style={{ maxWidth: 1280, margin: '0 auto', padding: '0 48px' }}>
<div style={{ marginBottom: 40 }}>
<h2
style={{
fontSize: 24,
fontWeight: 900,
color: 'var(--ink)',
letterSpacing: '-0.5px',
marginBottom: 6,
}}
>
مقایسه کامل امکانات
</h2>
<p style={{ fontSize: 13, color: 'var(--ink-5)' }}>
مقایسه تفصیلی امکانات هر سه طرح اشتراک
</p>
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{ borderCollapse: 'collapse', width: '100%', minWidth: 600 }}>
<thead>
<tr>
<th
style={{
padding: '16px 20px',
textAlign: 'right',
background: 'var(--paper-3)',
borderBottom: '2px solid var(--ink)',
fontSize: 11,
fontWeight: 700,
letterSpacing: '0.5px',
color: 'var(--ink-4)',
textTransform: 'uppercase',
}}
>
امکانات
</th>
{plans.map((plan) => (
<th
key={plan.id}
style={{
padding: '16px 24px',
textAlign: 'center',
background: plan.featured ? 'var(--ink)' : 'var(--paper-3)',
borderBottom: '2px solid var(--ink)',
borderRight: '1px solid var(--rule-thin)',
fontSize: 13,
fontWeight: 900,
color: plan.featured ? 'var(--paper)' : 'var(--ink)',
letterSpacing: '-0.3px',
}}
>
{plan.name}
</th>
))}
</tr>
</thead>
<tbody>
{FEATURES.map((feat, i) => (
<tr
key={feat.label}
style={{
background: i % 2 === 0 ? 'var(--paper)' : 'var(--paper-2)',
}}
>
<td
style={{
padding: '13px 20px',
fontSize: 13,
color: 'var(--ink-2)',
borderBottom: '1px solid var(--rule-thin)',
}}
>
{feat.label}
</td>
<FeatureCell value={feat.basic} />
<FeatureCell value={feat.pro} />
<FeatureCell value={feat.gov} />
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* ─── Section 3: FAQ ──────────────────────────────── */}
<div style={{ padding: '64px 0', borderBottom: '2px solid var(--ink)' }}>
<div style={{ maxWidth: 860, margin: '0 auto', padding: '0 48px' }}>
<div style={{ marginBottom: 40 }}>
<h2
style={{
fontSize: 24,
fontWeight: 900,
color: 'var(--ink)',
letterSpacing: '-0.5px',
marginBottom: 6,
}}
>
پرسشهای متداول
</h2>
<p style={{ fontSize: 13, color: 'var(--ink-5)' }}>
پاسخ به سوالهای رایج درباره اشتراک و گزارشها
</p>
</div>
<div style={{ borderTop: '2px solid var(--ink)' }}>
{FAQS.map((faq, i) => (
<FaqItem key={i} q={faq.q} a={faq.a} index={i} />
))}
</div>
</div>
</div>
{/* ─── Section 4: CTA Band ─────────────────────────── */}
<div
style={{
background: 'var(--ink)',
padding: '80px 48px',
textAlign: 'center',
}}
>
<div style={{ maxWidth: 700, margin: '0 auto' }}>
<p
style={{
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '3px',
color: 'rgba(244,239,231,0.4)',
marginBottom: 16,
}}
>
اندیشکده فولاد ایران
</p>
<h2
style={{
fontSize: 32,
fontWeight: 900,
color: 'var(--paper)',
letterSpacing: '-1px',
lineHeight: 1.2,
marginBottom: 16,
}}
>
آماده شروع هستید؟
</h2>
<p
style={{
fontSize: 15,
color: 'rgba(244,239,231,0.65)',
fontWeight: 300,
lineHeight: 1.7,
marginBottom: 40,
maxWidth: 500,
margin: '0 auto 40px',
}}
>
به بیش از ۲۸۰۰ متخصص و مدیر صنعت فولاد بپیوندید که تصمیمات خود را با تحلیلهای
اندیشکده میگیرند.
</p>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
flexWrap: 'wrap',
}}
>
<CtaButton label="اشتراک سازمانی" />
<OutlineCtaLink label="تماس با ما" />
</div>
</div>
</div>
{/* ─── Responsive styles ─────────────────────────── */}
<style>{`
@media (max-width: 767px) {
.plans-grid {
grid-template-columns: 1fr !important;
}
}
@media (max-width: 1024px) and (min-width: 768px) {
.plans-grid {
grid-template-columns: 1fr 1fr !important;
}
}
`}</style>
</div>
)
}
/* ─── outline CTA link ───────────────────────────────────── */
function OutlineCtaLink({ label }: { label: string }) {
const [hovered, setHovered] = useState(false)
return (
<Link
to="/contact"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
padding: '14px 32px',
fontSize: 13,
fontWeight: 700,
color: 'var(--paper)',
background: 'transparent',
border: hovered
? '1px solid rgba(244,239,231,0.6)'
: '1px solid rgba(244,239,231,0.35)',
textDecoration: 'none',
letterSpacing: '0.3px',
transition: 'border-color 150ms',
display: 'inline-flex',
alignItems: 'center',
gap: 8,
}}
>
<Phone size={14} />
{label}
</Link>
)
}

333
src/pages/Team/Team.tsx Normal file
View File

@ -0,0 +1,333 @@
import { useState } from 'react'
import { team } from '@/data/team'
/* ─── derived stats ──────────────────────────────────────── */
const totalReports = team.reduce((sum, m) => sum + m.reportCount, 0)
const memberCount = team.length
const combinedYears = 85
/* ─── member card ────────────────────────────────────────── */
function MemberCard({ member }: { member: (typeof team)[0] }) {
const [hovered, setHovered] = useState(false)
const [bioExpanded, setBioExpanded] = useState(false)
return (
<article
style={{
border: '1px solid var(--rule-thin)',
background: hovered ? 'var(--paper-2)' : 'var(--paper)',
transition: 'background 140ms',
display: 'flex',
flexDirection: 'column',
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{/* card header */}
<div
style={{
padding: '24px 20px 16px',
borderBottom: '1px solid var(--rule-thin)',
display: 'flex',
alignItems: 'flex-start',
gap: 16,
}}
>
{/* initial avatar */}
<div
style={{
width: 56,
height: 56,
background: 'var(--ink)',
color: 'var(--paper)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 22,
fontWeight: 900,
flexShrink: 0,
letterSpacing: '-0.5px',
}}
>
{member.initial}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<h3
style={{
fontSize: 15,
fontWeight: 800,
color: 'var(--ink)',
lineHeight: 1.3,
letterSpacing: '-0.3px',
marginBottom: 4,
}}
>
{member.name}
</h3>
<span
style={{
fontSize: 10,
fontWeight: 700,
color: 'var(--red)',
textTransform: 'uppercase',
letterSpacing: '1.5px',
display: 'block',
}}
>
{member.role}
</span>
</div>
</div>
{/* bio */}
<div style={{ padding: '14px 20px', borderBottom: '1px solid var(--rule-thin)' }}>
<p
style={{
fontSize: 13,
fontWeight: 300,
color: 'var(--ink-3)',
lineHeight: 1.85,
display: '-webkit-box',
WebkitLineClamp: bioExpanded ? undefined : 3,
WebkitBoxOrient: 'vertical' as const,
overflow: bioExpanded ? 'visible' : 'hidden',
}}
>
{member.bio}
</p>
{!bioExpanded && member.bio.length > 120 && (
<button
onClick={() => setBioExpanded(true)}
style={{
marginTop: 6,
fontSize: 11,
fontWeight: 600,
color: 'var(--ink-4)',
background: 'none',
border: 'none',
cursor: 'pointer',
fontFamily: 'inherit',
padding: 0,
textDecoration: 'underline',
textDecorationStyle: 'dotted',
}}
>
بیشتر
</button>
)}
</div>
{/* expertise tags */}
<div style={{ padding: '12px 20px', borderBottom: '1px solid var(--rule-thin)' }}>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{member.expertise.map((tag) => (
<span
key={tag}
style={{
fontSize: 10,
fontWeight: 500,
color: 'var(--ink-3)',
background: 'var(--paper-3)',
padding: '3px 8px',
letterSpacing: '0.2px',
}}
>
{tag}
</span>
))}
</div>
</div>
{/* footer: report count + email */}
<div
style={{
padding: '12px 20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginTop: 'auto',
}}
>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
<span
style={{
fontSize: 20,
fontWeight: 900,
color: 'var(--ink)',
letterSpacing: '-0.5px',
lineHeight: 1,
}}
>
{member.reportCount.toLocaleString('fa-IR')}
</span>
<span style={{ fontSize: 11, fontWeight: 400, color: 'var(--ink-5)' }}>
گزارش منتشرشده
</span>
</div>
{member.email && (
<a
href={`mailto:${member.email}`}
style={{
fontSize: 10,
color: 'var(--ink-5)',
textDecoration: 'none',
fontWeight: 400,
letterSpacing: '0.2px',
direction: 'ltr',
display: 'inline-block',
}}
>
{member.email}
</a>
)}
</div>
</article>
)
}
/* ─── main component ─────────────────────────────────────── */
export default function Team() {
return (
<div
style={{
maxWidth: 1280,
margin: '0 auto',
padding: '64px 48px',
direction: 'rtl',
}}
>
{/* ─── Section header ────────────────────────────────── */}
<div
style={{
display: 'flex',
alignItems: 'stretch',
borderBottom: '3px solid var(--ink)',
marginBottom: 40,
gap: 0,
}}
>
<div
style={{
background: 'var(--ink)',
padding: '14px 28px',
display: 'flex',
alignItems: 'center',
}}
>
<h1
style={{
fontSize: 24,
fontWeight: 900,
color: 'var(--paper)',
letterSpacing: '-0.5px',
whiteSpace: 'nowrap',
}}
>
تیم تحریریه
</h1>
</div>
<div style={{ flex: 1 }} />
<div
style={{
display: 'flex',
alignItems: 'center',
padding: '14px 0',
}}
>
<span
style={{
fontSize: 13,
fontWeight: 300,
color: 'var(--ink-4)',
fontStyle: 'italic',
}}
>
متخصصان برجسته صنعت فولاد ایران
</span>
</div>
</div>
{/* ─── Stats row ─────────────────────────────────────── */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
border: '1px solid var(--rule-thin)',
marginBottom: 48,
}}
>
{[
{ value: totalReports.toLocaleString('fa-IR'), label: 'گزارش منتشرشده' },
{ value: memberCount.toLocaleString('fa-IR'), label: 'عضو تحریریه' },
{ value: `${combinedYears.toLocaleString('fa-IR')} سال`, label: 'تجربه ترکیبی' },
].map((stat, i) => (
<div
key={i}
style={{
padding: '28px 32px',
borderLeft: i < 2 ? '1px solid var(--rule-thin)' : 'none',
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<span
style={{
fontSize: 42,
fontWeight: 900,
color: 'var(--ink)',
lineHeight: 1,
letterSpacing: '-1.5px',
display: 'block',
}}
>
{stat.value}
</span>
<span
style={{
fontSize: 12,
fontWeight: 400,
color: 'var(--ink-5)',
letterSpacing: '0.3px',
}}
>
{stat.label}
</span>
</div>
))}
</div>
{/* ─── Member grid ───────────────────────────────────── */}
<div
className="team-grid"
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 1,
background: 'var(--rule-thin)',
}}
>
{team.map((member) => (
<MemberCard key={member.id} member={member} />
))}
</div>
<style>{`
@media (max-width: 767px) {
.team-grid {
grid-template-columns: 1fr !important;
}
}
@media (max-width: 1024px) and (min-width: 768px) {
.team-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
}
`}</style>
</div>
)
}

30
tsconfig.app.json Normal file
View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
/* Path aliases */
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
tsconfig.node.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

13
vite.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})