Add Docker deployment for Hugging Face Spaces

This commit is contained in:
alireza 2026-05-26 16:04:01 +03:30
parent 8c5d2a9739
commit 848f03374e
22 changed files with 1514 additions and 509 deletions

40
.dockerignore Normal file
View File

@ -0,0 +1,40 @@
# Version control
.git
.gitignore
.gitattributes
# Build output (rebuilt inside the image)
dist
dist-ssr
build
.cache
# Dependencies (reinstalled in the image)
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor / OS
.vscode
.idea
.DS_Store
Thumbs.db
*.swp
*.swo
# Env (never bake secrets into the image)
.env
.env.*
!.env.example
# Docker
Dockerfile
docker-compose.yml
.dockerignore
# Misc
*.log
coverage
.eslintcache

40
Dockerfile Normal file
View File

@ -0,0 +1,40 @@
# syntax=docker/dockerfile:1.7
# =====================================================================
# Stage 1 — Build the Vite/React app
# =====================================================================
FROM node:22-alpine AS builder
WORKDIR /app
# Install only what's locked, deterministically.
# --legacy-peer-deps avoids ERESOLVE failures on bleeding-edge peer ranges
# (React 19 / Vite 8 / TS 6 / ESLint 10).
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund --legacy-peer-deps
# Build the static bundle
COPY . .
RUN npm run build
# =====================================================================
# Stage 2 — Serve with non-root nginx (Hugging Face Spaces compatible)
# =====================================================================
# nginx-unprivileged runs as uid 101 by default — required by HF Spaces,
# which forbids running as root.
FROM nginxinc/nginx-unprivileged:alpine AS runner
# SPA-aware server config that listens on 7860 (HF Spaces default port)
COPY --chown=nginx:nginx nginx.conf /etc/nginx/conf.d/default.conf
# Static assets produced by `vite build`
COPY --chown=nginx:nginx --from=builder /app/dist /usr/share/nginx/html
EXPOSE 7860
# Healthcheck (works both locally and on HF Spaces)
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://127.0.0.1:7860/ >/dev/null 2>&1 || exit 1
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,7 +1,38 @@
---
title: Andishkade Foolad
emoji: 🏭
colorFrom: indigo
colorTo: gray
sdk: docker
app_port: 7860
pinned: false
---
# React + TypeScript + Vite # React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
## Run locally with Docker
```bash
# Build and start (foreground)
docker compose up --build
# Or detached
docker compose up -d --build
# Stop
docker compose down
```
The app will be available at <http://localhost:7860>.
## Deploy to Hugging Face Spaces
1. Create a new Space → **SDK: Docker****Blank**.
2. Push this repo (including `Dockerfile`, `nginx.conf`, and the README frontmatter above).
3. Spaces reads `app_port: 7860` from the frontmatter and routes traffic to the container automatically — no further config needed.
Currently, two official plugins are available: Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)

21
docker-compose.yml Normal file
View File

@ -0,0 +1,21 @@
# Spec-less Compose file (Docker Compose v2+). The top-level `version:` key
# is intentionally omitted — it is deprecated and emits a warning on modern
# Compose versions.
services:
web:
build:
context: .
dockerfile: Dockerfile
image: andishkade-foolad:latest
container_name: andishkade-foolad
ports:
# host:container — HF Spaces also exposes 7860 publicly
- "7860:7860"
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:7860/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s

55
nginx.conf Normal file
View File

@ -0,0 +1,55 @@
server {
listen 7860;
listen [::]:7860;
server_name _;
root /usr/share/nginx/html;
index index.html;
# gzip
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
application/xml+rss
application/wasm
image/svg+xml
font/ttf
font/otf
font/woff
font/woff2;
# Long-lived cache for hashed Vite assets
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Never cache index.html (so deploys take effect immediately)
location = /index.html {
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
try_files $uri =404;
}
# SPA history-fallback
location / {
try_files $uri $uri/ /index.html;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
}

View File

@ -96,7 +96,7 @@ export function Footer() {
gridTemplateColumns: '2fr 1px 1fr 1px 1fr 1px 1fr', gridTemplateColumns: '2fr 1px 1fr 1px 1fr 1px 1fr',
gap: 0, gap: 0,
}} }}
className="max-md:grid-cols-1 max-md:gap-10" className="mq-stack max-md:grid-cols-1 max-md:gap-10"
> >
{/* Brand */} {/* Brand */}
<div style={{ paddingLeft: 0, paddingRight: 48 }} className="max-md:pr-0"> <div style={{ paddingLeft: 0, paddingRight: 48 }} className="max-md:pr-0">

View File

@ -101,8 +101,17 @@ export default function Header() {
const [mobileOpen, setMobileOpen] = useState(false) const [mobileOpen, setMobileOpen] = useState(false)
const [isMobile, setIsMobile] = useState(false) const [isMobile, setIsMobile] = useState(false)
const [searchOpen, setSearchOpen] = useState(false) const [searchOpen, setSearchOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
const searchInputRef = useRef<HTMLInputElement>(null) const searchInputRef = useRef<HTMLInputElement>(null)
/* scroll listener — toggles glass + collapses meta strip */
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8)
onScroll()
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
/* viewport listener */ /* viewport listener */
useEffect(() => { useEffect(() => {
const mq = window.matchMedia('(max-width: 1023px)') const mq = window.matchMedia('(max-width: 1023px)')
@ -129,17 +138,25 @@ export default function Header() {
position: 'sticky', position: 'sticky',
top: 0, top: 0,
zIndex: 50, zIndex: 50,
background: T.paper, background: scrolled ? 'rgba(244,239,231,0.72)' : T.paper,
backdropFilter: scrolled ? 'blur(18px) saturate(160%)' : 'none',
WebkitBackdropFilter: scrolled ? 'blur(18px) saturate(160%)' : 'none',
borderBottom: scrolled ? '1px solid rgba(26,23,18,0.08)' : '1px solid transparent',
direction: 'rtl', direction: 'rtl',
transition: 'background 220ms ease, backdrop-filter 220ms ease, border-color 220ms ease',
}} }}
> >
{/* {/*
TOP META STRIP slim, dark TOP META STRIP slim, dark, collapses on scroll
*/} */}
<div <div
style={{ style={{
background: T.ink, background: T.ink,
color: 'rgba(244,239,231,0.7)', color: 'rgba(244,239,231,0.7)',
height: scrolled ? 0 : 32,
opacity: scrolled ? 0 : 1,
overflow: 'hidden',
transition: 'height 220ms ease, opacity 180ms ease',
}} }}
className="max-md:hidden" className="max-md:hidden"
> >
@ -207,7 +224,8 @@ export default function Header() {
*/} */}
<div <div
style={{ style={{
borderBottom: `2px solid ${T.ink}`, borderBottom: scrolled ? 'none' : `2px solid ${T.ink}`,
transition: 'border-color 220ms ease',
}} }}
> >
<div <div

View File

