diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6943949 --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..471360c --- /dev/null +++ b/Dockerfile @@ -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;"] diff --git a/README.md b/README.md index e2bd34f..48ccd33 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,38 @@ +--- +title: Andishkade Foolad +emoji: 🏭 +colorFrom: indigo +colorTo: gray +sdk: docker +app_port: 7860 +pinned: false +--- + # React + TypeScript + Vite 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 . + +## 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: - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c4ad717 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..3fe63f8 --- /dev/null +++ b/nginx.conf @@ -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; +} diff --git a/src/app/layout/Footer.tsx b/src/app/layout/Footer.tsx index bc5072e..1f2a2d7 100644 --- a/src/app/layout/Footer.tsx +++ b/src/app/layout/Footer.tsx @@ -96,7 +96,7 @@ export function Footer() { gridTemplateColumns: '2fr 1px 1fr 1px 1fr 1px 1fr', 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 */}
diff --git a/src/app/layout/Header.tsx b/src/app/layout/Header.tsx index e1f5be4..1891399 100644 --- a/src/app/layout/Header.tsx +++ b/src/app/layout/Header.tsx @@ -101,8 +101,17 @@ export default function Header() { const [mobileOpen, setMobileOpen] = useState(false) const [isMobile, setIsMobile] = useState(false) const [searchOpen, setSearchOpen] = useState(false) + const [scrolled, setScrolled] = useState(false) const searchInputRef = useRef(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 */ useEffect(() => { const mq = window.matchMedia('(max-width: 1023px)') @@ -129,17 +138,25 @@ export default function Header() { position: 'sticky', top: 0, 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', + 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 ═══════════════════════════════════════════ */}
@@ -207,7 +224,8 @@ export default function Header() { ═══════════════════════════════════════════ */}
(null) const [currentIndex, setCurrentIndex] = useState(0) + const [paused, setPaused] = useState(false) useEffect(() => { if (carouselRef.current) { @@ -51,6 +57,23 @@ export function Carousel({ } }, [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) { if (!carouselRef.current) return const cardWidth = window.innerWidth < 768 ? 230 : 350 @@ -58,23 +81,34 @@ export function Carousel({ 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 ( -
+
setPaused(true)} + onMouseLeave={() => setPaused(false)} + > {/* Scrollable track */}
setPaused(true)} + onTouchEnd={() => setPaused(false)} > -
+
{items.map((item, index) => ( {item} @@ -82,6 +116,103 @@ export function Carousel({
+ {/* ── Glass pill controls — prev/next + auto state ── */} +
+ {/* Prev */} + + + {/* Auto state pill */} +
+ + {paused ? 'PAUSED' : 'AUTO'} +
+ + {/* Next */} + +
+
) @@ -188,47 +319,144 @@ export function Card({ )} - {/* ── Card thumbnail ── */} + {/* ── Card thumbnail — MUI ActionAreaCard pattern, editorial palette ── */} { + 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 */} -
- - {/* Top centered category badge — prominent, eye-catching */} -
- - {card.category} - + {/* Media — image strip */} +
+ + {/* Category badge — overlaid bottom-right of image (RTL: visual start) */} +
+ + {card.category} + +
- {/* Bottom: title — bright white, readable */} -
-

- {card.title} -

-
+ {/* Content — title + summary + meta */} +
+ {/* Title + summary group — vertically centered in remaining space */} +
+ {/* Title */} +

+ {card.title} +

- {/* Background image */} - + {/* Summary */} + {card.summary && ( +

+ {card.summary} +

+ )} +
+ + {/* Meta footer — tight below summary */} + {(card.author || card.date || card.pages) && ( +
+
+ {card.author && ( + + {card.author} + + )} + {card.date && ( + + {card.date} + + )} +
+ {card.pages && ( + + {card.pages.toLocaleString('fa-IR')} ص ← + + )} +
+ )} +
) diff --git a/src/components/ui/marquee.tsx b/src/components/ui/marquee.tsx new file mode 100644 index 0000000..ba9408c --- /dev/null +++ b/src/components/ui/marquee.tsx @@ -0,0 +1,52 @@ +import type { CSSProperties, ReactNode } from 'react' +import { clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +function cn(...inputs: Parameters) { + 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 ( +
+ {[0, 1].map((i) => ( +
+ {children} +
+ ))} +
+ ) +} diff --git a/src/index.css b/src/index.css index 4cb6dd4..9ba7551 100644 --- a/src/index.css +++ b/src/index.css @@ -246,6 +246,44 @@ body { .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 ═══════════════════════════════════════════════ */ diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 10e5354..a4e6034 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -7,73 +7,100 @@ import EventsSection from './sections/EventsSection' import RiskSection from './sections/RiskSection' import TeamSection from './sections/TeamSection' import PlansSection from './sections/PlansSection' +import PartnersSection from './sections/PartnersSection' import NewsletterSection from './sections/NewsletterSection' /* ─── Section chapter marker ───────────────────────────── A thin full-bleed dark bar that stamps the start of every 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 (
- {/* Red accent bar on the right edge (RTL → visual start) */} -
-
- - {n} - + {/* Left (visual end in RTL): chapter index */} +
+ + {n} + +
+ + CHAPTER + + + بخش {n} + +
+
-
- - - {title} -
) @@ -90,33 +117,37 @@ export default function Home() { {/* ── 01 · اسکنر جهانی · bg: paper ─────────────── */} - + {/* ── 02 · تحلیل‌ها · bg: paper-2 ──────────────── */} - + {/* ── 03 · اخبار · bg: paper ────────────────────── */} - + {/* ── 04 · رویدادها · bg: paper-2 ──────────────── */} - + {/* ── 05 · ریسک · bg: paper ─────────────────────── */} - + {/* ── 06 · تیم · bg: paper-2 ────────────────────── */} - + {/* ── 07 · اشتراک · bg: paper ───────────────────── */} - + + {/* ── 08 · همراهان · bg: paper-2 ────────────────── */} + + + {/* ── خبرنامه (last, no label) ───────────────────── */}
diff --git a/src/pages/Home/sections/EventsSection.tsx b/src/pages/Home/sections/EventsSection.tsx index 9c4709e..7203e50 100644 --- a/src/pages/Home/sections/EventsSection.tsx +++ b/src/pages/Home/sections/EventsSection.tsx @@ -27,11 +27,38 @@ export default function EventsSection() { const displayed = events.slice(0, 4) return ( -
-
- {/* Sleek top row */} -
- تقویم کامل ← +
+
+ {/* ── Editorial header ── */} +
+
+
+
+ + تقویم رویداد · ۱۴۰۳–۱۴۰۴ + +
+

+ رویدادهای پیش‌رو +

+

+ همایش‌ها، نمایشگاه‌ها و نشست‌های تخصصی صنعت فولاد در ماه‌های آینده — برای حضور یا حمایت رسانه‌ای. +

+
+ + تقویم کامل ← +
{/* 4-col grid */} diff --git a/src/pages/Home/sections/GlobalScannerSection.tsx b/src/pages/Home/sections/GlobalScannerSection.tsx index 7a50192..2271bac 100644 --- a/src/pages/Home/sections/GlobalScannerSection.tsx +++ b/src/pages/Home/sections/GlobalScannerSection.tsx @@ -51,9 +51,8 @@ export default function GlobalScannerSection() { return (
diff --git a/src/pages/Home/sections/HeroSection.tsx b/src/pages/Home/sections/HeroSection.tsx index 658717b..2cf717e 100644 --- a/src/pages/Home/sections/HeroSection.tsx +++ b/src/pages/Home/sections/HeroSection.tsx @@ -4,15 +4,11 @@ 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: 'توصیه سیاستی' }, + { n: '۴', label: 'سناریو محتمل' }, + { n: '۱۵', label: 'کشور رقیب' }, + { n: '۸۴', label: 'صفحه تحلیلی' }, + { n: '۱۲', label: 'توصیه سیاستی' }, ] export default function HeroSection() { @@ -23,226 +19,283 @@ export default function HeroSection() {
- {/* ── Background image ── */} - - {/* ── Gradient overlay — dark vignette from bottom-right ── */} -
- - {/* ── Top-right badge ── */} -
-
- گزارش ویژه -
- - بهمن ۱۴۰۳ - -
- - {/* ── Main content — anchored bottom ── */}
+ {/* ─── Top meta row ─────────────────────────── */}
+
+
+ گزارش ویژه · شماره ۰۱ +
+ + {featured.publishDate} · {featured.category} + +
- {/* ── Left: headline + meta ── */} + + ISSN 2783-XXXX · VOL. XII + +
+ + {/* ─── Main grid — 8/12 + 4/12 ─────────────── */} +
+ {/* ═══ Left col ═══ */}
{/* Overline */} -
+
- گزارش راهبردی · تحلیل سناریو + تحلیل سناریو · راهبردی - {/* Headline */} + {/* Massive editorial headline */} - {featured.title.split(':').map((part, i) => ( - - {i === 0 ? part + ':' : ( - {part} - )} - {i === 0 &&
} -
- ))} + {(() => { + const [main, sub] = featured.title.split(':') + return ( + <> + {main} + {sub && ( + + {sub.trim()} + + )} + + ) + })()}
- {/* Summary — one line */} + {/* Lede */} مدل‌سازی کمی بر اساس داده‌های تجارت ۱۵ کشور رقیب — از بدبینانه تا خوش‌بینانه، با توصیه‌های سیاستی مستقیم برای وزارت صمت و سازمان بنادر. - {/* Divider */} -
+ {/* Stats inline row */} + + {STATS.map((s, i) => ( +
+
+ {s.n} +
+
+ {s.label} +
+
+ ))} +
- {/* Author + Price + CTA in one bar */} + {/* Author + CTAs */} - {/* Author */}
{featured.authorInitial}
-
+
{featured.author}
-
+
{featured.authorRole}
- {/* Separator */} -
+
- {/* Price */} -
- - {featured.price.toLocaleString('fa-IR')} - - تومان -
- - {/* Buttons */}
- {/* ── Right: floating stats panel ── */} + {/* ═══ Right col: dossier mini-cover — frosted glass over ink ═══ */}
- در این گزارش -
- {STATS.map((s, i) => ( -
- - {s.label} - - - {s.n} - -
- ))} -
+ position: 'absolute', + top: 0, right: 0, left: 0, + height: 4, + background: 'var(--red)', + }} /> +
+ Nº 01 / 2025 +
+ +
+ DOSSIER +
+ +
+ ۰۴ +
+ +
+ سناریو راهبردی +
+ +
+ برای آینده فولاد ایران در بازار جهانی ۱۴۰۴ +
+ +
    + {[ + { k: 'صفحات', v: `${featured.pages.toLocaleString('fa-IR')} صفحه` }, + { k: 'نوع', v: 'گزارش راهبردی' }, + { k: 'انتشار', v: featured.publishDate }, + { k: 'مخاطب', v: 'سیاست‌گذار · مدیر ارشد' }, + ].map((row) => ( +
  • + {row.k} + {row.v} +
  • + ))} +
+
diff --git a/src/pages/Home/sections/LatestNewsBentoSection.tsx b/src/pages/Home/sections/LatestNewsBentoSection.tsx index f7f4d52..813e798 100644 --- a/src/pages/Home/sections/LatestNewsBentoSection.tsx +++ b/src/pages/Home/sections/LatestNewsBentoSection.tsx @@ -51,7 +51,7 @@ function Accent({ color = 'var(--red)' }: { color?: string }) { /* ─── Section ────────────────────────────────────────────── */ export default function LatestNewsBentoSection() { return ( -
+
{/* ── Header ── */}
diff --git a/src/pages/Home/sections/NewsletterSection.tsx b/src/pages/Home/sections/NewsletterSection.tsx index a55a730..8dd5ee2 100644 --- a/src/pages/Home/sections/NewsletterSection.tsx +++ b/src/pages/Home/sections/NewsletterSection.tsx @@ -17,7 +17,7 @@ export default function NewsletterSection() { {/* Outer grid — strip inline padding, use Tailwind */}
{/* Left: description */}
{ + 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)' + }} + > +
+ {p.short} +
+ +
+
+ {p.name} +
+
+ {p.sector} +
+
+
+ ) +} + +export default function PartnersSection() { + return ( +
+
+
+
+
+
+ + همراهان و مشتریان + +
+

+ کسانی که با ما کار می‌کنند +

+

+ تولیدکنندگان فولاد، شرکت‌های معدنی، نهادهای سیاست‌گذار و + مؤسسات مالی که از گزارش‌های راهبردی ما استفاده می‌کنند. +

+
+ + + {partners.length.toLocaleString('fa-IR')} · NETWORK + +
+ +
+ + {rowA.map((p) => ( + + ))} + + + + {rowB.map((p) => ( + + ))} + + + +
+ ) +} diff --git a/src/pages/Home/sections/PlansSection.tsx b/src/pages/Home/sections/PlansSection.tsx index e6e1e0e..584c557 100644 --- a/src/pages/Home/sections/PlansSection.tsx +++ b/src/pages/Home/sections/PlansSection.tsx @@ -215,7 +215,7 @@ function EnterpriseCard() { gap: '32px', 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 */}
@@ -259,11 +259,14 @@ function EnterpriseCard() {
{/* Right: feature grid */} -
+
{enterpriseFeatures.map(f => (
@@ -281,13 +284,12 @@ function EnterpriseCard() { export default function PlansSection() { return (
{/* Header */} -
+
{plans.map((plan, idx) => ( +
{[ { label: 'نویسنده', value: report.author }, { label: 'صفحات', value: `${report.pages} صفحه` }, @@ -222,12 +222,16 @@ function ReportCardContent({ report }: { report: Report }) { /* ─── Section ────────────────────────────────────────── */ 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 => ({ src: reportImage[report.id] ?? fallbackImage, title: report.title, category: typeLabel[report.type] ?? report.type, + summary: report.summary, + author: report.author, + date: report.publishDate, + pages: report.pages, content: , })) @@ -236,27 +240,89 @@ export default function ReportsCarouselSection() { )) return ( -
- {/* Legend & Link Row */} -
-
+
+ {/* ── Editorial header ── */} +
+
+
+
+
+ + آرشیو پژوهش · ۱۴۰۳ + +
+

+ تحلیل‌های اخیر +

+

+ مجموعه‌ای از گزارش‌های راهبردی، تحلیل‌های فصلی و توصیه‌های سیاستی + منتشرشده در ماه‌های اخیر. +

+
+ + + همه گزارش‌ها ← + +
+ + {/* Legend row */} +
{Object.entries(typeLabel).map(([key, label]) => ( -
-
- {label} +
+
+ {label}
))}
- - همه گزارش‌ها ← -
-
+
diff --git a/src/pages/Home/sections/RiskSection.tsx b/src/pages/Home/sections/RiskSection.tsx index 752c37a..9774246 100644 --- a/src/pages/Home/sections/RiskSection.tsx +++ b/src/pages/Home/sections/RiskSection.tsx @@ -90,10 +90,43 @@ export default function RiskSection() { }, []) return ( -
- {/* ── Legend, active count, and link row ── */} -
-
+
+ {/* ── Editorial header ── */} +
+
+
+
+
+ + پایش لحظه‌ای · {riskItems.length.toLocaleString('fa-IR')} سیگنال + +
+

+ نقشه ریسک صنعت +

+

+ ریسک‌های ژئوپلیتیک، مقرراتی و بازار که در ماه‌های اخیر پایش شده‌اند — + با سطح‌بندی از بحرانی تا فرصت. +

+
+ + همه ریسک‌ها ← + +
+ + {/* Legend row */} +
{[ { color: '#7f1d1d', label: 'بحرانی' }, { color: '#9b1c1c', label: 'بالا' }, @@ -101,25 +134,12 @@ export default function RiskSection() { { color: '#166534', label: 'پایین' }, { color: '#1e40af', label: 'فرصت' }, ].map(l => ( -
+
- {l.label} + {l.label}
))}
-
- {riskItems.length} سیگنال فعال - - همه ریسک‌ها ← - -
{/* ── Auto-scroll ticker ── */} diff --git a/src/pages/Home/sections/SnapshotBar.tsx b/src/pages/Home/sections/SnapshotBar.tsx index 41c3196..4478b3b 100644 --- a/src/pages/Home/sections/SnapshotBar.tsx +++ b/src/pages/Home/sections/SnapshotBar.tsx @@ -16,7 +16,7 @@ export default function SnapshotBar() { display: 'grid', 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) => { const isLast = idx === prices.length - 1 @@ -49,12 +49,12 @@ export default function SnapshotBar() { {/* Label */}
{price.name} diff --git a/src/pages/Home/sections/TeamSection.tsx b/src/pages/Home/sections/TeamSection.tsx index 6edeac9..f755bf4 100644 --- a/src/pages/Home/sections/TeamSection.tsx +++ b/src/pages/Home/sections/TeamSection.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { Link } from 'react-router-dom' import { team } from '@/data/team' -function MemberCard({ member }: { member: (typeof team)[0] }) { +function ProfileCard({ member }: { member: (typeof team)[0] }) { const [hovered, setHovered] = useState(false) return ( @@ -10,188 +10,164 @@ function MemberCard({ member }: { member: (typeof team)[0] }) { onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} style={{ - background: hovered ? 'var(--paper-2)' : 'var(--paper)', - display: 'flex', + background: hovered ? 'var(--paper)' : 'transparent', + border: '1px solid var(--rule-thin)', + padding: '24px 18px 18px', + display: 'flex', flexDirection: 'column', - transition: 'background 150ms ease', - position: 'relative', + alignItems: 'center', + 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 */} -
- {/* Avatar */} -
- {member.initial} -
- - {/* Name + role */} -
-

- {member.name} -

- - {member.role} - -
- - {/* Report count — top left (RTL → visual end) */} -
-
- {member.reportCount.toLocaleString('fa-IR')} -
-
- گزارش -
-
-
- - {/* Bio */} -

- {member.bio} -

- - {/* Tags — max 3 */} -
- {member.expertise.slice(0, 3).map(tag => ( - - {tag} - - ))} + {member.reportCount.toLocaleString('fa-IR')} + گزارش
- {/* Footer: email */} - {member.email && ( + {/* Avatar */} +
+ {member.initial} +
+ + {/* Name */} +

+ {member.name} +

+ + {/* Role */} + + {member.role} + + + {/* Top expertise chip */} + {member.expertise[0] && (
- - {member.email} - + {member.expertise[0]}
)} + + {/* Email footer */} + {member.email && ( + 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} + + )}
) } export default function TeamSection() { - const displayedMembers = team.slice(0, 4) - return ( -
-
+
+
{/* Editorial sub-bar */}
- اعضای هیئت تحریریه و متخصصان راهبردی + هیئت تحریریه و متخصصان راهبردی @@ -199,17 +175,17 @@ export default function TeamSection() {
- {/* 2x2 grid on desktop, single column on mobile */} + {/* Profile grid — 4 col desktop, 2 col tablet, 1 col mobile */}
- {displayedMembers.map(member => ( - + {team.slice(0, 4).map(member => ( + ))}