@ -21,6 +21,11 @@ export interface CardData {
title: string title: string
category: string category: string
content: ReactNode content: ReactNode
/** Optional thumbnail body fields (MUI-style card layout) */
summary?: string
author?: string
date?: string
pages?: number
} }
interface CarouselContextType { interface CarouselContextType {
@ -44,6 +49,7 @@ export function Carousel({
}) { }) {
const carouselRef = useRef<HTMLDivElement>(null) const carouselRef = useRef<HTMLDivElement>(null)
const [currentIndex, setCurrentIndex] = useState(0) const [currentIndex, setCurrentIndex] = useState(0)
const [paused, setPaused] = useState(false)
useEffect(() => { useEffect(() => {
if (carouselRef.current) { if (carouselRef.current) {
@ -51,6 +57,23 @@ export function Carousel({
} }
}, [initialScroll]) }, [initialScroll])
/* Auto-advance: scroll one card-width every 4s. Pauses on hover. */
useEffect(() => {
if (paused) return
const el = carouselRef.current
if (!el) return
const id = window.setInterval(() => {
if (!el) return
const step = window.innerWidth < 768 ? 280 : 360 // card width + gap
const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 4
el.scrollTo({
left: atEnd ? 0 : el.scrollLeft + step,
behavior: 'smooth',
})
}, 4000)
return () => window.clearInterval(id)
}, [paused])
function handleCardClose(index: number) { function handleCardClose(index: number) {
if (!carouselRef.current) return if (!carouselRef.current) return
const cardWidth = window.innerWidth < 768 ? 230 : 350 const cardWidth = window.innerWidth < 768 ? 230 : 350
@ -58,23 +81,34 @@ export function Carousel({
setCurrentIndex(index) setCurrentIndex(index)
} }
function scrollByStep(direction: -1 | 1) {
if (!carouselRef.current) return
const step = window.innerWidth < 768 ? 280 : 360
carouselRef.current.scrollBy({ left: direction * step, behavior: 'smooth' })
}
return ( return (
<CarouselContext.Provider value={{ onCardClose: handleCardClose, currentIndex }}> <CarouselContext.Provider value={{ onCardClose: handleCardClose, currentIndex }}>
<div className="relative w-full"> <div
className="relative w-full"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{/* Scrollable track */} {/* Scrollable track */}
<div <div
ref={carouselRef} ref={carouselRef}
className="flex w-full overflow-x-scroll overscroll-x-auto scroll-smooth py-10 md:py-14 [scrollbar-width:none]" className="flex w-full overflow-x-scroll overscroll-x-auto scroll-smooth py-10 md:py-14 [scrollbar-width:none]"
style={{ direction: 'ltr' }} style={{ direction: 'ltr' }}
onTouchStart={() => setPaused(true)}
onTouchEnd={() => setPaused(false)}
> >
<div className="flex flex-row justify-start gap-4 px-4 md:px-10"> <div className="flex flex-row justify-start gap-5 px-6 md:px-12 md:pr-24">
{items.map((item, index) => ( {items.map((item, index) => (
<motion.div <motion.div
key={'card' + index} key={'card' + index}
initial={{ opacity: 0, y: 24 }} initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, delay: 0.1 * index, ease: 'easeOut' }} transition={{ duration: 0.45, delay: 0.08 * index, ease: 'easeOut' }}
className="last:pl-[10%] md:last:pl-[20%]"
> >
{item} {item}
</motion.div> </motion.div>
@ -82,6 +116,103 @@ export function Carousel({
</div> </div>
</div> </div>
{/* ── Glass pill controls — prev/next + auto state ── */}
<div
className="absolute bottom-3 right-6 md:right-12 z-20 flex items-center"
style={{
background: 'rgba(244,239,231,0.5)',
backdropFilter: 'blur(14px) saturate(160%)',
WebkitBackdropFilter: 'blur(14px) saturate(160%)',
border: '1px solid rgba(26,23,18,0.08)',
borderRadius: 9999,
padding: '4px',
boxShadow: '0 4px 16px rgba(26,23,18,0.08)',
gap: 2,
}}
>
{/* Prev */}
<button
aria-label="کارت قبلی"
onClick={() => scrollByStep(-1)}
style={{
width: 36,
height: 36,
borderRadius: '50%',
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: 'var(--ink)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background 140ms',
}}
onMouseEnter={e => (e.currentTarget.style.background = 'rgba(26,23,18,0.06)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<path d="M15 19l-7-7 7-7" />
</svg>
</button>
{/* Auto state pill */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '0 12px',
height: 36,
fontSize: 9,
fontWeight: 700,
letterSpacing: '2px',
color: 'var(--ink-4)',
fontFamily: 'ui-monospace, monospace',
direction: 'ltr',
whiteSpace: 'nowrap',
userSelect: 'none',
}}
className="max-md:hidden"
>
<span
style={{
width: 6,
height: 6,
borderRadius: '50%',
background: paused ? 'var(--ink-5)' : 'var(--red)',
boxShadow: paused ? 'none' : '0 0 8px rgba(155,28,28,0.55)',
transition: 'background 160ms, box-shadow 160ms',
}}
/>
{paused ? 'PAUSED' : 'AUTO'}
</div>
{/* Next */}
<button
aria-label="کارت بعدی"
onClick={() => scrollByStep(1)}
style={{
width: 36,
height: 36,
borderRadius: '50%',
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: 'var(--ink)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background 140ms',
}}
onMouseEnter={e => (e.currentTarget.style.background = 'rgba(26,23,18,0.06)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div> </div>
</CarouselContext.Provider> </CarouselContext.Provider>
) )
@ -188,47 +319,144 @@ export function Card({
)} )}
</AnimatePresence> </AnimatePresence>
{/* ── Card thumbnail ── */} {/* ── Card thumbnail — MUI ActionAreaCard pattern, editorial palette ── */}
<motion.button <motion.button
onClick={handleOpen} onClick={handleOpen}
className={cn( className={cn(
'rounded-3xl overflow-hidden flex flex-col items-start justify-end relative z-10 cursor-pointer', 'overflow-hidden flex flex-col relative cursor-pointer shrink-0 text-right',
'h-72 w-48 md:h-[420px] md:w-80', 'w-[260px] h-[440px] md:w-[340px] md:h-[480px]',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--red)]', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--red)]',
)} )}
style={{ direction: 'rtl' }} style={{
whileHover={{ scale: 1.015 }} direction: 'rtl',
fontFamily: 'Vazir, sans-serif',
background: 'var(--paper)',
border: '1px solid var(--rule-thin)',
borderRadius: 8,
boxShadow: '0 1px 2px rgba(26,23,18,0.04)',
transition: 'box-shadow 200ms, border-color 200ms',
}}
whileHover={{ y: -3 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
onMouseEnter={(e) => {
e.currentTarget.style.boxShadow = '0 12px 32px rgba(26,23,18,0.12)'
e.currentTarget.style.borderColor = 'var(--ink-6)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.boxShadow = '0 1px 2px rgba(26,23,18,0.04)'
e.currentTarget.style.borderColor = 'var(--rule-thin)'
}}
> >
{/* Strong bottom-to-top gradient — fully opaque at bottom so title pops */} {/* Media — image strip */}
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/75 via-[45%] to-transparent z-20 pointer-events-none" /> <div className="relative w-full" style={{ height: 180, overflow: 'hidden', flexShrink: 0 }}>
<BlurImage
{/* Top centered category badge — prominent, eye-catching */} src={card.src}
<div className="absolute top-6 left-1/2 -translate-x-1/2 z-30"> alt={card.title}
<span className="object-cover w-full h-full"
className="inline-block text-white px-4 py-1.5 text-[12px] font-bold tracking-[2px] whitespace-nowrap" />
style={{ background: 'var(--red)' }} {/* Category badge — overlaid bottom-right of image (RTL: visual start) */}
> <div className="absolute bottom-3 right-3">
{card.category} <span
</span> className="inline-block text-white px-3 py-1 text-[10px] font-bold tracking-[2px] whitespace-nowrap"
style={{ background: 'var(--red)' }}
>
{card.category}
</span>
</div>
</div> </div>
{/* Bottom: title — bright white, readable */} {/* Content — title + summary + meta */}
<div className="relative z-30 px-7 pb-6 pt-10 w-full text-right overflow-hidden"> <div className="flex flex-col flex-1 px-6 py-5 max-md:px-5 max-md:py-4" style={{ minHeight: 0 }}>
<p {/* Title + summary group — vertically centered in remaining space */}
className="text-white text-[16px] font-extrabold leading-snug line-clamp-3" <div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 10 }}>
style={{ textShadow: '0 2px 8px rgba(0,0,0,0.6)' }} {/* Title */}
> <h3
{card.title} style={{
</p> fontSize: 16,
</div> fontWeight: 800,
color: 'var(--ink)',
letterSpacing: '-0.3px',
lineHeight: 1.4,
overflowWrap: 'normal',
wordBreak: 'normal',
paddingLeft: 8,
paddingRight: 8,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{card.title}
</h3>
{/* Background image */} {/* Summary */}
<BlurImage {card.summary && (
src={card.src} <p
alt={card.title} style={{
className="object-cover absolute inset-0 w-full h-full z-10" fontSize: 13,
/> color: 'var(--ink-4)',
lineHeight: 1.65,
fontWeight: 400,
overflowWrap: 'normal',
wordBreak: 'normal',
paddingLeft: 8,
paddingRight: 8,
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
margin: 0,
}}
>
{card.summary}
</p>
)}
</div>
{/* Meta footer — tight below summary */}
{(card.author || card.date || card.pages) && (
<div
style={{
marginTop: 16,
paddingTop: 12,
borderTop: '1px solid var(--rule-thin)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8,
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 }}>
{card.author && (
<span style={{ fontSize: 11, fontWeight: 700, color: 'var(--ink-2)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{card.author}
</span>
)}
{card.date && (
<span style={{ fontSize: 10, color: 'var(--ink-5)', fontWeight: 500 }}>
{card.date}
</span>
)}
</div>
{card.pages && (
<span
style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: '0.5px',
color: 'var(--red)',
fontVariantNumeric: 'tabular-nums',
whiteSpace: 'nowrap',
paddingRight: 2,
}}
>
{card.pages.toLocaleString('fa-IR')} ص
</span>
)}
</div>
)}
</div>
</motion.button> </motion.button>
</> </>
) )

View File

@ -0,0 +1,52 @@
import type { CSSProperties, ReactNode } from 'react'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
function cn(...inputs: Parameters<typeof clsx>) {
return twMerge(clsx(inputs))
}
interface MarqueeProps {
children: ReactNode
reverse?: boolean
pauseOnHover?: boolean
className?: string
style?: CSSProperties
}
/**
* Horizontal infinite marquee. Children duplicated 2x for seamless loop.
* Speed via CSS var --duration. Gap via CSS var --gap (default 1.5rem).
*/
export function Marquee({
children,
reverse = false,
pauseOnHover = false,
className,
style,
}: MarqueeProps) {
return (
<div
className={cn(
'group flex overflow-hidden',
'[--gap:1.5rem] [gap:var(--gap)]',
className,
)}
style={{ direction: 'ltr', ...style }}
>
{[0, 1].map((i) => (
<div
key={i}
aria-hidden={i === 1 ? 'true' : undefined}
className={cn(
'flex shrink-0 [gap:var(--gap)]',
reverse ? 'animate-marquee-reverse' : 'animate-marquee',
pauseOnHover && 'group-hover:[animation-play-state:paused]',
)}
>
{children}
</div>
))}
</div>
)
}

View File

@ -246,6 +246,44 @@ body {
.animate-scroll-risk { animation: none; } .animate-scroll-risk { animation: none; }
} }
/*
MOBILE STACK OVERRIDE
Inline `style={{ display:'grid', gridTemplateColumns: '...' }}`
always beats Tailwind `max-md:grid-cols-1`. This utility forces
any tagged grid container down to single/two columns below 768px,
regardless of the inline declaration.
*/
@media (max-width: 767px) {
.mq-stack {
grid-template-columns: 1fr !important;
gap: 1rem !important;
}
.mq-stack-tight {
grid-template-columns: 1fr !important;
}
.mq-stack-2 {
grid-template-columns: repeat(2, 1fr) !important;
}
}
/*
MARQUEE (horizontal infinite scroll)
*/
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(calc(-100% - var(--gap, 1rem))); }
}
@keyframes marquee-reverse {
from { transform: translateX(calc(-100% - var(--gap, 1rem))); }
to { transform: translateX(0); }
}
.animate-marquee { animation: marquee var(--duration, 40s) linear infinite; }
.animate-marquee-reverse { animation: marquee-reverse var(--duration, 40s) linear infinite; }
@media (prefers-reduced-motion: reduce) {
.animate-marquee,
.animate-marquee-reverse { animation: none; }
}
/* /*
PRINT PRINT
*/ */

View File

@ -7,73 +7,100 @@ import EventsSection from './sections/EventsSection'
import RiskSection from './sections/RiskSection' import RiskSection from './sections/RiskSection'
import TeamSection from './sections/TeamSection' import TeamSection from './sections/TeamSection'
import PlansSection from './sections/PlansSection' import PlansSection from './sections/PlansSection'
import PartnersSection from './sections/PartnersSection'
import NewsletterSection from './sections/NewsletterSection' import NewsletterSection from './sections/NewsletterSection'
/* Section chapter marker /* Section chapter marker
A thin full-bleed dark bar that stamps the start of every A thin full-bleed dark bar that stamps the start of every
major section creates instant, unmistakable boundaries. major section creates instant, unmistakable boundaries.
*/ */
function SectionLabel({ n, title }: { n: string; title: string }) { /* Minimal chapter mark — thin strip, matches next section's bg, no duplicate title */
function SectionLabel({
n,
bg = 'paper',
}: {
n: string
/** matches next section bg so there's no color seam */
bg?: 'paper' | 'paper-2'
title?: string // ignored — editorial h2 inside each section owns the title
}) {
return ( return (
<div <div
style={{ style={{
background: 'var(--ink)', background: `var(--${bg})`,
borderTop: '1px solid rgba(255,255,255,0.08)', borderTop: '1px solid var(--rule-thin)',
position: 'relative',
}} }}
> >
{/* Red accent bar on the right edge (RTL → visual start) */}
<div style={{
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
width: 4,
background: 'var(--red)',
}} />
<div <div
className="max-w-7xl mx-auto px-12 max-md:px-5"
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 24, justifyContent: 'space-between',
gap: 16,
paddingTop: 32,
paddingBottom: 8,
}} }}
className="max-w-7xl mx-auto w-full px-12 py-10 max-md:px-5 max-md:py-7 max-md:gap-4"
> >
<span style={{ {/* Left (visual end in RTL): chapter index */}
fontSize: 36, <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
fontWeight: 900, <span
color: 'var(--red)', style={{
letterSpacing: '-1.5px', fontSize: 48,
lineHeight: 1, fontWeight: 900,
direction: 'ltr', color: 'var(--red)',
fontVariantNumeric: 'tabular-nums', letterSpacing: '-2px',
flexShrink: 0, lineHeight: 0.85,
}} direction: 'ltr',
className="max-md:text-[26px]" fontVariantNumeric: 'tabular-nums',
> }}
{n} className="max-md:text-[32px]"
</span> >
{n}
</span>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 2,
lineHeight: 1.2,
}}
>
<span
style={{
fontSize: 9,
fontWeight: 700,
letterSpacing: '2.5px',
color: 'var(--ink-5)',
direction: 'ltr',
fontFamily: 'ui-monospace, monospace',
}}
>
CHAPTER
</span>
<span
style={{
fontSize: 11,
fontWeight: 600,
letterSpacing: '0.5px',
color: 'var(--ink-4)',
}}
>
بخش {n}
</span>
</div>
</div>
<div style={{ {/* Right (visual start RTL): thin rule fills */}
width: 40, <div
height: 2, style={{
background: 'rgba(255,255,255,0.25)', flex: 1,
flexShrink: 0, height: 1,
}} background: 'var(--rule-thin)',
className="max-md:w-6" maxWidth: 280,
}}
className="max-md:hidden"
/> />
<span style={{
fontSize: 17,
fontWeight: 800,
letterSpacing: '-0.3px',
color: 'var(--paper)',
}}
className="max-md:text-[14px]"
>
{title}
</span>
</div> </div>
</div> </div>
) )
@ -90,33 +117,37 @@ export default function Home() {
<SnapshotBar /> <SnapshotBar />
{/* ── 01 · اسکنر جهانی · bg: paper ─────────────── */} {/* ── 01 · اسکنر جهانی · bg: paper ─────────────── */}
<SectionLabel n="01" title="اسکنر جهانی · رصد ۲۴ ساعته" /> <SectionLabel n="01" bg="paper" />
<GlobalScannerSection /> <GlobalScannerSection />
{/* ── 02 · تحلیل‌ها · bg: paper-2 ──────────────── */} {/* ── 02 · تحلیل‌ها · bg: paper-2 ──────────────── */}
<SectionLabel n="02" title="تحلیل‌های اخیر" /> <SectionLabel n="02" bg="paper-2" />
<ReportsCarouselSection /> <ReportsCarouselSection />
{/* ── 03 · اخبار · bg: paper ────────────────────── */} {/* ── 03 · اخبار · bg: paper ────────────────────── */}
<SectionLabel n="03" title="آخرین اخبار بازار" /> <SectionLabel n="03" bg="paper" />
<LatestNewsBentoSection /> <LatestNewsBentoSection />
{/* ── 04 · رویدادها · bg: paper-2 ──────────────── */} {/* ── 04 · رویدادها · bg: paper-2 ──────────────── */}
<SectionLabel n="04" title="رویدادهای پیش‌رو" /> <SectionLabel n="04" bg="paper-2" />
<EventsSection /> <EventsSection />
{/* ── 05 · ریسک · bg: paper ─────────────────────── */} {/* ── 05 · ریسک · bg: paper ─────────────────────── */}
<SectionLabel n="05" title="پایش ریسک · لحظه‌ای" /> <SectionLabel n="05" bg="paper" />
<RiskSection /> <RiskSection />
{/* ── 06 · تیم · bg: paper-2 ────────────────────── */} {/* ── 06 · تیم · bg: paper-2 ────────────────────── */}
<SectionLabel n="06" title="تیم تحریریه" /> <SectionLabel n="06" bg="paper-2" />
<TeamSection /> <TeamSection />
{/* ── 07 · اشتراک · bg: paper ───────────────────── */} {/* ── 07 · اشتراک · bg: paper ───────────────────── */}
<SectionLabel n="07" title="اشتراک و دسترسی" /> <SectionLabel n="07" bg="paper" />
<PlansSection /> <PlansSection />
{/* ── 08 · همراهان · bg: paper-2 ────────────────── */}
<SectionLabel n="08" bg="paper-2" />
<PartnersSection />
{/* ── خبرنامه (last, no label) ───────────────────── */} {/* ── خبرنامه (last, no label) ───────────────────── */}
<NewsletterSection /> <NewsletterSection />
</div> </div>

View File

@ -27,11 +27,38 @@ export default function EventsSection() {
const displayed = events.slice(0, 4) const displayed = events.slice(0, 4)
return ( return (
<section style={{ borderBottom: '2px solid var(--ink)', background: 'var(--paper-2)' }}> <section style={{ background: 'var(--paper-2)' }}>
<div className="max-w-7xl mx-auto px-12 py-20 max-md:px-5 max-md:py-14"> <div className="max-w-7xl mx-auto px-12 py-28 max-md:px-5 max-md:py-20">
{/* Sleek top row */} {/* ── Editorial header ── */}
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 24 }}> <div
<Link to="/events" style={{ fontSize: '10px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--ink-4)', textDecoration: 'none' }}>تقویم کامل </Link> style={{
display: 'grid',
gridTemplateColumns: '1fr auto',
alignItems: 'end',
gap: 32,
paddingBottom: 28,
borderBottom: '1px solid var(--rule-thin)',
marginBottom: 48,
}}
className="mq-stack max-md:grid-cols-1 max-md:gap-4"
>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 18 }}>
<div style={{ width: 32, height: 2, background: 'var(--red)' }} />
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>
تقویم رویداد · ۱۴۰۳۱۴۰۴
</span>
</div>
<h2 style={{ fontSize: 'clamp(28px, 3.6vw, 48px)', fontWeight: 900, letterSpacing: '-1.2px', color: 'var(--ink)', lineHeight: 1.1, marginBottom: 12 }}>
رویدادهای پیشرو
</h2>
<p style={{ fontSize: 14, lineHeight: 1.7, color: 'var(--ink-4)', maxWidth: 560 }}>
همایشها، نمایشگاهها و نشستهای تخصصی صنعت فولاد در ماههای آینده برای حضور یا حمایت رسانهای.
</p>
</div>
<Link to="/events" style={{ fontSize: 11, fontWeight: 700, letterSpacing: '1.5px', textTransform: 'uppercase', color: 'var(--red)', textDecoration: 'none', whiteSpace: 'nowrap', paddingBottom: 4 }}>
تقویم کامل
</Link>
</div> </div>
{/* 4-col grid */} {/* 4-col grid */}

View File

@ -51,9 +51,8 @@ export default function GlobalScannerSection() {
return ( return (
<section <section
style={{ style={{
background: 'var(--paper)', background: 'var(--paper)',
borderBottom: '2px solid var(--ink)', overflow: 'hidden',
overflow: 'hidden',
}} }}
> >
<div className="max-w-7xl mx-auto w-full px-12 max-md:px-5 pb-12"> <div className="max-w-7xl mx-auto w-full px-12 max-md:px-5 pb-12">

View File

@ -4,15 +4,11 @@ import { reports } from '@/data/reports'
const featured = reports.find(r => r.featured)! 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 = [ const STATS = [
{ n: '۴', label: 'سناریو محتمل' }, { n: '۴', label: 'سناریو محتمل' },
{ n: '۱۵', label: 'کشور رقیب' }, { n: '۱۵', label: 'کشور رقیب' },
{ n: '۸۴', label: 'صفحه تحلیلی' }, { n: '۸۴', label: 'صفحه تحلیلی' },
{ n: '۱۲', label: 'توصیه سیاستی' }, { n: '۱۲', label: 'توصیه سیاستی' },
] ]
export default function HeroSection() { export default function HeroSection() {
@ -23,226 +19,283 @@ export default function HeroSection() {
<section <section
style={{ style={{
position: 'relative', position: 'relative',
height: 'min(88vh, 780px)', background: 'var(--paper)',
minHeight: 540,
overflow: 'hidden',
borderBottom: '3px solid var(--ink)', borderBottom: '3px solid var(--ink)',
overflow: 'hidden',
}} }}
> >
{/* ── Background image ── */} {/* Subtle grid texture */}
<img <div
src={BG_IMAGE}
alt=""
aria-hidden="true" aria-hidden="true"
style={{ style={{
position: 'absolute', position: 'absolute',
inset: 0, inset: 0,
width: '100%', backgroundImage: `
height: '100%', linear-gradient(to right, rgba(26,23,18,0.04) 1px, transparent 1px),
objectFit: 'cover', linear-gradient(to bottom, rgba(26,23,18,0.04) 1px, transparent 1px)
objectPosition: 'center 40%', `,
backgroundSize: '64px 64px',
pointerEvents: 'none',
}} }}
/> />
{/* ── 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 <div
style={{ className="relative max-w-7xl mx-auto px-12 max-md:px-5"
position: 'absolute', style={{ paddingTop: 56, paddingBottom: 64 }}
inset: 0,
display: 'flex',
alignItems: 'flex-end',
}}
> >
{/* ─── Top meta row ─────────────────────────── */}
<div <div
className="max-w-7xl mx-auto w-full px-14 pb-[52px] grid grid-cols-1 md:grid-cols-[1fr_240px] items-end gap-12 max-md:px-5 max-md:pb-8 max-md:gap-6" style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
paddingBottom: 24,
borderBottom: '1px solid var(--rule-thin)',
marginBottom: 48,
}}
className="max-md:flex-wrap max-md:gap-3 max-md:mb-8"
> >
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<div style={{
background: 'var(--red)',
color: 'white',
fontSize: 9,
fontWeight: 700,
letterSpacing: '2.5px',
padding: '5px 12px',
textTransform: 'uppercase',
}}>
گزارش ویژه · شماره ۰۱
</div>
<span style={{
fontSize: 10,
color: 'var(--ink-5)',
letterSpacing: '1.5px',
textTransform: 'uppercase',
fontWeight: 600,
}}>
{featured.publishDate} · {featured.category}
</span>
</div>
{/* ── Left: headline + meta ── */} <span
style={{
fontSize: 10,
color: 'var(--ink-5)',
fontWeight: 600,
letterSpacing: '1.5px',
fontFamily: 'ui-monospace, monospace',
direction: 'ltr',
}}
className="max-md:hidden"
>
ISSN 2783-XXXX · VOL. XII
</span>
</div>
{/* ─── Main grid — 8/12 + 4/12 ─────────────── */}
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 320px',
gap: 56,
alignItems: 'start',
}}
className="mq-stack max-md:grid-cols-1 max-md:gap-10"
>
{/* ═══ Left col ═══ */}
<div> <div>
{/* Overline */} {/* Overline */}
<motion.div <motion.div
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.4 }}
style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 20 }} style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 28 }}
> >
<div style={{ width: 24, height: 2, background: 'var(--red)', flexShrink: 0 }} /> <div style={{ width: 40, height: 2, background: 'var(--red)' }} />
<span style={{ <span style={{
fontSize: 10, fontSize: 11,
fontWeight: 700, fontWeight: 700,
color: 'rgba(255,255,255,0.5)', letterSpacing: '3px',
textTransform: 'uppercase',
color: 'var(--ink-4)',
}}> }}>
گزارش راهبردی · تحلیل سناریو تحلیل سناریو · راهبردی
</span> </span>
</motion.div> </motion.div>
{/* Headline */} {/* Massive editorial headline */}
<motion.h1 <motion.h1
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }} transition={{ duration: 0.55, delay: 0.08 }}
style={{ style={{
fontSize: 'clamp(32px, 5vw, 64px)', fontSize: 'clamp(34px, 5.2vw, 76px)',
fontWeight: 900, fontWeight: 900,
lineHeight: 1.2, lineHeight: 1.05,
color: 'white', letterSpacing: '-1.5px',
marginBottom: 24, color: 'var(--ink)',
maxWidth: 700, marginBottom: 28,
}} }}
> >
{featured.title.split(':').map((part, i) => ( {(() => {
<span key={i}> const [main, sub] = featured.title.split(':')
{i === 0 ? part + ':' : ( return (
<span style={{ color: 'rgba(255,255,255,0.45)' }}>{part}</span> <>
)} <span style={{ display: 'block' }}>{main}</span>
{i === 0 && <br />} {sub && (
</span> <span style={{
))} display: 'block',
color: 'var(--red)',
fontWeight: 900,
marginTop: 8,
}}>
{sub.trim()}
</span>
)}
</>
)
})()}
</motion.h1> </motion.h1>
{/* Summary — one line */} {/* Lede */}
<motion.p <motion.p
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.25 }} transition={{ duration: 0.5, delay: 0.18 }}
style={{ style={{
fontSize: 14, fontSize: 'clamp(15px, 1.2vw, 18px)',
lineHeight: 1.8, lineHeight: 1.7,
fontWeight: 400, fontWeight: 400,
color: 'rgba(255,255,255,0.6)', color: 'var(--ink-3)',
maxWidth: 580, maxWidth: 620,
marginBottom: 32, marginBottom: 36,
borderRight: '2px solid var(--red)',
paddingRight: 16,
}} }}
> >
مدلسازی کمی بر اساس دادههای تجارت ۱۵ کشور رقیب از بدبینانه تا خوشبینانه، مدلسازی کمی بر اساس دادههای تجارت ۱۵ کشور رقیب از بدبینانه تا خوشبینانه،
با توصیههای سیاستی مستقیم برای وزارت صمت و سازمان بنادر. با توصیههای سیاستی مستقیم برای وزارت صمت و سازمان بنادر.
</motion.p> </motion.p>
{/* Divider */} {/* Stats inline row */}
<div style={{ width: '100%', height: 1, background: 'rgba(255,255,255,0.12)', marginBottom: 24 }} /> <motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.25 }}
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: 0,
borderTop: '1px solid var(--rule-thin)',
borderBottom: '1px solid var(--rule-thin)',
marginBottom: 36,
}}
className="mq-stack-2 max-md:grid-cols-2"
>
{STATS.map((s, i) => (
<div
key={i}
style={{
padding: '16px 12px 16px 0',
borderRight: i < STATS.length - 1 ? '1px solid var(--rule-thin)' : 'none',
}}
className={i < 2 ? 'max-md:border-b max-md:border-[var(--rule-thin)]' : ''}
>
<div style={{
fontSize: 'clamp(24px, 2.8vw, 36px)',
fontWeight: 900,
letterSpacing: '-1px',
color: 'var(--ink)',
lineHeight: 1,
fontVariantNumeric: 'tabular-nums',
}}>
{s.n}
</div>
<div style={{
fontSize: 10,
color: 'var(--ink-5)',
fontWeight: 600,
letterSpacing: '0.5px',
marginTop: 6,
}}>
{s.label}
</div>
</div>
))}
</motion.div>
{/* Author + Price + CTA in one bar */} {/* Author + CTAs */}
<motion.div <motion.div
initial={{ opacity: 0, y: 8 }} initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.35 }} transition={{ duration: 0.4, delay: 0.35 }}
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 24, gap: 24,
flexWrap: 'wrap', flexWrap: 'wrap',
}} }}
> >
{/* Author */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ <div style={{
width: 36, height: 36, width: 40, height: 40,
background: 'var(--red)', background: 'var(--ink)',
color: 'white', color: 'var(--paper)',
fontSize: 14, fontWeight: 900, fontSize: 14, fontWeight: 900,
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0, flexShrink: 0,
borderRadius: '50%',
}}> }}>
{featured.authorInitial} {featured.authorInitial}
</div> </div>
<div> <div>
<div style={{ fontSize: 12, fontWeight: 700, color: 'white', lineHeight: 1.2 }}> <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.2 }}>
{featured.author} {featured.author}
</div> </div>
<div style={{ fontSize: 10, color: 'rgba(255,255,255,0.4)', lineHeight: 1.3 }}> <div style={{ fontSize: 10, color: 'var(--ink-5)', lineHeight: 1.3, marginTop: 2 }}>
{featured.authorRole} {featured.authorRole}
</div> </div>
</div> </div>
</div> </div>
{/* Separator */} <div className="max-sm:hidden" style={{ width: 1, height: 36, background: 'var(--rule-thin)' }} />
<div className="max-sm:hidden" 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,
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 className="flex gap-2.5 max-sm:flex-col max-sm:w-full"> <div className="flex gap-2.5 max-sm:flex-col max-sm:w-full">
<button <button
onMouseEnter={() => setBuyHov(true)} onMouseEnter={() => setBuyHov(true)}
onMouseLeave={() => setBuyHov(false)} onMouseLeave={() => setBuyHov(false)}
style={{ style={{
background: buyHov ? 'var(--paper-2)' : 'var(--paper)', background: buyHov ? 'var(--red)' : 'var(--ink)',
color: 'var(--ink)', color: 'var(--paper)',
border: 'none', border: 'none',
padding: '11px 26px', padding: '12px 28px',
fontSize: 12, fontSize: 12,
fontWeight: 700, fontWeight: 700,
fontFamily: 'Vazir, sans-serif', fontFamily: 'Vazir, sans-serif',
cursor: 'pointer', cursor: 'pointer',
transition: 'background 160ms', transition: 'background 160ms',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
letterSpacing: '0.3px',
}} }}
className="max-sm:w-full" className="max-sm:w-full"
> >
خرید گزارش خرید گزارش · {featured.price.toLocaleString('fa-IR')} تومان
</button> </button>
<button <button
onMouseEnter={() => setFreeHov(true)} onMouseEnter={() => setFreeHov(true)}
onMouseLeave={() => setFreeHov(false)} onMouseLeave={() => setFreeHov(false)}
style={{ style={{
background: freeHov ? 'rgba(255,255,255,0.1)' : 'transparent', background: freeHov ? 'var(--ink)' : 'transparent',
color: 'white', color: freeHov ? 'var(--paper)' : 'var(--ink)',
border: '1px solid rgba(255,255,255,0.3)', border: '1px solid var(--ink)',
padding: '11px 26px', padding: '12px 24px',
fontSize: 12, fontSize: 12,
fontWeight: 400, fontWeight: 600,
fontFamily: 'Vazir, sans-serif', fontFamily: 'Vazir, sans-serif',
cursor: 'pointer', cursor: 'pointer',
transition: 'background 160ms', transition: 'background 160ms, color 160ms',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}} }}
className="max-sm:w-full" className="max-sm:w-full"
> >
@ -252,60 +305,118 @@ export default function HeroSection() {
</motion.div> </motion.div>
</div> </div>
{/* ── Right: floating stats panel ── */} {/* ═══ Right col: dossier mini-cover — frosted glass over ink ═══ */}
<motion.div <motion.div
initial={{ opacity: 0, x: -16 }} initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.45 }} transition={{ duration: 0.55, delay: 0.4 }}
style={{ style={{
background: 'rgba(255,255,255,0.06)', position: 'relative',
backdropFilter: 'blur(12px)', background: 'rgba(26,23,18,0.78)',
WebkitBackdropFilter: 'blur(12px)', backdropFilter: 'blur(24px) saturate(170%)',
border: '1px solid rgba(255,255,255,0.1)', WebkitBackdropFilter: 'blur(24px) saturate(170%)',
padding: '20px', border: '1px solid rgba(244,239,231,0.08)',
marginBottom: 2, color: 'var(--paper)',
padding: '36px 30px',
boxShadow: '0 24px 60px rgba(26,23,18,0.22), inset 0 1px 0 rgba(244,239,231,0.05)',
transform: 'rotate(0.6deg)',
}} }}
className="max-md:hidden" className="max-md:rotate-0"
> >
<div style={{ <div style={{
fontSize: 9, position: 'absolute',
fontWeight: 700, top: 0, right: 0, left: 0,
letterSpacing: '2px', height: 4,
textTransform: 'uppercase', background: 'var(--red)',
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 style={{
position: 'absolute',
top: 18,
left: 22,
fontSize: 9,
fontWeight: 700,
letterSpacing: '2px',
color: 'rgba(244,239,231,0.4)',
direction: 'ltr',
fontFamily: 'ui-monospace, monospace',
}}>
01 / 2025
</div>
<div style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: '3px',
color: 'rgba(244,239,231,0.4)',
textTransform: 'uppercase',
marginBottom: 24,
marginTop: 8,
}}>
DOSSIER
</div>
<div style={{
fontSize: 120,
fontWeight: 900,
lineHeight: 0.85,
letterSpacing: '-6px',
color: 'var(--red)',
direction: 'ltr',
fontVariantNumeric: 'tabular-nums',
marginBottom: 12,
}}>
۰۴
</div>
<div style={{
fontSize: 13,
fontWeight: 700,
color: 'rgba(244,239,231,0.85)',
lineHeight: 1.4,
marginBottom: 4,
}}>
سناریو راهبردی
</div>
<div style={{
fontSize: 11,
color: 'rgba(244,239,231,0.5)',
lineHeight: 1.5,
marginBottom: 28,
}}>
برای آینده فولاد ایران در بازار جهانی ۱۴۰۴
</div>
<ul style={{
listStyle: 'none',
padding: 0,
margin: 0,
borderTop: '1px solid rgba(244,239,231,0.1)',
paddingTop: 16,
}}>
{[
{ k: 'صفحات', v: `${featured.pages.toLocaleString('fa-IR')} صفحه` },
{ k: 'نوع', v: 'گزارش راهبردی' },
{ k: 'انتشار', v: featured.publishDate },
{ k: 'مخاطب', v: 'سیاست‌گذار · مدیر ارشد' },
].map((row) => (
<li
key={row.k}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px 0',
fontSize: 11,
}}
>
<span style={{ color: 'rgba(244,239,231,0.5)' }}>{row.k}</span>
<span style={{ color: 'rgba(244,239,231,0.9)', fontWeight: 600 }}>{row.v}</span>
</li>
))}
</ul>
</motion.div>
</div> </div>
</div> </div>
</section> </section>

View File

@ -51,7 +51,7 @@ function Accent({ color = 'var(--red)' }: { color?: string }) {
/* ─── Section ────────────────────────────────────────────── */ /* ─── Section ────────────────────────────────────────────── */
export default function LatestNewsBentoSection() { export default function LatestNewsBentoSection() {
return ( return (
<section style={{ background: 'var(--paper)', borderBottom: '2px solid var(--ink)' }}> <section style={{ background: 'var(--paper)' }}>
{/* ── Header ── */} {/* ── Header ── */}
<div className="max-w-7xl mx-auto px-12 pt-8 pb-4 max-md:px-5 max-md:pt-6 flex justify-between items-center text-xs text-[var(--ink-4)]"> <div className="max-w-7xl mx-auto px-12 pt-8 pb-4 max-md:px-5 max-md:pt-6 flex justify-between items-center text-xs text-[var(--ink-4)]">

View File

@ -17,7 +17,7 @@ export default function NewsletterSection() {
{/* Outer grid — strip inline padding, use Tailwind */} {/* Outer grid — strip inline padding, use Tailwind */}
<div <div
style={{ display: 'grid', gridTemplateColumns: '1fr 1px 1fr', minHeight: 180 }} style={{ display: 'grid', gridTemplateColumns: '1fr 1px 1fr', minHeight: 180 }}
className="max-w-7xl mx-auto w-full px-12 max-md:px-5 max-md:grid-cols-1" className="mq-stack max-w-7xl mx-auto w-full px-12 max-md:px-5 max-md:grid-cols-1"
> >
{/* Left: description */} {/* Left: description */}
<div <div

View File

@ -0,0 +1,250 @@
import { Marquee } from '@/components/ui/marquee'
interface Partner {
short: string
name: string
sector: string
}
const partners: Partner[] = [
{ short: 'MSC', name: 'فولاد مبارکه اصفهان', sector: 'تولید · اصفهان' },
{ short: 'KSC', name: 'فولاد خوزستان', sector: 'تولید · اهواز' },
{ short: 'ESC', name: 'ذوب آهن اصفهان', sector: 'تولید · اصفهان' },
{ short: 'HSC', name: 'فولاد هرمزگان', sector: 'تولید · بندرعباس' },
{ short: 'KJK', name: 'فولاد کاوه جنوب کیش', sector: 'تولید · کیش' },
{ short: 'CMG', name: 'چادرملو', sector: 'معدن · یزد' },
{ short: 'GEG', name: 'گل گهر سیرجان', sector: 'معدن · سیرجان' },
{ short: 'IMD', name: 'ایمیدرو', sector: 'هلدینگ دولتی' },
{ short: 'MIM', name: 'وزارت صمت', sector: 'سیاست‌گذاری' },
{ short: 'ICC', name: 'اتاق بازرگانی ایران', sector: 'بخش خصوصی' },
{ short: 'BIM', name: 'بانک صنعت و معدن', sector: 'تأمین مالی' },
{ short: 'NDF', name: 'صندوق توسعه ملی', sector: 'سرمایه‌گذاری' },
]
const half = Math.ceil(partners.length / 2)
const rowA = partners.slice(0, half)
const rowB = partners.slice(half)
function PartnerCard({ p }: { p: Partner }) {
return (
<div
style={{
width: 260,
flexShrink: 0,
background: 'var(--paper)',
border: '1px solid var(--rule-thin)',
padding: '18px 20px',
display: 'flex',
alignItems: 'center',
gap: 14,
direction: 'rtl',
fontFamily: 'Vazir, sans-serif',
transition: 'border-color 160ms, background 160ms',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'var(--ink)'
e.currentTarget.style.background = 'var(--paper-2)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--rule-thin)'
e.currentTarget.style.background = 'var(--paper)'
}}
>
<div
style={{
width: 44,
height: 44,
background: 'var(--ink)',
color: 'var(--paper)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
fontWeight: 900,
letterSpacing: '0.5px',
direction: 'ltr',
fontFamily: 'ui-monospace, monospace',
flexShrink: 0,
borderTop: '2px solid var(--red)',
}}
>
{p.short}
</div>
<div style={{ minWidth: 0, flex: 1 }}>
<div
style={{
fontSize: 13,
fontWeight: 800,
color: 'var(--ink)',
letterSpacing: '-0.2px',
lineHeight: 1.3,
marginBottom: 3,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{p.name}
</div>
<div
style={{
fontSize: 10,
color: 'var(--ink-5)',
fontWeight: 500,
letterSpacing: '0.3px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{p.sector}
</div>
</div>
</div>
)
}
export default function PartnersSection() {
return (
<section
style={{
background: 'var(--paper-2)',
borderBottom: '2px solid var(--ink)',
position: 'relative',
overflow: 'hidden',
}}
>
<div className="max-w-7xl mx-auto px-12 pt-24 pb-20 max-md:px-5 max-md:pt-16 max-md:pb-14">
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr auto',
alignItems: 'end',
gap: 32,
paddingBottom: 28,
borderBottom: '1px solid var(--rule-thin)',
marginBottom: 48,
}}
className="mq-stack max-md:grid-cols-1 max-md:gap-4"
>
<div>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
marginBottom: 18,
}}
>
<div style={{ width: 32, height: 2, background: 'var(--red)' }} />
<span
style={{
fontSize: 11,
fontWeight: 700,
letterSpacing: '3px',
textTransform: 'uppercase',
color: 'var(--ink-4)',
}}
>
همراهان و مشتریان
</span>
</div>
<h2
style={{
fontSize: 'clamp(28px, 3.6vw, 48px)',
fontWeight: 900,
letterSpacing: '-1.2px',
color: 'var(--ink)',
lineHeight: 1.1,
marginBottom: 12,
}}
>
کسانی که با ما کار میکنند
</h2>
<p
style={{
fontSize: 14,
lineHeight: 1.7,
color: 'var(--ink-4)',
maxWidth: 560,
}}
>
تولیدکنندگان فولاد، شرکتهای معدنی، نهادهای سیاستگذار و
مؤسسات مالی که از گزارشهای راهبردی ما استفاده میکنند.
</p>
</div>
<span
style={{
fontSize: 11,
fontWeight: 700,
letterSpacing: '2px',
color: 'var(--ink-5)',
fontFamily: 'ui-monospace, monospace',
direction: 'ltr',
whiteSpace: 'nowrap',
paddingBottom: 4,
}}
className="max-md:hidden"
>
{partners.length.toLocaleString('fa-IR')} · NETWORK
</span>
</div>
<div
style={{
position: 'relative',
padding: '8px 0',
}}
>
<Marquee
pauseOnHover
className="py-2"
style={{ ['--duration' as never]: '38s' }}
>
{rowA.map((p) => (
<PartnerCard key={p.short} p={p} />
))}
</Marquee>
<Marquee
reverse
pauseOnHover
className="py-2 mt-3"
style={{ ['--duration' as never]: '46s' }}
>
{rowB.map((p) => (
<PartnerCard key={p.short} p={p} />
))}
</Marquee>
<div
aria-hidden="true"
style={{
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
width: '18%',
background: 'linear-gradient(to left, var(--paper-2), transparent)',
pointerEvents: 'none',
}}
/>
<div
aria-hidden="true"
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
width: '18%',
background: 'linear-gradient(to right, var(--paper-2), transparent)',
pointerEvents: 'none',
}}
/>
</div>
</div>
</section>
)
}

View File

@ -215,7 +215,7 @@ function EnterpriseCard() {
gap: '32px', gap: '32px',
alignItems: 'start', alignItems: 'start',
}} }}
className="max-md:grid-cols-1 max-md:gap-6 max-md:p-6" className="mq-stack max-md:grid-cols-1 max-md:gap-6 max-md:p-6"
> >
{/* Left: description */} {/* Left: description */}
<div> <div>
@ -259,11 +259,14 @@ function EnterpriseCard() {
</div> </div>
{/* Right: feature grid */} {/* Right: feature grid */}
<div style={{ <div
display: 'grid', className="mq-stack"
gridTemplateColumns: '1fr 1fr', style={{
gap: '8px 18px', display: 'grid',
}}> gridTemplateColumns: '1fr 1fr',
gap: '8px 18px',
}}
>
{enterpriseFeatures.map(f => ( {enterpriseFeatures.map(f => (
<div key={f} style={{ display: 'flex', alignItems: 'center', gap: 7 }}> <div key={f} style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
<Check featured={false} /> <Check featured={false} />
@ -281,13 +284,12 @@ function EnterpriseCard() {
export default function PlansSection() { export default function PlansSection() {
return ( return (
<section style={{ <section style={{
background: 'var(--paper)', background: 'var(--paper)',
borderBottom: '2px solid var(--ink)',
}}> }}>
<div className="max-w-6xl mx-auto px-12 py-16 max-md:px-5 max-md:py-12"> <div className="max-w-6xl mx-auto px-12 py-16 max-md:px-5 max-md:py-12">
{/* Header */} {/* Header */}
<div style={{ textAlign: 'center', marginBottom: 40 }}> <div style={{ textAlign: 'center', marginBottom: 64 }}>
<div style={{ <div style={{
fontSize: 10, fontWeight: 700, letterSpacing: '2.5px', fontSize: 10, fontWeight: 700, letterSpacing: '2.5px',
textTransform: 'uppercase', color: 'var(--red)', textTransform: 'uppercase', color: 'var(--red)',
@ -323,7 +325,7 @@ export default function PlansSection() {
marginBottom: 20, marginBottom: 20,
alignItems: 'stretch', alignItems: 'stretch',
}} }}
className="max-md:grid-cols-1 max-md:gap-4" className="mq-stack max-md:grid-cols-1 max-md:gap-4"
> >
{plans.map((plan, idx) => ( {plans.map((plan, idx) => (
<PlanCard <PlanCard

View File

@ -155,7 +155,7 @@ function ReportCardContent({ report }: { report: Report }) {
)} )}
{/* Meta grid */} {/* Meta grid */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10, marginBottom: 20 }}> <div className="mq-stack" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10, marginBottom: 20 }}>
{[ {[
{ label: 'نویسنده', value: report.author }, { label: 'نویسنده', value: report.author },
{ label: 'صفحات', value: `${report.pages} صفحه` }, { label: 'صفحات', value: `${report.pages} صفحه` },
@ -222,12 +222,16 @@ function ReportCardContent({ report }: { report: Report }) {
/* ─── Section ────────────────────────────────────────── */ /* ─── Section ────────────────────────────────────────── */
export default function ReportsCarouselSection() { export default function ReportsCarouselSection() {
const carouselReports = reports.filter(r => !r.featured).slice(0, 10) const carouselReports = reports.filter(r => !r.featured)
const cards: CardData[] = carouselReports.map(report => ({ const cards: CardData[] = carouselReports.map(report => ({
src: reportImage[report.id] ?? fallbackImage, src: reportImage[report.id] ?? fallbackImage,
title: report.title, title: report.title,
category: typeLabel[report.type] ?? report.type, category: typeLabel[report.type] ?? report.type,
summary: report.summary,
author: report.author,
date: report.publishDate,
pages: report.pages,
content: <ReportCardContent report={report} />, content: <ReportCardContent report={report} />,
})) }))
@ -236,27 +240,89 @@ export default function ReportsCarouselSection() {
)) ))
return ( return (
<section style={{ borderBottom: '2px solid var(--ink)', background: 'var(--paper-2)', paddingBottom: 40 }}> <section style={{ background: 'var(--paper-2)', paddingBottom: 96 }}>
{/* Legend & Link Row */} {/* ── Editorial header ── */}
<div className="max-w-7xl mx-auto px-12 pt-10 max-md:px-5 flex items-center justify-between flex-wrap gap-4"> <div className="max-w-7xl mx-auto px-12 pt-24 max-md:px-5 max-md:pt-16">
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}> <div
style={{
display: 'grid',
gridTemplateColumns: '1fr auto',
alignItems: 'end',
gap: 32,
paddingBottom: 28,
borderBottom: '1px solid var(--rule-thin)',
marginBottom: 32,
}}
className="mq-stack max-md:grid-cols-1 max-md:gap-4"
>
<div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 12,
marginBottom: 18,
}}>
<div style={{ width: 32, height: 2, background: 'var(--red)' }} />
<span style={{
fontSize: 11,
fontWeight: 700,
letterSpacing: '3px',
textTransform: 'uppercase',
color: 'var(--ink-4)',
}}>
آرشیو پژوهش · ۱۴۰۳
</span>
</div>
<h2 style={{
fontSize: 'clamp(28px, 3.6vw, 48px)',
fontWeight: 900,
letterSpacing: '-1.2px',
color: 'var(--ink)',
lineHeight: 1.1,
marginBottom: 12,
}}>
تحلیلهای اخیر
</h2>
<p style={{
fontSize: 14,
lineHeight: 1.7,
color: 'var(--ink-4)',
maxWidth: 560,
}}>
مجموعهای از گزارشهای راهبردی، تحلیلهای فصلی و توصیههای سیاستی
منتشرشده در ماههای اخیر.
</p>
</div>
<Link
to="/reports"
style={{
fontSize: 11,
fontWeight: 700,
letterSpacing: '1.5px',
textTransform: 'uppercase',
color: 'var(--red)',
textDecoration: 'none',
whiteSpace: 'nowrap',
paddingBottom: 4,
}}
>
همه گزارشها
</Link>
</div>
{/* Legend row */}
<div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }}>
{Object.entries(typeLabel).map(([key, label]) => ( {Object.entries(typeLabel).map(([key, label]) => (
<div key={key} style={{ display: 'flex', alignItems: 'center', gap: 5 }}> <div key={key} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: typeColor[key] }} /> <div style={{ width: 7, height: 7, borderRadius: '50%', background: typeColor[key] }} />
<span style={{ fontSize: 10, color: 'var(--ink-4)', fontWeight: 600 }}>{label}</span> <span style={{ fontSize: 11, color: 'var(--ink-4)', fontWeight: 600 }}>{label}</span>
</div> </div>
))} ))}
</div> </div>
<Link to="/reports" style={{
fontSize: 10, fontWeight: 700, letterSpacing: '1.5px',
textTransform: 'uppercase', color: 'var(--ink-4)',
textDecoration: 'none',
}}>
همه گزارشها
</Link>
</div> </div>
<div className="max-w-7xl mx-auto w-full px-12 max-md:px-5 mt-6"> <div className="max-w-7xl mx-auto w-full px-12 max-md:px-5 mt-10">
<Carousel items={cardElements} /> <Carousel items={cardElements} />
</div> </div>
</section> </section>

View File

@ -90,10 +90,43 @@ export default function RiskSection() {
}, []) }, [])
return ( return (
<section style={{ borderBottom: '2px solid var(--ink)', background: 'var(--paper)' }}> <section style={{ background: 'var(--paper)' }}>
{/* ── Legend, active count, and link row ── */} {/* ── Editorial header ── */}
<div className="max-w-7xl mx-auto px-12 pt-8 pb-2 max-md:px-5 max-md:pt-6 flex justify-between items-center flex-wrap gap-4 text-xs"> <div className="max-w-7xl mx-auto px-12 pt-24 max-md:px-5 max-md:pt-16">
<div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }}> <div
style={{
display: 'grid',
gridTemplateColumns: '1fr auto',
alignItems: 'end',
gap: 32,
paddingBottom: 28,
borderBottom: '1px solid var(--rule-thin)',
marginBottom: 28,
}}
className="mq-stack max-md:grid-cols-1 max-md:gap-4"
>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 18 }}>
<div style={{ width: 32, height: 2, background: 'var(--red)' }} />
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>
پایش لحظهای · {riskItems.length.toLocaleString('fa-IR')} سیگنال
</span>
</div>
<h2 style={{ fontSize: 'clamp(28px, 3.6vw, 48px)', fontWeight: 900, letterSpacing: '-1.2px', color: 'var(--ink)', lineHeight: 1.1, marginBottom: 12 }}>
نقشه ریسک صنعت
</h2>
<p style={{ fontSize: 14, lineHeight: 1.7, color: 'var(--ink-4)', maxWidth: 580 }}>
ریسکهای ژئوپلیتیک، مقرراتی و بازار که در ماههای اخیر پایش شدهاند
با سطحبندی از بحرانی تا فرصت.
</p>
</div>
<Link to="/risks" style={{ fontSize: 11, fontWeight: 700, letterSpacing: '1.5px', textTransform: 'uppercase', color: 'var(--red)', textDecoration: 'none', whiteSpace: 'nowrap', paddingBottom: 4 }}>
همه ریسکها
</Link>
</div>
{/* Legend row */}
<div style={{ display: 'flex', gap: 22, flexWrap: 'wrap', marginBottom: 8 }}>
{[ {[
{ color: '#7f1d1d', label: 'بحرانی' }, { color: '#7f1d1d', label: 'بحرانی' },
{ color: '#9b1c1c', label: 'بالا' }, { color: '#9b1c1c', label: 'بالا' },
@ -101,25 +134,12 @@ export default function RiskSection() {
{ color: '#166534', label: 'پایین' }, { color: '#166534', label: 'پایین' },
{ color: '#1e40af', label: 'فرصت' }, { color: '#1e40af', label: 'فرصت' },
].map(l => ( ].map(l => (
<div key={l.label} style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div key={l.label} style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: l.color }} /> <div style={{ width: 8, height: 8, borderRadius: '50%', background: l.color }} />
<span style={{ fontSize: 10, color: 'var(--ink-4)', fontWeight: 600 }}>{l.label}</span> <span style={{ fontSize: 12, color: 'var(--ink-4)', fontWeight: 600 }}>{l.label}</span>
</div> </div>
))} ))}
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span style={{ color: 'var(--ink-5)', fontWeight: 600 }}>{riskItems.length} سیگنال فعال</span>
<Link to="/risks" style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: '1.5px',
textTransform: 'uppercase',
color: 'var(--ink-4)',
textDecoration: 'none',
}}>
همه ریسکها
</Link>
</div>
</div> </div>
{/* ── Auto-scroll ticker ── */} {/* ── Auto-scroll ticker ── */}

View File

@ -16,7 +16,7 @@ export default function SnapshotBar() {
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)', gridTemplateColumns: 'repeat(4, 1fr)',
}} }}
className="max-w-7xl mx-auto w-full px-12 max-md:px-6 max-md:grid-cols-2" className="mq-stack-2 max-w-7xl mx-auto w-full px-12 max-md:px-6 max-md:grid-cols-2"
> >
{prices.map((price, idx) => { {prices.map((price, idx) => {
const isLast = idx === prices.length - 1 const isLast = idx === prices.length - 1
@ -49,12 +49,12 @@ export default function SnapshotBar() {
{/* Label */} {/* Label */}
<div <div
style={{ style={{
fontSize: '9px', fontSize: 11,
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: '2px', letterSpacing: '2px',
color: 'var(--ink-5)', color: 'var(--ink-5)',
marginBottom: '6px', marginBottom: 8,
fontWeight: 600, fontWeight: 700,
}} }}
> >
{price.name} {price.name}

View File

@ -2,7 +2,7 @@ import { useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { team } from '@/data/team' import { team } from '@/data/team'
function MemberCard({ member }: { member: (typeof team)[0] }) { function ProfileCard({ member }: { member: (typeof team)[0] }) {
const [hovered, setHovered] = useState(false) const [hovered, setHovered] = useState(false)
return ( return (
@ -10,188 +10,164 @@ function MemberCard({ member }: { member: (typeof team)[0] }) {
onMouseEnter={() => setHovered(true)} onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)} onMouseLeave={() => setHovered(false)}
style={{ style={{
background: hovered ? 'var(--paper-2)' : 'var(--paper)', background: hovered ? 'var(--paper)' : 'transparent',
display: 'flex', border: '1px solid var(--rule-thin)',
padding: '24px 18px 18px',
display: 'flex',
flexDirection: 'column', flexDirection: 'column',
transition: 'background 150ms ease', alignItems: 'center',
position: 'relative', textAlign: 'center',
transition: 'background 160ms, transform 160ms',
transform: hovered ? 'translateY(-2px)' : 'none',
cursor: 'pointer',
position: 'relative',
}} }}
className="p-6 max-md:p-5 cursor-pointer border border-[var(--rule-thin)]"
> >
{/* Top row: avatar + name/role + reports stat */} {/* Reports count badge — top-end */}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}> <div
{/* Avatar */}
<div
style={{
width: 44,
height: 44,
background: 'var(--ink)',
color: 'var(--paper)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 16,
fontWeight: 900,
borderRadius: '50%',
flexShrink: 0,
}}
>
{member.initial}
</div>
{/* Name + role */}
<div style={{ flex: 1, minWidth: 0 }}>
<h3
style={{
fontSize: 14,
fontWeight: 800,
color: 'var(--ink)',
lineHeight: 1.3,
marginBottom: 2,
letterSpacing: '-0.2px',
}}
>
{member.name}
</h3>
<span
style={{
fontSize: 10,
fontWeight: 700,
color: 'var(--red)',
letterSpacing: '1px',
textTransform: 'uppercase',
}}
>
{member.role}
</span>
</div>
{/* Report count — top left (RTL → visual end) */}
<div style={{ textAlign: 'left', flexShrink: 0 }}>
<div
style={{
fontSize: 18,
fontWeight: 900,
color: 'var(--ink)',
lineHeight: 1,
fontVariantNumeric: 'tabular-nums',
}}
>
{member.reportCount.toLocaleString('fa-IR')}
</div>
<div
style={{
fontSize: 9,
color: 'var(--ink-5)',
marginTop: 3,
fontWeight: 500,
}}
>
گزارش
</div>
</div>
</div>
{/* Bio */}
<p
style={{ style={{
fontSize: 12, position: 'absolute',
color: 'var(--ink-3)', top: 10,
lineHeight: 1.7, left: 10,
fontWeight: 400, background: 'var(--ink)',
marginTop: 14, color: 'var(--paper)',
marginBottom: 14, fontSize: 9,
display: '-webkit-box', fontWeight: 700,
WebkitLineClamp: 3, letterSpacing: '0.5px',
WebkitBoxOrient: 'vertical', padding: '3px 7px',
overflow: 'hidden', fontVariantNumeric: 'tabular-nums',
display: 'flex',
alignItems: 'center',
gap: 3,
}} }}
> >
{member.bio} <span>{member.reportCount.toLocaleString('fa-IR')}</span>
</p> <span style={{ opacity: 0.55 }}>گزارش</span>
{/* Tags — max 3 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: 12 }}>
{member.expertise.slice(0, 3).map(tag => (
<span
key={tag}
style={{
fontSize: 10,
padding: '2px 8px',
color: 'var(--ink-4)',
border: '1px solid var(--rule-thin)',
fontWeight: 500,
whiteSpace: 'nowrap',
}}
>
{tag}
</span>
))}
</div> </div>
{/* Footer: email */} {/* Avatar */}
{member.email && ( <div
style={{
width: 72,
height: 72,
background: 'var(--ink)',
color: 'var(--paper)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 26,
fontWeight: 900,
borderRadius: '50%',
marginBottom: 12,
border: '3px solid var(--paper-2)',
boxShadow: '0 4px 12px rgba(26,23,18,0.08)',
}}
>
{member.initial}
</div>
{/* Name */}
<h3
style={{
fontSize: 14,
fontWeight: 800,
color: 'var(--ink)',
letterSpacing: '-0.2px',
lineHeight: 1.3,
marginBottom: 4,
}}
>
{member.name}
</h3>
{/* Role */}
<span
style={{
fontSize: 10,
fontWeight: 700,
color: 'var(--red)',
letterSpacing: '1.2px',
textTransform: 'uppercase',
marginBottom: 12,
}}
>
{member.role}
</span>
{/* Top expertise chip */}
{member.expertise[0] && (
<div <div
style={{ style={{
paddingTop: 10, fontSize: 10,
borderTop: '1px solid var(--rule-thin)', padding: '3px 9px',
marginTop: 'auto', background: 'var(--paper-2)',
color: 'var(--ink-3)',
border: '1px solid var(--rule-thin)',
fontWeight: 500,
marginBottom: 14,
}} }}
> >
<a {member.expertise[0]}
href={`mailto:${member.email}`}
style={{
fontSize: 10,
color: 'var(--ink-5)',
textDecoration: 'none',
fontWeight: 500,
direction: 'ltr',
fontFamily: 'ui-monospace, monospace',
}}
>
{member.email}
</a>
</div> </div>
)} )}
{/* Email footer */}
{member.email && (
<a
href={`mailto:${member.email}`}
onClick={e => e.stopPropagation()}
style={{
fontSize: 10,
color: 'var(--ink-5)',
textDecoration: 'none',
fontWeight: 500,
direction: 'ltr',
fontFamily: 'ui-monospace, monospace',
paddingTop: 10,
borderTop: '1px solid var(--rule-thin)',
width: '100%',
textAlign: 'center',
}}
>
{member.email}
</a>
)}
</div> </div>
) )
} }
export default function TeamSection() { export default function TeamSection() {
const displayedMembers = team.slice(0, 4)
return ( return (
<section style={{ borderBottom: '2px solid var(--ink)', background: 'var(--paper-2)' }}> <section style={{ background: 'var(--paper-2)' }}>
<div className="max-w-6xl mx-auto px-12 py-16 max-md:px-5 max-md:py-12"> <div className="max-w-6xl mx-auto px-12 py-24 max-md:px-5 max-md:py-16">
{/* Editorial sub-bar */} {/* Editorial sub-bar */}
<div <div
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
borderBottom: '1px solid var(--rule-thin)', borderBottom: '1px solid var(--rule-thin)',
paddingBottom: 14, paddingBottom: 16,
marginBottom: 24, marginBottom: 40,
}} }}
className="flex-wrap gap-3" className="flex-wrap gap-3"
> >
<span <span
style={{ style={{
fontSize: 12, fontSize: 12,
fontWeight: 400, color: 'var(--ink-4)',
color: 'var(--ink-4)',
fontStyle: 'italic', fontStyle: 'italic',
}} }}
> >
اعضای هیئت تحریریه و متخصصان راهبردی هیئت تحریریه و متخصصان راهبردی
</span> </span>
<Link <Link
to="/team" to="/team"
style={{ style={{
fontSize: 11, fontSize: 11,
fontWeight: 700, fontWeight: 700,
letterSpacing: '1px', letterSpacing: '1px',
color: 'var(--red)', color: 'var(--red)',
textDecoration: 'none', textDecoration: 'none',
}} }}
> >
@ -199,17 +175,17 @@ export default function TeamSection() {
</Link> </Link>
</div> </div>
{/* 2x2 grid on desktop, single column on mobile */} {/* Profile grid — 4 col desktop, 2 col tablet, 1 col mobile */}
<div <div
style={{ style={{
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateColumns: 'repeat(4, 1fr)',
gap: 12, gap: 20,
}} }}
className="max-md:grid-cols-1" className="mq-stack-2 max-lg:grid-cols-2 max-sm:grid-cols-1"
> >
{displayedMembers.map(member => ( {team.slice(0, 4).map(member => (
<MemberCard key={member.id} member={member} /> <ProfileCard key={member.id} member={member} />
))} ))}
</div> </div>
</div> </div>