feat: add RadarPost page with membership gate and related sidebar

fix: redirect to archive instead of reports on missing report in ReportDetail

refactor: remove Reports page and its related components

chore: add proxy configuration for API and uploads in Vite
This commit is contained in:
alireza 2026-06-18 08:45:02 +03:30
parent 07239bd811
commit 4246b3a68a
91 changed files with 6491 additions and 818 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 KiB

View File

@ -0,0 +1,390 @@
- generic [ref=e3]:
- banner [ref=e4]:
- generic [ref=e6]:
- link "اندیشکده فولاد آینده The Steel Futures Institute" [ref=e7] [cursor=pointer]:
- /url: /
- img [ref=e8]
- generic [ref=e9]:
- generic [ref=e10]: اندیشکده فولاد آینده
- generic [ref=e11]: The Steel Futures Institute
- navigation "ناوبری اصلی" [ref=e12]:
- button "رادار آینده" [ref=e14] [cursor=pointer]:
- text: رادار آینده
- img [ref=e15]
- link "آمار و داده" [ref=e17] [cursor=pointer]:
- /url: /pulse
- link "در یک نگاه" [ref=e18] [cursor=pointer]:
- /url: /at-a-glance
- button "رسانه و رویداد" [ref=e20] [cursor=pointer]:
- text: رسانه و رویداد
- img [ref=e21]
- link "شبکه خبرگان" [ref=e23] [cursor=pointer]:
- /url: /experts
- button "درباره ما" [ref=e25] [cursor=pointer]:
- text: درباره ما
- img [ref=e26]
- generic [ref=e28]:
- button "جستجو" [ref=e29] [cursor=pointer]:
- img [ref=e30]
- button "ورود / ثبت‌نام" [ref=e33] [cursor=pointer]
- button "EN" [ref=e34] [cursor=pointer]
- main [ref=e35]:
- generic [ref=e36]:
- generic [ref=e44]:
- img [ref=e47]
- heading "بینشی نو برای توسعه‌ی صنعتی" [level=2] [ref=e53]
- generic:
- paragraph
- link "آشنایی با چشم‌انداز ما" [ref=e55] [cursor=pointer]:
- /url: /about
- img [ref=e56]
- generic [ref=e58]: آشنایی با چشم‌انداز ما
- generic [ref=e65]:
- generic [ref=e66]:
- generic [ref=e67]:
- generic [ref=e68]: FUTURE RADAR
- heading "رادار آینده" [level=2] [ref=e69]
- paragraph [ref=e70]: تحلیل چندوجهی آینده صنعت فولاد
- link "همه سیگنال‌ها ←" [ref=e71] [cursor=pointer]:
- /url: /radar
- generic [ref=e72]:
- article [ref=e73] [cursor=pointer]:
- generic [ref=e75]:
- img [ref=e77]
- generic [ref=e80]:
- generic [ref=e81]: پیش‌بینی بازار با هوش مصنوعی
- generic [ref=e82]: ۱۴ اردیبهشت ۱۴۰۵
- article [ref=e83] [cursor=pointer]:
- generic [ref=e85]:
- img [ref=e87]
- generic [ref=e90]:
- generic [ref=e91]: پیش‌بینی بازار با هوش مصنوعی
- generic [ref=e92]: ۱۴ اردیبهشت ۱۴۰۵
- article [ref=e93] [cursor=pointer]:
- generic [ref=e95]:
- img [ref=e97]
- generic [ref=e99]:
- generic [ref=e100]: پیش‌بینی بازار با هوش مصنوعی
- generic [ref=e101]: ۱۴ اردیبهشت ۱۴۰۵
- article [ref=e102] [cursor=pointer]:
- generic [ref=e104]:
- img [ref=e106]
- generic [ref=e109]:
- generic [ref=e110]: پیش‌بینی بازار با هوش مصنوعی
- generic [ref=e111]: ۱۴ اردیبهشت ۱۴۰۵
- generic [ref=e113]:
- generic [ref=e114]:
- generic [ref=e117]:
- generic [ref=e118]: At a Glance
- heading "در یک نگاه" [level=2] [ref=e119]
- link "مشاهده همه" [ref=e120] [cursor=pointer]:
- /url: /at-a-glance
- img [ref=e122]
- generic [ref=e124]: مشاهده همه
- generic [ref=e130]:
- generic [ref=e131]:
- heading "شوک هیدروژنی به بازار جهانی؛ آغاز رسمی عصر فولادسازی بدون کربن" [level=3] [ref=e132]
- generic [ref=e133]:
- generic [ref=e134]: Financial Times
- generic [ref=e135]:
- img [ref=e136]
- text: اردیبهشت ۱۴۰۵
- paragraph [ref=e138]: پروژه عظیم «فولاد سبز H2» در سوئد با جذب بی‌سابقه ۶.۵ میلیارد یورو سرمایه، رسماً وارد فاز اجرایی شد تا نخستین کارخانه فولادسازی در مقیاس تجاری بر پایه هیدروژن سبز (بدون نیاز به زغال‌سنگ) در جهان متولد شود. این تأسیسات با هدف کاهش ۹۵ درصدی انتشار CO₂، قواعد رقابت در زنجیره ارزش اروپا را برای همیشه بازتعریف می‌کند.
- generic [ref=e139]:
- button "prev" [ref=e140] [cursor=pointer]:
- button "next" [ref=e141] [cursor=pointer]:
- generic [ref=e144]:
- generic [ref=e146]:
- generic [ref=e147]: OUR FRAMEWORK
- heading "چارچوب اندیشه ما" [level=2] [ref=e148]
- paragraph [ref=e149]: ترکیب داده، تحلیل، آینده‌نگاری و خرد جمعی برای فهم تحولات صنعت فولاد.
- generic [ref=e150]:
- generic [ref=e151]:
- img [ref=e153]
- generic [ref=e157]:
- heading "تحلیل داده‌محور" [level=3] [ref=e158]
- paragraph [ref=e159]: استفاده از داده‌های بازار، اقتصاد، فناوری و زنجیره ارزش فولاد برای استخراج بینش‌های قابل اتکا.
- generic [ref=e160]:
- img [ref=e162]
- generic [ref=e167]:
- heading "نگاه نظام‌مند" [level=3] [ref=e168]
- paragraph [ref=e169]: تحلیل صنعت فولاد در تعامل با اقتصاد، انرژی، فناوری، محیط زیست و سیاست‌های جهانی.
- generic [ref=e170]:
- img [ref=e172]
- generic [ref=e177]:
- heading "هم‌اندیشی خبرگان" [level=3] [ref=e178]
- paragraph [ref=e179]: بهره‌گیری مستمر از شبکه‌ای از خبرگان صنعت، دانشگاه و فناوری.
- generic [ref=e180]:
- img [ref=e182]
- generic [ref=e190]:
- heading "آینده‌نگاری علمی" [level=3] [ref=e191]
- paragraph [ref=e192]: تحلیل روندها، شناسایی عدم‌قطعیت‌ها و طراحی سناریوها.
- generic [ref=e193]:
- img [ref=e195]
- generic [ref=e197]:
- heading "از تحلیل تا اقدام" [level=3] [ref=e198]
- paragraph [ref=e199]: تبدیل تحلیل‌ها به توصیه‌ها و تجویزهای استراتژیک.
- generic [ref=e200]:
- img [ref=e202]
- generic [ref=e206]:
- heading "بی‌طرفی و استقلال" [level=3] [ref=e207]
- paragraph [ref=e208]: اندیشه‌های مستقل و متکی بر شواهد، داده‌ها و نگاه بلندمدت.
- generic [ref=e214]:
- generic [ref=e215]:
- generic [ref=e216]: ماهنامه شماره ۱۲ منتشر شد…
- heading "دیپلماسی معدنی در عصر انسدادهای ژئوپلیتیک" [level=2] [ref=e217]
- generic [ref=e218]: Mineral Diplomacy in the Era of Geopolitical Blockades
- button "دانلود گزارش" [ref=e220] [cursor=pointer]:
- img [ref=e221]
- text: دانلود گزارش
- img "دیپلماسی معدنی در عصر انسدادهای ژئوپلیتیک" [ref=e225]
- generic [ref=e228]:
- generic [ref=e230]:
- generic [ref=e231]: DATA SOURCES
- heading "نبض صنعت" [level=2] [ref=e232]
- paragraph [ref=e233]: پنجره‌ای واحد به جهان داده‌های فولاد؛ دسترسی به شاخص‌ها، قیمت‌ها، تجارت جهانی و داده‌های کلیدی از معدن تا محصول نهایی.
- link "مشاهده منابع داده" [ref=e234] [cursor=pointer]:
- /url: /pulse
- text: مشاهده منابع داده
- img [ref=e235]
- generic [ref=e238]:
- generic "London Metal Exchange" [ref=e244] [cursor=pointer]:
- img [ref=e246]
- generic "Shanghai Futures Exchange" [ref=e256] [cursor=pointer]:
- img [ref=e258]
- generic "S&P Global Platts" [ref=e262] [cursor=pointer]:
- img [ref=e264]
- generic "Reuters" [ref=e271] [cursor=pointer]:
- img [ref=e273]
- generic "Fastmarkets" [ref=e279] [cursor=pointer]:
- img [ref=e281]
- generic "World Steel Association" [ref=e287] [cursor=pointer]:
- img [ref=e289]
- generic "Mysteel" [ref=e296] [cursor=pointer]:
- img [ref=e298]
- generic "Dalian Commodity Exchange" [ref=e304] [cursor=pointer]:
- img [ref=e306]
- generic "CME Group" [ref=e311] [cursor=pointer]:
- img [ref=e313]
- generic [ref=e323]:
- generic [ref=e324]:
- heading "دیده‌بان رویداد‌ها" [level=2] [ref=e327]
- link "مشاهده همه" [ref=e328] [cursor=pointer]:
- /url: /events
- img [ref=e330]
- generic [ref=e332]: مشاهده همه
- generic [ref=e333]:
- generic [ref=e334]:
- button "اجلاس جهانی فولاد سبز | استکهلم، سوئد" [ref=e335] [cursor=pointer]:
- generic [ref=e336]: اجلاس جهانی فولاد سبز | استکهلم، سوئد
- img [ref=e337]
- paragraph [ref=e340]:
- text: بررسی آخرین تکنولوژی‌های احیای مستقیم بر پایه هیدروژن و جذب سرمایه برای پروژه‌های بدون کربن. حضور در این رویداد برای درک آینده مقررات زیست‌محیطی اروپا حیاتی است.
- generic [ref=e341]: "(زمان: مهر ۱۴۰۵)"
- generic [ref=e342]:
- button "نشست سالانه انجمن جهانی فولاد (worldsteel) | شانگهای، چین" [ref=e343] [cursor=pointer]:
- generic [ref=e344]: نشست سالانه انجمن جهانی فولاد (worldsteel) | شانگهای، چین
- img [ref=e345]
- paragraph [ref=e347]:
- text: "گردهمایی بزرگ‌ترین رهبران صنعت. محور اصلی امسال: پیش‌بینی تقاضای جهانی در سایه رکود بخش مسکن چین و تغییر مسیر زنجیره تامین به سمت هند و خاورمیانه."
- generic [ref=e348]: "(زمان: آبان ۱۴۰۵)"
- generic [ref=e349]:
- button "مجمع بین‌المللی هوش مصنوعی در تولید | برلین، آلمان" [ref=e350] [cursor=pointer]:
- generic [ref=e351]: مجمع بین‌المللی هوش مصنوعی در تولید | برلین، آلمان
- img [ref=e352]
- paragraph [ref=e354]:
- text: رونمایی از جدیدترین نسل دوقلوهای دیجیتال (Digital Twins) و الگوریتم‌های پیش‌بینی تعمیرات. فرصتی برای آشنایی با راهکارهای کاهش هزینه و افزایش بهره‌وری خطوط نورد.
- generic [ref=e355]: "(زمان: آذر ۱۴۰۵)"
- generic [ref=e362]:
- generic [ref=e363]:
- generic [ref=e364]:
- generic [ref=e365]: Monthly Industry Report
- heading "ماهنامه تحلیلی کارخانه" [level=2] [ref=e366]
- link "مشاهده همه" [ref=e367] [cursor=pointer]:
- /url: https://www.msc.ir/fa-IR/Portal/8029/page/%D9%85%D8%A7%D9%87%D9%86%D8%A7%D9%85%D9%87-%D8%AA%D8%AD%D9%84%DB%8C%D9%84%DB%8C-%D8%B1%D8%A7%D9%87%D8%A8%D8%B1%D8%AF%DB%8C-%DA%A9%D8%A7%D8%B1%D8%AE%D8%A7%D9%86%D9%87
- generic [ref=e368]:
- article [ref=e370] [cursor=pointer]:
- generic [ref=e373]: تولید و صادرات
- generic [ref=e374]:
- generic [ref=e375]: بهمن ۱۴۰۳
- heading "تحلیل عملکرد تولید و صادرات" [level=3] [ref=e376]
- paragraph [ref=e377]: بررسی آمار تولید ماهانه، مقایسه با بودجه سالانه و روند صادرات فولاد تخت در بازارهای منطقه‌ای.
- link "دریافت گزارش" [ref=e379]:
- /url: https://www.msc.ir/fa-IR/Portal/8029/page/%D9%85%D8%A7%D9%87%D9%86%D8%A7%D9%85%D9%87-%D8%AA%D8%AD%D9%84%DB%8C%D9%84%DB%8C-%D8%B1%D8%A7%D9%87%D8%A8%D8%B1%D8%AF%DB%8C-%DA%A9%D8%A7%D8%B1%D8%AE%D8%A7%D9%86%D9%87
- img [ref=e380]
- generic [ref=e383]: دریافت گزارش
- article [ref=e385] [cursor=pointer]:
- generic [ref=e388]: بازار و قیمت
- generic [ref=e389]:
- generic [ref=e390]: دی ۱۴۰۳
- heading "وضعیت بازار و قیمت‌گذاری" [level=3] [ref=e391]
- paragraph [ref=e392]: تحلیل نوسانات قیمت فولاد تخت در بازار داخلی و اثر نرخ ارز بر چشم‌انداز زمستان.
- link "دریافت گزارش" [ref=e394]:
- /url: https://www.msc.ir/fa-IR/Portal/8029/page/%D9%85%D8%A7%D9%87%D9%86%D8%A7%D9%85%D9%87-%D8%AA%D8%AD%D9%84%DB%8C%D9%84%DB%8C-%D8%B1%D8%A7%D9%87%D8%A8%D8%B1%D8%AF%DB%8C-%DA%A9%D8%A7%D8%B1%D8%AE%D8%A7%D9%86%D9%87
- img [ref=e395]
- generic [ref=e398]: دریافت گزارش
- article [ref=e400] [cursor=pointer]:
- generic [ref=e403]: فناوری
- generic [ref=e404]:
- generic [ref=e405]: آذر ۱۴۰۳
- heading "دیجیتال‌سازی خطوط تولید" [level=3] [ref=e406]
- paragraph [ref=e407]: گزارشی از پیاده‌سازی سامانه‌های کنترل هوشمند و اثر آن بر بهره‌وری خطوط نورد.
- link "دریافت گزارش" [ref=e409]:
- /url: https://www.msc.ir/fa-IR/Portal/8029/page/%D9%85%D8%A7%D9%87%D9%86%D8%A7%D9%85%D9%87-%D8%AA%D8%AD%D9%84%DB%8C%D9%84%DB%8C-%D8%B1%D8%A7%D9%87%D8%A8%D8%B1%D8%AF%DB%8C-%DA%A9%D8%A7%D8%B1%D8%AE%D8%A7%D9%86%D9%87
- img [ref=e410]
- generic [ref=e413]: دریافت گزارش
- article [ref=e415] [cursor=pointer]:
- generic [ref=e418]: نیروی انسانی
- generic [ref=e419]:
- generic [ref=e420]: آبان ۱۴۰۳
- heading "تحلیل نیروی انسانی و بهره‌وری" [level=3] [ref=e421]
- paragraph [ref=e422]: شاخص‌های بهره‌وری نیروی کار، آموزش‌های ماهانه و آمار ایمنی شغلی.
- link "دریافت گزارش" [ref=e424]:
- /url: https://www.msc.ir/fa-IR/Portal/8029/page/%D9%85%D8%A7%D9%87%D9%86%D8%A7%D9%85%D9%87-%D8%AA%D8%AD%D9%84%DB%8C%D9%84%DB%8C-%D8%B1%D8%A7%D9%87%D8%A8%D8%B1%D8%AF%DB%8C-%DA%A9%D8%A7%D8%B1%D8%AE%D8%A7%D9%86%D9%87
- img [ref=e425]
- generic [ref=e428]: دریافت گزارش
- generic [ref=e432]:
- generic [ref=e433]:
- heading "ویدیوکست" [level=2] [ref=e436]
- button "مشاهده همه" [ref=e437] [cursor=pointer]:
- img [ref=e439]
- generic [ref=e441]: مشاهده همه
- generic [ref=e442]:
- button "play" [ref=e444] [cursor=pointer]
- generic [ref=e450]:
- generic [ref=e451]:
- generic [ref=e452]:
- generic [ref=e453]:
- generic [ref=e454]: 🔊
- generic [ref=e455]: ۰۹:۲۷ / ۴۲ دقیقه
- generic [ref=e456]:
- generic [ref=e457]:
- generic [ref=e458]:
- generic [ref=e459]:
- generic [ref=e460]:
- generic [ref=e461]: 🎙
- heading "نگاهی به آن سوی مرزهای تولید" [level=3] [ref=e462]
- paragraph [ref=e463]: در ویدیوکست «Steel Horizon»، ما فراتر از داده‌های خام قیمت و تناژ، به سراغ مهندسی تغییر و معماری آینده صنعت می‌رویم. اینجا محل تلاقی استراتژی‌های کلان، تکنولوژی‌های برهم‌زننده و رهبرانی است که فولاد فردا را نه با کوره‌ها، که با «بینش» می‌سازند. ما طوفان‌های بازار را تحلیل می‌کنیم تا شما با اطمینان، مسیر آینده را ترسیم کنید. صنعت فولاد در حال ورود به پیچیده‌ترین چرخه حیات خود است؛ جایی که هوش مصنوعی، مقررات کربنی و ژئواکونومی، قواعد بازی را بازنویسی کرده‌اند. در «Steel Logic» با تحلیل‌گران ارشد و استراتژیست‌های تراز اول، به گفتگو می‌نشینیم تا لایه‌های پنهان این پیچیدگی‌ها را آشکار کنیم. اینجا خبری از هیجان‌زدگی نیست؛ فقط تحلیل‌های داده‌محور و تصمیم‌ساز.
- generic [ref=e468]:
- generic [ref=e469]:
- heading "پادکست‌ها" [level=2] [ref=e472]
- button "مشاهده همه" [ref=e473] [cursor=pointer]:
- generic [ref=e474]: مشاهده همه
- img [ref=e476]
- generic [ref=e478]:
- generic [ref=e480]:
- generic [ref=e481]: ★★★★★
- generic [ref=e482]: ۴/۵
- generic [ref=e483]:
- generic [ref=e484]:
- generic [ref=e485]:
- button [ref=e486] [cursor=pointer]:
- img [ref=e487]
- button [ref=e490] [cursor=pointer]:
- img [ref=e491]
- generic [ref=e493]: اپیزود ۷۷
- heading "کالبدشکافی عقلانی پیچیدگی‌های صنعتی" [level=3] [ref=e495]
- generic [ref=e497]:
- img [ref=e498]
- generic [ref=e500]: ۲۵ / خرداد / ۱۴۰۵
- generic [ref=e502]:
- button "play" [ref=e503] [cursor=pointer]:
- img [ref=e504]
- generic [ref=e506]:
- generic [ref=e507]: ۰۲:۳۱
- generic [ref=e589]: ۱۰:۵۶
- generic [ref=e590]:
- generic [ref=e591]:
- button "آزمایشگاه کوره ۱:۴۰:۱۸ «The Foundry Lab» محلی برای کالبدشکافی دقیق تحولات فناورانه و مدل‌های نوین فولادسازی است. ما در این پادکست با نگاهی موشکافانه، چالش‌های فنی را بررسی می‌کنیم." [ref=e592] [cursor=pointer]:
- generic [ref=e593]:
- generic [ref=e594]:
- generic [ref=e595]: آزمایشگاه کوره
- generic [ref=e596]:
- img [ref=e597]
- generic [ref=e600]: ۱:۴۰:۱۸
- generic [ref=e601]: «The Foundry Lab» محلی برای کالبدشکافی دقیق تحولات فناورانه و مدل‌های نوین فولادسازی است. ما در این پادکست با نگاهی موشکافانه، چالش‌های فنی را بررسی می‌کنیم.
- button "معمار فولاد ۱:۴۰:۱۸ در «Steel Architect»، ما به سراغ داستان ساختن فولاد فردا می‌رویم؛ روایتی که رهبرانی که از مدل‌های سنتی کسب‌وکار به چالش کشیده‌اند را به تصویر می‌کشد." [ref=e602] [cursor=pointer]:
- generic [ref=e603]:
- generic [ref=e604]:
- generic [ref=e605]: معمار فولاد
- generic [ref=e606]:
- img [ref=e607]
- generic [ref=e610]: ۱:۴۰:۱۸
- generic [ref=e611]: در «Steel Architect»، ما به سراغ داستان ساختن فولاد فردا می‌رویم؛ روایتی که رهبرانی که از مدل‌های سنتی کسب‌وکار به چالش کشیده‌اند را به تصویر می‌کشد.
- generic [ref=e612]:
- generic [ref=e613]:
- heading "کالبدشکافی عقلانی پیچیدگی‌های صنعتی" [level=3] [ref=e614]
- img [ref=e616]
- paragraph [ref=e621]: «صنعت فولاد در عصر حاضر، در پیچیده‌ترین چرخه حیات خود قرار گرفته است؛ جایی که همگرایی هوش مصنوعی، الزامات زیست‌محیطی و تنش‌های ژئواکونومیک، قواعد کلاسیک تولید و تجارت را به‌کلی بازنویسی کرده‌اند. در پادکست «نام انتخابی»، ما با عبور از هیاهوی رسانه‌ای و تحلیل‌های سطحی، به کالبدشکافی عقلانی این پیچیدگی‌ها می‌پردازیم. ما با دعوت از استراتژیست‌های تراز اول و خبرگان این حوزه، لایه‌های پنهان چالش‌های صنعتی را می‌شکافیم تا بینشی شفاف، داده‌محور و تصمیم‌ساز را در اختیار رهبری قرار دهیم که برای عبور از طوفان‌های تغییر، به قطب‌نمای دقیق‌تر از عرف‌های بازار نیاز دارند.»
- generic [ref=e622]: "مهمان برنامه: مهندس سیامک شجاعی"
- contentinfo [ref=e623]:
- generic [ref=e624]:
- img [ref=e625]
- button "بازگشت به بالا" [ref=e627] [cursor=pointer]:
- img [ref=e628]
- generic [ref=e633]:
- generic [ref=e634]:
- img "اندیشکده فولاد آینده" [ref=e635]
- generic [ref=e638]: اندیشکده فولاد آینده
- generic [ref=e640]: The Steel Futures Institute
- paragraph [ref=e641]: فضایی همدلانه برای اندیشیدن به فردای صنعت؛ جایی که دانش، آینده‌پژوهی و نوآوری به بینش‌هایی برای توسعه صنعتی تبدیل می‌شوند.
- generic [ref=e642]: ما را در شبکه‌های اجتماعی دنبال کنید.
- generic [ref=e643]:
- link "آپارات" [ref=e644] [cursor=pointer]:
- /url: "#"
- img [ref=e645]
- link "X" [ref=e647] [cursor=pointer]:
- /url: "#"
- img [ref=e648]
- link "لینکداین" [ref=e650] [cursor=pointer]:
- /url: "#"
- img [ref=e651]
- link "تلگرام" [ref=e653] [cursor=pointer]:
- /url: "#"
- img [ref=e654]
- navigation "پیوندهای پایین" [ref=e656]:
- link "رادار آینده" [ref=e657] [cursor=pointer]:
- /url: /radar
- text: رادار آینده
- link "آمار و داده" [ref=e659] [cursor=pointer]:
- /url: "#"
- text: آمار و داده
- link "رسانه و رویداد" [ref=e661] [cursor=pointer]:
- /url: "#"
- text: رسانه و رویداد
- link "در یک نگاه" [ref=e663] [cursor=pointer]:
- /url: /at-a-glance
- text: در یک نگاه
- link "شبکه خبرگان" [ref=e665] [cursor=pointer]:
- /url: "#"
- text: شبکه خبرگان
- link "درباره ما" [ref=e667] [cursor=pointer]:
- /url: "#"
- text: درباره ما
- generic [ref=e669]:
- generic [ref=e670]: اطلاعات تماس
- generic [ref=e671]:
- generic [ref=e672]:
- img [ref=e673]
- generic [ref=e676]: "نشانی:"
- generic [ref=e677]: اصفهان، خیابان سعادت‌آباد
- generic [ref=e678]:
- img [ref=e679]
- generic [ref=e681]: "شماره تماس:"
- generic [ref=e682]: ۰۳۱ - ۳۸۸۸۴۵۳۹
- generic [ref=e683]:
- img [ref=e684]
- generic [ref=e687]: "پست الکترونیک:"
- generic [ref=e688]: info@steelforesight.com
- generic [ref=e689]:
- generic [ref=e691]: نشان
- generic [ref=e693]: نشان
- generic [ref=e695]: نشان
- generic [ref=e696]:
- generic [ref=e697]: عضویت در خبرنامه اندیشکده
- generic [ref=e698]:
- textbox "ایمیل خود را وارد کنید..." [ref=e699]
- button "ثبت" [ref=e700] [cursor=pointer]
- generic [ref=e702]:
- generic [ref=e703]: تمامی حقوق مادی و معنوی این وب‌سایت متعلق به شرکت فولاد مبارکه می‌باشد.
- generic [ref=e704]: طراح و پشتیبان؛ شرکت دانش‌بنیان نواندیشان آتی نگار فرتاک

View File

@ -0,0 +1,390 @@
- generic [ref=e3]:
- banner [ref=e4]:
- generic [ref=e6]:
- link "اندیشکده فولاد آینده The Steel Futures Institute" [ref=e7] [cursor=pointer]:
- /url: /
- img [ref=e8]
- generic [ref=e9]:
- generic [ref=e10]: اندیشکده فولاد آینده
- generic [ref=e11]: The Steel Futures Institute
- navigation "ناوبری اصلی" [ref=e12]:
- button "رادار آینده" [ref=e14] [cursor=pointer]:
- text: رادار آینده
- img [ref=e15]
- link "آمار و داده" [ref=e17] [cursor=pointer]:
- /url: /pulse
- link "در یک نگاه" [ref=e18] [cursor=pointer]:
- /url: /at-a-glance
- button "رسانه و رویداد" [ref=e20] [cursor=pointer]:
- text: رسانه و رویداد
- img [ref=e21]
- link "شبکه خبرگان" [ref=e23] [cursor=pointer]:
- /url: /experts
- button "درباره ما" [ref=e25] [cursor=pointer]:
- text: درباره ما
- img [ref=e26]
- generic [ref=e28]:
- button "جستجو" [ref=e29] [cursor=pointer]:
- img [ref=e30]
- button "ورود / ثبت‌نام" [ref=e33] [cursor=pointer]
- button "EN" [ref=e34] [cursor=pointer]
- main [ref=e35]:
- generic [ref=e36]:
- generic [ref=e44]:
- img [ref=e47]
- heading "بینشی نو برای توسعه‌ی صنعتی" [level=2] [ref=e53]
- generic:
- paragraph
- link "آشنایی با چشم‌انداز ما" [ref=e55] [cursor=pointer]:
- /url: /about
- img [ref=e56]
- generic [ref=e58]: آشنایی با چشم‌انداز ما
- generic [ref=e65]:
- generic [ref=e66]:
- generic [ref=e67]:
- generic [ref=e68]: FUTURE RADAR
- heading "رادار آینده" [level=2] [ref=e69]
- paragraph [ref=e70]: تحلیل چندوجهی آینده صنعت فولاد
- link "همه سیگنال‌ها ←" [ref=e71] [cursor=pointer]:
- /url: /radar
- generic [ref=e72]:
- article [ref=e73] [cursor=pointer]:
- generic [ref=e75]:
- img [ref=e77]
- generic [ref=e80]:
- generic [ref=e81]: پیش‌بینی بازار با هوش مصنوعی
- generic [ref=e82]: ۱۴ اردیبهشت ۱۴۰۵
- article [ref=e83] [cursor=pointer]:
- generic [ref=e85]:
- img [ref=e87]
- generic [ref=e90]:
- generic [ref=e91]: پیش‌بینی بازار با هوش مصنوعی
- generic [ref=e92]: ۱۴ اردیبهشت ۱۴۰۵
- article [ref=e93] [cursor=pointer]:
- generic [ref=e95]:
- img [ref=e97]
- generic [ref=e99]:
- generic [ref=e100]: پیش‌بینی بازار با هوش مصنوعی
- generic [ref=e101]: ۱۴ اردیبهشت ۱۴۰۵
- article [ref=e102] [cursor=pointer]:
- generic [ref=e104]:
- img [ref=e106]
- generic [ref=e109]:
- generic [ref=e110]: پیش‌بینی بازار با هوش مصنوعی
- generic [ref=e111]: ۱۴ اردیبهشت ۱۴۰۵
- generic [ref=e113]:
- generic [ref=e114]:
- generic [ref=e117]:
- generic [ref=e118]: At a Glance
- heading "در یک نگاه" [level=2] [ref=e119]
- link "مشاهده همه" [ref=e120] [cursor=pointer]:
- /url: /at-a-glance
- img [ref=e122]
- generic [ref=e124]: مشاهده همه
- generic [ref=e130]:
- generic [ref=e131]:
- heading "شوک هیدروژنی به بازار جهانی؛ آغاز رسمی عصر فولادسازی بدون کربن" [level=3] [ref=e132]
- generic [ref=e133]:
- generic [ref=e134]: Financial Times
- generic [ref=e135]:
- img [ref=e136]
- text: اردیبهشت ۱۴۰۵
- paragraph [ref=e138]: پروژه عظیم «فولاد سبز H2» در سوئد با جذب بی‌سابقه ۶.۵ میلیارد یورو سرمایه، رسماً وارد فاز اجرایی شد تا نخستین کارخانه فولادسازی در مقیاس تجاری بر پایه هیدروژن سبز (بدون نیاز به زغال‌سنگ) در جهان متولد شود. این تأسیسات با هدف کاهش ۹۵ درصدی انتشار CO₂، قواعد رقابت در زنجیره ارزش اروپا را برای همیشه بازتعریف می‌کند.
- generic [ref=e139]:
- button "prev" [ref=e140] [cursor=pointer]:
- button "next" [ref=e141] [cursor=pointer]:
- generic [ref=e144]:
- generic [ref=e146]:
- generic [ref=e147]: OUR FRAMEWORK
- heading "چارچوب اندیشه ما" [level=2] [ref=e148]
- paragraph [ref=e149]: ترکیب داده، تحلیل، آینده‌نگاری و خرد جمعی برای فهم تحولات صنعت فولاد.
- generic [ref=e150]:
- generic [ref=e151]:
- img [ref=e153]
- generic [ref=e157]:
- heading "تحلیل داده‌محور" [level=3] [ref=e158]
- paragraph [ref=e159]: استفاده از داده‌های بازار، اقتصاد، فناوری و زنجیره ارزش فولاد برای استخراج بینش‌های قابل اتکا.
- generic [ref=e160]:
- img [ref=e162]
- generic [ref=e167]:
- heading "نگاه نظام‌مند" [level=3] [ref=e168]
- paragraph [ref=e169]: تحلیل صنعت فولاد در تعامل با اقتصاد، انرژی، فناوری، محیط زیست و سیاست‌های جهانی.
- generic [ref=e170]:
- img [ref=e172]
- generic [ref=e177]:
- heading "هم‌اندیشی خبرگان" [level=3] [ref=e178]
- paragraph [ref=e179]: بهره‌گیری مستمر از شبکه‌ای از خبرگان صنعت، دانشگاه و فناوری.
- generic [ref=e180]:
- img [ref=e182]
- generic [ref=e190]:
- heading "آینده‌نگاری علمی" [level=3] [ref=e191]
- paragraph [ref=e192]: تحلیل روندها، شناسایی عدم‌قطعیت‌ها و طراحی سناریوها.
- generic [ref=e193]:
- img [ref=e195]
- generic [ref=e197]:
- heading "از تحلیل تا اقدام" [level=3] [ref=e198]
- paragraph [ref=e199]: تبدیل تحلیل‌ها به توصیه‌ها و تجویزهای استراتژیک.
- generic [ref=e200]:
- img [ref=e202]
- generic [ref=e206]:
- heading "بی‌طرفی و استقلال" [level=3] [ref=e207]
- paragraph [ref=e208]: اندیشه‌های مستقل و متکی بر شواهد، داده‌ها و نگاه بلندمدت.
- generic [ref=e214]:
- generic [ref=e215]:
- generic [ref=e216]: ماهنامه شماره ۱۲ منتشر شد…
- heading "دیپلماسی معدنی در عصر انسدادهای ژئوپلیتیک" [level=2] [ref=e217]
- generic [ref=e218]: Mineral Diplomacy in the Era of Geopolitical Blockades
- button "دانلود گزارش" [ref=e220] [cursor=pointer]:
- img [ref=e221]
- text: دانلود گزارش
- img "دیپلماسی معدنی در عصر انسدادهای ژئوپلیتیک" [ref=e225]
- generic [ref=e228]:
- generic [ref=e230]:
- generic [ref=e231]: DATA SOURCES
- heading "نبض صنعت" [level=2] [ref=e232]
- paragraph [ref=e233]: پنجره‌ای واحد به جهان داده‌های فولاد؛ دسترسی به شاخص‌ها، قیمت‌ها، تجارت جهانی و داده‌های کلیدی از معدن تا محصول نهایی.
- link "مشاهده منابع داده" [ref=e234] [cursor=pointer]:
- /url: /pulse
- text: مشاهده منابع داده
- img [ref=e235]
- generic [ref=e238]:
- generic "London Metal Exchange" [ref=e244] [cursor=pointer]:
- img [ref=e246]
- generic "Shanghai Futures Exchange" [ref=e256] [cursor=pointer]:
- img [ref=e258]
- generic "S&P Global Platts" [ref=e262] [cursor=pointer]:
- img [ref=e264]
- generic "Reuters" [ref=e271] [cursor=pointer]:
- img [ref=e273]
- generic "Fastmarkets" [ref=e279] [cursor=pointer]:
- img [ref=e281]
- generic "World Steel Association" [ref=e287] [cursor=pointer]:
- img [ref=e289]
- generic "Mysteel" [ref=e296] [cursor=pointer]:
- img [ref=e298]
- generic "Dalian Commodity Exchange" [ref=e304] [cursor=pointer]:
- img [ref=e306]
- generic "CME Group" [ref=e311] [cursor=pointer]:
- img [ref=e313]
- generic [ref=e323]:
- generic [ref=e324]:
- heading "دیده‌بان رویداد‌ها" [level=2] [ref=e327]
- link "مشاهده همه" [ref=e328] [cursor=pointer]:
- /url: /events
- img [ref=e330]
- generic [ref=e332]: مشاهده همه
- generic [ref=e333]:
- generic [ref=e334]:
- button "اجلاس جهانی فولاد سبز | استکهلم، سوئد" [ref=e335] [cursor=pointer]:
- generic [ref=e336]: اجلاس جهانی فولاد سبز | استکهلم، سوئد
- img [ref=e337]
- paragraph [ref=e340]:
- text: بررسی آخرین تکنولوژی‌های احیای مستقیم بر پایه هیدروژن و جذب سرمایه برای پروژه‌های بدون کربن. حضور در این رویداد برای درک آینده مقررات زیست‌محیطی اروپا حیاتی است.
- generic [ref=e341]: "(زمان: مهر ۱۴۰۵)"
- generic [ref=e342]:
- button "نشست سالانه انجمن جهانی فولاد (worldsteel) | شانگهای، چین" [ref=e343] [cursor=pointer]:
- generic [ref=e344]: نشست سالانه انجمن جهانی فولاد (worldsteel) | شانگهای، چین
- img [ref=e345]
- paragraph [ref=e347]:
- text: "گردهمایی بزرگ‌ترین رهبران صنعت. محور اصلی امسال: پیش‌بینی تقاضای جهانی در سایه رکود بخش مسکن چین و تغییر مسیر زنجیره تامین به سمت هند و خاورمیانه."
- generic [ref=e348]: "(زمان: آبان ۱۴۰۵)"
- generic [ref=e349]:
- button "مجمع بین‌المللی هوش مصنوعی در تولید | برلین، آلمان" [ref=e350] [cursor=pointer]:
- generic [ref=e351]: مجمع بین‌المللی هوش مصنوعی در تولید | برلین، آلمان
- img [ref=e352]
- paragraph [ref=e354]:
- text: رونمایی از جدیدترین نسل دوقلوهای دیجیتال (Digital Twins) و الگوریتم‌های پیش‌بینی تعمیرات. فرصتی برای آشنایی با راهکارهای کاهش هزینه و افزایش بهره‌وری خطوط نورد.
- generic [ref=e355]: "(زمان: آذر ۱۴۰۵)"
- generic [ref=e362]:
- generic [ref=e363]:
- generic [ref=e364]:
- generic [ref=e365]: Monthly Industry Report
- heading "ماهنامه تحلیلی کارخانه" [level=2] [ref=e366]
- link "مشاهده همه" [ref=e367] [cursor=pointer]:
- /url: https://www.msc.ir/fa-IR/Portal/8029/page/%D9%85%D8%A7%D9%87%D9%86%D8%A7%D9%85%D9%87-%D8%AA%D8%AD%D9%84%DB%8C%D9%84%DB%8C-%D8%B1%D8%A7%D9%87%D8%A8%D8%B1%D8%AF%DB%8C-%DA%A9%D8%A7%D8%B1%D8%AE%D8%A7%D9%86%D9%87
- generic [ref=e368]:
- article [ref=e370] [cursor=pointer]:
- generic [ref=e373]: تولید و صادرات
- generic [ref=e374]:
- generic [ref=e375]: بهمن ۱۴۰۳
- heading "تحلیل عملکرد تولید و صادرات" [level=3] [ref=e376]
- paragraph [ref=e377]: بررسی آمار تولید ماهانه، مقایسه با بودجه سالانه و روند صادرات فولاد تخت در بازارهای منطقه‌ای.
- link "دریافت گزارش" [ref=e379]:
- /url: https://www.msc.ir/fa-IR/Portal/8029/page/%D9%85%D8%A7%D9%87%D9%86%D8%A7%D9%85%D9%87-%D8%AA%D8%AD%D9%84%DB%8C%D9%84%DB%8C-%D8%B1%D8%A7%D9%87%D8%A8%D8%B1%D8%AF%DB%8C-%DA%A9%D8%A7%D8%B1%D8%AE%D8%A7%D9%86%D9%87
- img [ref=e380]
- generic [ref=e383]: دریافت گزارش
- article [ref=e385] [cursor=pointer]:
- generic [ref=e388]: بازار و قیمت
- generic [ref=e389]:
- generic [ref=e390]: دی ۱۴۰۳
- heading "وضعیت بازار و قیمت‌گذاری" [level=3] [ref=e391]
- paragraph [ref=e392]: تحلیل نوسانات قیمت فولاد تخت در بازار داخلی و اثر نرخ ارز بر چشم‌انداز زمستان.
- link "دریافت گزارش" [ref=e394]:
- /url: https://www.msc.ir/fa-IR/Portal/8029/page/%D9%85%D8%A7%D9%87%D9%86%D8%A7%D9%85%D9%87-%D8%AA%D8%AD%D9%84%DB%8C%D9%84%DB%8C-%D8%B1%D8%A7%D9%87%D8%A8%D8%B1%D8%AF%DB%8C-%DA%A9%D8%A7%D8%B1%D8%AE%D8%A7%D9%86%D9%87
- img [ref=e395]
- generic [ref=e398]: دریافت گزارش
- article [ref=e400] [cursor=pointer]:
- generic [ref=e403]: فناوری
- generic [ref=e404]:
- generic [ref=e405]: آذر ۱۴۰۳
- heading "دیجیتال‌سازی خطوط تولید" [level=3] [ref=e406]
- paragraph [ref=e407]: گزارشی از پیاده‌سازی سامانه‌های کنترل هوشمند و اثر آن بر بهره‌وری خطوط نورد.
- link "دریافت گزارش" [ref=e409]:
- /url: https://www.msc.ir/fa-IR/Portal/8029/page/%D9%85%D8%A7%D9%87%D9%86%D8%A7%D9%85%D9%87-%D8%AA%D8%AD%D9%84%DB%8C%D9%84%DB%8C-%D8%B1%D8%A7%D9%87%D8%A8%D8%B1%D8%AF%DB%8C-%DA%A9%D8%A7%D8%B1%D8%AE%D8%A7%D9%86%D9%87
- img [ref=e410]
- generic [ref=e413]: دریافت گزارش
- article [ref=e415] [cursor=pointer]:
- generic [ref=e418]: نیروی انسانی
- generic [ref=e419]:
- generic [ref=e420]: آبان ۱۴۰۳
- heading "تحلیل نیروی انسانی و بهره‌وری" [level=3] [ref=e421]
- paragraph [ref=e422]: شاخص‌های بهره‌وری نیروی کار، آموزش‌های ماهانه و آمار ایمنی شغلی.
- link "دریافت گزارش" [ref=e424]:
- /url: https://www.msc.ir/fa-IR/Portal/8029/page/%D9%85%D8%A7%D9%87%D9%86%D8%A7%D9%85%D9%87-%D8%AA%D8%AD%D9%84%DB%8C%D9%84%DB%8C-%D8%B1%D8%A7%D9%87%D8%A8%D8%B1%D8%AF%DB%8C-%DA%A9%D8%A7%D8%B1%D8%AE%D8%A7%D9%86%D9%87
- img [ref=e425]
- generic [ref=e428]: دریافت گزارش
- generic [ref=e432]:
- generic [ref=e433]:
- heading "ویدیوکست" [level=2] [ref=e436]
- button "مشاهده همه" [ref=e437] [cursor=pointer]:
- img [ref=e439]
- generic [ref=e441]: مشاهده همه
- generic [ref=e442]:
- button "play" [ref=e444] [cursor=pointer]
- generic [ref=e450]:
- generic [ref=e451]:
- generic [ref=e452]:
- generic [ref=e453]:
- generic [ref=e454]: 🔊
- generic [ref=e455]: ۰۹:۲۷ / ۴۲ دقیقه
- generic [ref=e456]:
- generic [ref=e457]:
- generic [ref=e458]:
- generic [ref=e459]:
- generic [ref=e460]:
- generic [ref=e461]: 🎙
- heading "نگاهی به آن سوی مرزهای تولید" [level=3] [ref=e462]
- paragraph [ref=e463]: در ویدیوکست «Steel Horizon»، ما فراتر از داده‌های خام قیمت و تناژ، به سراغ مهندسی تغییر و معماری آینده صنعت می‌رویم. اینجا محل تلاقی استراتژی‌های کلان، تکنولوژی‌های برهم‌زننده و رهبرانی است که فولاد فردا را نه با کوره‌ها، که با «بینش» می‌سازند. ما طوفان‌های بازار را تحلیل می‌کنیم تا شما با اطمینان، مسیر آینده را ترسیم کنید. صنعت فولاد در حال ورود به پیچیده‌ترین چرخه حیات خود است؛ جایی که هوش مصنوعی، مقررات کربنی و ژئواکونومی، قواعد بازی را بازنویسی کرده‌اند. در «Steel Logic» با تحلیل‌گران ارشد و استراتژیست‌های تراز اول، به گفتگو می‌نشینیم تا لایه‌های پنهان این پیچیدگی‌ها را آشکار کنیم. اینجا خبری از هیجان‌زدگی نیست؛ فقط تحلیل‌های داده‌محور و تصمیم‌ساز.
- generic [ref=e468]:
- generic [ref=e469]:
- heading "پادکست‌ها" [level=2] [ref=e472]
- button "مشاهده همه" [ref=e473] [cursor=pointer]:
- generic [ref=e474]: مشاهده همه
- img [ref=e476]
- generic [ref=e478]:
- generic [ref=e480]:
- generic [ref=e481]: ★★★★★
- generic [ref=e482]: ۴/۵
- generic [ref=e483]:
- generic [ref=e484]:
- generic [ref=e485]:
- button [ref=e486] [cursor=pointer]:
- img [ref=e487]
- button [ref=e490] [cursor=pointer]:
- img [ref=e491]
- generic [ref=e493]: اپیزود ۷۷
- heading "کالبدشکافی عقلانی پیچیدگی‌های صنعتی" [level=3] [ref=e495]
- generic [ref=e497]:
- img [ref=e498]
- generic [ref=e500]: ۲۵ / خرداد / ۱۴۰۵
- generic [ref=e502]:
- button "play" [ref=e503] [cursor=pointer]:
- img [ref=e504]
- generic [ref=e506]:
- generic [ref=e507]: ۰۲:۳۱
- generic [ref=e589]: ۱۰:۵۶
- generic [ref=e590]:
- generic [ref=e591]:
- button "آزمایشگاه کوره ۱:۴۰:۱۸ «The Foundry Lab» محلی برای کالبدشکافی دقیق تحولات فناورانه و مدل‌های نوین فولادسازی است. ما در این پادکست با نگاهی موشکافانه، چالش‌های فنی را بررسی می‌کنیم." [ref=e592] [cursor=pointer]:
- generic [ref=e593]:
- generic [ref=e594]:
- generic [ref=e595]: آزمایشگاه کوره
- generic [ref=e596]:
- img [ref=e597]
- generic [ref=e600]: ۱:۴۰:۱۸
- generic [ref=e601]: «The Foundry Lab» محلی برای کالبدشکافی دقیق تحولات فناورانه و مدل‌های نوین فولادسازی است. ما در این پادکست با نگاهی موشکافانه، چالش‌های فنی را بررسی می‌کنیم.
- button "معمار فولاد ۱:۴۰:۱۸ در «Steel Architect»، ما به سراغ داستان ساختن فولاد فردا می‌رویم؛ روایتی که رهبرانی که از مدل‌های سنتی کسب‌وکار به چالش کشیده‌اند را به تصویر می‌کشد." [ref=e602] [cursor=pointer]:
- generic [ref=e603]:
- generic [ref=e604]:
- generic [ref=e605]: معمار فولاد
- generic [ref=e606]:
- img [ref=e607]
- generic [ref=e610]: ۱:۴۰:۱۸
- generic [ref=e611]: در «Steel Architect»، ما به سراغ داستان ساختن فولاد فردا می‌رویم؛ روایتی که رهبرانی که از مدل‌های سنتی کسب‌وکار به چالش کشیده‌اند را به تصویر می‌کشد.
- generic [ref=e612]:
- generic [ref=e613]:
- heading "کالبدشکافی عقلانی پیچیدگی‌های صنعتی" [level=3] [ref=e614]
- img [ref=e616]
- paragraph [ref=e621]: «صنعت فولاد در عصر حاضر، در پیچیده‌ترین چرخه حیات خود قرار گرفته است؛ جایی که همگرایی هوش مصنوعی، الزامات زیست‌محیطی و تنش‌های ژئواکونومیک، قواعد کلاسیک تولید و تجارت را به‌کلی بازنویسی کرده‌اند. در پادکست «نام انتخابی»، ما با عبور از هیاهوی رسانه‌ای و تحلیل‌های سطحی، به کالبدشکافی عقلانی این پیچیدگی‌ها می‌پردازیم. ما با دعوت از استراتژیست‌های تراز اول و خبرگان این حوزه، لایه‌های پنهان چالش‌های صنعتی را می‌شکافیم تا بینشی شفاف، داده‌محور و تصمیم‌ساز را در اختیار رهبری قرار دهیم که برای عبور از طوفان‌های تغییر، به قطب‌نمای دقیق‌تر از عرف‌های بازار نیاز دارند.»
- generic [ref=e622]: "مهمان برنامه: مهندس سیامک شجاعی"
- contentinfo [ref=e623]:
- generic [ref=e624]:
- img [ref=e625]
- button "بازگشت به بالا" [ref=e627] [cursor=pointer]:
- img [ref=e628]
- generic [ref=e633]:
- generic [ref=e634]:
- img "اندیشکده فولاد آینده" [ref=e635]
- generic [ref=e638]: اندیشکده فولاد آینده
- generic [ref=e640]: The Steel Futures Institute
- paragraph [ref=e641]: فضایی همدلانه برای اندیشیدن به فردای صنعت؛ جایی که دانش، آینده‌پژوهی و نوآوری به بینش‌هایی برای توسعه صنعتی تبدیل می‌شوند.
- generic [ref=e642]: ما را در شبکه‌های اجتماعی دنبال کنید.
- generic [ref=e643]:
- link "آپارات" [ref=e644] [cursor=pointer]:
- /url: "#"
- img [ref=e645]
- link "X" [ref=e647] [cursor=pointer]:
- /url: "#"
- img [ref=e648]
- link "لینکداین" [ref=e650] [cursor=pointer]:
- /url: "#"
- img [ref=e651]
- link "تلگرام" [ref=e653] [cursor=pointer]:
- /url: "#"
- img [ref=e654]
- navigation "پیوندهای پایین" [ref=e656]:
- link "رادار آینده" [ref=e657] [cursor=pointer]:
- /url: /radar
- text: رادار آینده
- link "آمار و داده" [ref=e659] [cursor=pointer]:
- /url: "#"
- text: آمار و داده
- link "رسانه و رویداد" [ref=e661] [cursor=pointer]:
- /url: "#"
- text: رسانه و رویداد
- link "در یک نگاه" [ref=e663] [cursor=pointer]:
- /url: /at-a-glance
- text: در یک نگاه
- link "شبکه خبرگان" [ref=e665] [cursor=pointer]:
- /url: "#"
- text: شبکه خبرگان
- link "درباره ما" [ref=e667] [cursor=pointer]:
- /url: "#"
- text: درباره ما
- generic [ref=e669]:
- generic [ref=e670]: اطلاعات تماس
- generic [ref=e671]:
- generic [ref=e672]:
- img [ref=e673]
- generic [ref=e676]: "نشانی:"
- generic [ref=e677]: اصفهان، خیابان سعادت‌آباد
- generic [ref=e678]:
- img [ref=e679]
- generic [ref=e681]: "شماره تماس:"
- generic [ref=e682]: ۰۳۱ - ۳۸۸۸۴۵۳۹
- generic [ref=e683]:
- img [ref=e684]
- generic [ref=e687]: "پست الکترونیک:"
- generic [ref=e688]: info@steelforesight.com
- generic [ref=e689]:
- generic [ref=e691]: نشان
- generic [ref=e693]: نشان
- generic [ref=e695]: نشان
- generic [ref=e696]:
- generic [ref=e697]: عضویت در خبرنامه اندیشکده
- generic [ref=e698]:
- textbox "ایمیل خود را وارد کنید..." [ref=e699]
- button "ثبت" [ref=e700] [cursor=pointer]
- generic [ref=e702]:
- generic [ref=e703]: تمامی حقوق مادی و معنوی این وب‌سایت متعلق به شرکت فولاد مبارکه می‌باشد.
- generic [ref=e704]: طراح و پشتیبان؛ شرکت دانش‌بنیان نواندیشان آتی نگار فرتاک

View File

@ -0,0 +1,121 @@
- generic [ref=e3]:
- banner [ref=e4]:
- generic [ref=e6]:
- link "اندیشکده فولاد آینده The Steel Futures Institute" [ref=e7] [cursor=pointer]:
- /url: /
- img [ref=e8]
- generic [ref=e9]:
- generic [ref=e10]: اندیشکده فولاد آینده
- generic [ref=e11]: The Steel Futures Institute
- navigation "ناوبری اصلی" [ref=e12]:
- button "رادار آینده" [ref=e14] [cursor=pointer]:
- text: رادار آینده
- img [ref=e15]
- link "آمار و داده" [ref=e17] [cursor=pointer]:
- /url: /pulse
- link "در یک نگاه" [ref=e18] [cursor=pointer]:
- /url: /at-a-glance
- button "رسانه و رویداد" [ref=e20] [cursor=pointer]:
- text: رسانه و رویداد
- img [ref=e21]
- link "شبکه خبرگان" [ref=e23] [cursor=pointer]:
- /url: /experts
- button "درباره ما" [ref=e25] [cursor=pointer]:
- text: درباره ما
- img [ref=e26]
- generic [ref=e28]:
- button "جستجو" [ref=e29] [cursor=pointer]:
- img [ref=e30]
- link "alire" [ref=e33] [cursor=pointer]:
- /url: /profile
- img [ref=e34]
- text: alire
- button "Switch to English" [ref=e37] [cursor=pointer]: 🇺🇸
- main [ref=e38]:
- generic [ref=e40]:
- generic [ref=e41]:
- img [ref=e43]
- heading "ورود به حساب" [level=1] [ref=e46]
- generic [ref=e47]:
- generic [ref=e48]:
- generic [ref=e49]: ایمیل
- textbox "example@email.com" [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: رمز عبور
- textbox [ref=e53]
- button "ورود" [ref=e54] [cursor=pointer]
- paragraph [ref=e55]:
- text: حساب ندارید؟
- link "ثبت‌نام کنید" [ref=e56] [cursor=pointer]:
- /url: /register
- contentinfo [ref=e57]:
- generic [ref=e58]:
- img [ref=e59]
- button "بازگشت به بالا" [ref=e61] [cursor=pointer]:
- img [ref=e62]
- generic [ref=e67]:
- generic [ref=e68]:
- img "اندیشکده فولاد آینده" [ref=e69]
- generic [ref=e72]: اندیشکده فولاد آینده
- generic [ref=e74]: The Steel Futures Institute
- paragraph [ref=e75]: فضایی همدلانه برای اندیشیدن به فردای صنعت؛ جایی که دانش، آینده‌پژوهی و نوآوری به بینش‌هایی برای توسعه صنعتی تبدیل می‌شوند.
- generic [ref=e76]: ما را در شبکه‌های اجتماعی دنبال کنید.
- generic [ref=e77]:
- link "آپارات" [ref=e78] [cursor=pointer]:
- /url: "#"
- img [ref=e79]
- link "X" [ref=e81] [cursor=pointer]:
- /url: "#"
- img [ref=e82]
- link "لینکداین" [ref=e84] [cursor=pointer]:
- /url: "#"
- img [ref=e85]
- link "تلگرام" [ref=e87] [cursor=pointer]:
- /url: "#"
- img [ref=e88]
- navigation "پیوندهای پایین" [ref=e90]:
- link "رادار آینده" [ref=e91] [cursor=pointer]:
- /url: /radar
- text: رادار آینده
- link "آمار و داده" [ref=e93] [cursor=pointer]:
- /url: "#"
- text: آمار و داده
- link "رسانه و رویداد" [ref=e95] [cursor=pointer]:
- /url: "#"
- text: رسانه و رویداد
- link "در یک نگاه" [ref=e97] [cursor=pointer]:
- /url: /at-a-glance
- text: در یک نگاه
- link "شبکه خبرگان" [ref=e99] [cursor=pointer]:
- /url: "#"
- text: شبکه خبرگان
- link "درباره ما" [ref=e101] [cursor=pointer]:
- /url: "#"
- text: درباره ما
- generic [ref=e103]:
- generic [ref=e104]: اطلاعات تماس
- generic [ref=e105]:
- generic [ref=e106]:
- img [ref=e107]
- generic [ref=e110]: "نشانی:"
- generic [ref=e111]: اصفهان، خیابان سعادت‌آباد
- generic [ref=e112]:
- img [ref=e113]
- generic [ref=e115]: "شماره تماس:"
- generic [ref=e116]: ۰۳۱ - ۳۸۸۸۴۵۳۹
- generic [ref=e117]:
- img [ref=e118]
- generic [ref=e121]: "پست الکترونیک:"
- generic [ref=e122]: info@steelforesight.com
- generic [ref=e123]:
- generic [ref=e125]: نشان
- generic [ref=e127]: نشان
- generic [ref=e129]: نشان
- generic [ref=e130]:
- generic [ref=e131]: عضویت در خبرنامه اندیشکده
- generic [ref=e132]:
- textbox "ایمیل خود را وارد کنید..." [ref=e133]
- button "ثبت" [ref=e134] [cursor=pointer]
- generic [ref=e136]:
- generic [ref=e137]: تمامی حقوق مادی و معنوی این وب‌سایت متعلق به شرکت فولاد مبارکه می‌باشد.
- generic [ref=e138]: طراح و پشتیبان؛ شرکت دانش‌بنیان نواندیشان آتی نگار فرتاک

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -0,0 +1,154 @@
- generic [ref=e3]:
- banner [ref=e4]:
- generic [ref=e6]:
- link "اندیشکده فولاد آینده The Steel Futures Institute" [ref=e7] [cursor=pointer]:
- /url: /
- img [ref=e8]
- generic [ref=e9]:
- generic [ref=e10]: اندیشکده فولاد آینده
- generic [ref=e11]: The Steel Futures Institute
- navigation "ناوبری اصلی" [ref=e12]:
- button "رادار آینده" [ref=e14] [cursor=pointer]:
- text: رادار آینده
- img [ref=e15]
- link "آمار و داده" [ref=e17] [cursor=pointer]:
- /url: /pulse
- link "در یک نگاه" [ref=e18] [cursor=pointer]:
- /url: /at-a-glance
- button "رسانه و رویداد" [ref=e20] [cursor=pointer]:
- text: رسانه و رویداد
- img [ref=e21]
- link "شبکه خبرگان" [ref=e23] [cursor=pointer]:
- /url: /experts
- button "درباره ما" [ref=e25] [cursor=pointer]:
- text: درباره ما
- img [ref=e26]
- generic [ref=e28]:
- button "جستجو" [ref=e29] [cursor=pointer]:
- img [ref=e30]
- link "ورود / ثبت‌نام" [ref=e33] [cursor=pointer]:
- /url: /login
- button "Switch to English" [ref=e34] [cursor=pointer]: 🇺🇸
- main [ref=e35]:
- generic [ref=e36]:
- generic [ref=e37]:
- img [ref=e38]
- img [ref=e40]
- link "اندیشکده فولاد آینده The Steel Futures Institute" [ref=e48] [cursor=pointer]:
- /url: /
- generic [ref=e49]:
- generic [ref=e50]: اندیشکده فولاد آینده
- generic [ref=e51]: The Steel Futures Institute
- generic [ref=e52]:
- generic [ref=e53]: عضویت در اندیشکده
- heading "دسترسی به تحلیل‌های تخصصی صنعت فولاد ایران" [level=1] [ref=e54]:
- text: دسترسی به تحلیل‌های تخصصی
- text: صنعت فولاد ایران
- paragraph [ref=e55]: گزارش‌های بازار، رصد فناوری، و تحلیل ژئوپلیتیک — همه در یک پلتفرم
- generic [ref=e56]:
- generic [ref=e57]:
- generic [ref=e58]: +۲۰۰
- generic [ref=e59]: گزارش تخصصی
- generic [ref=e60]:
- generic [ref=e61]: ۵۰+
- generic [ref=e62]: کارشناس
- generic [ref=e63]:
- generic [ref=e64]: ۳ سال
- generic [ref=e65]: سابقه
- generic [ref=e67]:
- generic [ref=e68]:
- heading "خوش آمدید" [level=2] [ref=e69]
- paragraph [ref=e70]: برای ادامه وارد حساب خود شوید
- generic [ref=e71]:
- generic [ref=e72]:
- generic [ref=e73]: ایمیل
- generic [ref=e74]:
- textbox "example@email.com" [ref=e75]
- img [ref=e76]
- generic [ref=e79]:
- generic [ref=e80]: رمز عبور
- generic [ref=e81]:
- textbox [ref=e82]
- img [ref=e83]
- button [ref=e86] [cursor=pointer]:
- img [ref=e87]
- button "ورود به حساب" [ref=e90] [cursor=pointer]
- generic [ref=e91]:
- text: حساب ندارید؟
- link "ثبت‌نام کنید" [ref=e92] [cursor=pointer]:
- /url: /register
- link "بازگشت به سایت" [ref=e94] [cursor=pointer]:
- /url: /
- img [ref=e95]
- text: بازگشت به سایت
- contentinfo [ref=e97]:
- generic [ref=e98]:
- img [ref=e99]
- button "بازگشت به بالا" [ref=e101] [cursor=pointer]:
- img [ref=e102]
- generic [ref=e107]:
- generic [ref=e108]:
- img "اندیشکده فولاد آینده" [ref=e109]
- generic [ref=e112]: اندیشکده فولاد آینده
- generic [ref=e114]: The Steel Futures Institute
- paragraph [ref=e115]: فضایی همدلانه برای اندیشیدن به فردای صنعت؛ جایی که دانش، آینده‌پژوهی و نوآوری به بینش‌هایی برای توسعه صنعتی تبدیل می‌شوند.
- generic [ref=e116]: ما را در شبکه‌های اجتماعی دنبال کنید.
- generic [ref=e117]:
- link "آپارات" [ref=e118] [cursor=pointer]:
- /url: "#"
- img [ref=e119]
- link "X" [ref=e121] [cursor=pointer]:
- /url: "#"
- img [ref=e122]
- link "لینکداین" [ref=e124] [cursor=pointer]:
- /url: "#"
- img [ref=e125]
- link "تلگرام" [ref=e127] [cursor=pointer]:
- /url: "#"
- img [ref=e128]
- navigation "پیوندهای پایین" [ref=e130]:
- link "رادار آینده" [ref=e131] [cursor=pointer]:
- /url: /radar
- text: رادار آینده
- link "آمار و داده" [ref=e133] [cursor=pointer]:
- /url: "#"
- text: آمار و داده
- link "رسانه و رویداد" [ref=e135] [cursor=pointer]:
- /url: "#"
- text: رسانه و رویداد
- link "در یک نگاه" [ref=e137] [cursor=pointer]:
- /url: /at-a-glance
- text: در یک نگاه
- link "شبکه خبرگان" [ref=e139] [cursor=pointer]:
- /url: "#"
- text: شبکه خبرگان
- link "درباره ما" [ref=e141] [cursor=pointer]:
- /url: "#"
- text: درباره ما
- generic [ref=e143]:
- generic [ref=e144]: اطلاعات تماس
- generic [ref=e145]:
- generic [ref=e146]:
- img [ref=e147]
- generic [ref=e150]: "نشانی:"
- generic [ref=e151]: اصفهان، خیابان سعادت‌آباد
- generic [ref=e152]:
- img [ref=e153]
- generic [ref=e155]: "شماره تماس:"
- generic [ref=e156]: ۰۳۱ - ۳۸۸۸۴۵۳۹
- generic [ref=e157]:
- img [ref=e158]
- generic [ref=e161]: "پست الکترونیک:"
- generic [ref=e162]: info@steelforesight.com
- generic [ref=e163]:
- generic [ref=e165]: نشان
- generic [ref=e167]: نشان
- generic [ref=e169]: نشان
- generic [ref=e170]:
- generic [ref=e171]: عضویت در خبرنامه اندیشکده
- generic [ref=e172]:
- textbox "ایمیل خود را وارد کنید..." [ref=e173]
- button "ثبت" [ref=e174] [cursor=pointer]
- generic [ref=e176]:
- generic [ref=e177]: تمامی حقوق مادی و معنوی این وب‌سایت متعلق به شرکت فولاد مبارکه می‌باشد.
- generic [ref=e178]: طراح و پشتیبان؛ شرکت دانش‌بنیان نواندیشان آتی نگار فرتاک

View File

@ -0,0 +1,119 @@
- generic [ref=e3]:
- banner [ref=e4]:
- generic [ref=e6]:
- link "اندیشکده فولاد آینده The Steel Futures Institute" [ref=e7] [cursor=pointer]:
- /url: /
- img [ref=e8]
- generic [ref=e9]:
- generic [ref=e10]: اندیشکده فولاد آینده
- generic [ref=e11]: The Steel Futures Institute
- navigation "ناوبری اصلی" [ref=e12]:
- button "رادار آینده" [ref=e14] [cursor=pointer]:
- text: رادار آینده
- img [ref=e15]
- link "آمار و داده" [ref=e17] [cursor=pointer]:
- /url: /pulse
- link "در یک نگاه" [ref=e18] [cursor=pointer]:
- /url: /at-a-glance
- button "رسانه و رویداد" [ref=e20] [cursor=pointer]:
- text: رسانه و رویداد
- img [ref=e21]
- link "شبکه خبرگان" [ref=e23] [cursor=pointer]:
- /url: /experts
- button "درباره ما" [ref=e25] [cursor=pointer]:
- text: درباره ما
- img [ref=e26]
- generic [ref=e28]:
- button "جستجو" [ref=e29] [cursor=pointer]:
- img [ref=e30]
- link "ورود / ثبت‌نام" [ref=e33] [cursor=pointer]:
- /url: /login
- button "Switch to English" [ref=e34] [cursor=pointer]: 🇺🇸
- main [ref=e35]:
- generic [ref=e37]:
- generic [ref=e38]:
- img [ref=e40]
- heading "ورود به حساب" [level=1] [ref=e43]
- generic [ref=e44]:
- generic [ref=e45]:
- generic [ref=e46]: ایمیل
- textbox "example@email.com" [ref=e47]
- generic [ref=e48]:
- generic [ref=e49]: رمز عبور
- textbox [ref=e50]
- button "ورود" [ref=e51] [cursor=pointer]
- paragraph [ref=e52]:
- text: حساب ندارید؟
- link "ثبت‌نام کنید" [ref=e53] [cursor=pointer]:
- /url: /register
- contentinfo [ref=e54]:
- generic [ref=e55]:
- img [ref=e56]
- button "بازگشت به بالا" [ref=e58] [cursor=pointer]:
- img [ref=e59]
- generic [ref=e64]:
- generic [ref=e65]:
- img "اندیشکده فولاد آینده" [ref=e66]
- generic [ref=e69]: اندیشکده فولاد آینده
- generic [ref=e71]: The Steel Futures Institute
- paragraph [ref=e72]: فضایی همدلانه برای اندیشیدن به فردای صنعت؛ جایی که دانش، آینده‌پژوهی و نوآوری به بینش‌هایی برای توسعه صنعتی تبدیل می‌شوند.
- generic [ref=e73]: ما را در شبکه‌های اجتماعی دنبال کنید.
- generic [ref=e74]:
- link "آپارات" [ref=e75] [cursor=pointer]:
- /url: "#"
- img [ref=e76]
- link "X" [ref=e78] [cursor=pointer]:
- /url: "#"
- img [ref=e79]
- link "لینکداین" [ref=e81] [cursor=pointer]:
- /url: "#"
- img [ref=e82]
- link "تلگرام" [ref=e84] [cursor=pointer]:
- /url: "#"
- img [ref=e85]
- navigation "پیوندهای پایین" [ref=e87]:
- link "رادار آینده" [ref=e88] [cursor=pointer]:
- /url: /radar
- text: رادار آینده
- link "آمار و داده" [ref=e90] [cursor=pointer]:
- /url: "#"
- text: آمار و داده
- link "رسانه و رویداد" [ref=e92] [cursor=pointer]:
- /url: "#"
- text: رسانه و رویداد
- link "در یک نگاه" [ref=e94] [cursor=pointer]:
- /url: /at-a-glance
- text: در یک نگاه
- link "شبکه خبرگان" [ref=e96] [cursor=pointer]:
- /url: "#"
- text: شبکه خبرگان
- link "درباره ما" [ref=e98] [cursor=pointer]:
- /url: "#"
- text: درباره ما
- generic [ref=e100]:
- generic [ref=e101]: اطلاعات تماس
- generic [ref=e102]:
- generic [ref=e103]:
- img [ref=e104]
- generic [ref=e107]: "نشانی:"
- generic [ref=e108]: اصفهان، خیابان سعادت‌آباد
- generic [ref=e109]:
- img [ref=e110]
- generic [ref=e112]: "شماره تماس:"
- generic [ref=e113]: ۰۳۱ - ۳۸۸۸۴۵۳۹
- generic [ref=e114]:
- img [ref=e115]
- generic [ref=e118]: "پست الکترونیک:"
- generic [ref=e119]: info@steelforesight.com
- generic [ref=e120]:
- generic [ref=e122]: نشان
- generic [ref=e124]: نشان
- generic [ref=e126]: نشان
- generic [ref=e127]:
- generic [ref=e128]: عضویت در خبرنامه اندیشکده
- generic [ref=e129]:
- textbox "ایمیل خود را وارد کنید..." [ref=e130]
- button "ثبت" [ref=e131] [cursor=pointer]
- generic [ref=e133]:
- generic [ref=e134]: تمامی حقوق مادی و معنوی این وب‌سایت متعلق به شرکت فولاد مبارکه می‌باشد.
- generic [ref=e135]: طراح و پشتیبان؛ شرکت دانش‌بنیان نواندیشان آتی نگار فرتاک

View File

@ -0,0 +1,121 @@
- generic [ref=e3]:
- banner [ref=e4]:
- generic [ref=e6]:
- link "اندیشکده فولاد آینده The Steel Futures Institute" [ref=e7] [cursor=pointer]:
- /url: /
- img [ref=e8]
- generic [ref=e9]:
- generic [ref=e10]: اندیشکده فولاد آینده
- generic [ref=e11]: The Steel Futures Institute
- navigation "ناوبری اصلی" [ref=e12]:
- button "رادار آینده" [ref=e14] [cursor=pointer]:
- text: رادار آینده
- img [ref=e15]
- link "آمار و داده" [ref=e17] [cursor=pointer]:
- /url: /pulse
- link "در یک نگاه" [ref=e18] [cursor=pointer]:
- /url: /at-a-glance
- button "رسانه و رویداد" [ref=e20] [cursor=pointer]:
- text: رسانه و رویداد
- img [ref=e21]
- link "شبکه خبرگان" [ref=e23] [cursor=pointer]:
- /url: /experts
- button "درباره ما" [ref=e25] [cursor=pointer]:
- text: درباره ما
- img [ref=e26]
- generic [ref=e28]:
- button "جستجو" [ref=e29] [cursor=pointer]:
- img [ref=e30]
- link "علیرضا" [ref=e33] [cursor=pointer]:
- /url: /profile
- img [ref=e34]
- text: علیرضا
- button "Switch to English" [ref=e37] [cursor=pointer]: 🇺🇸
- main [ref=e38]:
- generic [ref=e40]:
- generic [ref=e41]:
- img [ref=e43]
- heading "ورود به حساب" [level=1] [ref=e46]
- generic [ref=e47]:
- generic [ref=e48]:
- generic [ref=e49]: ایمیل
- textbox "example@email.com" [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: رمز عبور
- textbox [ref=e53]
- button "ورود" [ref=e54] [cursor=pointer]
- paragraph [ref=e55]:
- text: حساب ندارید؟
- link "ثبت‌نام کنید" [ref=e56] [cursor=pointer]:
- /url: /register
- contentinfo [ref=e57]:
- generic [ref=e58]:
- img [ref=e59]
- button "بازگشت به بالا" [ref=e61] [cursor=pointer]:
- img [ref=e62]
- generic [ref=e67]:
- generic [ref=e68]:
- img "اندیشکده فولاد آینده" [ref=e69]
- generic [ref=e72]: اندیشکده فولاد آینده
- generic [ref=e74]: The Steel Futures Institute
- paragraph [ref=e75]: فضایی همدلانه برای اندیشیدن به فردای صنعت؛ جایی که دانش، آینده‌پژوهی و نوآوری به بینش‌هایی برای توسعه صنعتی تبدیل می‌شوند.
- generic [ref=e76]: ما را در شبکه‌های اجتماعی دنبال کنید.
- generic [ref=e77]:
- link "آپارات" [ref=e78] [cursor=pointer]:
- /url: "#"
- img [ref=e79]
- link "X" [ref=e81] [cursor=pointer]:
- /url: "#"
- img [ref=e82]
- link "لینکداین" [ref=e84] [cursor=pointer]:
- /url: "#"
- img [ref=e85]
- link "تلگرام" [ref=e87] [cursor=pointer]:
- /url: "#"
- img [ref=e88]
- navigation "پیوندهای پایین" [ref=e90]:
- link "رادار آینده" [ref=e91] [cursor=pointer]:
- /url: /radar
- text: رادار آینده
- link "آمار و داده" [ref=e93] [cursor=pointer]:
- /url: "#"
- text: آمار و داده
- link "رسانه و رویداد" [ref=e95] [cursor=pointer]:
- /url: "#"
- text: رسانه و رویداد
- link "در یک نگاه" [ref=e97] [cursor=pointer]:
- /url: /at-a-glance
- text: در یک نگاه
- link "شبکه خبرگان" [ref=e99] [cursor=pointer]:
- /url: "#"
- text: شبکه خبرگان
- link "درباره ما" [ref=e101] [cursor=pointer]:
- /url: "#"
- text: درباره ما
- generic [ref=e103]:
- generic [ref=e104]: اطلاعات تماس
- generic [ref=e105]:
- generic [ref=e106]:
- img [ref=e107]
- generic [ref=e110]: "نشانی:"
- generic [ref=e111]: اصفهان، خیابان سعادت‌آباد
- generic [ref=e112]:
- img [ref=e113]
- generic [ref=e115]: "شماره تماس:"
- generic [ref=e116]: ۰۳۱ - ۳۸۸۸۴۵۳۹
- generic [ref=e117]:
- img [ref=e118]
- generic [ref=e121]: "پست الکترونیک:"
- generic [ref=e122]: info@steelforesight.com
- generic [ref=e123]:
- generic [ref=e125]: نشان
- generic [ref=e127]: نشان
- generic [ref=e129]: نشان
- generic [ref=e130]:
- generic [ref=e131]: عضویت در خبرنامه اندیشکده
- generic [ref=e132]:
- textbox "ایمیل خود را وارد کنید..." [ref=e133]
- button "ثبت" [ref=e134] [cursor=pointer]
- generic [ref=e136]:
- generic [ref=e137]: تمامی حقوق مادی و معنوی این وب‌سایت متعلق به شرکت فولاد مبارکه می‌باشد.
- generic [ref=e138]: طراح و پشتیبان؛ شرکت دانش‌بنیان نواندیشان آتی نگار فرتاک

View File

@ -0,0 +1,119 @@
- generic [ref=e3]:
- banner [ref=e4]:
- generic [ref=e6]:
- link "اندیشکده فولاد آینده The Steel Futures Institute" [ref=e7] [cursor=pointer]:
- /url: /
- img [ref=e8]
- generic [ref=e9]:
- generic [ref=e10]: اندیشکده فولاد آینده
- generic [ref=e11]: The Steel Futures Institute
- navigation "ناوبری اصلی" [ref=e12]:
- button "رادار آینده" [ref=e14] [cursor=pointer]:
- text: رادار آینده
- img [ref=e15]
- link "آمار و داده" [ref=e17] [cursor=pointer]:
- /url: /pulse
- link "در یک نگاه" [ref=e18] [cursor=pointer]:
- /url: /at-a-glance
- button "رسانه و رویداد" [ref=e20] [cursor=pointer]:
- text: رسانه و رویداد
- img [ref=e21]
- link "شبکه خبرگان" [ref=e23] [cursor=pointer]:
- /url: /experts
- button "درباره ما" [ref=e25] [cursor=pointer]:
- text: درباره ما
- img [ref=e26]
- generic [ref=e28]:
- button "جستجو" [ref=e29] [cursor=pointer]:
- img [ref=e30]
- link "ورود / ثبت‌نام" [ref=e33] [cursor=pointer]:
- /url: /login
- button "Switch to English" [ref=e34] [cursor=pointer]: 🇺🇸
- main [ref=e35]:
- generic [ref=e37]:
- generic [ref=e38]:
- img [ref=e40]
- heading "ورود به حساب" [level=1] [ref=e43]
- generic [ref=e44]:
- generic [ref=e45]:
- generic [ref=e46]: ایمیل
- textbox "example@email.com" [ref=e47]
- generic [ref=e48]:
- generic [ref=e49]: رمز عبور
- textbox [ref=e50]
- button "ورود" [ref=e51] [cursor=pointer]
- paragraph [ref=e52]:
- text: حساب ندارید؟
- link "ثبت‌نام کنید" [ref=e53] [cursor=pointer]:
- /url: /register
- contentinfo [ref=e54]:
- generic [ref=e55]:
- img [ref=e56]
- button "بازگشت به بالا" [ref=e58] [cursor=pointer]:
- img [ref=e59]
- generic [ref=e64]:
- generic [ref=e65]:
- img "اندیشکده فولاد آینده" [ref=e66]
- generic [ref=e69]: اندیشکده فولاد آینده
- generic [ref=e71]: The Steel Futures Institute
- paragraph [ref=e72]: فضایی همدلانه برای اندیشیدن به فردای صنعت؛ جایی که دانش، آینده‌پژوهی و نوآوری به بینش‌هایی برای توسعه صنعتی تبدیل می‌شوند.
- generic [ref=e73]: ما را در شبکه‌های اجتماعی دنبال کنید.
- generic [ref=e74]:
- link "آپارات" [ref=e75] [cursor=pointer]:
- /url: "#"
- img [ref=e76]
- link "X" [ref=e78] [cursor=pointer]:
- /url: "#"
- img [ref=e79]
- link "لینکداین" [ref=e81] [cursor=pointer]:
- /url: "#"
- img [ref=e82]
- link "تلگرام" [ref=e84] [cursor=pointer]:
- /url: "#"
- img [ref=e85]
- navigation "پیوندهای پایین" [ref=e87]:
- link "رادار آینده" [ref=e88] [cursor=pointer]:
- /url: /radar
- text: رادار آینده
- link "آمار و داده" [ref=e90] [cursor=pointer]:
- /url: "#"
- text: آمار و داده
- link "رسانه و رویداد" [ref=e92] [cursor=pointer]:
- /url: "#"
- text: رسانه و رویداد
- link "در یک نگاه" [ref=e94] [cursor=pointer]:
- /url: /at-a-glance
- text: در یک نگاه
- link "شبکه خبرگان" [ref=e96] [cursor=pointer]:
- /url: "#"
- text: شبکه خبرگان
- link "درباره ما" [ref=e98] [cursor=pointer]:
- /url: "#"
- text: درباره ما
- generic [ref=e100]:
- generic [ref=e101]: اطلاعات تماس
- generic [ref=e102]:
- generic [ref=e103]:
- img [ref=e104]
- generic [ref=e107]: "نشانی:"
- generic [ref=e108]: اصفهان، خیابان سعادت‌آباد
- generic [ref=e109]:
- img [ref=e110]
- generic [ref=e112]: "شماره تماس:"
- generic [ref=e113]: ۰۳۱ - ۳۸۸۸۴۵۳۹
- generic [ref=e114]:
- img [ref=e115]
- generic [ref=e118]: "پست الکترونیک:"
- generic [ref=e119]: info@steelforesight.com
- generic [ref=e120]:
- generic [ref=e122]: نشان
- generic [ref=e124]: نشان
- generic [ref=e126]: نشان
- generic [ref=e127]:
- generic [ref=e128]: عضویت در خبرنامه اندیشکده
- generic [ref=e129]:
- textbox "ایمیل خود را وارد کنید..." [ref=e130]
- button "ثبت" [ref=e131] [cursor=pointer]
- generic [ref=e133]:
- generic [ref=e134]: تمامی حقوق مادی و معنوی این وب‌سایت متعلق به شرکت فولاد مبارکه می‌باشد.
- generic [ref=e135]: طراح و پشتیبان؛ شرکت دانش‌بنیان نواندیشان آتی نگار فرتاک

View File

@ -0,0 +1,174 @@
- generic [ref=e3]:
- banner [ref=e4]:
- generic [ref=e6]:
- link "اندیشکده فولاد آینده The Steel Futures Institute" [ref=e7] [cursor=pointer]:
- /url: /
- img [ref=e8]
- generic [ref=e9]:
- generic [ref=e10]: اندیشکده فولاد آینده
- generic [ref=e11]: The Steel Futures Institute
- navigation "ناوبری اصلی" [ref=e12]:
- button "رادار آینده" [ref=e14] [cursor=pointer]:
- text: رادار آینده
- img [ref=e15]
- link "آمار و داده" [ref=e17] [cursor=pointer]:
- /url: /pulse
- link "در یک نگاه" [ref=e18] [cursor=pointer]:
- /url: /at-a-glance
- button "رسانه و رویداد" [ref=e20] [cursor=pointer]:
- text: رسانه و رویداد
- img [ref=e21]
- link "شبکه خبرگان" [ref=e23] [cursor=pointer]:
- /url: /experts
- button "درباره ما" [ref=e25] [cursor=pointer]:
- text: درباره ما
- img [ref=e26]
- generic [ref=e28]:
- button "جستجو" [ref=e29] [cursor=pointer]:
- img [ref=e30]
- link "ورود / ثبت‌نام" [ref=e33] [cursor=pointer]:
- /url: /login
- button "Switch to English" [ref=e34] [cursor=pointer]: 🇺🇸
- main [ref=e35]:
- generic [ref=e36]:
- generic [ref=e37]:
- img [ref=e38]
- img
- link "اندیشکده فولاد آینده The Steel Futures Institute" [ref=e41] [cursor=pointer]:
- /url: /
- generic [ref=e42]:
- generic [ref=e43]: اندیشکده فولاد آینده
- generic [ref=e44]: The Steel Futures Institute
- generic [ref=e45]:
- generic [ref=e46]: عضویت رایگان
- heading "عضو اندیشکده شوید و به تحلیل‌ها دسترسی داشته باشید" [level=1] [ref=e47]:
- text: عضو اندیشکده شوید
- text: و به تحلیل‌ها دسترسی داشته باشید
- paragraph [ref=e48]: گزارش‌های بازار، رصد فناوری، و تحلیل ژئوپلیتیک — همه در یک پلتفرم
- generic [ref=e50]:
- generic [ref=e51]:
- img [ref=e53]
- text: دسترسی به گزارش‌های تخصصی
- generic [ref=e55]:
- img [ref=e57]
- text: رصد بازار و قیمت‌ها
- generic [ref=e59]:
- img [ref=e61]
- text: شبکه خبرگان صنعت
- generic [ref=e64]:
- generic [ref=e65]:
- button "ورود" [ref=e66] [cursor=pointer]
- button "ثبت‌نام" [ref=e67] [cursor=pointer]
- generic [ref=e68]:
- generic [ref=e69]:
- heading "ثبت‌نام" [level=2] [ref=e70]
- paragraph [ref=e71]: یک حساب کاربری جدید بسازید
- generic [ref=e72]:
- generic [ref=e73]: نام و نام خانوادگی
- generic [ref=e74]:
- textbox "علی محمدی" [ref=e75]
- generic:
- img
- generic [ref=e76]:
- generic [ref=e77]: ایمیل
- generic [ref=e78]:
- textbox "example@email.com" [ref=e79]
- generic:
- img
- generic [ref=e80]:
- generic [ref=e81]: شماره تماس*
- generic [ref=e82]:
- textbox "09123456789" [ref=e83]
- generic:
- img
- generic [ref=e84]:
- generic [ref=e85]: نام سازمان / شرکت (اختیاری)
- generic [ref=e86]:
- textbox "شرکت فولاد..." [ref=e87]
- generic:
- img
- generic [ref=e88]:
- generic [ref=e89]: رمز عبور
- generic [ref=e90]:
- textbox [ref=e91]
- generic:
- img
- button [ref=e93] [cursor=pointer]:
- img [ref=e94]
- paragraph [ref=e97]: حداقل ۶ کاراکتر
- button "ساخت حساب کاربری" [ref=e98] [cursor=pointer]
- link "بازگشت به سایت" [ref=e100] [cursor=pointer]:
- /url: /
- img [ref=e101]
- text: بازگشت به سایت
- contentinfo [ref=e103]:
- generic [ref=e104]:
- img [ref=e105]
- button "بازگشت به بالا" [ref=e107] [cursor=pointer]:
- img [ref=e108]
- generic [ref=e113]:
- generic [ref=e114]:
- img "اندیشکده فولاد آینده" [ref=e115]
- generic [ref=e118]: اندیشکده فولاد آینده
- generic [ref=e120]: The Steel Futures Institute
- paragraph [ref=e121]: فضایی همدلانه برای اندیشیدن به فردای صنعت؛ جایی که دانش، آینده‌پژوهی و نوآوری به بینش‌هایی برای توسعه صنعتی تبدیل می‌شوند.
- generic [ref=e122]: ما را در شبکه‌های اجتماعی دنبال کنید.
- generic [ref=e123]:
- link "آپارات" [ref=e124] [cursor=pointer]:
- /url: "#"
- img [ref=e125]
- link "X" [ref=e127] [cursor=pointer]:
- /url: "#"
- img [ref=e128]
- link "لینکداین" [ref=e130] [cursor=pointer]:
- /url: "#"
- img [ref=e131]
- link "تلگرام" [ref=e133] [cursor=pointer]:
- /url: "#"
- img [ref=e134]
- navigation "پیوندهای پایین" [ref=e136]:
- link "رادار آینده" [ref=e137] [cursor=pointer]:
- /url: /radar
- text: رادار آینده
- link "آمار و داده" [ref=e139] [cursor=pointer]:
- /url: "#"
- text: آمار و داده
- link "رسانه و رویداد" [ref=e141] [cursor=pointer]:
- /url: "#"
- text: رسانه و رویداد
- link "در یک نگاه" [ref=e143] [cursor=pointer]:
- /url: /at-a-glance
- text: در یک نگاه
- link "شبکه خبرگان" [ref=e145] [cursor=pointer]:
- /url: "#"
- text: شبکه خبرگان
- link "درباره ما" [ref=e147] [cursor=pointer]:
- /url: "#"
- text: درباره ما
- generic [ref=e149]:
- generic [ref=e150]: اطلاعات تماس
- generic [ref=e151]:
- generic [ref=e152]:
- img [ref=e153]
- generic [ref=e156]: "نشانی:"
- generic [ref=e157]: اصفهان، خیابان سعادت‌آباد
- generic [ref=e158]:
- img [ref=e159]
- generic [ref=e161]: "شماره تماس:"
- generic [ref=e162]: ۰۳۱ - ۳۸۸۸۴۵۳۹
- generic [ref=e163]:
- img [ref=e164]
- generic [ref=e167]: "پست الکترونیک:"
- generic [ref=e168]: info@steelforesight.com
- generic [ref=e169]:
- generic [ref=e171]: نشان
- generic [ref=e173]: نشان
- generic [ref=e175]: نشان
- generic [ref=e176]:
- generic [ref=e177]: عضویت در خبرنامه اندیشکده
- generic [ref=e178]:
- textbox "ایمیل خود را وارد کنید..." [ref=e179]
- button "ثبت" [ref=e180] [cursor=pointer]
- generic [ref=e182]:
- generic [ref=e183]: تمامی حقوق مادی و معنوی این وب‌سایت متعلق به شرکت فولاد مبارکه می‌باشد.
- generic [ref=e184]: طراح و پشتیبان؛ شرکت دانش‌بنیان نواندیشان آتی نگار فرتاک

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -0,0 +1,135 @@
- generic [ref=e3]:
- banner [ref=e4]:
- generic [ref=e6]:
- link "اندیشکده فولاد آینده" [ref=e7] [cursor=pointer]:
- /url: /
- img [ref=e8]
- generic [ref=e10]: اندیشکده فولاد آینده
- generic [ref=e11]:
- button "جستجو" [ref=e12] [cursor=pointer]:
- img [ref=e13]
- button "باز کردن منو" [ref=e16] [cursor=pointer]:
- img [ref=e17]
- main:
- generic [ref=e19]:
- generic [ref=e20]:
- link "اندیشکده فولاد آینده" [ref=e21] [cursor=pointer]:
- /url: /
- generic [ref=e22]: اندیشکده فولاد آینده
- link "بازگشت" [ref=e23] [cursor=pointer]:
- /url: /
- img [ref=e24]
- text: بازگشت
- generic [ref=e26]:
- generic [ref=e27]:
- button "ورود" [ref=e28] [cursor=pointer]
- button "ثبت‌نام" [ref=e29] [cursor=pointer]
- generic [ref=e30]:
- generic [ref=e31]:
- heading "ثبت‌نام" [level=2] [ref=e32]
- paragraph [ref=e33]: یک حساب کاربری جدید بسازید
- generic [ref=e34]:
- generic [ref=e35]: نام و نام خانوادگی
- generic [ref=e36]:
- textbox "علی محمدی" [ref=e37]
- generic:
- img
- generic [ref=e38]:
- generic [ref=e39]: ایمیل
- generic [ref=e40]:
- textbox "example@email.com" [ref=e41]
- generic:
- img
- generic [ref=e42]:
- generic [ref=e43]: شماره تماس*
- generic [ref=e44]:
- textbox "09123456789" [ref=e45]
- generic:
- img
- generic [ref=e46]:
- generic [ref=e47]: نام سازمان / شرکت (اختیاری)
- generic [ref=e48]:
- textbox "شرکت فولاد..." [ref=e49]
- generic:
- img
- generic [ref=e50]:
- generic [ref=e51]: رمز عبور
- generic [ref=e52]:
- textbox [ref=e53]
- generic:
- img
- button [ref=e55] [cursor=pointer]:
- img [ref=e56]
- paragraph [ref=e59]: حداقل ۶ کاراکتر
- button "ساخت حساب کاربری" [ref=e60] [cursor=pointer]
- contentinfo [ref=e61]:
- generic [ref=e62]:
- img [ref=e63]
- button "بازگشت به بالا" [ref=e65] [cursor=pointer]:
- img [ref=e66]
- generic [ref=e71]:
- generic [ref=e72]:
- img "اندیشکده فولاد آینده" [ref=e73]
- generic [ref=e76]: اندیشکده فولاد آینده
- generic [ref=e78]: The Steel Futures Institute
- paragraph [ref=e79]: فضایی همدلانه برای اندیشیدن به فردای صنعت؛ جایی که دانش، آینده‌پژوهی و نوآوری به بینش‌هایی برای توسعه صنعتی تبدیل می‌شوند.
- generic [ref=e80]: ما را در شبکه‌های اجتماعی دنبال کنید.
- generic [ref=e81]:
- link "آپارات" [ref=e82] [cursor=pointer]:
- /url: "#"
- img [ref=e83]
- link "X" [ref=e85] [cursor=pointer]:
- /url: "#"
- img [ref=e86]
- link "لینکداین" [ref=e88] [cursor=pointer]:
- /url: "#"
- img [ref=e89]
- link "تلگرام" [ref=e91] [cursor=pointer]:
- /url: "#"
- img [ref=e92]
- navigation "پیوندهای پایین" [ref=e94]:
- link "رادار آینده" [ref=e95] [cursor=pointer]:
- /url: /radar
- text: رادار آینده
- link "آمار و داده" [ref=e97] [cursor=pointer]:
- /url: "#"
- text: آمار و داده
- link "رسانه و رویداد" [ref=e99] [cursor=pointer]:
- /url: "#"
- text: رسانه و رویداد
- link "در یک نگاه" [ref=e101] [cursor=pointer]:
- /url: /at-a-glance
- text: در یک نگاه
- link "شبکه خبرگان" [ref=e103] [cursor=pointer]:
- /url: "#"
- text: شبکه خبرگان
- link "درباره ما" [ref=e105] [cursor=pointer]:
- /url: "#"
- text: درباره ما
- generic [ref=e107]:
- generic [ref=e108]: اطلاعات تماس
- generic [ref=e109]:
- generic [ref=e110]:
- img [ref=e111]
- generic [ref=e114]: "نشانی:"
- generic [ref=e115]: اصفهان، خیابان سعادت‌آباد
- generic [ref=e116]:
- img [ref=e117]
- generic [ref=e119]: "شماره تماس:"
- generic [ref=e120]: ۰۳۱ - ۳۸۸۸۴۵۳۹
- generic [ref=e121]:
- img [ref=e122]
- generic [ref=e125]: "پست الکترونیک:"
- generic [ref=e126]: info@steelforesight.com
- generic [ref=e127]:
- generic [ref=e129]: نشان
- generic [ref=e131]: نشان
- generic [ref=e133]: نشان
- generic [ref=e134]:
- generic [ref=e135]: عضویت در خبرنامه اندیشکده
- generic [ref=e136]:
- textbox "ایمیل خود را وارد کنید..." [ref=e137]
- button "ثبت" [ref=e138] [cursor=pointer]
- generic [ref=e140]:
- generic [ref=e141]: تمامی حقوق مادی و معنوی این وب‌سایت متعلق به شرکت فولاد مبارکه می‌باشد.
- generic [ref=e142]: طراح و پشتیبان؛ شرکت دانش‌بنیان نواندیشان آتی نگار فرتاک

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,10 @@
- generic [ref=e4]:
- img [ref=e5]:
- generic:
- generic: "403"
- generic [ref=e11]: شما به این محتوا دسترسی ندارید!
- link [ref=e12] [cursor=pointer]:
- /url: https://www.aspaco.org
- img [ref=e13]
- link "تماس با پشتیبان" [ref=e15] [cursor=pointer]:
- /url: https://www.aspaco.org/?_=/fa#contact

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

View File

@ -9,6 +9,12 @@ export const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
// safe migrations — ignore if column already exists
for (const sql of [
"ALTER TABLE articles ADD COLUMN status TEXT DEFAULT 'published'",
"ALTER TABLE articles ADD COLUMN key_data TEXT",
]) { try { db.exec(sql) } catch {} }
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -290,6 +296,29 @@ db.exec(`
source_url TEXT,
fetched_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
phone TEXT,
avatar TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_members_email ON members(email);
CREATE TABLE IF NOT EXISTS purchases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
member_id INTEGER NOT NULL REFERENCES members(id) ON DELETE CASCADE,
article_id TEXT NOT NULL REFERENCES articles(id) ON DELETE CASCADE,
amount INTEGER NOT NULL DEFAULT 0,
purchased_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(member_id, article_id)
);
CREATE INDEX IF NOT EXISTS idx_purchases_member ON purchases(member_id, purchased_at DESC);
`);
/* idempotent column migrations for already-created tables */

194
panel/imports-data.json Normal file
View File

@ -0,0 +1,194 @@
[
{
"report": {
"id": "green-premia-trends",
"coverImage": "/news/photo-1473341304170-971dccb5ac1e.jpg",
"title": "روندها و چشم‌انداز پاداش سبز مواد تا سال ۲۰۳۰",
"category": "انرژی و ESG",
"type": "free",
"author": "کریستین هافمن",
"authorRole": "شریک ارشد",
"authorInitial": "ه",
"publishDate": "شهریور ۱۴۰۳",
"pages": 9,
"price": 0,
"isFree": true,
"summary": "خریداران و تامین‌کنندگان در حال ارزیابی مجدد تقاضا برای مواد کم‌کربن در مواجهه با چالش‌های اقتصاد کلان هستند. پیش‌بینی می‌شود که تا انتهای دهه جاری میزان تقاضا برای این کالاها تا چندین برابر افزایش یابد. تولیدکنندگان برای بهره‌گیری از این فرصت ملزم به افزایش شفافیت زنجیره تامین خود می‌باشند.",
"tags": ["مواد سبز", "کربن‌زدایی", "پاداش سبز", "توسعه پایدار"],
"featured": false
},
"fa_summary_long": "بازار کالاها و مواد پایدار به تازگی با موانع اقتصادی متعددی روبه‌رو شده است که پرسش‌های مهمی را درباره آینده پاداش‌های پرداختی برای این محصولات ایجاد می‌کند. با وجود هزینه‌های بالای انرژی و کاهش قیمت کربن که موجب نگرانی‌هایی درباره کند شدن پروژه‌های زیست‌محیطی شده است تقاضا همچنان قدرتمند باقی مانده است. خریداران در صنایع مختلف اراده جدی خود را برای ادامه مسیر کاهش آلایندگی‌ها نشان می‌دهند.\n\nنظرسنجیها نشان می‌دهد که بیش از ۹۰ درصد پاسخ‌دهندگان شاهد ثبات یا رشد تقاضا برای کالاهای سبز طی یک سال گذشته بوده‌اند. این رشد تا حد زیادی با تعهدات شرکت‌ها برای کاهش انتشار در دامنه ۳ (Scope 3) هدایت می‌شود. در واقع انتظار می‌رود تا سال ۲۰۳۰ سهم این مواد در خریدهای کلی صنایع از یک چهارم برای مس تا بیش از ۴۰ درصد برای فولاد و پلاستیک متغیر باشد.\n\nنکته جالب توجه در این گزارش تفاوت‌های چشمگیر در میزان تمایل به پرداخت این پاداش‌ها میان بخش‌ها و مناطق مختلف است. بازارهای اروپایی به طور مداوم هزینه‌های بیشتری را نسبت به همتایان خود در آمریکای شمالی برای این مواد پرداخت می‌کنند. با این وجود حدود ۶۰ درصد از پاسخ‌دهندگان اظهار داشته‌اند که در صورت کمبود عرضه تا سال ۲۰۳۰ آماده پرداخت مبالغ اضافی برای تضمین تامین کالاهای مورد نیاز خود هستند.\n\nدر کنار رشد تقاضا تعاریف و استانداردهای مربوط به محصولات سبز نیز به شدت سخت‌گیرانه‌تر خواهند شد. به عنوان نمونه نیمی از خریداران فولاد انتظار دارند که معیار فولاد سبز تا سال ۲۰۳۰ به کمتر از ۰.۳ تن دی‌اکسید کربن به ازای هر تن محصول محدود شود. این تغییرات نیازمند ردیابی دقیق و تایید انتشار گازهای گلخانه‌ای در تمامی مراحل زنجیره تامین است.\n\nبرای تامین‌کنندگان این شرایط فرصت‌های بی‌نظیری را برای کسب درآمد بیشتر رقم می‌زند اما موفقیت در آن نیازمند استراتژی‌های هدفمند است. اولویت‌بندی بخش‌های بازار با بالاترین تقاضای بی‌پاسخ و ایمن‌سازی زودهنگام مواد خام پایدار از جمله اقداماتی است که به شرکت‌ها برتری رقابتی می‌دهد. در نهایت همکاری نزدیک در سراسر زنجیره ارزش برای تضمین پایداری اقتصادی و زیست‌محیطی ضروری خواهد بود.",
"radar_items": [
{
"id": "green-demand-growth",
"title": "رشد تقاضای مواد پایدار",
"summary": "پیش‌بینی افزایش ۴.۵ برابری تقاضا برای کالاهای پایدار تا سال ۲۰۳۰ توسط خریداران ثبت شده است.",
"category": "market",
"date": "شهریور ۱۴۰۳"
},
{
"id": "stricter-green-definitions",
"title": "سخت‌گیرانه‌تر شدن تعاریف",
"summary": "استانداردهای تشخیص کالای پاک تا انتهای دهه جاری با تمرکز بر دامنه ۳ تغییرات جدی خواهند کرد.",
"category": "tech",
"date": "شهریور ۱۴۰۳"
},
{
"id": "premium-willingness",
"title": "تمایل به پرداخت حق بیمه",
"summary": "نزدیک به ۶۰ درصد صنایع آماده پرداخت مبالغ اضافی برای تضمین تامین مواد اولیه پاک در آینده هستند.",
"category": "energy",
"date": "شهریور ۱۴۰۳"
}
],
"key_data": {
"سهم فولاد سبز در ۲۰۳۰": "بیش از ۴۰ درصد",
"رشد تقاضای مواد سبز": "تا ۴.۵ برابر",
"استاندارد انتشار فولاد پاک ۲۰۳۰": "کمتر از ۰.۳ تن کربن"
}
},
{
"report": {
"id": "commodity-trading-scurve",
"coverImage": "/news/photo-1611974789855-9c2a0a7236a3.jpg",
"title": "چگونه منحنی اس بعدی را در تجارت کالا تسخیر کنیم",
"category": "بازار جهانی",
"type": "free",
"author": "یوشا شابرام",
"authorRole": "شریک ارشد",
"authorInitial": "ش",
"publishDate": "بهمن ۱۴۰۳",
"pages": 16,
"price": 0,
"isFree": true,
"summary": "سودآوری استثنایی سال‌های گذشته در بازارهای تجاری کالا در حال عادی شدن است و حاشیه سودها را کاهش می‌دهد. تجارت کالاها با وجود این فشارهای رقابتی همچنان مسیری رو به رشد تا انتهای این دهه خواهد داشت. معامله‌گران برای بقا و موفقیت باید مدل‌های عملیاتی خود را بازنگری کرده ابزارهای دیجیتال را یکپارچه سازند.",
"tags": ["تجارت کالا", "بهینه‌سازی زنجیره ارزش", "دیجیتال‌سازی", "بازارهای نوظهور"],
"featured": false
},
"fa_summary_long": "تجارت کالاهای پایه پس از گذراندن یک دوره رونق و سودآوری بی‌سابقه اکنون در حال ورود به فاز جدیدی از عادی‌سازی بازار است. در سال‌های ۲۰۲۲ و ۲۰۲۳ نوسانات شدید باعث افزایش چشمگیر حاشیه سود در این صنعت شد اما سال ۲۰۲۴ با فشرده شدن این حاشیه‌ها و افزایش رقابت همراه بود. با این وجود تحقیقات نشان می‌دهد که استخرهای ارزش در این حوزه با رشد سالانه ۱۰ درصد تا پایان دهه جاری به گسترش خود ادامه می‌دهند.\n\nیکی از تحولات مهم در این بازار تغییر مرکز ثقل سودآوری از کالاهای نفتی به سمت برق و گاز طبیعی مایع (LNG) است. افزایش استفاده از انرژی‌های تجدیدپذیر به همراه رشد چشمگیر تقاضا از سوی مراکز داده (Data Centers) باعث ایجاد نوسانات و فرصت‌های تجاری جدید در بخش برق شده است. این در حالی است که حاشیه سود فرآورده‌های نفتی در مسیر کاهشی قرار دارد.\n\nبرای پاسخ به این محیط تجاری جدید شرکت‌ها نیازمند بهینه‌سازی جامع زنجیره ارزش خود هستند. اجرای راهکارهای پیشرفته دیجیتال و شکستن سیلوهای سازمانی می‌تواند ارزش قابل توجهی را به دارایی‌های موجود اضافه کند. شرکت‌هایی که بتوانند این یکپارچگی را ایجاد کنند درآمد مازاد چشمگیری را نسبت به رقبای سنتی خود تجربه خواهند کرد.\n\nاستفاده از فناوری‌ها و قابلیت‌های تحلیلی جدید به عنوان یک عامل متمایزکننده کلیدی در آینده این صنعت نقش ایفا می‌کند. پیاده‌سازی هوش مصنوعی و مدل‌های پردازش داده پیشرفته به معامله‌گران اجازه می‌دهد تا با سرعت و دقت بیشتری در بازارهای پیچیده تصمیم‌گیری کنند. ایجاد فرهنگی که نوآوری دیجیتال را تشویق کند برای رقابت در این فضای جدید الزامی است.\n\nدر نهایت معامله‌گران موفق باید افق دید خود را به فراتر از بازارهای سنتی گسترش دهند و وارد بازارهای نوظهور مرتبط با گذار انرژی شوند. بازارهای کربن داوطلبانه فلزات سبز و گواهی‌های مبدا (GOOs) از جمله این فرصت‌های جدید هستند که پتانسیل رشد چهار برابری را در یک دهه آینده نشان می‌دهند.",
"radar_items": [
{
"id": "trading-value-growth",
"title": "رشد استخرهای ارزش تجاری",
"summary": "ارزش کلی تجارت کالاها تا سال ۲۰۳۰ با نرخ رشد سالانه ۱۰ درصد افزایش خواهد یافت.",
"category": "market",
"date": "بهمن ۱۴۰۳"
},
{
"id": "power-demand-surge",
"title": "افزایش تقاضای انرژی پایگاه‌های داده",
"summary": "مصرف برق مراکز داده و هوش مصنوعی با رشد سالانه ۱۵ درصدی در بازارهای پیشرو روبرو خواهد بود.",
"category": "energy",
"date": "بهمن ۱۴۰۳"
},
{
"id": "digital-trading-edge",
"title": "برتری تجارت داده‌محور",
"summary": "توسعه زیرساخت‌های دیجیتال بالغ بر ۵ میلیارد دلار صرفه‌جویی در هزینه‌های صنعت به همراه دارد.",
"category": "tech",
"date": "بهمن ۱۴۰۳"
}
],
"key_data": {
"پتانسیل افزایش سود با بهینه‌سازی": "بیش از ۳۰ میلیارد دلار سالانه",
"رشد بازارهای دارایی گذار انرژی": "۴ برابر در دهه آینده",
"صرفه‌جویی ناشی از دیجیتال‌سازی": "بیش از ۵ میلیارد دلار"
}
},
{
"report": {
"id": "green-steel-hubs-pathway",
"coverImage": "/news/photo-1581092160607-ee22731c9b4e.jpg",
"title": "هاب‌های فولاد سبز: مسیری برای کربن‌زدایی صنعت فولاد",
"category": "انرژی و ESG",
"type": "free",
"author": "کریستین هافمن",
"authorRole": "شریک ارشد",
"authorInitial": "ه",
"publishDate": "آذر ۱۴۰۳",
"pages": 9,
"price": 0,
"isFree": true,
"summary": "صنعت فولاد برای دستیابی به اهداف زیست‌محیطی خود نیازمند بازنگری در مکان تولید محصولات در سراسر جهان است. ایجاد هاب‌های جدید آهن سبز در مناطقی با دسترسی ارزان به انرژی پاک می‌تواند این روند را تسریع کند. این مراکز با تولید آهن اسفنجی بریکت‌شده (HBI) امکان انتقال کارآمد خوراک به کوره‌های قوس الکتریکی را فراهم می‌آورند.",
"tags": ["فولاد سبز", "هاب تولید", "کربن‌زدایی", "آهن اسفنجی"],
"featured": false
},
"fa_summary_long": "کربن‌زدایی از زنجیره تامین فولاد یکی از چالش‌برانگیزترین اقداماتی است که جهان امروز با آن روبه‌رو است و نیازمند تغییراتی در مقیاس وسیع می‌باشد. این صنعت به تنهایی ۷ درصد از کل گازهای گلخانه‌ای جهان را تولید می‌کند و اصلاح آن نیازمند بازسازی بیش از ۱۶۵۰ کارخانه در سطح جهانی است. یکی از راهکارهای نوین برای پاسخ به این بحران تغییر مکان و ساختار فیزیکی زنجیره تولید می‌باشد.\n\nمفهوم هاب‌های فولاد سبز به معنای جداسازی فرآیند تولید آهن از تولید نهایی فولاد است که اجازه می‌دهد هر بخش در مکان بهینه خود مستقر شود. در این مدل تولید آهن اسفنجی مستقیماً در مناطقی احداث می‌گردد که دسترسی فراوان به منابع گاز طبیعی یا هیدروژن ارزان و سنگ آهن دارند. محصولات این مراکز سپس به صورت آهن اسفنجی بریکت‌شده در مسافت‌های طولانی به کارخانه‌های نهایی منتقل می‌شوند.\n\nنیروی محرکه اصلی در این تغییر ساختار هزینه‌های تامین برق است که به زودی جایگزین زغال‌سنگ به عنوان عامل تعیین‌کننده رقابت‌پذیری خواهد شد. تولید فولاد سبز وابستگی شدیدی به انرژی الکتریکی به ویژه در فرآیند تولید هیدروژن دارد. تحلیل‌ها مشخص می‌کند که هزینه برق به تنهایی حدود ۴۰ تا ۵۰ درصد از مخارج تولید این نوع فولاد را شامل می‌گردد.\n\nتوسعه چنین مراکزی مزایای استراتژیک متعددی به همراه دارد و می‌تواند سرعت کربن‌زدایی را به شکل معناداری افزایش دهد. با ایزوله کردن بخش آهن‌سازی شرکت‌ها قادر خواهند بود جذب سرمایه را برای پروژه‌های کاملاً پاک و بدون آلایندگی تسهیل کنند. مناطقی نظیر خاورمیانه استرالیا و آمریکای جنوبی پتانسیل بالایی برای میزبانی از این هاب‌ها دارند.\n\nبا این حال راه‌اندازی این شبکه‌های تولیدی جدید به ایجاد اکوسیستمی هماهنگ از ذینفعان گوناگون وابسته است. شرکت‌های معدنی تامین‌کنندگان انرژی دولت‌ها و بخش‌های لجستیک باید برای تضمین موفقیت این زیرساخت‌ها با یکدیگر قراردادهای بلندمدت منعقد کنند. تنها در این صورت هاب‌های فولاد سبز می‌توانند ابزاری تحول‌آفرین برای آینده این صنعت باشند.",
"radar_items": [
{
"id": "electricity-cost-driver",
"title": "برق عامل کلیدی رقابت‌پذیری",
"summary": "نزدیک به نیمی از هزینه‌های تولید فولاد پایدار مستقیماً به تامین برق وابسته خواهد بود.",
"category": "energy",
"date": "آذر ۱۴۰۳"
},
{
"id": "hbi-decoupling",
"title": "جداسازی تولید آهن و فولاد",
"summary": "تجزیه زنجیره ارزش از طریق انتقال تولید HBI به مناطق دارای انرژی ارزان امکان‌پذیر می‌گردد.",
"category": "tech",
"date": "آذر ۱۴۰۳"
},
{
"id": "massive-asset-reconfiguration",
"title": "بزرگترین تغییر ساختار دارایی‌ها",
"summary": "بیش از ۱۶۵۰ کارخانه فعال نیازمند تحول اساسی برای همگامی با اهداف اقلیمی هستند.",
"category": "market",
"date": "آذر ۱۴۰۳"
}
],
"key_data": {
"سهم فولاد از انتشار جهانی": "۷ درصد",
"هزینه برق در فولاد سبز": "۴۰ تا ۵۰ درصد کل هزینه‌ها",
"انرژی برای یک تن آهن اسفنجی": "۲۷۰۰ کیلووات ساعت"
}
},
{
"report": {
"id": "india-steel-growth-resilience",
"coverImage": "/news/photo-1567789884554-0b844b597180.jpg",
"title": "تقویت آینده: فولاد برای رشد و تاب‌آوری",
"category": "بازار جهانی",
"type": "special",
"author": "پرابهاو شارما",
"authorRole": "شریک ارشد",
"authorInitial": "ش",
"publishDate": "آذر ۱۴۰۴",
"pages": 40,
"price": 500000,
"isFree": false,
"summary": "صنعت جهانی با چالش‌های سه‌گانه تغییر مراکز تقاضا نیاز به کربن‌زدایی و جریان‌های تجاری پرنوسان دست و پنجه نرم می‌کند. هند به عنوان یکی از بازارهای اصلی با قابلیت دو برابر کردن تقاضای خود تا سال ۲۰۳۵ در حال ظهور است. این کشور برای دستیابی به رشد پایدار نیازمند تمرکز بر بخش‌های با پتانسیل بالا و تامین امن مواد اولیه می‌باشد.",
"tags": ["رشد تقاضا", "هند", "کربن‌زدایی", "زنجیره تامین"],
"featured": true
},
"fa_summary_long": "چشم‌انداز جهانی صنعت فولاد در حال تجربه یک تغییر مسیر ساختاری است که در آن مراکز سنتی تقاضا با بلوغ روبه‌رو شده و بازارهای نوظهور رهبری را به دست می‌گیرند. تقاضای چین به دلیل رکود در بخش ساخت‌وساز در مسیر نزولی قرار گرفته و این موضوع پویایی صادرات در بازارهای بین‌المللی را به شدت تحت تاثیر قرار داده است. در مقابل هند و جنوب شرقی آسیا به موتورهای اصلی رشد تبدیل گشته‌اند.\n\nکشور هند با اجرای پروژه‌های زیربنایی عظیم و سرمایه‌گذاری ۱.۴ تریلیون دلاری در خط لوله زیرساخت ملی (NIP) نقشی محوری در آینده این کالا ایفا می‌کند. مصرف در این کشور با نرخ رشد سالانه ۵ تا ۶ درصد پیش می‌رود و انتظار می‌رود تا سال ۲۰۳۵ به ارقامی بین ۲۴۰ تا ۲۶۰ میلیون تن برسد. این تقاضای فزاینده به ویژه در بخش‌های با ارزش افزوده بالا مانند خودرو انرژی‌های تجدیدپذیر و دفاعی دیده می‌شود.\n\nبرای پاسخگویی به این نیاز فزاینده تولیدکنندگان هندی در حال برنامه‌ریزی برای افزودن حدود ۷۰ میلیون تن ظرفیت جدید طی ده سال آینده هستند. مشکل اساسی اینجاست که بخش اعظم این توسعه بر پایه روش سنتی کوره بلند (BF-BOF) بنا شده که ریسک وابستگی به دارایی‌های پرکربن را افزایش می‌دهد. این تصمیم‌گیری‌ها فشار مضاعفی را در مسیر پایبندی به اهداف اقلیمی ایجاد خواهد کرد.\n\nعلاوه بر چالش‌های زیست‌محیطی تامین مواد اولیه نیز یک گلوگاه حیاتی برای این کشور محسوب می‌گردد. در حالی که تقاضا برای زغال‌سنگ متالورژیک در سطح جهانی رو به کاهش است هند با افزایش نیاز مواجه خواهد بود و مجبور به تامین بخش قابل توجهی از آن از طریق واردات است. همچنین کمبود منابع داخلی قراضه با کیفیت رقابت برای به دست آوردن این نهاده ارزشمند را به شدت تشدید می‌کند.\n\nدر نهایت بازیگران این صنعت برای حفظ رقابت‌پذیری در عرصه جهانی باید استراتژی‌های خود را اصلاح کنند. استفاده از مسیرهای جایگزین نظیر آهن اسفنجی مبتنی بر هیدروژن و توسعه زیرساخت‌های انرژی پاک در کنار هم‌افزایی با برنامه‌های حمایتی دولت از ملزومات این گذار تاریخی هستند. استقرار سیستم‌های هوش مصنوعی برای بهینه‌سازی فرآیندها نیز نقشی اساسی در بهبود راندمان ایفا می‌کند.",
"radar_items": [
{
"id": "india-demand-doubling",
"title": "دو برابر شدن تقاضای هند",
"summary": "پیش‌بینی می‌گردد مصرف داخلی این کشور تا سال ۲۰۳۵ به ۲۴۰ تا ۲۶۰ میلیون تن بالغ شود.",
"category": "market",
"date": "آذر ۱۴۰۴"
},
{
"id": "scrap-shortage-crisis",
"title": "بحران تامین قراضه",
"summary": "کمبود ۱۰ تا ۱۵ میلیون تنی قراضه در بازار هند تا سال ۲۰۳۰ چالش‌های جدی برای تولیدات پاک ایجاد می‌کند.",
"category": "commodity",
"date": "آذر ۱۴۰۴"
},
{
"id": "china-demand-decline",
"title": "کاهش ساختاری تقاضای چین",
"summary": "مصرف فولاد در چین با افت سالانه ۰.۸ درصدی به دلیل رکود بخش ساخت‌وساز مواجه خواهد بود.",
"category": "geo",
"date": "آذر ۱۴۰۴"
}
],
"key_data": {
"تقاضای جهانی تا ۲۰۳۵": "۱۸۵۰ تا ۱۹۰۰ میلیون تن",
"سرمایه‌گذاری زیرساختی هند": "۱.۴ تریلیون دلار (NIP)",
"ظرفیت جدید در هند": "حدود ۷۰ میلیون تن در دهه آینده"
}
}
]

108
panel/insert-events.mjs Normal file
View File

@ -0,0 +1,108 @@
import Database from 'better-sqlite3';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const db = new Database(path.join(__dirname, 'data.db'));
// Gregorian ISO → Jalali parts using Intl (same engine the site's calendar uses)
function jalali(iso) {
const d = new Date(iso + 'T00:00:00');
const en = new Intl.DateTimeFormat('en-US-u-ca-persian', { day: 'numeric', month: 'numeric', year: 'numeric' }).formatToParts(d);
const get = (t) => en.find((p) => p.type === t).value;
const day = Number(get('day'));
const year = Number(get('year'));
const monthFa = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { month: 'long' }).format(d);
const monthEn = new Intl.DateTimeFormat('en-US-u-ca-persian', { month: 'long' }).format(d);
return { day, year, monthFa, monthEn };
}
// 9 real, verified, upcoming steel-industry events (all after 2026-06-15)
const events = [
{ id: 'gulf-steel-expo-2026', iso: '2026-09-20', type: 'exhibition',
titleFa: 'نمایشگاه فولاد خلیج ۲۰۲۶', titleEn: 'Gulf Steel Expo 2026',
cityFa: 'دمام', cityEn: 'Dammam', countryFa: 'عربستان سعودی',
descFa: 'نمایشگاه و کنفرانس عربستان سعودی با پوشش کامل زنجیره ارزش صنعت فولاد.',
descEn: "Saudi Arabia's exhibition and conference covering the entire steel value chain." },
{ id: 'arab-steel-summit-2026', iso: '2026-11-03', type: 'conference',
titleFa: 'نوزدهمین اجلاس فولاد عرب ۲۰۲۶', titleEn: '19th Arab Steel Summit 2026',
cityFa: 'دوحه', cityEn: 'Doha', countryFa: 'قطر',
descFa: 'اجلاس پرچم‌دار اتحادیه آهن و فولاد عرب همراه با نمایشگاه بین‌المللی آهن و فولاد.',
descEn: "The Arab Iron and Steel Union's flagship summit and international iron & steel exhibition." },
{ id: 'meis-2026', iso: '2026-11-16', type: 'conference',
titleFa: 'کنفرانس آهن و فولاد خاورمیانه ۲۰۲۶', titleEn: 'Middle East Iron & Steel 2026',
cityFa: 'دبی', cityEn: 'Dubai', countryFa: 'امارات متحده عربی',
descFa: 'مهم‌ترین کنفرانس تجارت آهن و فولاد خاورمیانه با حضور بیش از ۱۴۵۰ مدیر ارشد صنعت.',
descEn: "Fastmarkets' premier Middle East iron and steel trading conference with 1,450+ senior executives." },
{ id: 'iran-metafo-2026', iso: '2026-11-20', type: 'exhibition',
titleFa: 'نمایشگاه بین‌المللی متالورژی ایران (متافو) ۲۰۲۶', titleEn: 'Iran Metafo 2026',
cityFa: 'تهران', cityEn: 'Tehran', countryFa: 'ایران',
descFa: 'بزرگ‌ترین نمایشگاه بین‌المللی متالورژی، فولاد، معدن و ریخته‌گری ایران در محل دائمی نمایشگاه‌های بین‌المللی تهران.',
descEn: "Iran's largest international metallurgy, steel, mining and foundry exhibition in Tehran." },
{ id: 'made-in-steel-2027', iso: '2027-05-11', type: 'exhibition',
titleFa: 'نمایشگاه و کنفرانس مید این استیل ۲۰۲۷', titleEn: 'Made in Steel 2027',
cityFa: 'میلان', cityEn: 'Milan', countryFa: 'ایتالیا',
descFa: 'بزرگ‌ترین نمایشگاه و کنفرانس دوسالانه صنعت فولاد ایتالیا در مرکز نمایشگاهی میلان.',
descEn: "Italy's leading biennial steel-industry conference and exhibition at Fiera Milano Rho." },
{ id: 'seaisi-2027', iso: '2027-05-17', type: 'conference',
titleFa: 'کنفرانس و نمایشگاه فولاد جنوب شرق آسیا (SEAISI) ۲۰۲۷', titleEn: 'SEAISI Conference & Exhibition 2027',
cityFa: 'کوالالامپور', cityEn: 'Kuala Lumpur', countryFa: 'مالزی',
descFa: 'کنفرانس سالانه مؤسسه آهن و فولاد جنوب شرق آسیا برای صنعت فولاد منطقه آ‌سه‌آن.',
descEn: "The South East Asia Iron and Steel Institute's annual conference for the ASEAN steel industry." },
{ id: 'future-steel-forum-2027', iso: '2027-06-09', type: 'conference',
titleFa: 'فروم آینده فولاد ۲۰۲۷', titleEn: 'Future Steel Forum 2027',
cityFa: 'بولونیا', cityEn: 'Bologna', countryFa: 'ایتالیا',
descFa: 'کنفرانس شاخص جهانی در حوزه تحول دیجیتال، تولید هوشمند و کربن‌زدایی صنعت فولاد.',
descEn: 'A flagship conference on digital transformation, smart manufacturing and decarbonisation in steel.' },
{ id: 'gifa-metec-2027', iso: '2027-06-21', type: 'exhibition',
titleFa: 'نمایشگاه متالورژی و ریخته‌گری METEC / GIFA ۲۰۲۷', titleEn: 'GIFA / METEC 2027',
cityFa: 'دوسلدورف', cityEn: 'Düsseldorf', countryFa: 'آلمان',
descFa: 'مجموعه نمایشگاه‌های پیشروی جهان در حوزه ریخته‌گری و متالورژی، شامل متک برای فناوری فولادسازی.',
descEn: "The world's leading combined foundry and metallurgy trade-fairs, including METEC for steelmaking technology." },
{ id: 'stainless-steel-world-2027', iso: '2027-11-30', type: 'exhibition',
titleFa: 'کنفرانس و نمایشگاه جهانی فولاد ضدزنگ ۲۰۲۷', titleEn: 'Stainless Steel World 2027',
cityFa: 'ماستریخت', cityEn: 'Maastricht', countryFa: 'هلند',
descFa: 'کنفرانس و نمایشگاه پیشروی جهانی در حوزه فولاد ضدزنگ و آلیاژهای مقاوم به خوردگی.',
descEn: 'The leading global conference and exhibition dedicated to stainless steel and corrosion-resistant alloys.' },
];
// replace the old mock events with the real future ones
db.prepare('DELETE FROM events').run();
const stmt = db.prepare(`
INSERT INTO events
(id, title_fa, title_en, date_fa, date_en, month_fa, month_en, year, type,
location_fa, location_en, city_fa, city_en, description_fa, description_en,
registration_open, sort_order)
VALUES
(@id, @title_fa, @title_en, @date_fa, @date_en, @month_fa, @month_en, @year, @type,
@location_fa, @location_en, @city_fa, @city_en, @description_fa, @description_en,
@registration_open, @sort_order)
`);
events.forEach((e, i) => {
const j = jalali(e.iso);
stmt.run({
id: e.id,
title_fa: e.titleFa, title_en: e.titleEn,
date_fa: j.day, date_en: j.day,
month_fa: j.monthFa, month_en: j.monthEn,
year: j.year,
type: e.type,
location_fa: `${e.cityFa}، ${e.countryFa}`, location_en: `${e.cityEn}, ${e.countryFa}`,
city_fa: e.cityFa, city_en: e.cityEn,
description_fa: e.descFa, description_en: e.descEn,
registration_open: 1, sort_order: i,
});
console.log(`${e.id}${j.day} ${j.monthFa} ${j.year}`);
});
console.log(`\nDone: ${events.length} events inserted`);

60
panel/insert-factory.mjs Normal file
View File

@ -0,0 +1,60 @@
import Database from 'better-sqlite3';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const db = new Database(path.join(__dirname, 'data.db'));
// Real issues of Mobarakeh Steel's "ماهنامه تحلیلی کارخانه" (titles/descriptions/images
// taken from msc.ir). The listing page itself is WAF-gated, so only the latest 3 issues
// were retrievable automatically; images hotlink fine from msc.ir.
const MSC = 'https://www.msc.ir/_DouranPortal/Album/Articles';
const reports = [
{
id: 'msc-55', dir: 'left',
img: `${MSC}/%DB%B5%DB%B5_14050314_083241.jpg`,
titleFa: 'ماهنامه تحلیلی کارخانه — شماره ۵۵', titleEn: 'Factory Analysis Monthly — No. 55',
dateFa: 'خرداد ۱۴۰۵', dateEn: 'May 2026',
excerptFa: 'در این شماره می‌خوانید: راهبرد یکپارچه چگونه انسجام راهبردی و مزیت رقابتی پایدار فولاد مبارکه را تضمین می‌کند؟ هم‌افزایی شش محور استراتژیک، تبدیل ثروت معدنی به قدرت صنعتی، و دوام مزیت‌های نسبی فولاد ایران زیر فشار ناترازی انرژی.',
excerptEn: 'Integrated strategy, synergy of six strategic axes, turning mineral wealth into industrial power, and the resilience of Irans steel advantages under energy imbalance.',
},
{
id: 'msc-54', dir: 'right',
img: `${MSC}/;hvohki%2054_14050323_095959.jpg`,
titleFa: 'ماهنامه تحلیلی کارخانه — شماره ۵۴', titleEn: 'Factory Analysis Monthly — No. 54',
dateFa: 'مهر ۱۴۰۴', dateEn: 'Oct 2025',
excerptFa: 'شماره ۵۴ ماهنامه تحلیلی راهبردی کارخانه فولاد مبارکه.',
excerptEn: 'Issue No. 54 of Mobarakeh Steels strategic factory analysis monthly.',
},
{
id: 'msc-53', dir: 'up',
img: `${MSC}/01_20250921_103454.jpg`,
titleFa: 'ماهنامه تحلیلی کارخانه — شماره ۵۳', titleEn: 'Factory Analysis Monthly — No. 53',
dateFa: 'شهریور ۱۴۰۴', dateEn: 'Sep 2025',
excerptFa: 'عبور از ملاحظات؛ آیا زمان جراحی نهادی برای تحقق توسعه پایدار صنایع معدنی فرا رسیده است؟',
excerptEn: 'Beyond considerations: has the time come for institutional surgery to achieve sustainable development of the mining industries?',
},
];
db.prepare('DELETE FROM factory_reports').run();
const stmt = db.prepare(`
INSERT INTO factory_reports
(id, img, tag_fa, tag_en, title_fa, title_en, date_fa, date_en, excerpt_fa, excerpt_en, scroll_dir, sort_order)
VALUES
(@id, @img, @tag_fa, @tag_en, @title_fa, @title_en, @date_fa, @date_en, @excerpt_fa, @excerpt_en, @scroll_dir, @sort_order)
`);
reports.forEach((r, i) => {
stmt.run({
id: r.id, img: r.img,
tag_fa: 'ماهنامه تحلیلی کارخانه', tag_en: 'Factory Analysis',
title_fa: r.titleFa, title_en: r.titleEn,
date_fa: r.dateFa, date_en: r.dateEn,
excerpt_fa: r.excerptFa, excerpt_en: r.excerptEn,
scroll_dir: r.dir, sort_order: i,
});
console.log(`${r.id}${r.titleFa}`);
});
console.log(`\nDone: ${reports.length} factory reports inserted`);

308
panel/package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "andishkade-foolad-panel",
"version": "1.0.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.104.1",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^11.3.0",
"cheerio": "^1.2.0",
@ -21,12 +22,44 @@
"multer": "^1.4.5-lts.1",
"nanoid": "^5.0.7",
"nodemailer": "^8.0.10",
"openai": "^6.42.0",
"pdf-parse": "^2.4.5",
"sharp": "^0.35.1"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.104.1",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.104.1.tgz",
"integrity": "sha512-gGACa/+IaiXzRRmF96aOhamoBgapKRBiFWbmmTFP8aMkpaEcuStF+Q61bjo4vPxBM7gqWJNZqsngslRdnLHv0Q==",
"license": "MIT",
"dependencies": {
"json-schema-to-ts": "^3.1.1",
"standardwebhooks": "^1.0.0"
},
"bin": {
"anthropic-ai-sdk": "bin/cli"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/@babel/runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
@ -537,6 +570,196 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@napi-rs/canvas": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz",
"integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==",
"license": "MIT",
"workspaces": [
"e2e/*"
],
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "0.1.80",
"@napi-rs/canvas-darwin-arm64": "0.1.80",
"@napi-rs/canvas-darwin-x64": "0.1.80",
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80",
"@napi-rs/canvas-linux-arm64-gnu": "0.1.80",
"@napi-rs/canvas-linux-arm64-musl": "0.1.80",
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.80",
"@napi-rs/canvas-linux-x64-gnu": "0.1.80",
"@napi-rs/canvas-linux-x64-musl": "0.1.80",
"@napi-rs/canvas-win32-x64-msvc": "0.1.80"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz",
"integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz",
"integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz",
"integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz",
"integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz",
"integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz",
"integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz",
"integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz",
"integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz",
"integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz",
"integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@stablelib/base64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
"license": "MIT"
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -1238,6 +1461,12 @@
"express": ">= 4.11"
}
},
"node_modules/fast-sha256": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
"license": "Unlicense"
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@ -1496,6 +1725,19 @@
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/json-schema-to-ts": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"ts-algebra": "^2.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
@ -1828,6 +2070,24 @@
"wrappy": "1"
}
},
"node_modules/openai": {
"version": "6.42.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.42.0.tgz",
"integrity": "sha512-1WFEt/uXMXOLhYRNkgJWo08Y2YNvNwpVU72K7ibrWgWpNOXd4VojXLbe6SQ4bLiUQ3Y8jz4IiyVkylJCL1DtZg==",
"license": "Apache-2.0",
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@ -1892,6 +2152,38 @@
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
"node_modules/pdf-parse": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz",
"integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==",
"license": "Apache-2.0",
"dependencies": {
"@napi-rs/canvas": "0.1.80",
"pdfjs-dist": "5.4.296"
},
"bin": {
"pdf-parse": "bin/cli.mjs"
},
"engines": {
"node": ">=20.16.0 <21 || >=22.3.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/mehmet-kozan"
}
},
"node_modules/pdfjs-dist": {
"version": "5.4.296",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
"integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
"license": "Apache-2.0",
"engines": {
"node": ">=20.16.0 || >=22.3.0"
},
"optionalDependencies": {
"@napi-rs/canvas": "^0.1.80"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
@ -2285,6 +2577,16 @@
"simple-concat": "^1.0.0"
}
},
"node_modules/standardwebhooks": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
"license": "MIT",
"dependencies": {
"@stablelib/base64": "^1.0.0",
"fast-sha256": "^1.3.0"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@ -2377,6 +2679,12 @@
"node": ">=0.6"
}
},
"node_modules/ts-algebra": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",

View File

@ -11,6 +11,7 @@
"seed": "node seed.js"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.104.1",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^11.3.0",
"cheerio": "^1.2.0",
@ -24,6 +25,8 @@
"multer": "^1.4.5-lts.1",
"nanoid": "^5.0.7",
"nodemailer": "^8.0.10",
"openai": "^6.42.0",
"pdf-parse": "^2.4.5",
"sharp": "^0.35.1"
},
"engines": {

View File

@ -111,6 +111,7 @@ function renderLogin(errorMsg = '') {
method: 'POST',
body: { username: fd.get('username'), password: fd.get('password') },
});
if (data.token) sessionStorage.setItem('sf_token', data.token);
renderList();
} catch {
renderLogin('نام کاربری یا رمز عبور اشتباه است');
@ -139,6 +140,8 @@ function topbarHtml(active, primaryLabel) {
<button data-tab="experts" class="${active==='experts' ?'tab-active':''}">خبرگان</button>
<button data-tab="plans" class="${active==='plans' ?'tab-active':''}">پلانها</button>
<button data-tab="banners" class="${active==='banners' ?'tab-active':''}">بنرها</button>
<button data-tab="pdfAnalyze" class="${active==='pdfAnalyze'?'tab-active':''}">📄 آنالیز PDF</button>
<button data-tab="import" class="${active==='import' ?'tab-active':''}"> ایمپورت</button>
<button data-tab="users" class="${active==='users' ?'tab-active':''}">کاربران</button>
</nav>
<div class="actions">
@ -166,9 +169,11 @@ function wireTabs() {
else if (t === 'advisory') renderAdvisoryList();
else if (t === 'team') renderTeamList(false);
else if (t === 'experts') renderTeamList(true);
else if (t === 'plans') renderPlansList();
else if (t === 'banners') renderBannersList();
else if (t === 'users') renderUsersList();
else if (t === 'plans') renderPlansList();
else if (t === 'banners') renderBannersList();
else if (t === 'pdfAnalyze') renderPdfAnalyze();
else if (t === 'import') renderImport();
else if (t === 'users') renderUsersList();
});
});
$('#logoutBtn').addEventListener('click', async () => {
@ -234,10 +239,12 @@ async function renderList() {
}
}
function renderEditor(article) {
function renderEditor(article, draft = {}) {
const a = article || {
title: '', category: '', type: '', author: '', authorRole: '', authorInitial: '',
publishDate: '', pages: 0, price: 0, isFree: true, summary: '', body: '',
title: draft.title || '', category: draft.category || '', type: draft.type || '',
author: '', authorRole: '', authorInitial: '',
publishDate: '', pages: 0, price: 0, isFree: true,
summary: draft.summary || '', body: '',
coverImage: '', tags: [], featured: false,
};
const isEdit = !!article;
@ -1889,5 +1896,331 @@ function renderBannerEditor(banner) {
});
}
// ─────────────────────────────────────────────
function renderImport() {
root.innerHTML = `
${topbarHtml('import', '')}
<div class="page-body" style="max-width:820px;margin:2rem auto;padding:0 1rem">
<h2 style="margin-bottom:.25rem"> ایمپورت از Gemini</h2>
<p style="color:#888;font-size:.875rem;margin-bottom:1.5rem">
خروجی JSON که Gemini داد رو اینجا paste کن و دکمه ایمپورت رو بزن.
گزارش توی <strong>مطالب</strong> و آیتمهای رادار توی <strong>رادار</strong> ذخیره میشن.
</p>
<div id="import-status"></div>
<textarea id="import-json" rows="22" style="
width:100%;box-sizing:border-box;font-family:monospace;font-size:.8rem;
border:1.5px solid #ddd;border-radius:8px;padding:1rem;resize:vertical;
direction:ltr;background:#fafafa;outline:none;
" placeholder='{ "report": { "id": "...", "title": "...", ... }, "radar_items": [...], "fa_summary_long": "..." }'></textarea>
<div style="display:flex;gap:.75rem;margin-top:1rem;align-items:center">
<button id="import-btn" class="primary" style="padding:.7rem 2rem;font-size:1rem">ایمپورت</button>
<button id="import-clear" class="ghost">پاک کردن</button>
<span id="import-count" style="color:#aaa;font-size:.82rem"></span>
</div>
<div id="import-result" style="margin-top:1.5rem"></div>
</div>
`;
setupTopbar();
const jsonTA = document.getElementById('import-json');
const statusEl = document.getElementById('import-status');
const resultEl = document.getElementById('import-result');
const countEl = document.getElementById('import-count');
jsonTA.addEventListener('input', () => {
try {
const d = JSON.parse(jsonTA.value);
const ra = d.radar_items?.length || 0;
countEl.textContent = `✓ JSON معتبر — گزارش: ${d.report?.title ? '1' : '0'} | رادار: ${ra}`;
countEl.style.color = '#22c55e';
} catch {
countEl.textContent = 'JSON نامعتبر';
countEl.style.color = '#ef4444';
}
});
document.getElementById('import-clear').onclick = () => {
jsonTA.value = ''; countEl.textContent = ''; resultEl.innerHTML = '';
};
document.getElementById('import-btn').onclick = async () => {
let data;
try { data = JSON.parse(jsonTA.value); }
catch { statusEl.innerHTML = '<div class="alert alert-error">JSON نامعتبر است</div>'; return; }
const btn = document.getElementById('import-btn');
btn.disabled = true; btn.textContent = 'در حال ایمپورت...';
statusEl.innerHTML = '';
try {
const res = await apiFetch('/api/admin/import', { method:'POST', body: JSON.stringify(data) });
if (!res.ok) { const e = await res.json(); throw new Error(e.error || res.status); }
const result = await res.json();
resultEl.innerHTML = `
<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:1.25rem 1.5rem">
<div style="font-weight:700;color:#166534;font-size:1rem;margin-bottom:.75rem"> ایمپورت موفق</div>
<div style="display:flex;flex-direction:column;gap:.4rem;font-size:.875rem;color:#166534">
<div>📄 گزارش: <code style="background:#dcfce7;padding:2px 6px;border-radius:4px">${result.article}</code></div>
${result.radar.length ? `<div>🔭 آیتم‌های رادار: ${result.radar.length} مورد اضافه شد</div>` : ''}
</div>
<div style="margin-top:1rem;display:flex;gap:.5rem">
<a href="#" onclick="document.querySelector('[data-tab=articles]').click();return false"
style="font-size:.8rem;color:#16a34a;text-decoration:underline">مشاهده در مطالب </a>
${result.radar.length ? `<a href="#" onclick="document.querySelector('[data-tab=radar]').click();return false"
style="font-size:.8rem;color:#16a34a;text-decoration:underline">مشاهده در رادار </a>` : ''}
</div>
</div>
`;
jsonTA.value = ''; countEl.textContent = '';
} catch (err) {
statusEl.innerHTML = `<div class="alert alert-error">خطا: ${err.message}</div>`;
} finally {
btn.disabled = false; btn.textContent = 'ایمپورت';
}
};
}
// ─────────────────────────────────────────────
function renderPdfAnalyze() {
root.innerHTML = `
${topbarHtml('pdfAnalyze', '')}
<main class="page">
<h2>آنالیز و دستهبندی PDF</h2>
<p class="muted" style="margin-bottom:24px">فایل PDF گزارش را آپلود کنید سیستم متن را استخراج کرده و پیشنهاد بخش میدهد.</p>
<div class="card" style="max-width:640px;padding:28px 32px">
<form id="pdfForm">
<label style="display:block;margin-bottom:16px">
<span style="font-weight:700;display:block;margin-bottom:8px">انتخاب فایل PDF</span>
<input type="file" name="pdf" accept="application/pdf" required style="width:100%" />
</label>
<button type="submit" class="primary" style="width:100%">📄 استخراج و آنالیز</button>
</form>
</div>
<div id="pdfResult" style="margin-top:28px;max-width:760px"></div>
</main>
`;
wireTabs();
$('#newBtn') && ($('#newBtn').style.display = 'none');
$('#pdfForm').addEventListener('submit', async (e) => {
e.preventDefault();
const file = e.target.pdf.files[0];
if (!file) return;
const result = $('#pdfResult');
result.innerHTML = `<p class="muted">در حال پردازش PDF… (${(file.size/1024).toFixed(0)} KB)</p>`;
try {
const base64 = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(file);
});
const _token = sessionStorage.getItem('sf_token') || '';
const _res = await fetch('/api/pdf-analyze', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json', ...(_token ? { 'Authorization': 'Bearer ' + _token } : {}) },
body: JSON.stringify({ base64 }),
});
if (!_res.ok) {
const errText = await _res.text();
result.innerHTML = `<div class="error">خطای سرور ${_res.status}: <pre style="font-size:11px;white-space:pre-wrap">${escapeHtml(errText)}</pre></div>`;
return;
}
const data = await _res.json();
const SECTION_LABELS = {
market: 'بازار و زنجیره فولاد', technology: 'فناوری صنعتی',
geopolitics: 'ژئواکونومی', sustainability: 'پایداری محیطی', foresight: 'آینده‌نگاری',
};
if (data.is_garbled) {
// PDF has non-standard font encoding — offer manual text input
result.innerHTML = `
<div class="card" style="padding:24px 28px;margin-bottom:16px;border-right:4px solid #e67e22">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
<span style="font-size:22px"></span>
<strong style="font-size:15px;color:#032340">متن PDF قابل استخراج نیست</strong>
</div>
<p style="margin:0;font-size:13px;color:#555;line-height:1.9">
این PDF از رمزگذاری فونت غیراستاندارد استفاده میکند (رایج در نشریات فارسی).
متن بهدرستی خوانده نمیشود و نیاز به OCR دارد.<br>
میتوانید متن رو دستی paste کنید تا بخش پیشنهادی و خلاصه ساخته بشه.
</p>
<p class="muted" style="margin:10px 0 0;font-size:12px">${data.page_count} صفحه · ${(data.text_length/1000).toFixed(1)}k کاراکتر (ناخوانا)</p>
</div>
<div class="card" style="padding:20px 28px;margin-bottom:16px">
<h3 style="margin:0 0 10px;font-size:15px">عنوان گزارش</h3>
<input id="manualTitle" type="text" placeholder="عنوان را وارد کنید..." style="width:100%;padding:10px 14px;border:1px solid #ddd;border-radius:8px;font-family:inherit;font-size:14px;direction:rtl;box-sizing:border-box">
<h3 style="margin:18px 0 10px;font-size:15px">متن / خلاصه (paste کنید)</h3>
<textarea id="manualText" rows="8" placeholder="متن یا خلاصه گزارش را اینجا paste کنید..." style="width:100%;padding:10px 14px;border:1px solid #ddd;border-radius:8px;font-family:inherit;font-size:13px;direction:rtl;resize:vertical;box-sizing:border-box"></textarea>
<h3 style="margin:18px 0 10px;font-size:15px">بخش مناسب</h3>
<select id="manualCategory" style="width:100%;padding:10px 14px;border:1px solid #ddd;border-radius:8px;font-family:inherit;font-size:14px;direction:rtl">
${Object.entries(SECTION_LABELS).map(([k,v]) => `<option value="${k}">${v}</option>`).join('')}
</select>
<div style="margin-top:18px;display:flex;gap:10px;flex-wrap:wrap">
<button id="generateContentBtn" style="background:#032340;color:#fff;border:none;padding:10px 18px;border-radius:8px;font-family:inherit;font-size:14px;cursor:pointer;font-weight:700"> تولید محتوا با هوش مصنوعی</button>
<button id="createArticleBtn" class="primary">+ ایجاد مقاله مستقیم</button>
</div>
<div id="generateResult" style="margin-top:16px"></div>
</div>
`;
async function runGenerate(textSrc, titleSrc, catKey) {
const gr = $('#generateResult');
gr.innerHTML = '<p class="muted" style="font-size:13px">در حال تولید محتوا... ⏳</p>';
const _token = sessionStorage.getItem('sf_token') || '';
try {
const r = await fetch('/api/pdf-generate', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json', ...(_token ? { 'Authorization': 'Bearer ' + _token } : {}) },
body: JSON.stringify({ text: textSrc, category: SECTION_LABELS[catKey] || catKey }),
});
const d = await r.json();
if (!r.ok) { gr.innerHTML = `<div class="error">${escapeHtml(d.error || 'خطا')}</div>`; return; }
const generated = d.generated || '';
gr.innerHTML = `
<div class="card" style="padding:16px 20px;background:#f9f6f2;border-radius:10px;margin-bottom:12px">
<h4 style="margin:0 0 10px;font-size:14px;color:#032340">محتوای تولیدشده</h4>
<pre id="generatedText" style="white-space:pre-wrap;font-size:13px;line-height:2;color:#333;direction:rtl;text-align:right;font-family:inherit;margin:0">${escapeHtml(generated)}</pre>
</div>
<button id="useGeneratedBtn" class="primary">+ ایجاد مقاله از این محتوا</button>
`;
$('#useGeneratedBtn').addEventListener('click', () => {
const lines = generated.split('\n').filter(l => l.trim());
const autoTitle = titleSrc || lines[0]?.replace(/^#+\s*|\*\*/g,'').slice(0,100) || '';
window._pdfDraft = { title: autoTitle, summary: generated.slice(0, 600), category: SECTION_LABELS[catKey] || catKey, type: 'quarterly' };
renderEditor(null, window._pdfDraft);
});
} catch(e) { gr.innerHTML = `<div class="error">${escapeHtml(e.message)}</div>`; }
}
$('#generateContentBtn').addEventListener('click', () => {
const t = $('#manualText').value.trim();
if (!t) { alert('لطفاً ابتدا متن را paste کنید'); return; }
runGenerate(t, $('#manualTitle').value.trim(), $('#manualCategory').value);
});
$('#createArticleBtn').addEventListener('click', () => {
const manualTitle = $('#manualTitle').value.trim();
const manualText = $('#manualText').value.trim();
const catKey = $('#manualCategory').value;
window._pdfDraft = {
title: manualTitle,
summary: manualText.slice(0, 300),
category: SECTION_LABELS[catKey] || catKey,
type: 'quarterly',
};
renderEditor(null, window._pdfDraft);
});
return;
}
const c = data.classification;
const allRows = Object.entries(c.allScores)
.sort((a,b) => b[1].score - a[1].score)
.map(([cat, d]) => `
<tr style="opacity:${cat===c.category?1:0.55}">
<td>${cat === c.category ? '✅' : ''} ${SECTION_LABELS[cat] || cat}</td>
<td style="direction:ltr;text-align:left">
<div style="display:flex;align-items:center;gap:8px">
<div style="background:#CD9E53;height:8px;border-radius:4px;width:${Math.round((d.score/(c.allScores[c.category]?.score||1))*100)}%"></div>
<span style="font-size:12px;color:#666">${d.score}</span>
</div>
</td>
</tr>`).join('');
result.innerHTML = `
<div class="card" style="padding:24px 28px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;flex-wrap:wrap">
<span style="background:#CD9E53;color:#032340;font-weight:800;padding:6px 14px;border-radius:8px;font-size:14px">${escapeHtml(c.label)}</span>
<span style="background:#f0f0f0;padding:6px 14px;border-radius:8px;font-size:13px">اطمینان: ${c.confidence}٪</span>
<span class="muted" style="font-size:12px">${data.page_count} صفحه · ${(data.text_length/1000).toFixed(1)}k کاراکتر</span>
</div>
<h3 style="margin:0 0 6px;font-size:16px">عنوان پیشنهادی</h3>
<p style="margin:0 0 18px;background:#f9f6f2;padding:10px 14px;border-radius:8px;font-size:14px">${escapeHtml(data.title)}</p>
<h3 style="margin:0 0 6px;font-size:16px">کلیدواژههای شناساییشده</h3>
<div style="margin-bottom:18px;display:flex;flex-wrap:wrap;gap:6px">
${c.keywords.map(kw => `<span style="background:#032340;color:#fff;padding:4px 10px;border-radius:6px;font-size:12px">${escapeHtml(kw)}</span>`).join('')}
</div>
<h3 style="margin:0 0 10px;font-size:16px">امتیاز بخشها</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<tbody>${allRows}</tbody>
</table>
</div>
<div class="card" style="padding:20px 28px;margin-bottom:16px">
<h3 style="margin:0 0 10px;font-size:15px">متن استخراجشده (۸۰۰ کاراکتر اول)</h3>
<pre style="white-space:pre-wrap;font-size:12px;line-height:1.8;color:#444;max-height:220px;overflow-y:auto;background:#f9f6f2;padding:12px;border-radius:8px;direction:rtl;text-align:right">${escapeHtml(data.excerpt)}</pre>
</div>
<div class="card" style="padding:20px 28px">
<h3 style="margin:0 0 14px;font-size:15px">ایجاد مقاله از این PDF</h3>
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:12px">
<button id="generateContentBtn" style="background:#032340;color:#fff;border:none;padding:10px 18px;border-radius:8px;font-family:inherit;font-size:14px;cursor:pointer;font-weight:700"> تولید محتوا با هوش مصنوعی</button>
<button id="createArticleBtn" class="primary">+ ایجاد مقاله مستقیم</button>
</div>
<div id="generateResult"></div>
</div>
`;
$('#createArticleBtn').addEventListener('click', () => {
window._pdfDraft = {
title: data.title,
summary: data.excerpt.slice(0, 300),
category: c.label,
type: 'quarterly',
};
renderEditor(null, window._pdfDraft);
});
$('#generateContentBtn').addEventListener('click', async () => {
const gr = $('#generateResult');
gr.innerHTML = '<p class="muted" style="font-size:13px">در حال تولید محتوا... ⏳</p>';
const _token = sessionStorage.getItem('sf_token') || '';
try {
const r = await fetch('/api/pdf-generate', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json', ...(_token ? { 'Authorization': 'Bearer ' + _token } : {}) },
body: JSON.stringify({ text: data.excerpt + '\n\n' + (data.text || '').slice(0, 5000), category: c.label }),
});
const d = await r.json();
if (!r.ok) { gr.innerHTML = `<div class="error">${escapeHtml(d.error || 'خطا')}</div>`; return; }
const generated = d.generated || '';
gr.innerHTML = `
<div class="card" style="padding:16px 20px;background:#f9f6f2;border-radius:10px;margin-bottom:12px">
<h4 style="margin:0 0 10px;font-size:14px;color:#032340">محتوای تولیدشده</h4>
<pre style="white-space:pre-wrap;font-size:13px;line-height:2;color:#333;direction:rtl;text-align:right;font-family:inherit;margin:0">${escapeHtml(generated)}</pre>
</div>
<button id="useGeneratedBtn" class="primary">+ ایجاد مقاله از این محتوا</button>
`;
$('#useGeneratedBtn').addEventListener('click', () => {
const lines = generated.split('\n').filter(l => l.trim());
const autoTitle = data.title || lines[0]?.replace(/^#+\s*|\*\*/g,'').slice(0,100) || '';
window._pdfDraft = { title: autoTitle, summary: generated.slice(0, 600), category: c.label, type: 'quarterly' };
renderEditor(null, window._pdfDraft);
});
} catch(e) { gr.innerHTML = `<div class="error">${escapeHtml(e.message)}</div>`; }
});
} catch (err) {
result.innerHTML = `<div class="error">خطا: ${escapeHtml(err.message)}</div>`;
}
});
}
// ─────────────────────────────────────────────
api('/api/auth/me').then(() => renderList()).catch(() => renderLogin());

42
panel/run-import.mjs Normal file
View File

@ -0,0 +1,42 @@
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import dotenv from 'dotenv';
dotenv.config({ path: fileURLToPath(new URL('./.env', import.meta.url)) });
const BASE = `http://localhost:${process.env.PORT || 3001}`;
const USER = process.env.ADMIN_USERNAME || 'admin';
const PASS = process.env.ADMIN_PASSWORD;
const payloads = JSON.parse(fs.readFileSync(new URL('./imports-data.json', import.meta.url)));
// 1) login
const loginRes = await fetch(`${BASE}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: USER, password: PASS }),
});
if (!loginRes.ok) {
console.error('LOGIN FAILED', loginRes.status, await loginRes.text());
process.exit(1);
}
const { token } = await loginRes.json();
console.log('✓ logged in\n');
// 2) import each
let ok = 0;
for (const p of payloads) {
const res = await fetch(`${BASE}/api/admin/import`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(p),
});
const data = await res.json();
if (res.ok) {
ok++;
console.log(`${p.report.id} → article + ${data.radar.length} radar items`);
} else {
console.error(`${p.report.id}${res.status}`, data);
}
}
console.log(`\nDone: ${ok}/${payloads.length} imported`);

View File

@ -63,7 +63,7 @@ app.use(helmet({
},
frameguard: { action: 'deny' },
}));
app.use(express.json({ limit: '2mb' }));
app.use(express.json({ limit: '50mb' }));
app.use('/uploads', express.static(uploadsDir, { maxAge: '7d' }));
app.use('/', express.static(path.join(__dirname, 'public')));
@ -84,7 +84,11 @@ const contactLimiter = rateLimit({
});
function authRequired(req, res, next) {
const token = req.cookies?.session;
let token = req.cookies?.session;
if (!token) {
const auth = req.headers['authorization'];
if (auth?.startsWith('Bearer ')) token = auth.slice(7);
}
if (!token) return res.status(401).json({ error: 'unauthorized' });
try {
req.user = jwt.verify(token, JWT_SECRET);
@ -94,6 +98,20 @@ function authRequired(req, res, next) {
}
}
function memberRequired(req, res, next) {
const auth = req.headers['authorization'];
const token = (auth?.startsWith('Bearer ') ? auth.slice(7) : null) ?? req.query.token ?? null;
if (!token) return res.status(401).json({ error: 'unauthorized' });
try {
const payload = jwt.verify(token, JWT_SECRET);
if (payload.role !== 'member') return res.status(403).json({ error: 'forbidden' });
req.member = payload;
next();
} catch {
return res.status(401).json({ error: 'invalid_token' });
}
}
app.post('/api/auth/login', loginLimiter, (req, res) => {
const { username, password } = req.body || {};
if (!username || !password) return res.status(400).json({ error: 'username_password_required' });
@ -110,7 +128,7 @@ app.post('/api/auth/login', loginLimiter, (req, res) => {
maxAge: 24 * 60 * 60 * 1000,
path: '/',
});
res.json({ user: { id: row.id, username: row.username } });
res.json({ user: { id: row.id, username: row.username }, token });
});
app.post('/api/auth/logout', (_req, res) => {
@ -255,6 +273,91 @@ app.delete('/api/articles/:id', authRequired, (req, res) => {
res.status(204).end();
});
// ---------- bulk import (Gemini JSON) ----------
// Gemini grounding leaves [cite: 1921] / [cite: 1921, 1931] markers in the text — strip them
// (and the whitespace artifacts they leave behind) before anything hits the DB.
function stripCites(s) {
return s
.replace(/\[cite:[^\]]*\]/g, '')
.replace(/[ \t]+([.،؛:!؟])/g, '$1') // space before punctuation
.replace(/[ \t]{2,}/g, ' ') // runs of spaces
.replace(/[ \t]+\n/g, '\n') // trailing space before newline (keeps \n\n)
.trim();
}
function deepSanitize(v) {
if (typeof v === 'string') return stripCites(v);
if (Array.isArray(v)) return v.map(deepSanitize);
if (v && typeof v === 'object') {
const o = {};
for (const k of Object.keys(v)) o[k] = deepSanitize(v[k]);
return o;
}
return v;
}
app.post('/api/admin/import', authRequired, (req, res) => {
const { report, radar_items = [], fa_summary_long, key_data } = deepSanitize(req.body || {});
if (!report?.title) return res.status(400).json({ error: 'report.title required' });
const results = { article: null, radar: [] };
// upsert article
const articleId = report.id || nanoid(14);
const body = fa_summary_long
? (key_data ? fa_summary_long + '\n\n---KEY_DATA---\n' + JSON.stringify(key_data, null, 2) : fa_summary_long)
: (key_data ? JSON.stringify(key_data, null, 2) : null);
db.prepare(`
INSERT OR REPLACE INTO articles
(id, title, category, type, author, author_role, author_initial,
publish_date, pages, price, is_free, summary, body, tags, cover_image, featured, status)
VALUES
(@id, @title, @category, @type, @author, @author_role, @author_initial,
@publish_date, @pages, @price, @is_free, @summary, @body, @tags, @cover_image, @featured, 'published')
`).run({
id: articleId,
title: report.title,
category: report.category || null,
type: report.type || 'special',
author: report.author || null,
author_role: report.authorRole || report.author_role || null,
author_initial: report.authorInitial || report.author_initial || null,
publish_date: report.publishDate || report.publish_date || null,
pages: Number(report.pages) || 0,
price: Number(report.price) || 0,
is_free: (report.isFree ?? report.is_free ?? false) ? 1 : 0,
summary: report.summary || null,
body,
tags: JSON.stringify(Array.isArray(report.tags) ? report.tags : []),
cover_image: report.coverImage || report.cover_image || null,
featured: report.featured ? 1 : 0,
});
results.article = articleId;
// upsert radar items
const insertRadar = db.prepare(`
INSERT OR REPLACE INTO radar_items
(id, category, img, title_fa, excerpt_fa, date_fa, author_name, sort_order)
VALUES (@id, @category, @img, @title_fa, @excerpt_fa, @date_fa, @author_name, @sort_order)
`);
radar_items.forEach((item, i) => {
const rid = item.id || nanoid(12);
insertRadar.run({
id: rid,
category: item.category || 'market',
img: item.img || item.image || report.coverImage || '',
title_fa: item.title || '',
excerpt_fa: item.summary || item.excerpt_fa || '',
date_fa: item.date || '',
author_name: report.author || '',
sort_order: i,
});
results.radar.push(rid);
});
res.json({ ok: true, ...results });
});
// ---------- events ----------
const EVENT_FIELDS = `id, title_fa, title_en, date_fa, date_en, month_fa, month_en, year, type,
location_fa, location_en, city_fa, city_en, description_fa, description_en,
@ -1180,6 +1283,225 @@ app.post('/api/contact', contactLimiter, async (req, res) => {
}
});
// ---------- PDF analyser ----------
const SECTION_KEYWORDS = {
market: {
words: ['بازار','قیمت','صادرات','واردات','معامله','تجارت','فولاد','آهن','سنگ آهن','بلوم','تختال','کویل','عرضه','تقاضا','تولید','اقتصاد','market','price','trade','export','import','steel','iron ore','billet','slab','coil','supply','demand','production'],
label: 'بازار و زنجیره فولاد',
},
technology: {
words: ['فناوری','هوش مصنوعی','دیجیتال','اتوماسیون','نرم‌افزار','ربات','هوشمند','نوآوری','تکنولوژی','داده','technology','ai','digital','automation','software','robot','smart','innovation','iot','machine learning'],
label: 'فناوری صنعتی',
},
geopolitics: {
words: ['ژئوپلیتیک','تحریم','سیاست','دیپلماسی','جنگ','تنش','بین‌الملل','ژئواکونومی','قدرت','geopolitics','sanction','policy','diplomacy','war','tension','international','brics','conflict'],
label: 'ژئواکونومی',
},
sustainability: {
words: ['محیط زیست','کربن','پایداری','سبز','آلودگی','اقلیم','انرژی پاک','انتشار','تجدیدپذیر','esg','carbon','sustainability','environment','green','climate','emission','renewable','clean energy'],
label: 'پایداری محیطی',
},
foresight: {
words: ['آینده','سناریو','پیش‌بینی','روند','چشم‌انداز','آینده‌نگاری','استراتژی','رصد','تحلیل','future','scenario','forecast','trend','outlook','foresight','strategy','horizon','intelligence'],
label: 'آینده‌نگاری',
},
};
function classifyPdfText(text) {
const lower = text.toLowerCase();
const scores = {};
for (const [cat, data] of Object.entries(SECTION_KEYWORDS)) {
let score = 0; const matched = [];
for (const kw of data.words) {
const hits = (lower.match(new RegExp(kw.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
if (hits > 0) { score += hits; matched.push(kw); }
}
scores[cat] = { score, matched, label: data.label };
}
const sorted = Object.entries(scores).sort((a, b) => b[1].score - a[1].score);
const [topCat, topData] = sorted[0];
const secondScore = sorted[1]?.[1].score || 0;
const confidence = topData.score > 0 ? Math.min(100, Math.round(((topData.score - secondScore) / (topData.score + 1)) * 100 + 40)) : 0;
return {
category: topCat, label: topData.label, score: topData.score, confidence,
keywords: [...new Set(topData.matched)].slice(0, 8),
allScores: Object.fromEntries(sorted.map(([c, d]) => [c, { score: d.score, label: d.label }])),
};
}
function extractPdfTitle(text) {
const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 8 && l.length < 180);
return lines[0] || 'بدون عنوان';
}
app.post('/api/pdf-analyze', authRequired, async (req, res) => {
try {
const { base64 } = req.body || {};
if (!base64) return res.status(400).json({ error: 'فایل PDF ارسال نشده' });
const buf = Buffer.from(base64, 'base64');
const { PDFParse } = await import('pdf-parse');
const parser = new PDFParse({ data: buf });
const result = await parser.getText();
const text = result.text || '';
const pageCount = result.total || result.pages?.length || 0;
await parser.destroy();
// detect garbled text: check ratio of Persian/Arabic Unicode chars
const persianChars = (text.match(/[؀-ۿ]/g) || []).length;
const meaningfulChars = text.replace(/\s/g, '').length;
const isGarbled = meaningfulChars > 50 && (persianChars / meaningfulChars) < 0.15;
const title = isGarbled ? '' : extractPdfTitle(text);
const excerpt = isGarbled ? '' : text.replace(/\n{3,}/g, '\n\n').trim().slice(0, 800);
const classification = isGarbled ? null : classifyPdfText(text);
res.json({ title, excerpt, text_length: text.length, page_count: pageCount, classification, is_garbled: isGarbled });
} catch (err) {
console.error('PDF parse error:', err);
res.status(500).json({ error: 'خطا در تجزیه PDF: ' + err.message });
}
});
app.post('/api/pdf-generate', authRequired, async (req, res) => {
const apiKey = process.env.AI_API_KEY || process.env.ANTHROPIC_API_KEY;
if (!apiKey) return res.status(503).json({ error: 'AI_API_KEY در فایل .env تنظیم نشده' });
const { text, category } = req.body || {};
if (!text || text.trim().length < 50) return res.status(400).json({ error: 'متن کافی ارسال نشده' });
const prompt = `متن زیر از یک گزارش تخصصی در حوزه صنعت فولاد است.
بر اساس این متن، یک مقاله جامع و تحلیلی به زبان فارسی بنویس که شامل این بخشها باشد:
۱. **عنوان** (یک عنوان جذاب و حرفهای)
۲. **خلاصه اجرایی** (۲ تا ۳ پاراگراف مهمترین یافتهها)
۳. **نکات کلیدی** (۴ تا ۶ گلوله bullet points)
۴. **تحلیل تفصیلی** (۳ تا ۵ پاراگراف بررسی عمیق)
۵. **نتیجهگیری** (۱ تا ۲ پاراگراف)
خروجی باید کاملاً فارسی، حرفهای، و مناسب برای یک اندیشکده تخصصی باشد.
${category ? `حوزه: ${category}` : ''}
متن منبع:
---
${text.slice(0, 6000)}
---
فقط محتوای مقاله را بنویس، بدون توضیح اضافه.`;
try {
const { default: OpenAI } = await import('openai');
const clientOpts = { apiKey };
if (process.env.AI_BASE_URL) clientOpts.baseURL = process.env.AI_BASE_URL;
const client = new OpenAI(clientOpts);
const model = process.env.AI_MODEL || 'gpt-4o-mini';
const completion = await client.chat.completions.create({
model,
max_tokens: 2048,
messages: [{ role: 'user', content: prompt }],
});
const generated = completion.choices[0]?.message?.content || '';
res.json({ generated });
} catch (err) {
console.error('AI generate error:', err);
res.status(500).json({ error: 'خطا در تولید محتوا: ' + err.message });
}
});
// ──────────────────────────────────────────
// Member auth & profile (public users, not admins)
// ──────────────────────────────────────────
app.post('/api/members/register', loginLimiter, (req, res) => {
const { name, email, password } = req.body || {};
if (!name || !email || !password) return res.status(400).json({ error: 'همه فیلدها الزامی است' });
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return res.status(400).json({ error: 'ایمیل نامعتبر است' });
if (password.length < 6) return res.status(400).json({ error: 'رمز عبور باید حداقل ۶ کاراکتر باشد' });
const exists = db.prepare('SELECT 1 FROM members WHERE email = ?').get(email);
if (exists) return res.status(409).json({ error: 'این ایمیل قبلاً ثبت شده است' });
const hash = bcrypt.hashSync(password, 10);
const info = db.prepare('INSERT INTO members (name, email, password_hash) VALUES (?, ?, ?)').run(name.trim(), email.toLowerCase().trim(), hash);
const token = jwt.sign({ sub: info.lastInsertRowid, email, name: name.trim(), role: 'member' }, JWT_SECRET, { expiresIn: '30d' });
res.status(201).json({ token, user: { id: info.lastInsertRowid, name: name.trim(), email } });
});
app.post('/api/members/login', loginLimiter, (req, res) => {
const { email, password } = req.body || {};
if (!email || !password) return res.status(400).json({ error: 'ایمیل و رمز عبور الزامی است' });
const row = db.prepare('SELECT id, name, email, password_hash, is_active FROM members WHERE email = ?').get(email.toLowerCase().trim());
if (!row || !bcrypt.compareSync(password, row.password_hash)) return res.status(401).json({ error: 'ایمیل یا رمز عبور اشتباه است' });
if (!row.is_active) return res.status(403).json({ error: 'حساب کاربری غیرفعال است' });
const token = jwt.sign({ sub: row.id, email: row.email, name: row.name, role: 'member' }, JWT_SECRET, { expiresIn: '30d' });
res.json({ token, user: { id: row.id, name: row.name, email: row.email } });
});
app.get('/api/members/me', memberRequired, (req, res) => {
const row = db.prepare('SELECT id, name, email, phone, avatar, created_at FROM members WHERE id = ?').get(req.member.sub);
if (!row) return res.status(404).json({ error: 'not_found' });
res.json({ id: row.id, name: row.name, email: row.email, phone: row.phone, avatar: row.avatar, createdAt: row.created_at });
});
app.put('/api/members/me', memberRequired, (req, res) => {
const { name, phone } = req.body || {};
const row = db.prepare('SELECT id FROM members WHERE id = ?').get(req.member.sub);
if (!row) return res.status(404).json({ error: 'not_found' });
const newName = name?.trim() || req.member.name;
db.prepare("UPDATE members SET name = ?, phone = ?, updated_at = datetime('now') WHERE id = ?").run(newName, phone || null, req.member.sub);
res.json({ ok: true });
});
app.put('/api/members/me/password', memberRequired, (req, res) => {
const { currentPassword, newPassword } = req.body || {};
if (!currentPassword || !newPassword) return res.status(400).json({ error: 'فیلدها الزامی است' });
if (newPassword.length < 6) return res.status(400).json({ error: 'رمز جدید باید حداقل ۶ کاراکتر باشد' });
const row = db.prepare('SELECT password_hash FROM members WHERE id = ?').get(req.member.sub);
if (!row || !bcrypt.compareSync(currentPassword, row.password_hash)) return res.status(401).json({ error: 'رمز فعلی اشتباه است' });
const hash = bcrypt.hashSync(newPassword, 10);
db.prepare("UPDATE members SET password_hash = ?, updated_at = datetime('now') WHERE id = ?").run(hash, req.member.sub);
res.json({ ok: true });
});
app.get('/api/members/me/purchases', memberRequired, (req, res) => {
const rows = db.prepare(`
SELECT p.id, p.amount, p.purchased_at,
a.id AS article_id, a.title, a.category, a.type, a.cover_image, a.pages, a.summary, a.author, a.publish_date
FROM purchases p JOIN articles a ON p.article_id = a.id
WHERE p.member_id = ?
ORDER BY p.purchased_at DESC
`).all(req.member.sub);
res.json(rows.map(r => ({
id: r.id, amount: r.amount, purchasedAt: r.purchased_at,
article: { id: r.article_id, title: r.title, category: r.category, type: r.type, coverImage: r.cover_image, pages: r.pages, summary: r.summary, author: r.author, publishDate: r.publish_date },
})));
});
// admin: grant purchase to a member
app.post('/api/members/:memberId/purchases', authRequired, (req, res) => {
const { articleId, amount = 0 } = req.body || {};
if (!articleId) return res.status(400).json({ error: 'articleId الزامی است' });
const member = db.prepare('SELECT id FROM members WHERE id = ?').get(req.params.memberId);
if (!member) return res.status(404).json({ error: 'عضو پیدا نشد' });
const article = db.prepare('SELECT id, cover_image FROM articles WHERE id = ?').get(articleId);
if (!article) return res.status(404).json({ error: 'مقاله پیدا نشد' });
try {
db.prepare('INSERT OR IGNORE INTO purchases (member_id, article_id, amount) VALUES (?, ?, ?)').run(req.params.memberId, articleId, amount);
res.status(201).json({ ok: true });
} catch (e) {
res.status(409).json({ error: 'قبلاً خریداری شده' });
}
});
// download purchased article file (cover image for now; swap for PDF path later)
app.get('/api/members/me/purchases/:purchaseId/download', memberRequired, (req, res) => {
const row = db.prepare(`
SELECT a.cover_image, a.title FROM purchases p JOIN articles a ON p.article_id = a.id
WHERE p.id = ? AND p.member_id = ?
`).get(req.params.purchaseId, req.member.sub);
if (!row) return res.status(404).json({ error: 'خرید پیدا نشد' });
const filePath = row.cover_image ? path.join(__dirname, 'uploads', path.basename(row.cover_image)) : null;
if (!filePath || !fs.existsSync(filePath)) return res.status(404).json({ error: 'فایل موجود نیست' });
res.download(filePath, row.title + path.extname(filePath));
});
app.listen(PORT, () => {
console.log(`Panel running at ${PUBLIC_ORIGIN}`);
const intervalMs = Number(process.env.SCRAPER_INTERVAL_MS) || 2 * 60 * 1000;

View File

@ -0,0 +1,43 @@
import Database from 'better-sqlite3';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const db = new Database(path.join(__dirname, 'data.db'));
// id → image (served from the frontend /public/news folder at /news/...)
const radarImages = {
'green-demand-growth': '/news/photo-1551288049-bebda4e38f71.jpg',
'stricter-green-definitions': '/news/photo-1581092160607-ee22731c9b4e.jpg',
'premium-willingness': '/news/photo-1466611653911-95081537e5b7.jpg',
'trading-value-growth': '/news/photo-1611974789855-9c2a0a7236a3.jpg',
'power-demand-surge': '/news/photo-1518770660439-4636190af475.jpg',
'digital-trading-edge': '/news/photo-1504328345606-18bbc8c9d7d1.jpg',
'electricity-cost-driver': '/news/photo-1494412519320-aa613dfb7738.jpg',
'hbi-decoupling': '/news/photo-1578662996442-48f60103fc96.jpg',
'massive-asset-reconfiguration': '/news/photo-1504307651254-35680f356dfd.jpg',
'india-demand-doubling': '/news/photo-1567789884554-0b844b597180.jpg',
'scrap-shortage-crisis': '/news/photo-1566873535350-6586b0b7a1f2.jpg',
'china-demand-decline': '/news/photo-1570168007204-dfb528c6958f.jpg',
};
const stmt = db.prepare('UPDATE radar_items SET img = ? WHERE id = ?');
let updated = 0;
for (const [id, img] of Object.entries(radarImages)) {
updated += stmt.run(img, id).changes;
}
console.log(`✓ radar images updated: ${updated}/${Object.keys(radarImages).length}`);
// also set the matching article cover images (for home report cards)
const articleImages = {
'green-premia-trends': '/news/photo-1473341304170-971dccb5ac1e.jpg',
'commodity-trading-scurve': '/news/photo-1611974789855-9c2a0a7236a3.jpg',
'green-steel-hubs-pathway': '/news/photo-1581092160607-ee22731c9b4e.jpg',
'india-steel-growth-resilience': '/news/photo-1567789884554-0b844b597180.jpg',
};
const aStmt = db.prepare('UPDATE articles SET cover_image = ? WHERE id = ?');
let aUpdated = 0;
for (const [id, img] of Object.entries(articleImages)) {
aUpdated += aStmt.run(img, id).changes;
}
console.log(`✓ article covers updated: ${aUpdated}/${Object.keys(articleImages).length}`);

BIN
rtk.exe Normal file

Binary file not shown.

View File

@ -1,9 +1,10 @@
import React, { useState, useEffect, useRef } from 'react'
import type { CSSProperties } from 'react'
import { NavLink } from 'react-router-dom'
import { NavLink, Link } from 'react-router-dom'
import { Search, Menu, X, ChevronDown, Radar, Leaf, Globe, Cpu, BarChart3, Video, Mic, CalendarDays, Users, Eye, Phone, type LucideIcon } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
import { useLang } from '@/context/LangContext'
import { useAuth } from '@/context/AuthContext'
/* ─── tokens ─── */
const T = {
@ -30,9 +31,9 @@ const SECTIONS: Section[] = [
/* ─── dropdown sections under رسانه و رویداد ─── */
const MEDIA: Section[] = [
{ fa: 'ویدیوکست و پادکست', en: 'Videocast & Podcast', to: '#', Icon: Video },
{ fa: 'نشست‌های تخصصی', en: 'Expert Sessions', to: '#', Icon: Mic },
{ fa: 'رویدادها', en: 'Events', to: '#', Icon: CalendarDays },
{ fa: 'ویدیوکست و پادکست', en: 'Videocast & Podcast', to: '/podcast', Icon: Video },
{ fa: 'نشست‌های تخصصی', en: 'Expert Sessions', to: '#', Icon: Mic },
{ fa: 'رویدادها', en: 'Events', to: '/events', Icon: CalendarDays },
]
/* ─── dropdown sections under درباره ما ─── */
@ -194,7 +195,7 @@ export default function Header() {
const searchInputRef = useRef<HTMLInputElement>(null)
const { lang, toggle: toggleLang } = useLang()
const { member } = useAuth()
useEffect(() => {
const mq = window.matchMedia('(max-width: 1023px)')
@ -282,9 +283,28 @@ export default function Header() {
{!isMobile && (
<>
<OutlineButton style={{ height: 34, padding: '0 16px', fontSize: 12 }}>
{lang === 'fa' ? 'ورود / ثبت‌نام' : 'Login / Register'}
</OutlineButton>
{member ? (
<Link to="/profile" style={{
height: 34, padding: '0 14px', fontSize: 12, fontWeight: 700,
display: 'flex', alignItems: 'center', gap: 6,
background: '#CD9E53', color: '#032340', textDecoration: 'none',
border: 'none', cursor: 'pointer',
}}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
{member.name.split(' ')[0]}
</Link>
) : (
<Link to="/login" style={{
height: 34, padding: '0 16px', fontSize: 12, fontWeight: 600,
display: 'flex', alignItems: 'center',
border: `1px solid ${T.ruleThin}`, color: T.ink,
textDecoration: 'none', background: 'transparent',
}}>
{lang === 'fa' ? 'ورود / ثبت‌نام' : 'Login / Register'}
</Link>
)}
<button
onClick={toggleLang}
aria-label={lang === 'fa' ? 'Switch to English' : 'تغییر به فارسی'}

View File

@ -4,7 +4,7 @@ import type { MarketPrice, PriceTrend } from '@/data/market';
const PANEL_API =
(import.meta as ImportMeta & { env?: Record<string, string> }).env
?.VITE_PANEL_API || 'http://localhost:3001';
?.VITE_PANEL_API || '';
type ApiPrice = {
symbol: string;

View File

@ -1,7 +1,6 @@
import { createBrowserRouter } from 'react-router-dom'
import RootLayout from './layout/RootLayout'
import Home from '@/pages/Home/Home'
import Reports from '@/pages/Reports/Reports'
import ReportDetail from '@/pages/ReportDetail/ReportDetail'
import Market from '@/pages/Market/Market'
import Subscribe from '@/pages/Subscribe/Subscribe'
@ -13,6 +12,7 @@ import Risks from '@/pages/Risks/Risks'
import Scanner from '@/pages/Scanner/Scanner'
import Radar from '@/pages/Radar/Radar'
import RadarCategory from '@/pages/RadarCategory/RadarCategory'
import RadarPost from '@/pages/RadarPost/RadarPost'
import PostDetail from '@/pages/PostDetail/PostDetail'
import Technology from '@/pages/Technology/Technology'
import Pulse from '@/pages/Pulse/Pulse'
@ -23,6 +23,9 @@ import AtAGlance from '@/pages/AtAGlance/AtAGlance'
import Experts from '@/pages/Experts/Experts'
import Contact from '@/pages/Contact/Contact'
import About from '@/pages/About/About'
import AuthPage from '@/pages/Auth/AuthPage'
import ProfilePage from '@/pages/Profile/ProfilePage'
import Podcast from '@/pages/Podcast/Podcast'
export const router = createBrowserRouter([
{
@ -30,7 +33,6 @@ export const router = createBrowserRouter([
element: <RootLayout />,
children: [
{ index: true, element: <Home /> },
{ path: 'reports', element: <Reports /> },
{ path: 'reports/:id', element: <ReportDetail /> },
{ path: 'market', element: <Market /> },
{ path: 'subscribe', element: <Subscribe /> },
@ -42,6 +44,7 @@ export const router = createBrowserRouter([
{ path: 'scanner', element: <Scanner /> },
{ path: 'radar', element: <Radar /> },
{ path: 'radar/:category', element: <RadarCategory /> },
{ path: 'radar/post/:id', element: <RadarPost /> },
{ path: 'posts/:id', element: <PostDetail /> },
{ path: 'technology', element: <Technology /> },
{ path: 'pulse', element: <Pulse /> },
@ -52,6 +55,10 @@ export const router = createBrowserRouter([
{ path: 'experts', element: <Experts /> },
{ path: 'contact', element: <Contact /> },
{ path: 'about', element: <About /> },
{ path: 'login', element: <AuthPage defaultMode="login" /> },
{ path: 'register', element: <AuthPage defaultMode="register" /> },
{ path: 'profile', element: <ProfilePage /> },
{ path: 'podcast', element: <Podcast /> },
],
},
])

View File

@ -1,33 +1,23 @@
import { useState } from 'react'
import { ChevronDown, ChevronRight } from 'lucide-react'
import { Link } from 'react-router-dom'
import { events } from '@/data/events'
const NAVY = '#032340'
const GOLD = '#CD9E53'
type Ev = { title: string; desc: string; when: string }
const EVENTS: Ev[] = [
{
title: 'اجلاس جهانی فولاد سبز | استکهلم، سوئد',
desc: 'بررسی آخرین تکنولوژی‌های احیای مستقیم بر پایه هیدروژن و جذب سرمایه برای پروژه‌های بدون کربن. حضور در این رویداد برای درک آینده مقررات زیست‌محیطی اروپا حیاتی است.',
when: 'مهر ۱۴۰۵',
},
{
title: 'نشست سالانه انجمن جهانی فولاد (worldsteel) | شانگهای، چین',
desc: 'گردهمایی بزرگ‌ترین رهبران صنعت. محور اصلی امسال: پیش‌بینی تقاضای جهانی در سایه رکود بخش مسکن چین و تغییر مسیر زنجیره تامین به سمت هند و خاورمیانه.',
when: 'آبان ۱۴۰۵',
},
{
title: 'مجمع بین‌المللی هوش مصنوعی در تولید | برلین، آلمان',
desc: 'رونمایی از جدیدترین نسل دوقلوهای دیجیتال (Digital Twins) و الگوریتم‌های پیش‌بینی تعمیرات. فرصتی برای آشنایی با راهکارهای کاهش هزینه و افزایش بهره‌وری خطوط نورد.',
when: 'آذر ۱۴۰۵',
},
]
const faNum = (n: number | string) => String(n).replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[+d])
export default function StrategicEvents() {
const [open, setOpen] = useState<number | null>(0)
// live events from the panel API (bootstrapEvents fills this array; soonest first)
const list = events.slice(0, 4).map((e) => ({
title: `${e.title} | ${e.location}`,
desc: e.description,
when: `${faNum(e.date)} ${e.month} ${faNum(e.year)}`,
}))
return (
<div style={{ direction: 'rtl' }}>
{/* heading */}
@ -51,7 +41,7 @@ export default function StrategicEvents() {
{/* accordion list */}
<div>
{EVENTS.map((e, i) => {
{list.map((e, i) => {
const isOpen = open === i
return (
<div key={i} style={{ borderBottom: '1px solid var(--rule-thin)' }}>

View File

@ -71,7 +71,7 @@ export const FACTORY_REPORTS_META = {
const PANEL_API =
(import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API ||
'http://localhost:3001'
''
const SCROLL_DIRS: ScrollDir[] = ['left', 'right', 'up']
const asDir = (v: unknown): ScrollDir =>

View File

@ -52,7 +52,7 @@ export const INTEGRATIONS_META = {
const PANEL_API =
(import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API ||
'http://localhost:3001'
''
const ICON_TYPES: IntegrationIcon[] = [
'exchange', 'globe', 'chart', 'activity', 'database',

View File

@ -11,10 +11,9 @@ export type RadarItem = {
id: string
category: RadarCategory
img: string
/** author shown on hover (replaces the category icon) */
author: { name: string; avatar: string }
fa: { title: string; date: string; excerpt: string }
en: { title: string; date: string; excerpt: string }
fa: { title: string; date: string; excerpt: string; body?: string[]; analysis?: string }
en: { title: string; date: string; excerpt: string; body?: string[]; analysis?: string }
}
/* category → label (icon is chosen in the component from this key) */
@ -32,61 +31,205 @@ export const RADAR_ITEMS: RadarItem[] = [
id: 'r1', category: 'commodity',
img: '/news/photo-1611273426858-450d8e3c9fce.jpg',
author: { name: 'دکتر سارا احمدی', avatar: '/news/photo-1494790108377-be9c29b29330.jpg' },
fa: { title: 'پیش‌بینی بازار با هوش مصنوعی', date: '۱۴ اردیبهشت ۱۴۰۵', excerpt: 'مدل‌های یادگیری ماشین تقاضای لیتیوم و فلزات باتری را در افق ۲۰۳۵ بازتعریف می‌کنند.' },
en: { title: 'AI-Powered Market Forecasting', date: 'May 4, 2026', excerpt: 'Machine-learning models are redefining lithium and battery-metal demand on the 2035 horizon.' },
fa: {
title: 'پیش‌بینی بازار با هوش مصنوعی', date: '۱۴ اردیبهشت ۱۴۰۵',
excerpt: 'مدل‌های یادگیری ماشین تقاضای لیتیوم و فلزات باتری را در افق ۲۰۳۵ بازتعریف می‌کنند.',
body: [
'در سال‌های اخیر، کاربرد هوش مصنوعی در پیش‌بینی تقاضای مواد اولیه به یکی از جدی‌ترین تحولات بازارهای کالایی تبدیل شده است. مدل‌های یادگیری ماشین با پردازش میلیون‌ها نقطه داده از زنجیره تأمین، الگوهایی را شناسایی می‌کنند که از چشم تحلیلگران انسانی پنهان می‌ماند.',
'در حوزه لیتیوم و فلزات باتری، این مدل‌ها توانسته‌اند با دقتی بیش از ۸۵ درصد، نوسانات فصلی و بلندمدت تقاضا را پیش‌بینی کنند. این دقت بالا به شرکت‌های استخراج‌کننده امکان می‌دهد تصمیمات سرمایه‌گذاری خود را با اطمینان بیشتری بگیرند.',
'علاوه بر این، این مدل‌ها شوک‌های ناگهانی ناشی از تغییرات سیاسی و بحران‌های زنجیره تأمین را نیز وارد محاسبات خود می‌کنند. نتیجه، پیش‌بینی‌هایی است که نه‌تنها دقیق‌تر، بلکه انعطاف‌پذیرتر نسبت به شرایط غیرمنتظره هستند.',
],
analysis: 'با توجه به رشد انفجاری صنعت خودروهای الکتریکی، تقاضا برای لیتیوم تا سال ۲۰۳۵ می‌تواند تا ۵ برابر افزایش یابد. کشورهایی که اکنون در زیرساخت‌های استخراج و پالایش لیتیوم سرمایه‌گذاری می‌کنند، در دهه‌های آینده مزیت استراتژیک خواهند داشت.',
},
en: {
title: 'AI-Powered Market Forecasting', date: 'May 4, 2026',
excerpt: 'Machine-learning models are redefining lithium and battery-metal demand on the 2035 horizon.',
body: [
'In recent years, the application of AI in forecasting raw material demand has become one of the most significant developments in commodity markets. Machine learning models process millions of data points from the supply chain, identifying patterns invisible to human analysts.',
'In the lithium and battery metals space, these models have achieved over 85% accuracy in predicting seasonal and long-term demand fluctuations. This high accuracy allows mining companies to make investment decisions with greater confidence.',
'Additionally, these models incorporate sudden shocks from political changes and supply chain crises into their calculations, resulting in forecasts that are both more accurate and more resilient to unexpected conditions.',
],
analysis: 'Given the explosive growth of the electric vehicle industry, lithium demand could increase up to 5x by 2035. Countries investing now in lithium extraction and refining infrastructure will hold a strategic advantage in the coming decades.',
},
},
{
id: 'r2', category: 'tech',
img: '/news/photo-1581092160607-ee22731c9b4e.jpg',
author: { name: 'مهندس رضا کریمی', avatar: '/news/photo-1500648767791-00dcc994a43e.jpg' },
fa: { title: 'پیش‌بینی بازار با هوش مصنوعی', date: '۱۴ اردیبهشت ۱۴۰۵', excerpt: 'تحول دیجیتال و اتوماسیون هوشمند در حال تغییر بنیادین زنجیره ارزش فولاد هستند.' },
en: { title: 'AI-Powered Market Forecasting', date: 'May 4, 2026', excerpt: 'Digital transformation and smart automation are fundamentally reshaping the steel value chain.' },
fa: {
title: 'تحول دیجیتال در زنجیره ارزش فولاد', date: '۱۴ اردیبهشت ۱۴۰۵',
excerpt: 'تحول دیجیتال و اتوماسیون هوشمند در حال تغییر بنیادین زنجیره ارزش فولاد هستند.',
body: [
'صنعت فولاد که یکی از کهن‌ترین صنایع بشری است، اکنون در آستانه یک انقلاب دیجیتال قرار دارد. از کوره‌های ذوب هوشمند تا سیستم‌های نگهداری پیش‌گیرانه مبتنی بر یادگیری ماشین، فناوری در حال بازتعریف هر مرحله از تولید فولاد است.',
'کارخانه‌های پیشرو در اروپا و آسیا با نصب هزاران سنسور و اتصال آن‌ها به پلتفرم‌های تحلیل داده ابری، توانسته‌اند مصرف انرژی را تا ۱۸ درصد کاهش دهند و نرخ خرابی تجهیزات را به نصف برسانند.',
'اما چالش اصلی در این مسیر، کمبود نیروی انسانی آشنا با هر دو حوزه متالورژی و علوم داده است. این شکاف مهارتی در صورت عدم توجه، می‌تواند سرعت تحول دیجیتال را کُند کند.',
],
analysis: 'شرکت‌های فولادی ایران باید فوری‌ترین اولویت خود را ارتقای دیجیتال زنجیره ارزش قرار دهند. در غیر این صورت، شکاف رقابتی با تولیدکنندگان پیشرفته جهانی هر سال بزرگ‌تر خواهد شد.',
},
en: {
title: 'Digital Transformation in Steel Value Chain', date: 'May 4, 2026',
excerpt: 'Digital transformation and smart automation are fundamentally reshaping the steel value chain.',
body: [
'The steel industry, one of humanity\'s oldest industries, now stands on the brink of a digital revolution. From smart smelting furnaces to machine-learning-based predictive maintenance, technology is redefining every stage of steel production.',
'Leading plants in Europe and Asia have installed thousands of sensors connected to cloud analytics platforms, reducing energy consumption by up to 18% and halving equipment failure rates.',
'The main challenge, however, is the shortage of personnel skilled in both metallurgy and data science — a skills gap that, if unaddressed, could slow the pace of digital transformation.',
],
analysis: 'Iranian steel companies must make digital value chain upgrading their most urgent priority. Otherwise, the competitive gap with advanced global producers will widen every year.',
},
},
{
id: 'r3', category: 'market',
img: '/news/photo-1504307651254-35680f356dfd.jpg',
author: { name: 'دکتر مریم رضایی', avatar: '/news/photo-1438761681033-6461ffad8d80.jpg' },
fa: { title: 'پیش‌بینی بازار با هوش مصنوعی', date: '۱۴ اردیبهشت ۱۴۰۵', excerpt: 'تحلیل روند قیمت طلا و فلزات گران‌بها و اثر آن بر سرمایه‌گذاری در زنجیره فولاد.' },
en: { title: 'AI-Powered Market Forecasting', date: 'May 4, 2026', excerpt: 'Gold and precious-metal price trends and their effect on steel-chain investment.' },
fa: {
title: 'تأثیر نوسانات طلا بر سرمایه‌گذاری فولاد', date: '۱۴ اردیبهشت ۱۴۰۵',
excerpt: 'تحلیل روند قیمت طلا و فلزات گران‌بها و اثر آن بر سرمایه‌گذاری در زنجیره فولاد.',
body: [
'قیمت طلا در سال‌های اخیر به شاخصی مهم برای سنجش ریسک‌گریزی سرمایه‌گذاران جهانی تبدیل شده است. هنگامی که طلا افزایش قیمت می‌یابد، معمولاً نشانه‌ای از کاهش اشتیاق سرمایه‌گذاران به دارایی‌های ریسکی‌تر مانند سهام شرکت‌های فولادی است.',
'با این حال، رابطه بین طلا و فولاد پیچیده‌تر از یک همبستگی ساده است. در دوره‌های رشد اقتصادی قوی، هر دو می‌توانند همزمان رشد کنند — طلا به عنوان ذخیره ارزش و فولاد به خاطر تقاضای ساخت‌وساز.',
'تحلیل داده‌های ۲۰ ساله نشان می‌دهد که نوسانات شدید قیمت طلا معمولاً با کاهش سرمایه‌گذاری در پروژه‌های جدید فولادسازی همراه است، چرا که سرمایه‌گذاران به سمت دارایی‌های امن‌تر کشیده می‌شوند.',
],
analysis: 'در شرایط فعلی که قیمت طلا در اوج تاریخی قرار دارد، انتظار کاهش نسبی سرمایه‌گذاری در طرح‌های توسعه‌ای فولاد را داریم. این پنجره زمانی فرصتی است برای شرکت‌هایی که نقدینگی کافی دارند تا دارایی‌های ارزان‌شده را خریداری کنند.',
},
en: {
title: 'Gold Volatility Impact on Steel Investment', date: 'May 4, 2026',
excerpt: 'Gold and precious-metal price trends and their effect on steel-chain investment.',
body: [
'Gold prices have become an important indicator of global investor risk aversion in recent years. When gold rises, it typically signals reduced investor appetite for riskier assets like steel company equities.',
'However, the relationship between gold and steel is more complex than a simple correlation. During periods of strong economic growth, both can rise simultaneously — gold as a store of value and steel due to construction demand.',
'Analysis of 20 years of data shows that sharp gold price volatility typically coincides with reduced investment in new steelmaking projects, as investors gravitate toward safer assets.',
],
analysis: 'With gold prices at historic highs, we expect a relative reduction in investment in steel development projects. This time window is an opportunity for companies with sufficient liquidity to acquire distressed assets.',
},
},
{
id: 'r4', category: 'geo',
img: '/news/photo-1518770660439-4636190af475.jpg',
author: { name: 'دکتر علی موسوی', avatar: '/news/photo-1472099645785-5658abf4ff4e.jpg' },
fa: { title: 'پیش‌بینی بازار با هوش مصنوعی', date: '۱۴ اردیبهشت ۱۴۰۵', excerpt: 'هندسه قدرت جهانی و اثر تحولات ژئوپلیتیک بر بازارهای صادراتی فولاد ایران.' },
en: { title: 'AI-Powered Market Forecasting', date: 'May 4, 2026', excerpt: 'Global power geometry and the impact of geopolitics on Iran\'s steel export markets.' },
fa: {
title: 'ژئوپلیتیک و بازارهای صادراتی فولاد ایران', date: '۱۴ اردیبهشت ۱۴۰۵',
excerpt: 'هندسه قدرت جهانی و اثر تحولات ژئوپلیتیک بر بازارهای صادراتی فولاد ایران.',
body: [
'در دوران جدید تنش‌های جهانی، ژئوپلیتیک به یکی از تعیین‌کننده‌ترین عوامل در بازارهای کالایی تبدیل شده است. برای ایران، که یکی از ۱۰ تولیدکننده بزرگ فولاد جهان است، درک این پویایی‌ها حیاتی است.',
'رقابت آمریکا-چین، جنگ روسیه-اوکراین و تنش‌های خاورمیانه سه محور اصلی ژئوپلیتیک فعلی هستند که مستقیماً بر مسیرهای تجاری فولاد تأثیر می‌گذارند. ایران در این سه‌راه قرار دارد و می‌تواند از برخی از این تنش‌ها به نفع خود بهره‌برداری کند.',
'کشورهای در حال توسعه آسیا و آفریقا که از فشار کشورهای غربی برای تحریم ایران سرباز می‌زنند، بازارهای بالقوه‌ای هستند که فولاد ایران می‌تواند در آن‌ها حضور جدی داشته باشد.',
],
analysis: 'ایران باید استراتژی صادراتی خود را به سمت "محور جنوب جهانی" — یعنی آفریقا، آسیای جنوب‌شرقی و آمریکای لاتین — متمرکز کند. این کشورها نه‌تنها تقاضای رو به رشدی دارند، بلکه از فشارهای ژئوپلیتیک غرب نیز مصون‌ترند.',
},
en: {
title: 'Geopolitics and Iran\'s Steel Export Markets', date: 'May 4, 2026',
excerpt: 'Global power geometry and the impact of geopolitics on Iran\'s steel export markets.',
body: [
'In the new era of global tensions, geopolitics has become one of the most decisive factors in commodity markets. For Iran, one of the world\'s top-10 steel producers, understanding these dynamics is critical.',
'The US-China rivalry, the Russia-Ukraine war, and Middle East tensions are the three main current geopolitical axes directly affecting steel trade routes. Iran sits at this crossroads and can leverage some of these tensions to its advantage.',
'Developing countries in Asia and Africa that resist Western pressure to sanction Iran are potential markets where Iranian steel can establish a serious presence.',
],
analysis: 'Iran must focus its export strategy toward the "Global South" — Africa, Southeast Asia, and Latin America. These countries not only have growing demand but are also more insulated from Western geopolitical pressures.',
},
},
{
id: 'r5', category: 'energy',
img: '/news/photo-1466611653911-95081537e5b7.jpg',
author: { name: 'مهندس نازنین حسینی', avatar: '/news/photo-1544005313-94ddf0286df2.jpg' },
fa: { title: 'گذار انرژی و فولاد سبز', date: '۱۲ اردیبهشت ۱۴۰۵', excerpt: 'هیدروژن سبز و برق تجدیدپذیر مسیر کربن‌زدایی صنعت فولاد را شتاب می‌دهند.' },
en: { title: 'Energy Transition & Green Steel', date: 'May 2, 2026', excerpt: 'Green hydrogen and renewable power are accelerating steel decarbonization.' },
fa: {
title: 'گذار انرژی و فولاد سبز', date: '۱۲ اردیبهشت ۱۴۰۵',
excerpt: 'هیدروژن سبز و برق تجدیدپذیر مسیر کربن‌زدایی صنعت فولاد را شتاب می‌دهند.',
body: [
'صنعت فولاد مسئول حدود ۷ تا ۹ درصد از کل انتشار کربن جهانی است. این رقم بزرگ، این صنعت را به یکی از اهداف اصلی سیاست‌های اقلیمی تبدیل کرده است. اما راه‌حل چیست؟',
'هیدروژن سبز — هیدروژنی که با برق تجدیدپذیر تولید می‌شود — امیدوارکننده‌ترین مسیر برای کربن‌زدایی از فولادسازی است. به جای کک زغال‌سنگی، هیدروژن می‌تواند به عنوان عامل احیاکننده در فرایند تبدیل سنگ‌آهن به آهن استفاده شود.',
'کشورهایی مانند سوئد، آلمان و استرالیا در خط مقدم این تحول هستند. اولین واحدهای تجاری فولادسازی با هیدروژن تا پایان دهه ۲۰۲۰ راه‌اندازی خواهند شد.',
],
analysis: 'برای ایران، که منابع عظیم خورشیدی و بادی دارد، تولید هیدروژن سبز یک فرصت استراتژیک است. سرمایه‌گذاری در این زیرساخت نه‌تنها صنعت فولاد داخلی را پایدار می‌کند، بلکه می‌تواند ایران را به یک صادرکننده هیدروژن تبدیل کند.',
},
en: {
title: 'Energy Transition & Green Steel', date: 'May 2, 2026',
excerpt: 'Green hydrogen and renewable power are accelerating steel decarbonization.',
body: [
'The steel industry is responsible for around 7-9% of global carbon emissions. This large figure has made it a primary target of climate policies. But what is the solution?',
'Green hydrogen — hydrogen produced with renewable electricity — is the most promising path for decarbonizing steelmaking. Instead of metallurgical coke, hydrogen can be used as a reducing agent in converting iron ore to iron.',
'Countries like Sweden, Germany and Australia are at the forefront of this transformation. The first commercial hydrogen steelmaking units will be commissioned by the end of the 2020s.',
],
analysis: 'For Iran, which has vast solar and wind resources, green hydrogen production is a strategic opportunity. Investment in this infrastructure not only makes the domestic steel industry sustainable but could also turn Iran into a hydrogen exporter.',
},
},
{
id: 'r6', category: 'tech',
img: '/news/photo-1567789884554-0b844b597180.jpg',
author: { name: 'دکتر سارا احمدی', avatar: '/news/photo-1494790108377-be9c29b29330.jpg' },
fa: { title: 'اتوماسیون خطوط نورد', date: '۱۰ اردیبهشت ۱۴۰۵', excerpt: 'سیستم‌های کنترل هوشمند بهره‌وری خطوط نورد را تا ۲۲٪ افزایش داده‌اند.' },
en: { title: 'Rolling Line Automation', date: 'Apr 30, 2026', excerpt: 'Smart control systems have boosted rolling-line productivity by up to 22%.' },
fa: {
title: 'اتوماسیون خطوط نورد', date: '۱۰ اردیبهشت ۱۴۰۵',
excerpt: 'سیستم‌های کنترل هوشمند بهره‌وری خطوط نورد را تا ۲۲٪ افزایش داده‌اند.',
body: [
'خطوط نورد فولاد یکی از پیچیده‌ترین و پرانرژی‌ترین بخش‌های تولید فولاد هستند. کنترل دقیق دما، فشار و سرعت در هر مرحله از نورد، تعیین‌کننده کیفیت نهایی محصول است.',
'سیستم‌های هوشمند کنترل فرایند با استفاده از دوربین‌های پرسرعت، سنسورهای دما و الگوریتم‌های یادگیری ماشین، توانسته‌اند این پارامترها را در زمان واقعی بهینه کنند.',
'نتایج حاصل چشمگیر است: کاهش ضایعات تولید تا ۳۰ درصد، افزایش عمر تجهیزات تا ۴۰ درصد و بهبود یکنواختی کیفیت محصول تا ۲۵ درصد.',
],
analysis: 'سرمایه‌گذاری در اتوماسیون خطوط نورد با دوره بازگشت سرمایه کمتر از سه سال، یکی از اقتصادی‌ترین اقدامات برای بهبود رقابت‌پذیری کارخانه‌های فولاد ایران است.',
},
en: {
title: 'Rolling Line Automation', date: 'Apr 30, 2026',
excerpt: 'Smart control systems have boosted rolling-line productivity by up to 22%.',
body: [
'Steel rolling mills are among the most complex and energy-intensive parts of steel production. Precise control of temperature, pressure and speed at each rolling stage determines the final product quality.',
'Intelligent process control systems using high-speed cameras, temperature sensors and machine learning algorithms have been able to optimize these parameters in real time.',
'The results are impressive: up to 30% reduction in production waste, 40% increase in equipment life and 25% improvement in product quality consistency.',
],
analysis: 'Investment in rolling line automation, with a payback period of less than three years, is one of the most economical measures for improving the competitiveness of Iranian steel plants.',
},
},
{
id: 'r7', category: 'commodity',
img: '/news/photo-1570168007204-dfb528c6958f.jpg',
author: { name: 'مهندس رضا کریمی', avatar: '/news/photo-1500648767791-00dcc994a43e.jpg' },
fa: { title: 'زنجیره تأمین سنگ‌آهن', date: '۸ اردیبهشت ۱۴۰۵', excerpt: 'نوسانات عرضه سنگ‌آهن استرالیا و برزیل و پیامد آن برای تولیدکنندگان منطقه.' },
en: { title: 'Iron Ore Supply Chain', date: 'Apr 28, 2026', excerpt: 'Australian and Brazilian iron-ore supply swings and their regional fallout.' },
fa: {
title: 'زنجیره تأمین سنگ‌آهن', date: '۸ اردیبهشت ۱۴۰۵',
excerpt: 'نوسانات عرضه سنگ‌آهن استرالیا و برزیل و پیامد آن برای تولیدکنندگان منطقه.',
body: [
'استرالیا و برزیل با هم بیش از ۶۰ درصد از صادرات جهانی سنگ‌آهن را در اختیار دارند. هرگونه اختلال در این دو کشور — از طوفان‌های استوایی تا اعتصاب‌های کارگری — می‌تواند موجی از نوسانات قیمتی را در سراسر بازار جهانی فولاد ایجاد کند.',
'در سال ۲۰۲۵، بارندگی‌های شدید در ایالت پارا برزیل باعث توقف ۳ هفته‌ای عملیات معدن ساراماروکا شد و قیمت سنگ‌آهن را تا ۱۸ درصد در یک هفته افزایش داد.',
'راه‌حل بلندمدت برای کشورهای وابسته، تنوع‌بخشی به منابع تأمین سنگ‌آهن است. آفریقای غربی، به‌ویژه گینه، پتانسیل بزرگی برای تبدیل شدن به یک تأمین‌کننده اصلی در دهه آینده دارد.',
],
analysis: 'ایران با داشتن ذخایر قابل توجه سنگ‌آهن داخلی، در موقعیت نسبتاً مطلوبی قرار دارد. اما برای رسیدن به خودکفایی کامل، نیاز به سرمایه‌گذاری جدی در اکتشاف و توسعه معادن جدید وجود دارد.',
},
en: {
title: 'Iron Ore Supply Chain', date: 'Apr 28, 2026',
excerpt: 'Australian and Brazilian iron-ore supply swings and their regional fallout.',
body: [
'Australia and Brazil together control over 60% of global iron ore exports. Any disruption in these two countries — from tropical storms to labor strikes — can create waves of price volatility across the global steel market.',
'In 2025, heavy rains in Brazil\'s Pará state caused a 3-week halt in operations at the Saramarca mine, pushing iron ore prices up 18% in a single week.',
'The long-term solution for dependent countries is diversifying iron ore supply sources. West Africa, particularly Guinea, has significant potential to become a major supplier in the next decade.',
],
analysis: 'Iran, with its significant domestic iron ore reserves, is in a relatively favorable position. However, achieving full self-sufficiency requires serious investment in exploration and development of new mines.',
},
},
{
id: 'r8', category: 'market',
img: '/news/photo-1540575467063-178a50c2df87.jpg',
author: { name: 'دکتر مریم رضایی', avatar: '/news/photo-1438761681033-6461ffad8d80.jpg' },
fa: { title: 'تحلیل بازار جهانی فولاد', date: '۶ اردیبهشت ۱۴۰۵', excerpt: 'سیل صادرات فولاد چین و فشار قیمتی بر بازارهای هدف صادراتی ایران.' },
en: { title: 'Global Steel Market Analysis', date: 'Apr 26, 2026', excerpt: 'China\'s steel export flood and price pressure on Iran\'s target markets.' },
fa: {
title: 'تحلیل بازار جهانی فولاد', date: '۶ اردیبهشت ۱۴۰۵',
excerpt: 'سیل صادرات فولاد چین و فشار قیمتی بر بازارهای هدف صادراتی ایران.',
body: [
'چین با تولید بیش از ۱ میلیارد تن فولاد در سال — که بیش از نیمی از تولید جهانی است — بازیگر مسلط بازار جهانی است. در سال‌های اخیر، به دلیل رکود بخش مسکن داخلی، چین با مازاد ظرفیت روبرو شده و این مازاد را از طریق افزایش صادرات تخلیه کرده است.',
'این سیل صادرات چینی، قیمت‌ها را در بازارهای هدف — از جنوب‌شرق آسیا تا خاورمیانه — به شدت کاهش داده است. صادرکنندگان فولاد ایران که با تحریم‌ها هم دست‌وپنجه نرم می‌کنند، اکنون باید با این فشار قیمتی اضافی هم کنار بیایند.',
'اتحادیه اروپا و آمریکا با وضع تعرفه‌های سنگین بر فولاد چینی واکنش نشان داده‌اند، اما این کشورها بازار هدف اصلی ایران نیستند. رقابت در بازارهای آسیا و آفریقا همچنان شدید است.',
],
analysis: 'تنها راه رقابت با فولاد ارزان چینی، ارتقای کیفیت و تخصصی‌سازی است. محصولات فولادی با ارزش افزوده بالا — مانند فولادهای آلیاژی خاص، فولاد ضدزنگ و مقاطع پیچیده ساختمانی — کمتر تحت فشار قیمتی چین هستند.',
},
en: {
title: 'Global Steel Market Analysis', date: 'Apr 26, 2026',
excerpt: 'China\'s steel export flood and price pressure on Iran\'s target markets.',
body: [
'China, producing over 1 billion tons of steel per year — more than half of global output — is the dominant player in world markets. In recent years, due to a domestic real estate slump, China has faced excess capacity and has been releasing it through increased exports.',
'This flood of Chinese exports has severely depressed prices in target markets — from Southeast Asia to the Middle East. Iranian steel exporters, already wrestling with sanctions, must now cope with this additional pricing pressure.',
'The EU and US have responded with heavy tariffs on Chinese steel, but these are not Iran\'s primary target markets. Competition in Asia and Africa remains intense.',
],
analysis: 'The only way to compete with cheap Chinese steel is through quality upgrading and specialization. High value-added steel products — such as special alloy steels, stainless steel and complex structural sections — are less exposed to Chinese price pressure.',
},
},
]
const PANEL_API = (import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API || 'http://localhost:3001'
const PANEL_API = (import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API || ''
const RADAR_CATEGORIES: RadarCategory[] = ['market', 'tech', 'commodity', 'geo', 'energy']

View File

@ -220,7 +220,7 @@ export { BOOK_IMG }
*/
const PANEL_API =
(import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API ||
'http://localhost:3001'
''
const VALID_SLUGS = new Set(RADAR_SLUGS)
const VALID_CATS: RadarCategory[] = ['market', 'tech', 'commodity', 'geo', 'energy']

102
src/context/AuthContext.tsx Normal file
View File

@ -0,0 +1,102 @@
import React, { createContext, useContext, useState, useCallback } from 'react'
const BASE = import.meta.env.VITE_PANEL_API ?? ''
export interface Member {
id: number
name: string
email: string
phone?: string
avatar?: string
}
interface AuthState {
token: string | null
member: Member | null
}
interface AuthContextValue extends AuthState {
login: (email: string, password: string) => Promise<void>
register: (name: string, email: string, password: string, phone?: string, org?: string) => Promise<void>
logout: () => void
updateMember: (data: Partial<Member>) => void
}
const AuthContext = createContext<AuthContextValue | null>(null)
function load(): AuthState {
try {
const token = localStorage.getItem('member_token')
const raw = localStorage.getItem('member_info')
if (token && raw) return { token, member: JSON.parse(raw) }
} catch { /* ignore */ }
return { token: null, member: null }
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<AuthState>(load)
const login = useCallback(async (email: string, password: string) => {
const res = await fetch(`${BASE}/api/members/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'خطا در ورود')
localStorage.setItem('member_token', data.token)
localStorage.setItem('member_info', JSON.stringify(data.user))
setState({ token: data.token, member: data.user })
}, [])
const register = useCallback(async (name: string, email: string, password: string, phone?: string, org?: string) => {
const res = await fetch(`${BASE}/api/members/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, password, phone, org }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'خطا در ثبت‌نام')
localStorage.setItem('member_token', data.token)
localStorage.setItem('member_info', JSON.stringify(data.user))
setState({ token: data.token, member: data.user })
}, [])
const logout = useCallback(() => {
localStorage.removeItem('member_token')
localStorage.removeItem('member_info')
setState({ token: null, member: null })
}, [])
const updateMember = useCallback((data: Partial<Member>) => {
setState(prev => {
if (!prev.member) return prev
const updated = { ...prev.member, ...data }
localStorage.setItem('member_info', JSON.stringify(updated))
return { ...prev, member: updated }
})
}, [])
return (
<AuthContext.Provider value={{ ...state, login, register, logout, updateMember }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error('useAuth must be inside AuthProvider')
return ctx
}
export function authFetch(token: string, path: string, opts: RequestInit = {}) {
return fetch(`${BASE}${path}`, {
...opts,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
...(opts.headers as Record<string, string> ?? {}),
},
})
}

View File

@ -103,7 +103,7 @@ export const advisoryBoard: AdvisoryMember[] = [
},
];
const PANEL_API = (import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API || 'http://localhost:3001'
const PANEL_API = (import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API || ''
export async function bootstrapAbout(): Promise<void> {
try {

View File

@ -46,7 +46,7 @@ export function bannerFor(position: string): Banner | undefined {
const PANEL_API =
(import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API ||
'http://localhost:3001'
''
export async function bootstrapBanners(): Promise<void> {
try {

View File

@ -94,7 +94,7 @@ export const events: Event[] = [
},
];
const PANEL_API = (import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API || 'http://localhost:3001'
const PANEL_API = (import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API || ''
export async function bootstrapEvents(): Promise<void> {
try {

View File

@ -178,7 +178,7 @@ export const stats: InstituteStats = {
subscribers: 2800,
};
const PANEL_API = (import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API || 'http://localhost:3001'
const PANEL_API = (import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API || ''
/** Map an institute_stats row id back onto the InstituteStats object key. */
const STAT_KEY_BY_ID: Record<string, keyof InstituteStats> = {

View File

@ -35,62 +35,102 @@ export interface PodcastItem {
export const videos: VideoItem[] = [
{
id: 1,
fa: { title: 'نگاهی به آن سوی مرزهای تولید', guest: 'گفتگو با مهندس سیامک فجری',
body: 'در ویدیوکست «Steel Horizon»، ما فراتر از داده‌های خام قیمت و تناژ، به سراغ مهندسی تغییر و معماری آینده صنعت می‌رویم. اینجا محل تلاقی استراتژی‌های کلان، تکنولوژی‌های برهم‌زننده و رهبرانی است که فولاد فردا را نه با کوره‌ها، که با «بینش» می‌سازند. ما طوفان‌های بازار را تحلیل می‌کنیم تا شما با اطمینان، مسیر آینده را ترسیم کنید.\nصنعت فولاد در حال ورود به پیچیده‌ترین چرخه حیات خود است؛ جایی که هوش مصنوعی، مقررات کربنی و ژئواکونومی، قواعد بازی را بازنویسی کرده‌اند. در «Steel Logic» با تحلیل‌گران ارشد و استراتژیست‌های تراز اول، به گفتگو می‌نشینیم تا لایه‌های پنهان این پیچیدگی‌ها را آشکار کنیم. اینجا خبری از هیجان‌زدگی نیست؛ فقط تحلیل‌های داده‌محور و تصمیم‌ساز.' },
en: { title: 'Beyond the Frontiers of Production', guest: 'A talk with Eng. Siamak Fajri',
body: 'In the "Steel Horizon" videocast we go beyond raw price and tonnage data to the engineering of change and the architecture of the industry\'s future.' },
fa: { title: 'نگاهی به آن سوی مرزهای تولید', guest: 'مهندس سیامک فجری',
body: 'فراتر از داده‌های خام قیمت و تناژ، به سراغ مهندسی تغییر و معماری آینده صنعت می‌رویم.' },
en: { title: 'Beyond the Frontiers of Production', guest: 'Eng. Siamak Fajri',
body: 'Going beyond raw price and tonnage data to the engineering of change.' },
ep: 'EP.12', duration: '۴۲ دقیقه', date: '۲۵ خرداد ۱۴۰۵', thumb: '/news/photo-1504307651254-35680f356dfd.jpg',
},
{
id: 2,
fa: { title: 'تحلیل بازار جهانی فلزات پایه', guest: 'گفتگو با کارشناسان بازار',
body: 'نوسانات قیمت مس، آلومینیوم و روی و تأثیر آن بر زنجیره فولاد را با تحلیل‌گران بازار بررسی می‌کنیم.' },
en: { title: 'Global Base Metals Analysis', guest: 'With market analysts',
fa: { title: 'تحلیل بازار جهانی فلزات پایه', guest: 'دکتر نیلوفر احمدی',
body: 'نوسانات قیمت مس، آلومینیوم و روی و تأثیر آن بر زنجیره ارزش فولاد.' },
en: { title: 'Global Base Metals Analysis', guest: 'Dr. Niloofar Ahmadi',
body: 'Price volatility in copper, aluminum and zinc and its steel-chain impact.' },
ep: 'EP.11', duration: '۳۸ دقیقه', date: '۱۸ خرداد ۱۴۰۵', thumb: '/news/photo-1558618666-fcd25c85cd64.jpg',
},
{
id: 3,
fa: { title: 'ژئوپلیتیک و صادرات فولاد', guest: 'گفتگو با تحلیل‌گران ژئواکونومی',
fa: { title: 'ژئوپلیتیک و صادرات فولاد ایران', guest: 'مهندس فرید تهرانی',
body: 'تأثیر تحولات منطقه‌ای بر بازارهای صادراتی فولاد ایران و فرصت‌های پیش رو.' },
en: { title: 'Geopolitics & Steel Exports', guest: 'With geo-economics analysts',
body: 'How regional developments shape Iran\'s steel export markets.' },
en: { title: 'Geopolitics & Iran Steel Exports', guest: 'Eng. Farid Tehrani',
body: 'How regional developments shape Iran\'s steel export markets and opportunities ahead.' },
ep: 'EP.10', duration: '۵۱ دقیقه', date: '۱۱ خرداد ۱۴۰۵', thumb: '/news/photo-1504328345606-18bbc8c9d7d1.jpg',
},
{
id: 4,
fa: { title: 'سرمایه‌گذاری در صنعت فولاد', guest: 'گفتگو با مدیران سرمایه‌گذاری',
fa: { title: 'سرمایه‌گذاری در زنجیره ارزش فولاد', guest: 'دکتر آرش کریمی',
body: 'فرصت‌ها و چالش‌های سرمایه‌گذاری در بخش‌های مختلف زنجیره ارزش فولاد.' },
en: { title: 'Steel Industry Investment', guest: 'With investment managers',
body: 'Opportunities and challenges across the steel value chain.' },
en: { title: 'Investing in the Steel Value Chain', guest: 'Dr. Arash Karimi',
body: 'Opportunities and challenges across the steel value chain segments.' },
ep: 'EP.09', duration: '۴۵ دقیقه', date: '۴ خرداد ۱۴۰۵', thumb: '/news/photo-1611273426858-450d8e3c9fce.jpg',
},
{
id: 5,
fa: { title: 'آینده فولاد سبز در ایران', guest: 'مهندس کاوه رستمی',
body: 'مسیر گذار به فولاد کم‌کربن در ایران؛ چالش‌ها، فرصت‌ها و ضرورت سرمایه‌گذاری.' },
en: { title: 'The Future of Green Steel in Iran', guest: 'Eng. Kaveh Rostami',
body: 'The road to low-carbon steel in Iran: challenges, opportunities and investment needs.' },
ep: 'EP.08', duration: '۳۶ دقیقه', date: '۲۸ اردیبهشت ۱۴۰۵', thumb: '/news/photo-1566873535350-6586b0b7a1f2.jpg',
},
{
id: 6,
fa: { title: 'هوش مصنوعی و تحول کارخانه‌های فولاد', guest: 'دکتر رضا محمدی',
body: 'چگونه یادگیری ماشین کارایی خطوط تولید فولاد را متحول می‌کند.' },
en: { title: 'AI and the Steel Plant Revolution', guest: 'Dr. Reza Mohammadi',
body: 'How machine learning is transforming the efficiency of steel production lines.' },
ep: 'EP.07', duration: '۴۹ دقیقه', date: '۲۱ اردیبهشت ۱۴۰۵', thumb: '/news/photo-1578662996442-48f60103fc96.jpg',
},
]
export const podcasts: PodcastItem[] = [
{
id: 1,
epNum: 'اپیزود ۷۷', date: '۲۵ / خرداد / ۱۴۰۵', rating: '۴/۵', duration: '۱۰:۵۶', cover: '/news/photo-1540575467063-178a50c2df87.jpg',
fa: { title: 'کالبدشکافی عقلانی پیچیدگی‌های صنعتی', ep: 'قسمت ۱۲', guest: 'مهندس سیامک شجاعی',
body: '«صنعت فولاد در عصر حاضر، در پیچیده‌ترین چرخه حیات خود قرار گرفته است؛ جایی که همگرایی هوش مصنوعی، الزامات زیست‌محیطی و تنش‌های ژئواکونومیک، قواعد کلاسیک تولید و تجارت را به‌کلی بازنویسی کرده‌اند. در پادکست «نام انتخابی»، ما با عبور از هیاهوی رسانه‌ای و تحلیل‌های سطحی، به کالبدشکافی عقلانی این پیچیدگی‌ها می‌پردازیم. ما با دعوت از استراتژیست‌های تراز اول و خبرگان این حوزه، لایه‌های پنهان چالش‌های صنعتی را می‌شکافیم تا بینشی شفاف، داده‌محور و تصمیم‌ساز را در اختیار رهبری قرار دهیم که برای عبور از طوفان‌های تغییر، به قطب‌نمای دقیق‌تر از عرف‌های بازار نیاز دارند.»' },
en: { title: 'A Rational Dissection of Industrial Complexity', ep: 'Episode 12', guest: 'Eng. Siamak Shojaei',
epNum: 'اپیزود ۷۷', date: '۲۵ / خرداد / ۱۴۰۵', rating: /۵', duration: '۵۶:۱۲', cover: '/news/photo-1540575467063-178a50c2df87.jpg',
fa: { title: 'کالبدشکافی عقلانی پیچیدگی‌های صنعتی', ep: 'قسمت ۷۷', guest: 'مهندس سیامک شجاعی',
body: 'صنعت فولاد در عصر حاضر، در پیچیده‌ترین چرخه حیات خود قرار گرفته است؛ جایی که همگرایی هوش مصنوعی، الزامات زیست‌محیطی و تنش‌های ژئواکونومیک، قواعد کلاسیک تولید و تجارت را به‌کلی بازنویسی کرده‌اند.' },
en: { title: 'A Rational Dissection of Industrial Complexity', ep: 'Episode 77', guest: 'Eng. Siamak Shojaei',
body: 'Steel today is in its most complex cycle yet — where AI, environmental mandates and geo-economic tension rewrite the classic rules of production and trade.' },
},
{
id: 2,
epNum: 'اپیزود ۷۶', date: '۱۸ / خرداد / ۱۴۰۵', rating: '۴/۵', duration: '۱:۴۰:۱۸', cover: '/news/photo-1566873535350-6586b0b7a1f2.jpg',
fa: { title: 'آزمایشگاه کوره', ep: 'قسمت ۸', guest: 'کارشناسان بازار',
body: '«The Foundry Lab» محلی برای کالبدشکافی دقیق تحولات فناورانه و مدل‌های نوین فولادسازی است. ما در این پادکست با نگاهی موشکافانه، چالش‌های فنی را بررسی می‌کنیم.' },
en: { title: 'The Foundry Lab', ep: 'Episode 8', guest: 'Market experts',
body: 'The Foundry Lab dissects technological change and new steelmaking models with a meticulous engineering lens.' },
epNum: 'اپیزود ۷۶', date: '۱۸ / خرداد / ۱۴۰۵', rating: /۵', duration: '۱:۰۴:۳۳', cover: '/news/photo-1566873535350-6586b0b7a1f2.jpg',
fa: { title: 'آزمایشگاه کوره؛ فناوری‌های نوین فولادسازی', ep: 'قسمت ۷۶', guest: 'دکتر رضا محمدی',
body: 'در این اپیزود با نگاهی موشکافانه به چالش‌های فنی گذار از کوره‌های بلند به فولادسازی پاک می‌پردازیم.' },
en: { title: 'The Foundry Lab: New Steelmaking Technologies', ep: 'Episode 76', guest: 'Dr. Reza Mohammadi',
body: 'A meticulous look at the technical challenges of transitioning from blast furnaces to clean steelmaking.' },
},
{
id: 3,
epNum: 'اپیزود ۷۵', date: '۱۱ / خرداد / ۱۴۰۵', rating: '۴/۵', duration: '۱:۴۰:۱۸', cover: '/news/photo-1578662996442-48f60103fc96.jpg',
fa: { title: 'معمار فولاد', ep: 'قسمت ۵', guest: 'رهبران صنعت',
body: 'در «Steel Architect»، ما به سراغ داستان ساختن فولاد فردا می‌رویم؛ روایتی که رهبرانی که از مدل‌های سنتی کسب‌وکار به چالش کشیده‌اند را به تصویر می‌کشد.' },
en: { title: 'Steel Architect', ep: 'Episode 5', guest: 'Industry leaders',
body: 'Steel Architect tells the story of building tomorrow\'s steel — leaders challenging traditional business models.' },
epNum: 'اپیزود ۷۵', date: '۱۱ / خرداد / ۱۴۰۵', rating: '۴.۵/۵', duration: '۴۸:۵۷', cover: '/news/photo-1578662996442-48f60103fc96.jpg',
fa: { title: 'معمار فولاد؛ رهبری در عصر تحول', ep: 'قسمت ۷۵', guest: 'مهندس کاوه رستمی',
body: 'داستان ساختن فولاد فردا؛ روایت رهبرانی که مدل‌های سنتی کسب‌وکار را به چالش کشیده‌اند.' },
en: { title: 'Steel Architect: Leadership in the Age of Change', ep: 'Episode 75', guest: 'Eng. Kaveh Rostami',
body: 'The story of building tomorrow\'s steel — leaders challenging traditional business models.' },
},
{
id: 4,
epNum: 'اپیزود ۷۴', date: '۴ / خرداد / ۱۴۰۵', rating: '۴.۷/۵', duration: '۵۲:۱۸', cover: '/news/photo-1504328345606-18bbc8c9d7d1.jpg',
fa: { title: 'فولاد سبز؛ رویا یا واقعیت؟', ep: 'قسمت ۷۴', guest: 'دکتر نیلوفر احمدی',
body: 'بررسی مسیر دشوار و پرهزینه گذار به تولید فولاد با کمترین انتشار کربن و آنچه صنعت ایران باید بداند.' },
en: { title: 'Green Steel: Dream or Reality?', ep: 'Episode 74', guest: 'Dr. Niloofar Ahmadi',
body: 'Examining the costly and challenging road to near-zero carbon steel production and what Iranian industry must know.' },
},
{
id: 5,
epNum: 'اپیزود ۷۳', date: '۲۸ / اردیبهشت / ۱۴۰۵', rating: '۴.۴/۵', duration: '۱:۱۲:۴۴', cover: '/news/photo-1558618666-fcd25c85cd64.jpg',
fa: { title: 'بازی بزرگ؛ چین، هند و آینده بازار فولاد', ep: 'قسمت ۷۳', guest: 'مهندس فرید تهرانی',
body: 'چگونه رقابت دو غول آسیایی مسیر بازار جهانی فولاد را در دهه پیش رو تغییر خواهد داد.' },
en: { title: 'The Great Game: China, India & the Future of Steel', ep: 'Episode 73', guest: 'Eng. Farid Tehrani',
body: 'How competition between two Asian giants will reshape the global steel market in the decade ahead.' },
},
{
id: 6,
epNum: 'اپیزود ۷۲', date: '۲۱ / اردیبهشت / ۱۴۰۵', rating: '۴.۳/۵', duration: '۴۴:۰۲', cover: '/news/photo-1611273426858-450d8e3c9fce.jpg',
fa: { title: 'هوش مصنوعی در خط تولید فولاد', ep: 'قسمت ۷۲', guest: 'دکتر آرش کریمی',
body: 'کاربردهای عملی یادگیری ماشین در بهینه‌سازی فرآیند ذوب، کنترل کیفیت و نگهداری پیش‌گیرانه.' },
en: { title: 'AI on the Steel Production Line', ep: 'Episode 72', guest: 'Dr. Arash Karimi',
body: 'Practical applications of machine learning in melt optimization, quality control and predictive maintenance.' },
},
]
@ -129,7 +169,7 @@ function articleToPodcast(a: Record<string, unknown>, i: number): PodcastItem {
}
}
const PANEL_API = (import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API || 'http://localhost:3001'
const PANEL_API = (import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API || ''
export async function bootstrapMedia(): Promise<void> {
try {

View File

@ -75,7 +75,7 @@ export const plans: Plan[] = [
const PANEL_API =
(import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API ||
'http://localhost:3001';
'';
export async function bootstrapPlans(): Promise<void> {
try {

View File

@ -23,6 +23,7 @@ export interface Report {
summary: string;
tags: string[];
featured: boolean;
coverImage?: string;
}
export const reports: Report[] = [
@ -336,7 +337,7 @@ export const reports: Report[] = [
const PANEL_API =
(import.meta as ImportMeta & { env?: Record<string, string> }).env
?.VITE_PANEL_API || 'http://localhost:3001';
?.VITE_PANEL_API || '';
export async function bootstrapReports(): Promise<void> {
try {

View File

@ -47,7 +47,7 @@ function mapLevel(level: unknown): RiskLevel {
const PANEL_API =
(import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API ||
'http://localhost:3001';
'';
export async function bootstrapRisks(): Promise<void> {
try {

View File

@ -108,7 +108,7 @@ export const team: TeamMember[] = [
},
];
const PANEL_API = (import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API || 'http://localhost:3001'
const PANEL_API = (import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_PANEL_API || ''
export async function bootstrapTeam(): Promise<void> {
try {

View File

@ -1,4 +1,4 @@
const BASE = import.meta.env.VITE_PANEL_API ?? 'http://localhost:3001'
const BASE = import.meta.env.VITE_PANEL_API ?? ''
async function get<T>(path: string, params?: Record<string, string>): Promise<T> {
const url = new URL(`${BASE}${path}`)

View File

@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import './index.css'
import { router } from './app/router'
import { AuthProvider } from './context/AuthContext'
import { bootstrapReports } from './data/reports'
import { bootstrapEvents } from './data/events'
import { bootstrapTeam } from './data/team'
@ -25,6 +26,8 @@ await Promise.all([
createRoot(document.getElementById('root')!).render(
<StrictMode>
<RouterProvider router={router} />
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
</StrictMode>,
)

View File

@ -1,38 +1,490 @@
import { useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { Zap, X, AlertTriangle, Globe, Calendar } from 'lucide-react'
import { useLang } from '@/context/LangContext'
import AtAGlanceSection from '@/pages/Home/sections/AtAGlanceSection'
const NAVY = '#032340'
const GOLD = '#CD9E53'
/* ─── types ─── */
type Tab = 'market' | 'tech' | 'geo' | 'energy' | 'commodity'
type Card = {
id: string
tab: Tab
img: string
titleFa: string; titleEn: string
bodyFa: string[]; bodyEn: string[]
analysisFa: string; analysisEn: string
tags: string[]
source: string
dateFa: string; dateEn: string
}
/* ─── data ─── */
const TABS: { id: Tab; fa: string; en: string }[] = [
{ id: 'market', fa: 'بازار', en: 'Market' },
{ id: 'tech', fa: 'فناوری', en: 'Technology' },
{ id: 'geo', fa: 'ژئوپلیتیک', en: 'Geopolitics' },
{ id: 'energy', fa: 'انرژی', en: 'Energy' },
{ id: 'commodity', fa: 'کامودیتی', en: 'Commodity' },
]
const CARDS: Card[] = [
{
id: 'c1', tab: 'market',
img: '/news/photo-1504307651254-35680f356dfd.jpg',
titleFa: 'شوک هیدروژنی به بازار جهانی فولاد',
titleEn: 'Hydrogen Shock to Global Steel Markets',
bodyFa: [
'پروژه عظیم «فولاد سبز H2» در سوئد با جذب بی‌سابقه ۶.۵ میلیارد یورو سرمایه، رسماً وارد فاز اجرایی شد.',
'این تأسیسات با هدف کاهش ۹۵ درصدی انتشار CO₂، قواعد رقابت در زنجیره ارزش اروپا را برای همیشه بازتعریف می‌کند.',
],
bodyEn: [
'Sweden\'s massive Green Steel H2 project, raising an unprecedented €6.5bn, has officially entered execution phase.',
'Targeting a 95% cut in CO₂ emissions, this facility is set to redefine competitive rules in Europe\'s value chain.',
],
analysisFa: 'تحول فولاد سبز، فرصت صادراتی ایران را در بازارهای سنتی تهدید می‌کند.',
analysisEn: 'Green steel transformation threatens Iran\'s export opportunities in traditional markets.',
tags: ['فولاد سبز', 'هیدروژن', 'اروپا'],
source: 'Financial Times', dateFa: '۱۴ اردیبهشت ۱۴۰۵', dateEn: 'May 4, 2026',
},
{
id: 'c2', tab: 'market',
img: '/news/photo-1581092160607-ee22731c9b4e.jpg',
titleFa: 'CBAM اروپا وارد اجرای کامل شد',
titleEn: 'EU CBAM Enters Full Enforcement',
bodyFa: [
'با اجرای کامل مکانیسم تنظیم مرزهای کربنی، صادرکنندگان فولاد به اروپا موظف به پرداخت هزینه کامل کربن شدند.',
'این تحول فشار بی‌سابقه‌ای بر تولیدکنندگان مبتنی بر کوره بلند وارد کرده است.',
],
bodyEn: [
'With CBAM fully enforced, steel exporters to the EU must now pay the full cost of embedded carbon.',
'This places unprecedented pressure on blast-furnace producers globally.',
],
analysisFa: 'ایران باید فوری‌ترین اولویت خود را کربن‌زدایی از فرآیند تولید قرار دهد.',
analysisEn: 'Iran must make decarbonization its most urgent industrial priority.',
tags: ['CBAM', 'کربن', 'صادرات'],
source: 'Reuters', dateFa: '۱۲ اردیبهشت ۱۴۰۵', dateEn: 'May 2, 2026',
},
{
id: 'c3', tab: 'tech',
img: '/news/photo-1518770660439-4636190af475.jpg',
titleFa: 'هوش مصنوعی تقاضای فولاد مراکز داده را بازتعریف می‌کند',
titleEn: 'AI Rewrites Steel Demand for Data Centers',
bodyFa: [
'موج ساخت مراکز داده هوش مصنوعی، تقاضای جدیدی برای مقاطع ساختمانی و فولاد ویژه ایجاد کرده است.',
'تحلیلگران پیش‌بینی می‌کنند این روند تا پایان دهه یکی از پیشران‌های اصلی رشد مصرف فولاد باشد.',
],
bodyEn: [
'The AI data-center construction wave is creating fresh demand for structural sections and specialty steel.',
'Analysts project this trend to be a leading driver of steel consumption growth through the decade.',
],
analysisFa: 'فولادسازان باید ظرفیت تولید مقاطع ساختمانی پیشرفته را افزایش دهند.',
analysisEn: 'Steel producers must expand capacity for advanced structural sections.',
tags: ['هوش مصنوعی', 'مراکز داده', 'تقاضا'],
source: 'Bloomberg', dateFa: '۱۰ اردیبهشت ۱۴۰۵', dateEn: 'Apr 30, 2026',
},
{
id: 'c4', tab: 'tech',
img: '/news/photo-1567789884554-0b844b597180.jpg',
titleFa: 'اتوماسیون کوره‌های القایی در ایران',
titleEn: 'Induction Furnace Automation in Iran',
bodyFa: [
'چندین کارخانه بزرگ فولاد ایران سرمایه‌گذاری در سیستم‌های کنترل هوشمند کوره را آغاز کرده‌اند.',
'این سیستم‌ها توانایی کاهش مصرف انرژی تا ۱۵ درصد را دارند.',
],
bodyEn: [
'Several major Iranian steel plants have begun investing in smart furnace control systems.',
'These systems are capable of reducing energy consumption by up to 15%.',
],
analysisFa: 'گام مثبتی است اما سرعت پیاده‌سازی باید افزایش یابد.',
analysisEn: 'A positive step, but implementation speed must increase.',
tags: ['اتوماسیون', 'کوره', 'انرژی'],
source: 'فولاد نیوز', dateFa: '۸ اردیبهشت ۱۴۰۵', dateEn: 'Apr 28, 2026',
},
{
id: 'c5', tab: 'geo',
img: '/news/photo-1540575467063-178a50c2df87.jpg',
titleFa: 'تنش‌های خاورمیانه و تجارت فولاد',
titleEn: 'Middle East Tensions and Steel Trade',
bodyFa: [
'ناآرامی‌های منطقه‌ای مسیرهای حمل‌ونقل دریایی را مختل کرده و هزینه‌های لجستیک را افزایش داده است.',
'صادرکنندگان ایرانی با چالش‌های اضافی در انتقال محصولات روبرو هستند.',
],
bodyEn: [
'Regional instability has disrupted maritime shipping routes and increased logistics costs.',
'Iranian exporters face additional challenges in product delivery.',
],
analysisFa: 'تنوع‌بخشی در مسیرهای صادراتی اکنون یک ضرورت استراتژیک است.',
analysisEn: 'Diversifying export routes is now a strategic necessity.',
tags: ['ژئوپلیتیک', 'حمل دریایی', 'صادرات'],
source: 'Al Jazeera', dateFa: '۶ اردیبهشت ۱۴۰۵', dateEn: 'Apr 26, 2026',
},
{
id: 'c6', tab: 'geo',
img: '/news/photo-1570168007204-dfb528c6958f.jpg',
titleFa: 'رقابت چین و آمریکا بر سر فلزات استراتژیک',
titleEn: 'ChinaUS Competition Over Strategic Metals',
bodyFa: [
'تنش‌های ژئوپلیتیک میان قدرت‌های بزرگ، زنجیره تأمین فلزات حیاتی را بی‌ثبات کرده است.',
'این رقابت فرصت‌هایی برای تأمین‌کنندگان جایگزین ایجاد می‌کند.',
],
bodyEn: [
'Geopolitical tensions between major powers have destabilized critical metal supply chains.',
'This competition creates opportunities for alternative suppliers.',
],
analysisFa: 'ایران می‌تواند نقش تأمین‌کننده جایگزین برخی فلزات را ایفا کند.',
analysisEn: 'Iran can play the role of alternative supplier for some metals.',
tags: ['چین', 'آمریکا', 'فلزات'],
source: 'Reuters', dateFa: '۴ اردیبهشت ۱۴۰۵', dateEn: 'Apr 24, 2026',
},
{
id: 'c7', tab: 'energy',
img: '/news/photo-1466611653911-95081537e5b7.jpg',
titleFa: 'بحران گاز و صنعت فولاد ایران',
titleEn: 'Gas Crisis and Iran\'s Steel Industry',
bodyFa: [
'کمبود گاز در فصل سرما چندین واحد فولادسازی را مجبور به تعلیق موقت تولید کرد.',
'این چالش ساختاری نیاز به سرمایه‌گذاری جدی در زیرساخت انرژی دارد.',
],
bodyEn: [
'Gas shortages during winter forced several steelmaking units into temporary production suspension.',
'This structural challenge requires serious investment in energy infrastructure.',
],
analysisFa: 'بدون تأمین پایدار انرژی، اهداف تولید فولاد محقق نمی‌شود.',
analysisEn: 'Without stable energy supply, steel production targets cannot be met.',
tags: ['گاز', 'انرژی', 'تولید'],
source: 'ایسنا', dateFa: '۲ اردیبهشت ۱۴۰۵', dateEn: 'Apr 22, 2026',
},
{
id: 'c8', tab: 'energy',
img: '/news/photo-1611273426858-450d8e3c9fce.jpg',
titleFa: 'انرژی خورشیدی برای فولادسازی پایدار',
titleEn: 'Solar Energy for Sustainable Steelmaking',
bodyFa: [
'پتانسیل عظیم انرژی خورشیدی ایران می‌تواند پایه‌ای برای فولادسازی کم‌کربن باشد.',
'اولین پروژه‌های آزمایشی ترکیب پنل خورشیدی با کوره‌های الکتریکی در حال بررسی است.',
],
bodyEn: [
'Iran\'s vast solar energy potential could form the basis for low-carbon steelmaking.',
'First pilot projects combining solar panels with electric arc furnaces are under review.',
],
analysisFa: 'سرمایه‌گذاری در این حوزه می‌تواند چند مشکل را هم‌زمان حل کند.',
analysisEn: 'Investment in this area can solve several problems simultaneously.',
tags: ['خورشیدی', 'فولاد سبز', 'EAF'],
source: 'PV Magazine', dateFa: '۳۰ فروردین ۱۴۰۵', dateEn: 'Apr 19, 2026',
},
{
id: 'c9', tab: 'commodity',
img: '/news/photo-1540575467063-178a50c2df87.jpg',
titleFa: 'قیمت سنگ‌آهن در مسیر کاهش',
titleEn: 'Iron Ore Prices on Declining Path',
bodyFa: [
'کاهش تقاضای چین در بخش ساختمان قیمت سنگ‌آهن را به پایین‌ترین سطح دو سال گذشته رساند.',
'این کاهش برای تولیدکنندگان فولاد مبتنی بر کوره بلند مزیت هزینه‌ای ایجاد می‌کند.',
],
bodyEn: [
'China\'s declining construction-sector demand has pushed iron ore to a two-year low.',
'This drop creates a cost advantage for blast-furnace-based steel producers.',
],
analysisFa: 'فرصتی برای افزایش ذخیره‌سازی سنگ‌آهن با قیمت پایین وجود دارد.',
analysisEn: 'An opportunity exists to increase iron ore stockpiling at low prices.',
tags: ['سنگ‌آهن', 'چین', 'قیمت'],
source: 'Bloomberg', dateFa: '۲۸ فروردین ۱۴۰۵', dateEn: 'Apr 17, 2026',
},
{
id: 'c10', tab: 'commodity',
img: '/news/photo-1504307651254-35680f356dfd.jpg',
titleFa: 'نوسان قیمت زغال‌سنگ کک‌شو',
titleEn: 'Coking Coal Price Volatility',
bodyFa: [
'محدودیت‌های صادرات استرالیا و بارندگی در مناطق استخراجی، قیمت زغال کک‌شو را نوسانی کرده است.',
'این بی‌ثباتی برنامه‌ریزی تولید را برای کارخانه‌های کوره بلند دشوار می‌کند.',
],
bodyEn: [
'Australian export restrictions and rainfall in mining regions have made coking coal prices volatile.',
'This instability complicates production planning for blast furnace plants.',
],
analysisFa: 'تنوع در منابع تأمین زغال‌سنگ یک اولویت استراتژیک است.',
analysisEn: 'Diversifying coking coal sources is a strategic priority.',
tags: ['زغال‌سنگ', 'استرالیا', 'تولید'],
source: 'Metal Bulletin', dateFa: '۲۶ فروردین ۱۴۰۵', dateEn: 'Apr 15, 2026',
},
]
const NEWS_ITEMS = [
{ titleFa: 'فولاد مبارکه رکورد تولید ماهانه را شکست', titleEn: 'Mobarakeh Steel breaks monthly production record', dateFa: '۱۴ اردیبهشت', dateEn: 'May 4' },
{ titleFa: 'تفاهم‌نامه همکاری ایران و روسیه در حوزه فولاد', titleEn: 'Iran-Russia steel cooperation MOU signed', dateFa: '۱۲ اردیبهشت', dateEn: 'May 2' },
{ titleFa: 'افزایش ۸ درصدی صادرات فولاد در فروردین', titleEn: '8% rise in steel exports in April', dateFa: '۱۰ اردیبهشت', dateEn: 'Apr 30' },
{ titleFa: 'نشست وزرای صنعت کشورهای عضو ECO', titleEn: 'ECO member states industry ministers summit', dateFa: '۸ اردیبهشت', dateEn: 'Apr 28' },
{ titleFa: 'راه‌اندازی خط تولید ورق گالوانیزه در اصفهان', titleEn: 'Galvanized sheet production line launched in Isfahan', dateFa: '۶ اردیبهشت', dateEn: 'Apr 26' },
]
const TICKERS = [
{ fa: 'شمش فولاد: ۱۸,۴۵۰,۰۰۰ ریال/تن', en: 'Steel Billet: $450/t' },
{ fa: 'سنگ‌آهن دانه‌بندی: ۳,۲۰۰,۰۰۰ ریال/تن', en: 'Iron Ore Fines: $98/t' },
{ fa: 'میلگرد A3: ۲۱,۸۰۰,۰۰۰ ریال/تن', en: 'Rebar A3: $530/t' },
{ fa: 'ورق گرم: ۲۳,۱۰۰,۰۰۰ ریال/تن', en: 'HRC: $545/t' },
]
/* ─── modal ─── */
function CardModal({ card, lang, onClose }: { card: Card; lang: 'fa' | 'en'; onClose: () => void }) {
const isFa = lang === 'fa'
const dir = isFa ? 'rtl' : 'ltr'
const isLoggedIn = (() => { try { return localStorage.getItem('sf_logged_in') === '1' } catch { return false } })()
const body = isFa ? card.bodyFa : card.bodyEn
return (
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
onClick={onClose}
style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(3,35,64,0.65)', backdropFilter: 'blur(4px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px 16px' }}
>
<motion.div
initial={{ opacity: 0, y: 32, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 32, scale: 0.96 }}
transition={{ duration: 0.38, ease: [0.22, 1, 0.36, 1] }}
onClick={e => e.stopPropagation()}
dir={dir}
style={{ background: '#faf7f4', borderRadius: 20, width: '100%', maxWidth: 680, maxHeight: '90vh', overflowY: 'auto', position: 'relative', boxShadow: '0 40px 100px -20px rgba(3,35,64,0.5)' }}
>
{/* cover */}
<div style={{ position: 'relative', height: 260 }}>
<img src={card.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: '20px 20px 0 0' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,35,64,0.55) 0%, transparent 50%)', borderRadius: '20px 20px 0 0' }} />
</div>
{/* close */}
<button onClick={onClose} style={{ position: 'absolute', top: 14, ...(isFa ? { left: 14 } : { right: 14 }), width: 34, height: 34, borderRadius: '50%', border: 'none', background: 'rgba(3,35,64,0.7)', color: '#fff', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(4px)' }}>
<X size={16} />
</button>
<div style={{ padding: '24px 28px 32px' }}>
{/* tags */}
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 14, direction: 'ltr', justifyContent: isFa ? 'flex-end' : 'flex-start' }}>
{card.tags.map(t => (
<span key={t} style={{ fontSize: 10, fontWeight: 700, color: NAVY, background: `${GOLD}25`, borderRadius: 6, padding: '3px 10px' }}>{t}</span>
))}
</div>
<h2 style={{ fontSize: 'clamp(17px,2vw,22px)', fontWeight: 900, color: NAVY, lineHeight: 1.4, margin: '0 0 14px' }}>
{isFa ? card.titleFa : card.titleEn}
</h2>
{/* meta */}
<div style={{ display: 'flex', gap: 18, marginBottom: 20, direction: 'ltr', justifyContent: isFa ? 'flex-end' : 'flex-start' }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 12, color: 'rgba(7,29,73,0.55)', fontWeight: 600 }}>
<Globe size={13} color={GOLD} />{card.source}
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 12, color: 'rgba(7,29,73,0.55)', fontWeight: 600 }}>
<Calendar size={13} color={GOLD} />{isFa ? card.dateFa : card.dateEn}
</span>
</div>
{/* body */}
<div style={{ position: 'relative' }}>
<p style={{ fontSize: 14, lineHeight: 2, color: 'rgba(7,29,73,0.8)', margin: '0 0 14px' }}>{body[0]}</p>
{body.length > 1 && (
<div style={{ position: 'relative' }}>
<div style={{ filter: isLoggedIn ? 'none' : 'blur(7px)', opacity: isLoggedIn ? 1 : 0.55, maxHeight: isLoggedIn ? 'none' : 200, overflow: 'hidden', WebkitMaskImage: isLoggedIn ? 'none' : 'linear-gradient(to bottom, black 0%, transparent 90%)', userSelect: isLoggedIn ? 'auto' : 'none' }}>
{body.slice(1).map((p, i) => <p key={i} style={{ fontSize: 14, lineHeight: 2, color: 'rgba(7,29,73,0.8)', margin: '0 0 14px' }}>{p}</p>)}
</div>
{!isLoggedIn && (
<div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', gap: 10, paddingBottom: 8 }}>
<p style={{ fontSize: 13, fontWeight: 700, color: NAVY, margin: 0 }}>{isFa ? 'برای مطالعه کامل عضو شوید' : 'Join to read the full article'}</p>
<div style={{ display: 'flex', gap: 8, direction: 'ltr' }}>
<a href="/membership" style={{ padding: '8px 18px', borderRadius: 8, fontSize: 12, fontWeight: 700, background: NAVY, color: '#fff', textDecoration: 'none' }}>{isFa ? 'ورود' : 'Login'}</a>
<a href="/membership" style={{ padding: '8px 18px', borderRadius: 8, fontSize: 12, fontWeight: 700, background: GOLD, color: NAVY, textDecoration: 'none' }}>{isFa ? 'عضویت' : 'Sign Up'}</a>
</div>
</div>
)}
</div>
)}
</div>
{/* analysis */}
{isLoggedIn && (
<div style={{ marginTop: 20, borderRadius: 12, padding: '16px 18px', background: `${NAVY}08`, border: `1.5px solid ${NAVY}15` }}>
<div style={{ fontSize: 10, fontWeight: 800, letterSpacing: '2px', color: GOLD, marginBottom: 8 }}>{isFa ? 'تحلیل راهبردی' : 'STRATEGIC ANALYSIS'}</div>
<p style={{ fontSize: 13, lineHeight: 1.9, color: NAVY, margin: 0 }}>{isFa ? card.analysisFa : card.analysisEn}</p>
</div>
)}
{/* disclaimer */}
<div style={{ marginTop: 16, display: 'flex', alignItems: 'center', gap: 6, direction: 'ltr', justifyContent: isFa ? 'flex-end' : 'flex-start' }}>
<AlertTriangle size={12} color={GOLD} />
<span style={{ fontSize: 11, color: 'rgba(7,29,73,0.4)' }}>{isFa ? 'این تحلیل توسط هوش مصنوعی تولید شده است.' : 'This analysis was AI-assisted.'}</span>
</div>
</div>
</motion.div>
</motion.div>
)
}
/* ─── main page ─── */
export default function AtAGlance() {
const { lang } = useLang()
const isFa = lang === 'fa'
const [activeTab, setActiveTab] = useState<Tab>('market')
const [selectedCard, setSelectedCard] = useState<Card | null>(null)
const filteredCards = CARDS.filter(c => c.tab === activeTab)
return (
<div dir={isFa ? 'rtl' : 'ltr'} style={{ background: '#faf7f4', minHeight: '100vh' }}>
{/* page header */}
<div style={{ background: '#032340', padding: '64px 0 56px' }}>
<div className="max-w-7xl mx-auto px-12 max-md:px-5">
<div style={{ display: 'flex', alignItems: 'stretch', justifyContent: 'flex-start' }}>
<div style={{
[isFa ? 'borderRight' : 'borderLeft']: '4px solid #CD9E53',
[isFa ? 'paddingRight' : 'paddingLeft']: 18,
} as React.CSSProperties}>
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', color: '#CD9E53', marginBottom: 8 }}>
AT A GLANCE
</div>
<h1 style={{ fontSize: 'clamp(28px, 3vw, 44px)', fontWeight: 900, color: '#fff', letterSpacing: '-0.5px', margin: 0 }}>
{isFa ? 'در یک نگاه' : 'At a Glance'}
</h1>
<p style={{ fontSize: 'clamp(13px, 1.1vw, 16px)', color: 'rgba(255,255,255,0.7)', margin: '12px 0 0', maxWidth: 560, lineHeight: 1.8 }}>
{isFa
? 'مهم‌ترین تحولات و رویدادهای راهبردی صنعت فولاد جهان، گزیده و تحلیل‌شده در یک نگاه.'
: 'The most important strategic developments and events in the global steel industry — curated and analyzed at a glance.'}
</p>
{/* ── HERO ── */}
<div style={{ position: 'relative', minHeight: 480, padding: '36px 0 280px', overflow: 'hidden' }}>
{/* background image */}
<div style={{ position: 'absolute', inset: 0 }}>
<img
src={filteredCards[0]?.img ?? '/news/photo-1504307651254-35680f356dfd.jpg'}
alt=""
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
/>
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(3,35,64,0.72) 0%, rgba(3,35,64,0.45) 50%, rgba(3,35,64,0.7) 100%)' }} />
</div>
{/* title + tabs — physically right */}
<div style={{ position: 'absolute', top: 32, right: 48, zIndex: 2 }}>
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '3px', color: GOLD, marginBottom: 8 }}>
{isFa ? 'دیده‌بانِ تحولات' : 'AT A GLANCE'}
</div>
<h1 style={{ fontSize: 'clamp(26px,3vw,42px)', fontWeight: 900, color: '#fff', margin: '0 0 20px', letterSpacing: '-0.5px' }}>
{isFa ? 'در یک نگاه' : 'At a Glance'}
</h1>
{/* tab pills */}
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', direction: 'ltr', justifyContent: 'flex-end' }}>
{TABS.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
style={{
padding: '7px 16px', borderRadius: 20, border: 'none', cursor: 'pointer',
fontFamily: 'inherit', fontSize: 12, fontWeight: 700,
background: activeTab === tab.id ? GOLD : 'rgba(255,255,255,0.15)',
color: activeTab === tab.id ? NAVY : '#fff',
backdropFilter: 'blur(4px)',
transition: 'background 200ms, color 200ms',
}}
>
{isFa ? tab.fa : tab.en}
</button>
))}
</div>
</div>
{/* cards row — bottom of hero, overflowing down */}
<div style={{ position: 'absolute', bottom: 20, left: 0, right: 0, overflow: 'hidden', zIndex: 2 }}>
<div style={{ display: 'flex', gap: 16, paddingRight: 48, paddingLeft: 48, direction: 'ltr', justifyContent: 'flex-end' }}>
<AnimatePresence mode="wait">
{filteredCards.slice(0, 4).map((card, idx) => (
<motion.div
key={card.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.35, delay: idx * 0.06 }}
onClick={() => setSelectedCard(card)}
style={{
width: 220, flexShrink: 0, borderRadius: 14, overflow: 'hidden',
cursor: 'pointer', boxShadow: '0 20px 50px -15px rgba(3,35,64,0.5)',
}}
>
<div style={{ position: 'relative', height: 130 }}>
<img src={card.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,35,64,0.7) 0%, transparent 60%)' }} />
</div>
<div style={{ background: '#fff', padding: '10px 12px' }}>
<div style={{ fontSize: 11, fontWeight: 800, color: NAVY, lineHeight: 1.4, marginBottom: 4, direction: isFa ? 'rtl' : 'ltr' }}>
{isFa ? card.titleFa : card.titleEn}
</div>
<div style={{ fontSize: 10, color: 'rgba(7,29,73,0.5)', direction: 'ltr', display: 'flex', alignItems: 'center', gap: 4, justifyContent: isFa ? 'flex-end' : 'flex-start' }}>
<Globe size={10} color={GOLD} />
{card.source}
</div>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
</div>
{/* ── TICKER ── */}
<div style={{ background: NAVY, padding: '10px 0', overflow: 'hidden' }}>
<div style={{ display: 'flex', gap: 48, direction: 'ltr', paddingLeft: 48, paddingRight: 48, flexWrap: 'wrap' }}>
{TICKERS.map((t, i) => (
<span key={i} style={{ fontSize: 12, fontWeight: 700, color: '#fff', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 6 }}>
<Zap size={12} color={GOLD} />
{isFa ? t.fa : t.en}
</span>
))}
</div>
</div>
{/* ── BOTTOM: news list + more cards ── */}
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 52, paddingBottom: 72 }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 40 }} className="max-lg:!grid-cols-1">
{/* news list */}
<div>
<div style={{ fontSize: 10, fontWeight: 800, letterSpacing: '3px', color: GOLD, marginBottom: 16 }}>
{isFa ? 'آخرین اخبار' : 'LATEST NEWS'}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
{NEWS_ITEMS.map((n, i) => (
<div key={i} style={{ padding: '14px 0', borderBottom: '1px solid rgba(7,29,73,0.08)', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 8 }}>
<span style={{ fontSize: 13, fontWeight: 700, color: NAVY, lineHeight: 1.5, flex: 1 }}>
{isFa ? n.titleFa : n.titleEn}
</span>
<span style={{ fontSize: 11, color: 'rgba(7,29,73,0.4)', whiteSpace: 'nowrap', fontWeight: 600 }}>
{isFa ? n.dateFa : n.dateEn}
</span>
</div>
))}
</div>
</div>
{/* remaining cards grid */}
<div>
<div style={{ fontSize: 10, fontWeight: 800, letterSpacing: '3px', color: GOLD, marginBottom: 16 }}>
{isFa ? 'تحلیل‌های بیشتر' : 'MORE ANALYSES'}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 16 }}>
{filteredCards.slice(0, 4).map(card => (
<div
key={card.id}
onClick={() => setSelectedCard(card)}
style={{ borderRadius: 12, overflow: 'hidden', cursor: 'pointer', background: '#fff', boxShadow: '0 4px 20px rgba(7,29,73,0.08)', border: '1px solid rgba(7,29,73,0.07)' }}
>
<div style={{ height: 120, overflow: 'hidden' }}>
<img src={card.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
</div>
<div style={{ padding: '12px 14px', direction: isFa ? 'rtl' : 'ltr' }}>
<div style={{ fontSize: 12, fontWeight: 800, color: NAVY, lineHeight: 1.5, marginBottom: 6 }}>
{isFa ? card.titleFa : card.titleEn}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, direction: 'ltr', justifyContent: isFa ? 'flex-end' : 'flex-start' }}>
<Calendar size={10} color={GOLD} />
<span style={{ fontSize: 10, color: 'rgba(7,29,73,0.45)', fontWeight: 600 }}>{isFa ? card.dateFa : card.dateEn}</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* the slider (reuses the shared section component) */}
<AtAGlanceSection embedded />
{/* modal */}
<AnimatePresence>
{selectedCard && (
<CardModal card={selectedCard} lang={lang} onClose={() => setSelectedCard(null)} />
)}
</AnimatePresence>
</div>
)
}

427
src/pages/Auth/AuthPage.tsx Normal file
View File

@ -0,0 +1,427 @@
import React, { useState } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { useAuth } from '@/context/AuthContext'
import { useLang } from '@/context/LangContext'
const GOLD = '#CD9E53'
const NAVY = '#032340'
type Mode = 'login' | 'register'
export default function AuthPage({ defaultMode = 'login', onSuccess, onClose }: { defaultMode?: Mode; onSuccess?: () => void; onClose?: () => void }) {
const { lang } = useLang()
const { login, register } = useAuth()
const navigate = useNavigate()
const [mode, setMode] = useState<Mode>(defaultMode)
const handleSuccess = () => { if (onSuccess) { onSuccess() } else { navigate('/profile') } }
const [isMobile, setIsMobile] = useState(() => window.innerWidth < 1024)
React.useEffect(() => {
const mq = window.matchMedia('(max-width: 1023px)')
const h = (e: MediaQueryListEvent | MediaQueryList) => setIsMobile(e.matches)
h(mq); mq.addEventListener('change', h)
return () => mq.removeEventListener('change', h)
}, [])
return (
<div style={{
position: 'fixed', inset: 0, zIndex: 1100,
display: 'flex', flexDirection: lang === 'fa' ? 'row-reverse' : 'row',
background: isMobile ? '#fafaf8' : '#fff', overflow: 'hidden',
}}>
{onClose && (
<button onClick={onClose} style={{
position: 'absolute', top: 16, zIndex: 10,
...(lang === 'fa' ? { left: 16 } : { right: 16 }),
width: 36, height: 36, borderRadius: '50%', border: 'none',
background: 'rgba(7,29,73,0.1)', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#032340" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
)}
{/* ── Brand Panel — desktop only ── */}
{!isMobile && <BrandPanel mode={mode} lang={lang} />}
{/* ── Form Panel ── */}
<div style={{
flex: 1, display: 'flex', flexDirection: 'column',
alignItems: 'center', background: '#fafaf8', overflowY: 'auto',
}}>
{/* Mobile top bar */}
{isMobile && (
<div style={{
width: '100%', background: NAVY, padding: '1rem 1.5rem',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
direction: 'rtl', flexShrink: 0,
}}>
<Link to="/" style={{ display: 'flex', alignItems: 'center', gap: 10, textDecoration: 'none' }}>
<img src="/Frame%202095585206%20(2).png" alt="" style={{ height: 36, filter: 'brightness(0) invert(1)' }} />
<span style={{ color: '#fff', fontWeight: 800, fontSize: '0.95rem' }}>اندیشکده فولاد آینده</span>
</Link>
<Link to="/" style={{ color: 'rgba(255,255,255,0.5)', fontSize: '0.75rem', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 4 }}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>
</svg>
{lang === 'fa' ? 'بازگشت' : 'Back'}
</Link>
</div>
)}
<div style={{
width: '100%', maxWidth: 440,
padding: isMobile ? '1.5rem 1.25rem 3rem' : '2rem',
flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center',
}}>
{/* Tab switcher */}
<div style={{
display: 'flex', background: '#ececea', borderRadius: 12,
padding: 4, marginBottom: '1.75rem', direction: 'rtl',
}}>
{(['login', 'register'] as Mode[]).map(m => (
<button key={m} onClick={() => setMode(m)} style={{
flex: 1, padding: '0.65rem', borderRadius: 9, border: 'none',
background: mode === m ? '#fff' : 'transparent',
color: mode === m ? NAVY : '#aaa',
fontWeight: mode === m ? 700 : 500,
fontSize: '0.88rem', cursor: 'pointer', transition: 'all 0.2s',
boxShadow: mode === m ? '0 1px 6px rgba(0,0,0,0.1)' : 'none',
fontFamily: 'inherit',
}}>
{m === 'login'
? (lang === 'fa' ? 'ورود' : 'Login')
: (lang === 'fa' ? 'ثبت‌نام' : 'Register')}
</button>
))}
</div>
{mode === 'login'
? <LoginForm lang={lang} login={login} navigate={handleSuccess} />
: <RegisterForm lang={lang} register={register} navigate={handleSuccess} />
}
{!isMobile && (
<div style={{ marginTop: '1.75rem', paddingTop: '1.25rem', borderTop: '1px solid #eee', textAlign: 'center' }}>
<Link to="/" style={{
color: '#bbb', fontSize: '0.78rem', textDecoration: 'none',
display: 'inline-flex', alignItems: 'center', gap: 5, direction: 'rtl',
}}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>
</svg>
{lang === 'fa' ? 'بازگشت به سایت' : 'Back to site'}
</Link>
</div>
)}
</div>
</div>
</div>
)
}
/* ─── Login Form ─── */
function LoginForm({ lang, login, navigate }: {
lang: 'fa' | 'en'
login: (e: string, p: string) => Promise<void>
navigate: (to?: string) => void
}) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [showPass, setShowPass] = useState(false)
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
async function submit(e: React.FormEvent) {
e.preventDefault(); setError(''); setLoading(true)
try { await login(email, password); navigate('/profile') }
catch (err: unknown) { setError(err instanceof Error ? err.message : 'خطا در ورود') }
finally { setLoading(false) }
}
return (
<form onSubmit={submit} dir="rtl" style={{ display: 'flex', flexDirection: 'column', gap: '1.1rem' }}>
<div style={{ marginBottom: '0.5rem', direction: 'rtl' }}>
<h2 style={{ margin: '0 0 0.3rem', color: NAVY, fontSize: '1.5rem', fontWeight: 800 }}>
{lang === 'fa' ? 'خوش آمدید' : 'Welcome back'}
</h2>
<p style={{ margin: 0, color: '#999', fontSize: '0.875rem' }}>
{lang === 'fa' ? 'برای ادامه وارد حساب خود شوید' : 'Sign in to continue'}
</p>
</div>
<FField label={lang === 'fa' ? 'ایمیل' : 'Email'}>
<FInput type="email" required value={email} onChange={e => setEmail(e.target.value)}
placeholder="example@email.com" dir="ltr"
icon={<MailIcon />} />
</FField>
<FField label={lang === 'fa' ? 'رمز عبور' : 'Password'}>
<FInput type={showPass ? 'text' : 'password'} required value={password}
onChange={e => setPassword(e.target.value)} dir="ltr"
icon={<LockIcon />}
suffix={<EyeToggle show={showPass} onToggle={() => setShowPass(s => !s)} />} />
</FField>
{error && <ErrBox>{error}</ErrBox>}
<SubmitBtn loading={loading}>
{lang === 'fa' ? 'ورود به حساب' : 'Sign In'}
</SubmitBtn>
</form>
)
}
/* ─── Register Form ─── */
function RegisterForm({ lang, register, navigate }: {
lang: 'fa' | 'en'
register: (n: string, e: string, p: string, phone?: string, org?: string) => Promise<void>
navigate: (to?: string) => void
}) {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [phone, setPhone] = useState('')
const [org, setOrg] = useState('')
const [password, setPassword] = useState('')
const [showPass, setShowPass] = useState(false)
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
async function submit(e: React.FormEvent) {
e.preventDefault(); setError(''); setLoading(true)
try { await register(name, email, password, phone, org); navigate('/profile') }
catch (err: unknown) { setError(err instanceof Error ? err.message : 'خطا در ثبت‌نام') }
finally { setLoading(false) }
}
return (
<form onSubmit={submit} dir="rtl" style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div style={{ marginBottom: '0.5rem', direction: 'rtl' }}>
<h2 style={{ margin: '0 0 0.3rem', color: NAVY, fontSize: '1.5rem', fontWeight: 800 }}>
{lang === 'fa' ? 'ثبت‌نام' : 'Create account'}
</h2>
<p style={{ margin: 0, color: '#999', fontSize: '0.875rem' }}>
{lang === 'fa' ? 'یک حساب کاربری جدید بسازید' : 'Join the Steel Futures Institute'}
</p>
</div>
<FField label={lang === 'fa' ? 'نام و نام خانوادگی' : 'Full Name'}>
<FInput required value={name} onChange={e => setName(e.target.value)}
placeholder={lang === 'fa' ? 'علی محمدی' : 'John Doe'}
icon={<UserIcon />} />
</FField>
<FField label={lang === 'fa' ? 'ایمیل' : 'Email'}>
<FInput type="email" required value={email} onChange={e => setEmail(e.target.value)}
placeholder="example@email.com" dir="ltr"
icon={<MailIcon />} />
</FField>
<FField label={lang === 'fa' ? 'شماره تماس' : 'Phone'} required>
<FInput type="tel" required value={phone} onChange={e => setPhone(e.target.value)}
placeholder={lang === 'fa' ? '09123456789' : '+1 555 000 0000'} dir="ltr"
icon={<PhoneIcon />} />
</FField>
<FField label={lang === 'fa' ? 'نام سازمان / شرکت (اختیاری)' : 'Organization (optional)'}>
<FInput value={org} onChange={e => setOrg(e.target.value)}
placeholder={lang === 'fa' ? 'شرکت فولاد...' : 'Your company...'}
icon={<BldgIcon />} />
</FField>
<FField label={lang === 'fa' ? 'رمز عبور' : 'Password'}>
<FInput type={showPass ? 'text' : 'password'} required minLength={6}
value={password} onChange={e => setPassword(e.target.value)} dir="ltr"
icon={<LockIcon />}
suffix={<EyeToggle show={showPass} onToggle={() => setShowPass(s => !s)} />} />
<p style={{ margin: '4px 0 0', color: '#bbb', fontSize: '0.73rem', textAlign: 'right' }}>
{lang === 'fa' ? 'حداقل ۶ کاراکتر' : 'Min 6 characters'}
</p>
</FField>
{error && <ErrBox>{error}</ErrBox>}
<SubmitBtn loading={loading}>
{lang === 'fa' ? 'ساخت حساب کاربری' : 'Create Account'}
</SubmitBtn>
</form>
)
}
/* ─── Brand Panel ─── */
function BrandPanel({ mode, lang }: { mode: Mode; lang: 'fa' | 'en' }) {
return (
<div style={{
flex: '0 0 46%', position: 'relative', overflow: 'hidden',
background: `linear-gradient(145deg, ${NAVY} 0%, #043560 55%, #021828 100%)`,
display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
padding: '3rem',
}}
className="max-lg:hidden"
>
{/* grid */}
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: 0.04 }} preserveAspectRatio="xMidYMid slice">
<defs><pattern id="bg" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke={GOLD} strokeWidth="0.8"/>
</pattern></defs>
<rect width="100%" height="100%" fill="url(#bg)" />
</svg>
{/* hex rings */}
<svg style={{ position: 'absolute', bottom: -80, right: lang === 'fa' ? 'auto' : -80, left: lang === 'fa' ? -80 : 'auto', width: 440, height: 440, opacity: 0.07, pointerEvents: 'none' }} viewBox="0 0 440 440">
{[0,1,2,3,4].map(i => (
<polygon key={i} points="220,12 400,112 400,312 220,412 40,312 40,112"
fill="none" stroke={GOLD} strokeWidth="1.5"
transform={`translate(${220 * (1 - (0.35 + i*0.13))}, ${220 * (1 - (0.35 + i*0.13))}) scale(${0.35 + i*0.13})`}
/>
))}
</svg>
{/* Logo */}
<div style={{ position: 'relative', zIndex: 1 }}>
<Link to="/" style={{ display: 'flex', alignItems: 'center', gap: 12, textDecoration: 'none' }}>
<img src="/Frame%202095585206%20(2).png" alt="" style={{ height: 44, filter: 'brightness(0) invert(1)' }} />
<div>
<div style={{ color: '#fff', fontWeight: 800, fontSize: '1.05rem', lineHeight: 1.3 }}>اندیشکده فولاد آینده</div>
<div style={{ color: 'rgba(255,255,255,0.4)', fontSize: '0.6rem', letterSpacing: '2px', textTransform: 'uppercase' }}>The Steel Futures Institute</div>
</div>
</Link>
</div>
{/* Center copy */}
<div style={{ position: 'relative', zIndex: 1, direction: 'rtl' }}>
<div style={{
display: 'inline-block', background: `${GOLD}22`, border: `1px solid ${GOLD}44`,
color: GOLD, padding: '0.3rem 0.85rem', borderRadius: 4,
fontSize: '0.72rem', fontWeight: 600, marginBottom: '1.1rem',
}}>
{mode === 'login'
? (lang === 'fa' ? 'پلتفرم تخصصی فولاد' : 'Steel Industry Platform')
: (lang === 'fa' ? 'عضویت رایگان' : 'Free Membership')}
</div>
<h1 style={{ color: '#fff', fontSize: 'clamp(1.5rem, 2.3vw, 2.2rem)', fontWeight: 800, lineHeight: 1.45, margin: '0 0 1rem' }}>
{mode === 'login'
? <>{lang === 'fa' ? 'دسترسی به تحلیل‌های تخصصی' : 'Access Expert Analysis'}<br/><span style={{ color: GOLD }}>{lang === 'fa' ? 'صنعت فولاد ایران' : 'Steel Industry'}</span></>
: <>{lang === 'fa' ? 'عضو اندیشکده شوید' : 'Join the Institute'}<br/><span style={{ color: GOLD }}>{lang === 'fa' ? 'و به تحلیل‌ها دسترسی داشته باشید' : 'Get Exclusive Access'}</span></>
}
</h1>
<p style={{ color: 'rgba(255,255,255,0.5)', fontSize: '0.9rem', lineHeight: 1.85, margin: 0, maxWidth: 320 }}>
{lang === 'fa'
? 'گزارش‌های بازار، رصد فناوری، و تحلیل ژئوپلیتیک — همه در یک پلتفرم'
: 'Market reports, technology intelligence, and geopolitical analysis — all in one platform'}
</p>
</div>
{/* Bottom stats / checklist */}
<div style={{ position: 'relative', zIndex: 1, direction: 'rtl' }}>
{mode === 'login'
? (
<div style={{ display: 'flex', gap: '2rem' }}>
{[['+۲۰۰', lang === 'fa' ? 'گزارش' : 'Reports'], ['۵۰+', lang === 'fa' ? 'کارشناس' : 'Experts'], ['۳ سال', lang === 'fa' ? 'سابقه' : 'History']].map(([n, l]) => (
<div key={l as string}>
<div style={{ color: GOLD, fontSize: '1.35rem', fontWeight: 800 }}>{n}</div>
<div style={{ color: 'rgba(255,255,255,0.4)', fontSize: '0.72rem', marginTop: 2 }}>{l}</div>
</div>
))}
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.65rem' }}>
{[
lang === 'fa' ? 'دسترسی به گزارش‌های تخصصی' : 'Access to expert reports',
lang === 'fa' ? 'رصد بازار و قیمت‌ها' : 'Market and price monitoring',
lang === 'fa' ? 'شبکه خبرگان صنعت' : 'Industry expert network',
].map(item => (
<div key={item} style={{ display: 'flex', alignItems: 'center', gap: 10, color: 'rgba(255,255,255,0.65)', fontSize: '0.85rem' }}>
<div style={{ width: 20, height: 20, borderRadius: '50%', background: `${GOLD}30`, border: `1px solid ${GOLD}55`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke={GOLD} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
{item}
</div>
))}
</div>
)
}
</div>
</div>
)
}
/* ─── Shared primitives ─── */
function FField({ label, children, required }: { label: string; children: React.ReactNode; required?: boolean }) {
return (
<div>
<label style={{ display: 'block', color: '#444', fontSize: '0.8rem', fontWeight: 600, marginBottom: 6, direction: 'rtl' }}>
{label}{required && <span style={{ color: '#e74c3c', marginRight: 2 }}>*</span>}
</label>
{children}
</div>
)
}
function FInput({ icon, suffix, ...props }: React.InputHTMLAttributes<HTMLInputElement> & { icon?: React.ReactNode; suffix?: React.ReactNode }) {
const [focused, setFocused] = useState(false)
return (
<div style={{ position: 'relative' }}>
<input
{...props}
onFocus={e => { setFocused(true); props.onFocus?.(e) }}
onBlur={e => { setFocused(false); props.onBlur?.(e) }}
style={{
width: '100%',
padding: `0.82rem ${suffix ? '2.75rem' : '1rem'} 0.82rem ${icon ? '2.75rem' : '1rem'}`,
borderRadius: 10, border: `1.5px solid ${focused ? GOLD : '#e5e5e5'}`,
background: props.disabled ? '#f7f7f7' : '#fff',
color: '#1a1a1a', fontSize: '0.9rem', outline: 'none',
boxSizing: 'border-box', transition: 'border-color 0.18s',
fontFamily: 'inherit', ...props.style,
}}
/>
{icon && <span style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', opacity: 0.3, pointerEvents: 'none', display: 'flex' }}>{icon}</span>}
{suffix && <span style={{ position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)', display: 'flex' }}>{suffix}</span>}
</div>
)
}
function EyeToggle({ show, onToggle }: { show: boolean; onToggle: () => void }) {
return (
<button type="button" onClick={onToggle} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4, opacity: 0.4, display: 'flex', alignItems: 'center' }}>
{show
? <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#333" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
: <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#333" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
}
</button>
)
}
function ErrBox({ children }: { children: React.ReactNode }) {
return (
<div style={{ background: '#fff0f0', border: '1px solid #ffcccc', borderRadius: 8, padding: '0.65rem 1rem', color: '#c0392b', fontSize: '0.83rem', direction: 'rtl' }}>
{children}
</div>
)
}
function SubmitBtn({ loading, children }: { loading: boolean; children: React.ReactNode }) {
return (
<button type="submit" disabled={loading} style={{
width: '100%', padding: '0.9rem', borderRadius: 10, border: 'none',
background: loading ? '#e0c98a' : GOLD, color: NAVY,
fontWeight: 800, fontSize: '0.95rem', cursor: loading ? 'not-allowed' : 'pointer',
transition: 'all 0.2s', marginTop: '0.35rem',
boxShadow: loading ? 'none' : `0 4px 20px ${GOLD}55`,
fontFamily: 'inherit',
}}
onMouseEnter={e => { if (!loading) { e.currentTarget.style.transform = 'translateY(-1px)'; e.currentTarget.style.boxShadow = `0 6px 24px ${GOLD}66` } }}
onMouseLeave={e => { e.currentTarget.style.transform = 'none'; e.currentTarget.style.boxShadow = loading ? 'none' : `0 4px 20px ${GOLD}55` }}
>
{loading ? '...' : children}
</button>
)
}
/* ─── SVG Icons ─── */
const MailIcon = () => <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#555" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
const LockIcon = () => <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#555" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
const UserIcon = () => <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#555" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
const PhoneIcon = () => <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#555" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12 19.79 19.79 0 0 1 1.61 3.4 2 2 0 0 1 3.6 1.22h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L7.91 9a16 16 0 0 0 6.07 6.07l1.16-1.16a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
const BldgIcon = () => <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#555" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>

View File

@ -0,0 +1,112 @@
import React, { useState } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { useAuth } from '@/context/AuthContext'
import { useLang } from '@/context/LangContext'
const GOLD = '#CD9E53'
const NAVY = '#032340'
export default function LoginPage() {
const { lang } = useLang()
const { login } = useAuth()
const navigate = useNavigate()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const T = {
title: lang === 'fa' ? 'ورود به حساب' : 'Sign In',
email: lang === 'fa' ? 'ایمیل' : 'Email',
pass: lang === 'fa' ? 'رمز عبور' : 'Password',
submit: lang === 'fa' ? 'ورود' : 'Login',
noAcc: lang === 'fa' ? 'حساب ندارید؟' : "Don't have an account?",
register: lang === 'fa' ? 'ثبت‌نام کنید' : 'Register',
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setError('')
setLoading(true)
try {
await login(email, password)
navigate('/profile')
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'خطا در ورود')
} finally {
setLoading(false)
}
}
return (
<div style={{
minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'var(--paper)', padding: '2rem',
}}>
<div style={{
width: '100%', maxWidth: 420,
background: 'var(--paper-2)', borderRadius: 16,
padding: '2.5rem 2rem', boxShadow: '0 4px 40px rgba(0,0,0,0.08)',
border: `1px solid var(--rule-thin)`,
}}>
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
<div style={{ width: 48, height: 48, borderRadius: '50%', background: GOLD, margin: '0 auto 1rem', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke={NAVY} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
</div>
<h1 style={{ color: 'var(--ink)', fontSize: '1.5rem', fontWeight: 700, margin: 0 }}>{T.title}</h1>
</div>
<form onSubmit={handleSubmit} dir={lang === 'fa' ? 'rtl' : 'ltr'} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div>
<label style={{ display: 'block', color: 'var(--ink-3)', fontSize: '0.85rem', marginBottom: 6 }}>{T.email}</label>
<input
type="email" required value={email}
onChange={e => setEmail(e.target.value)}
style={inputStyle}
placeholder="example@email.com"
dir="ltr"
/>
</div>
<div>
<label style={{ display: 'block', color: 'var(--ink-3)', fontSize: '0.85rem', marginBottom: 6 }}>{T.pass}</label>
<input
type="password" required value={password}
onChange={e => setPassword(e.target.value)}
style={inputStyle}
dir="ltr"
/>
</div>
{error && (
<p style={{ color: 'var(--red)', fontSize: '0.85rem', margin: 0, textAlign: 'center' }}>{error}</p>
)}
<button type="submit" disabled={loading} style={btnStyle(loading)}>
{loading ? '...' : T.submit}
</button>
</form>
<p style={{ textAlign: 'center', marginTop: '1.5rem', color: 'var(--ink-3)', fontSize: '0.875rem' }}>
{T.noAcc}{' '}
<Link to="/register" style={{ color: GOLD, fontWeight: 600, textDecoration: 'none' }}>{T.register}</Link>
</p>
</div>
</div>
)
}
const inputStyle: React.CSSProperties = {
width: '100%', padding: '0.75rem 1rem', borderRadius: 8,
border: '1px solid var(--rule-thin)', background: 'var(--paper)',
color: 'var(--ink)', fontSize: '0.95rem', outline: 'none', boxSizing: 'border-box',
}
const btnStyle = (loading: boolean): React.CSSProperties => ({
width: '100%', padding: '0.85rem', borderRadius: 8,
background: loading ? 'var(--ink-5)' : GOLD,
color: NAVY, fontWeight: 700, fontSize: '1rem',
border: 'none', cursor: loading ? 'not-allowed' : 'pointer',
transition: 'opacity 0.2s',
})

View File

@ -0,0 +1,239 @@
import React, { useState } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { useAuth } from '@/context/AuthContext'
import { useLang } from '@/context/LangContext'
const GOLD = '#CD9E53'
const NAVY = '#032340'
export default function RegisterPage() {
const { lang } = useLang()
const { register } = useAuth()
const navigate = useNavigate()
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [showPass, setShowPass] = useState(false)
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const isRtl = lang === 'fa'
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setError('')
setLoading(true)
try {
await register(name, email, password)
navigate('/profile')
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'خطا در ثبت‌نام')
} finally {
setLoading(false)
}
}
return (
<div style={{
position: 'fixed', inset: 0, zIndex: 100,
display: 'flex', flexDirection: isRtl ? 'row-reverse' : 'row',
background: '#fff',
}}>
{/* ── Brand Panel ── */}
<div style={{
flex: '0 0 48%', position: 'relative', overflow: 'hidden',
background: `linear-gradient(145deg, ${NAVY} 0%, #043560 55%, #021828 100%)`,
display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
padding: '3rem',
}}
className="max-lg:hidden"
>
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: 0.05 }} preserveAspectRatio="xMidYMid slice">
<defs>
<pattern id="grid2" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#CD9E53" strokeWidth="0.8"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid2)" />
</svg>
<svg style={{ position: 'absolute', top: -60, left: isRtl ? -60 : 'auto', right: isRtl ? 'auto' : -60, width: 420, height: 420, opacity: 0.06 }} viewBox="0 0 420 420">
{[0,1,2,3,4].map(i => (
<polygon key={i} points="210,10 390,110 390,310 210,410 30,310 30,110"
fill="none" stroke={GOLD} strokeWidth="1.5"
transform={`scale(${0.3 + i*0.15}) translate(${(1-0.3-i*0.15)*280}, ${(1-0.3-i*0.15)*280})`}
/>
))}
</svg>
<div style={{ position: 'relative', zIndex: 1 }}>
<Link to="/" style={{ display: 'flex', alignItems: 'center', gap: 12, textDecoration: 'none' }}>
<img src="/Frame%202095585206%20(2).png" alt="" style={{ height: 44, filter: 'brightness(0) invert(1)' }} />
<div>
<div style={{ color: '#fff', fontWeight: 800, fontSize: '1.1rem', lineHeight: 1.3 }}>اندیشکده فولاد آینده</div>
<div style={{ color: 'rgba(255,255,255,0.45)', fontSize: '0.65rem', letterSpacing: '2px', textTransform: 'uppercase' }}>The Steel Futures Institute</div>
</div>
</Link>
</div>
<div style={{ position: 'relative', zIndex: 1, direction: 'rtl' }}>
<div style={{
display: 'inline-block', background: `${GOLD}22`, border: `1px solid ${GOLD}44`,
color: GOLD, padding: '0.35rem 0.9rem', borderRadius: 4, fontSize: '0.75rem',
fontWeight: 600, marginBottom: '1.25rem', letterSpacing: '0.5px',
}}>
عضویت رایگان
</div>
<h1 style={{ color: '#fff', fontSize: 'clamp(1.6rem, 2.5vw, 2.4rem)', fontWeight: 800, lineHeight: 1.4, margin: '0 0 1rem' }}>
عضو اندیشکده شوید<br/>
<span style={{ color: GOLD }}>و به تحلیلها دسترسی داشته باشید</span>
</h1>
<p style={{ color: 'rgba(255,255,255,0.55)', fontSize: '0.95rem', lineHeight: 1.8, margin: 0, maxWidth: 340 }}>
با ثبتنام رایگان به آرشیو گزارشهای تخصصی صنعت فولاد دسترسی پیدا کنید
</p>
</div>
<div style={{ position: 'relative', zIndex: 1, direction: 'rtl' }}>
{['دسترسی به گزارش‌های تخصصی', 'رصد بازار و قیمت‌ها', 'شبکه خبرگان صنعت'].map((item, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: i < 2 ? '0.75rem' : 0, color: 'rgba(255,255,255,0.7)', fontSize: '0.875rem' }}>
<div style={{ width: 20, height: 20, borderRadius: '50%', background: `${GOLD}33`, border: `1px solid ${GOLD}66`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke={GOLD} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
{item}
</div>
))}
</div>
</div>
{/* ── Form Panel ── */}
<div style={{
flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center',
padding: '2rem', background: '#fafaf8', overflowY: 'auto',
}}>
<div style={{ width: '100%', maxWidth: 400 }}>
<div style={{ marginBottom: '2.5rem', direction: 'rtl' }}>
<h2 style={{ color: NAVY, fontSize: '1.75rem', fontWeight: 800, margin: '0 0 0.5rem' }}>ثبتنام</h2>
<p style={{ color: '#8a8a8a', margin: 0, fontSize: '0.9rem' }}>یک حساب کاربری جدید بسازید</p>
</div>
<form onSubmit={handleSubmit} dir="rtl" style={{ display: 'flex', flexDirection: 'column', gap: '1.1rem' }}>
<div>
<label style={labelStyle}>نام و نام خانوادگی</label>
<InputWithIcon
type="text" required value={name}
onChange={e => setName(e.target.value)}
placeholder="علی محمدی"
icon={
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#333" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
}
/>
</div>
<div>
<label style={labelStyle}>ایمیل</label>
<InputWithIcon
type="email" required value={email} dir="ltr"
onChange={e => setEmail(e.target.value)}
placeholder="example@email.com"
icon={
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#333" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/>
</svg>
}
/>
</div>
<div>
<label style={labelStyle}>رمز عبور</label>
<div style={{ position: 'relative' }}>
<input
type={showPass ? 'text' : 'password'} required minLength={6}
value={password} onChange={e => setPassword(e.target.value)}
dir="ltr"
style={inputStyle}
onFocus={e => { e.target.style.borderColor = GOLD }}
onBlur={e => { e.target.style.borderColor = '#e0e0e0' }}
/>
<svg style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', opacity: 0.35, pointerEvents: 'none' }} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#333" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<button type="button" onClick={() => setShowPass(s => !s)} style={{ position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', cursor: 'pointer', padding: 4, opacity: 0.45, display: 'flex' }}>
{showPass
? <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#333" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#333" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
}
</button>
</div>
<p style={{ color: '#aaa', fontSize: '0.75rem', margin: '4px 0 0', textAlign: 'right' }}>حداقل ۶ کاراکتر</p>
</div>
{error && (
<div style={{ background: '#fff0f0', border: '1px solid #ffcccc', borderRadius: 8, padding: '0.65rem 1rem', color: '#c0392b', fontSize: '0.85rem' }}>
{error}
</div>
)}
<button type="submit" disabled={loading} style={{
width: '100%', padding: '0.95rem', borderRadius: 10, border: 'none',
background: loading ? '#d4b97a' : GOLD,
color: NAVY, fontWeight: 800, fontSize: '1rem',
cursor: loading ? 'not-allowed' : 'pointer',
transition: 'all 0.2s', marginTop: '0.4rem',
boxShadow: loading ? 'none' : `0 4px 20px ${GOLD}55`,
fontFamily: 'inherit',
}}
onMouseEnter={e => { if (!loading) e.currentTarget.style.transform = 'translateY(-1px)' }}
onMouseLeave={e => { e.currentTarget.style.transform = 'translateY(0)' }}
>
{loading ? 'در حال ثبت‌نام...' : 'ساخت حساب کاربری'}
</button>
</form>
<div style={{ marginTop: '2rem', textAlign: 'center', direction: 'rtl' }}>
<span style={{ color: '#888', fontSize: '0.875rem' }}>حساب دارید؟ </span>
<Link to="/login" style={{ color: GOLD, fontWeight: 700, textDecoration: 'none', fontSize: '0.875rem' }}>وارد شوید</Link>
</div>
<div style={{ marginTop: '2rem', paddingTop: '1.5rem', borderTop: '1px solid #eee', textAlign: 'center' }}>
<Link to="/" style={{ color: '#aaa', fontSize: '0.8rem', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 4, direction: 'rtl' }}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>
</svg>
بازگشت به سایت
</Link>
</div>
</div>
</div>
</div>
)
}
/* helpers */
const labelStyle: React.CSSProperties = {
display: 'block', color: '#444', fontSize: '0.82rem', fontWeight: 600, marginBottom: 6,
}
const inputStyle: React.CSSProperties = {
width: '100%', padding: '0.85rem 1rem 0.85rem 2.75rem', borderRadius: 10,
border: '1.5px solid #e0e0e0', background: '#fff',
color: '#1a1a1a', fontSize: '0.95rem', outline: 'none',
boxSizing: 'border-box', transition: 'border-color 0.2s', fontFamily: 'inherit',
}
function InputWithIcon({ icon, ...props }: React.InputHTMLAttributes<HTMLInputElement> & { icon: React.ReactNode }) {
return (
<div style={{ position: 'relative' }}>
<input
{...props}
style={inputStyle}
onFocus={e => { e.target.style.borderColor = GOLD }}
onBlur={e => { e.target.style.borderColor = '#e0e0e0' }}
/>
<span style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', opacity: 0.35, pointerEvents: 'none', display: 'flex' }}>
{icon}
</span>
</div>
)
}

View File

@ -1,7 +1,5 @@
import { useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { ChevronRight } from 'lucide-react'
import { Link } from 'react-router-dom'
import { useLang } from '@/context/LangContext'
const NAVY = '#032340'
@ -48,117 +46,101 @@ const ITEMS: Glance[] = [
export default function AtAGlanceSection({ embedded = false }: { embedded?: boolean }) {
const { lang } = useLang()
const isFa = lang === 'fa'
const [idx, setIdx] = useState(0)
const [i, setI] = useState(0)
const [dir, setDir] = useState<1 | -1>(1)
const it = ITEMS[idx]
const it = ITEMS[i]
const go = (d: 1 | -1) => {
setDir(d)
setIdx((prev) => (prev + d + ITEMS.length) % ITEMS.length)
setI((prev) => (prev + d + ITEMS.length) % ITEMS.length)
}
const T = {
heading: isFa ? 'در یک نگاه' : 'At a Glance',
}
return (
<section id="at-a-glance" style={{ background: '#faf7f4', padding: '52px 0', overflow: 'hidden', scrollMarginTop: '90px' }} dir={isFa ? 'rtl' : 'ltr'}>
<div className="max-w-7xl mx-auto px-12 max-md:px-5">
{/* ── heading row ── */}
{!embedded && (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 32 }}>
{/* title — bilingual stacked */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 4, height: 34, background: GOLD, borderRadius: 2, flexShrink: 0 }} />
<div>
<div style={{ fontSize: 11, fontWeight: 600, color: 'rgba(7,29,73,0.4)', letterSpacing: '1.5px', textTransform: 'uppercase', marginBottom: 3 }}>
At a Glance
</div>
<h2 style={{ fontSize: 'clamp(20px, 2vw, 28px)', fontWeight: 900, color: NAVY, letterSpacing: '-0.5px', margin: 0, lineHeight: 1.2 }}>
{isFa ? 'در یک نگاه' : 'At a Glance'}
</h2>
</div>
</div>
{/* مشاهده همه */}
<Link to="/at-a-glance" style={{
display: 'inline-flex', alignItems: 'center', gap: 6, direction: 'ltr',
fontSize: 13, fontWeight: 700, color: GOLD, textDecoration: 'none',
}}>
<div style={{ width: 24, height: 24, borderRadius: '50%', border: `1px solid ${GOLD}`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<ChevronRight size={14} strokeWidth={2.5} />
</div>
<span style={{ direction: isFa ? 'rtl' : 'ltr' }}>{isFa ? 'مشاهده همه' : 'View All'}</span>
</Link>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, justifyContent: 'flex-start', marginBottom: 24 }}>
<div style={{ width: 4, height: 28, background: GOLD, borderRadius: 2 }} />
<h2 style={{ fontSize: 'clamp(22px, 2.2vw, 30px)', fontWeight: 900, color: NAVY, letterSpacing: '-0.5px', margin: 0 }}>
{T.heading}
</h2>
</div>
)}
{/* ── card row: image left, text right ── */}
{/* image is taller; text card is centered vertically and overlaps image by ~48px */}
<div style={{ display: 'flex', alignItems: 'center', gap: 0, direction: 'ltr', position: 'relative' }} className="max-lg:!flex-col max-lg:!gap-8">
{/* IMAGE — taller than the text card */}
<div style={{ width: '40%', position: 'relative', flexShrink: 0, alignSelf: 'stretch' }} className="max-lg:!w-full">
{/* back shadow card */}
<div
style={{ display: 'flex', flexDirection: isFa ? 'row' : 'row-reverse', alignItems: 'center', gap: 0, minHeight: 380, direction: 'ltr' }}
className="max-lg:!flex-col max-lg:!gap-8"
>
{/* ── IMAGE side ── */}
<div style={{ width: '35%', position: 'relative', flexShrink: 0 }} className="max-lg:!w-full max-lg:!max-w-[400px]">
<div style={{
position: 'absolute', inset: 0,
transform: 'translate(-20px, -16px)',
background: '#e8e2db', borderRadius: 18,
transform: 'translate(-28px, -22px)',
background: '#ece7e1', borderRadius: 18,
boxShadow: '0 30px 70px -30px rgba(7,29,73,0.25)',
}} />
{/* image fills full height of its column */}
<div style={{ position: 'relative', borderRadius: 18, overflow: 'hidden', height: '100%', minHeight: 420 }} className="max-lg:!min-h-[260px]">
<div
style={{ position: 'relative', borderRadius: 18, overflow: 'hidden', boxShadow: '0 40px 90px -30px rgba(7,29,73,0.45)' }}
className="aspect-[3/3.2] max-lg:!aspect-[16/10]"
>
<AnimatePresence mode="wait" custom={dir}>
<motion.img
key={idx}
key={i}
src={it.img}
alt=""
initial={{ opacity: 0, scale: 1.06 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.02 }}
transition={{ duration: 0.55, ease: [0.22, 1, 0.36, 1] }}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
/>
</AnimatePresence>
</div>
</div>
{/* TEXT — shorter, overlaps image by 48px on the left */}
<div style={{ flex: 1, position: 'relative', marginLeft: -48, zIndex: 2 }} className="max-lg:!ms-0 max-lg:!me-0">
{/* ── TEXT card side ── */}
<div style={{ flex: 1, position: 'relative', zIndex: 2 }} className="max-lg:!w-full">
<div style={{
background: '#fff',
borderRadius: 6,
boxShadow: '0 20px 60px -30px rgba(7,29,73,0.25)',
padding: '32px 40px 48px',
background: '#fff', borderRadius: 6,
boxShadow: '0 30px 80px -40px rgba(7,29,73,0.30)',
padding: '40px 44px 32px',
marginLeft: isFa ? '-8%' : 0,
marginRight: isFa ? 0 : '-8%',
direction: isFa ? 'rtl' : 'ltr',
borderLeft: `4px solid ${GOLD}`,
position: 'relative',
}} className="max-md:!p-6">
}} className="max-lg:!ms-0 max-lg:!me-0 max-md:!p-6">
<AnimatePresence mode="wait">
<motion.div
key={idx}
key={i}
initial={{ opacity: 0, x: dir * 24 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: dir * -16 }}
transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
>
<h3 style={{ fontSize: 'clamp(18px, 1.7vw, 25px)', fontWeight: 900, color: NAVY, lineHeight: 1.5, letterSpacing: '-0.3px', margin: '0 0 18px' }}>
<h3 style={{ fontSize: 'clamp(20px, 1.9vw, 27px)', fontWeight: 900, color: NAVY, lineHeight: 1.4, letterSpacing: '-0.3px', margin: '0 0 18px' }}>
{isFa ? it.title : it.titleEn}
</h3>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20, flexWrap: 'wrap', gap: 8 }}>
<span style={{ fontSize: 14, fontWeight: 700, color: GOLD }}>{it.source}</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13, color: 'rgba(7,29,73,0.55)' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 13, color: 'rgba(7,29,73,0.55)' }}>
<CalendarIcon />
{isFa ? it.date : it.dateEn}
</span>
<span style={{ fontSize: 14, fontWeight: 700, color: GOLD, letterSpacing: '0.3px' }}>{it.source}</span>
</div>
<p style={{ fontSize: 'clamp(13px, 1vw, 15px)', lineHeight: 2.1, color: 'rgba(7,29,73,0.72)', margin: 0, textAlign: 'justify' }}>
<p style={{ fontSize: 'clamp(13px, 1.05vw, 15px)', lineHeight: 2.1, color: 'rgba(7,29,73,0.72)', margin: 0, textAlign: 'justify' }}>
{isFa ? it.desc : it.descEn}
</p>
</motion.div>
</AnimatePresence>
{/* nav buttons — half inside / half outside bottom of text card */}
<div style={{ position: 'absolute', bottom: -19, left: 40, display: 'flex', gap: 8, zIndex: 4 }}>
<div style={{ display: 'flex', gap: 12, marginTop: 28 }}>
<NavBtn onClick={() => go(-1)} aria="prev"></NavBtn>
<NavBtn onClick={() => go(1)} aria="next"></NavBtn>
</div>
@ -177,15 +159,13 @@ function NavBtn({ children, onClick, aria }: { children: React.ReactNode; onClic
aria-label={aria}
style={{
width: 38, height: 38, borderRadius: '50%',
background: GOLD, color: '#fff',
border: 'none', cursor: 'pointer',
background: '#fff', color: GOLD, border: `1.5px solid ${GOLD}`, cursor: 'pointer',
fontSize: 20, fontWeight: 700, lineHeight: 1,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: 'inherit', transition: 'background 180ms, transform 180ms',
boxShadow: '0 4px 14px rgba(205,158,83,0.45)',
fontFamily: 'inherit', transition: 'background 180ms, color 180ms',
}}
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.background = '#b88d46'; (e.currentTarget as HTMLElement).style.transform = 'translateY(-2px)' }}
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = GOLD; (e.currentTarget as HTMLElement).style.transform = 'translateY(0)' }}
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.background = GOLD; (e.currentTarget as HTMLElement).style.color = '#fff' }}
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = '#fff'; (e.currentTarget as HTMLElement).style.color = GOLD }}
>
{children}
</button>
@ -194,7 +174,7 @@ function NavBtn({ children, onClick, aria }: { children: React.ReactNode; onClic
function CalendarIcon() {
return (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={GOLD} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={GOLD} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />

View File

@ -0,0 +1,191 @@
import { useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { useLang } from '@/context/LangContext'
const NAVY = '#032340'
const GOLD = '#CD9E53'
type Glance = {
source: string
date: string; dateEn: string
title: string; titleEn: string
desc: string; descEn: string
img: string
}
const ITEMS: Glance[] = [
{
source: 'Financial Times',
date: 'اردیبهشت ۱۴۰۵', dateEn: 'May 2026',
title: 'شوک هیدروژنی به بازار جهانی؛ آغاز رسمی عصر فولادسازی بدون کربن',
titleEn: 'Hydrogen Shock to Global Markets; the Carbon-Free Steel Era Begins',
desc: 'پروژه عظیم «فولاد سبز H2» در سوئد با جذب بی‌سابقه ۶.۵ میلیارد یورو سرمایه، رسماً وارد فاز اجرایی شد تا نخستین کارخانه فولادسازی در مقیاس تجاری بر پایه هیدروژن سبز (بدون نیاز به زغال‌سنگ) در جهان متولد شود. این تأسیسات با هدف کاهش ۹۵ درصدی انتشار CO₂، قواعد رقابت در زنجیره ارزش اروپا را برای همیشه بازتعریف می‌کند.',
descEn: 'Sweden\'s massive "Green Steel H2" project, having raised an unprecedented €6.5bn, has officially entered its execution phase to birth the world\'s first commercial-scale steel plant powered by green hydrogen — redefining Europe\'s value-chain competition for good.',
img: '/news/photo-1504307651254-35680f356dfd.jpg',
},
{
source: 'Reuters',
date: 'اردیبهشت ۱۴۰۵', dateEn: 'May 2026',
title: 'مکانیسم تنظیم مرزهای کربنی اروپا وارد فاز اجرای کامل شد',
titleEn: 'EU Carbon Border Mechanism Enters Full Enforcement',
desc: 'با اجرای کامل CBAM، صادرکنندگان فولاد به اتحادیه اروپا اکنون موظف‌اند هزینه کامل کربن نهفته در محصولات خود را بپردازند. این تحول، فشار بی‌سابقه‌ای بر تولیدکنندگان مبتنی بر کوره بلند وارد کرده و مزیت رقابتی را به سمت فولاد کم‌کربن سوق می‌دهد.',
descEn: 'With CBAM now fully enforced, steel exporters to the EU must pay the full cost of embedded carbon — placing unprecedented pressure on blast-furnace producers and shifting the competitive edge toward low-carbon steel.',
img: '/news/photo-1581092160607-ee22731c9b4e.jpg',
},
{
source: 'Bloomberg',
date: 'فروردین ۱۴۰۵', dateEn: 'Apr 2026',
title: 'هوش مصنوعی، تقاضای جهانی فولاد مراکز داده را بازتعریف می‌کند',
titleEn: 'AI Is Rewriting Global Steel Demand for Data Centers',
desc: 'موج ساخت مراکز داده هوش مصنوعی، تقاضای جدیدی برای مقاطع ساختمانی و فولاد ویژه ایجاد کرده است. تحلیلگران پیش‌بینی می‌کنند این روند تا پایان دهه، یکی از پیشران‌های اصلی رشد مصرف فولاد در اقتصادهای پیشرفته باشد.',
descEn: 'The AI data-center construction wave is creating fresh demand for structural sections and specialty steel — projected to be a leading driver of steel consumption growth in advanced economies through the decade.',
img: '/news/photo-1518770660439-4636190af475.jpg',
},
]
export default function AtAGlanceSection({ embedded = false }: { embedded?: boolean }) {
const { lang } = useLang()
const isFa = lang === 'fa'
const [i, setI] = useState(0)
const [dir, setDir] = useState<1 | -1>(1)
const it = ITEMS[i]
const go = (d: 1 | -1) => {
setDir(d)
setI((prev) => (prev + d + ITEMS.length) % ITEMS.length)
}
const T = {
heading: isFa ? 'در یک نگاه' : 'At a Glance',
}
return (
<section id="at-a-glance" style={{ background: '#faf7f4', padding: '52px 0', overflow: 'hidden', scrollMarginTop: '90px' }} dir={isFa ? 'rtl' : 'ltr'}>
<div className="max-w-7xl mx-auto px-12 max-md:px-5">
{!embedded && (
<div style={{ display: 'flex', alignItems: 'center', gap: 12, justifyContent: 'flex-start', marginBottom: 24 }}>
<div style={{ width: 4, height: 28, background: GOLD, borderRadius: 2 }} />
<h2 style={{ fontSize: 'clamp(22px, 2.2vw, 30px)', fontWeight: 900, color: NAVY, letterSpacing: '-0.5px', margin: 0 }}>
{T.heading}
</h2>
</div>
)}
{/* image on the LEFT, text on the RIGHT (matches the reference).
Container is LTR; image is the first DOM child, so plain `row` lands it on the left. */}
<div
style={{ display: 'flex', flexDirection: isFa ? 'row' : 'row-reverse', alignItems: 'center', gap: 0, minHeight: 380, direction: 'ltr' }}
className="max-lg:!flex-col max-lg:!gap-8"
>
{/* ── IMAGE side (with stacked-card shadow) ── */}
<div style={{ width: '35%', position: 'relative', flexShrink: 0 }} className="max-lg:!w-full max-lg:!max-w-[400px]">
{/* back card (offset, behind) — offset away from the text card */}
<div style={{
position: 'absolute', inset: 0,
transform: 'translate(-28px, -22px)',
background: '#ece7e1', borderRadius: 18,
boxShadow: '0 30px 70px -30px rgba(7,29,73,0.25)',
}} />
{/* front image — portrait on desktop, shorter landscape on mobile */}
<div
style={{ position: 'relative', borderRadius: 18, overflow: 'hidden', boxShadow: '0 40px 90px -30px rgba(7,29,73,0.45)' }}
className="aspect-[3/3.2] max-lg:!aspect-[16/10]"
>
<AnimatePresence mode="wait" custom={dir}>
<motion.img
key={i}
src={it.img}
alt=""
initial={{ opacity: 0, scale: 1.06 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.02 }}
transition={{ duration: 0.55, ease: [0.22, 1, 0.36, 1] }}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
/>
</AnimatePresence>
</div>
</div>
{/* ── TEXT card side ── */}
<div style={{ flex: 1, position: 'relative', zIndex: 2 }} className="max-lg:!w-full">
<div style={{
background: '#fff', borderRadius: 6,
boxShadow: '0 30px 80px -40px rgba(7,29,73,0.30)',
padding: '40px 44px 32px',
// pull the card toward the image to overlap it (image is on the left in RTL)
marginLeft: isFa ? '-8%' : 0,
marginRight: isFa ? 0 : '-8%',
direction: isFa ? 'rtl' : 'ltr',
}} className="max-lg:!ms-0 max-lg:!me-0 max-md:!p-6">
<AnimatePresence mode="wait">
<motion.div
key={i}
initial={{ opacity: 0, x: dir * 24 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: dir * -16 }}
transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
>
<h3 style={{ fontSize: 'clamp(20px, 1.9vw, 27px)', fontWeight: 900, color: NAVY, lineHeight: 1.4, letterSpacing: '-0.3px', margin: '0 0 18px' }}>
{isFa ? it.title : it.titleEn}
</h3>
{/* date (leading side) + source (trailing side) */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20, flexWrap: 'wrap', gap: 8 }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 13, color: 'rgba(7,29,73,0.55)' }}>
<CalendarIcon />
{isFa ? it.date : it.dateEn}
</span>
<span style={{ fontSize: 14, fontWeight: 700, color: GOLD, letterSpacing: '0.3px' }}>{it.source}</span>
</div>
<p style={{ fontSize: 'clamp(13px, 1.05vw, 15px)', lineHeight: 2.1, color: 'rgba(7,29,73,0.72)', margin: 0, textAlign: 'justify' }}>
{isFa ? it.desc : it.descEn}
</p>
</motion.div>
</AnimatePresence>
{/* prev / next controls */}
<div style={{ display: 'flex', gap: 12, marginTop: 28 }}>
<NavBtn onClick={() => go(-1)} aria="prev"></NavBtn>
<NavBtn onClick={() => go(1)} aria="next"></NavBtn>
</div>
</div>
</div>
</div>
</div>
</section>
)
}
function NavBtn({ children, onClick, aria }: { children: React.ReactNode; onClick: () => void; aria: string }) {
return (
<button
onClick={onClick}
aria-label={aria}
style={{
width: 38, height: 38, borderRadius: '50%',
background: '#fff', color: GOLD, border: `1.5px solid ${GOLD}`, cursor: 'pointer',
fontSize: 20, fontWeight: 700, lineHeight: 1,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: 'inherit', transition: 'background 180ms, color 180ms',
}}
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.background = GOLD; (e.currentTarget as HTMLElement).style.color = '#fff' }}
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = '#fff'; (e.currentTarget as HTMLElement).style.color = GOLD }}
>
{children}
</button>
)
}
function CalendarIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={GOLD} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
)
}

View File

@ -21,7 +21,7 @@ function ScrollAnimation({ children, direction = 'up' }: { children: React.React
whileInView={{ opacity: 1, x: 0, y: 0 }}
viewport={{ once: true, amount: 0.35 }}
transition={{ duration: 0.7, ease: EASE }}
style={{ breakInside: 'avoid', marginBottom: 18 }}
style={{ breakInside: 'avoid', marginBottom: 18, height: '100%' }}
>
{children}
</motion.div>
@ -36,12 +36,13 @@ function ReportCard({ r, lang }: { r: FactoryReport; lang: 'fa' | 'en' }) {
background: '#fff', borderRadius: 14, overflow: 'hidden', cursor: 'pointer',
boxShadow: '0 10px 34px -20px rgba(7,29,73,0.30)',
transition: 'transform 200ms, box-shadow 200ms',
display: 'flex', flexDirection: 'column', height: '100%',
}}
onMouseEnter={(e) => { const d = e.currentTarget; d.style.transform = 'translateY(-4px)'; d.style.boxShadow = '0 22px 50px -22px rgba(7,29,73,0.38)' }}
onMouseLeave={(e) => { const d = e.currentTarget; d.style.transform = 'translateY(0)'; d.style.boxShadow = '0 10px 34px -20px rgba(7,29,73,0.30)' }}
>
<div style={{ position: 'relative', overflow: 'hidden' }}>
<img src={r.img} alt="" style={{ width: '100%', display: 'block' }} />
<div style={{ position: 'relative', overflow: 'hidden', flexShrink: 0 }}>
<img src={r.img} alt="" style={{ width: '100%', height: 220, objectFit: 'cover', display: 'block' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(7,29,73,0.55) 0%, transparent 55%)' }} />
<span style={{
position: 'absolute', top: 12, insetInlineStart: 12,
@ -49,11 +50,11 @@ function ReportCard({ r, lang }: { r: FactoryReport; lang: 'fa' | 'en' }) {
padding: '4px 10px', borderRadius: 999, letterSpacing: '0.3px',
}}>{t.tag}</span>
</div>
<div style={{ padding: '18px 18px 16px', direction: lang === 'fa' ? 'rtl' : 'ltr' }}>
<div style={{ padding: '18px 18px 16px', direction: lang === 'fa' ? 'rtl' : 'ltr', display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
<div style={{ fontSize: 11, color: 'var(--ink-5)', marginBottom: 8 }}>{t.date}</div>
<h3 style={{ fontSize: 16, fontWeight: 900, color: NAVY, lineHeight: 1.45, margin: '0 0 10px' }}>{t.title}</h3>
<p style={{ fontSize: 12.5, lineHeight: 1.9, color: 'rgba(7,29,73,0.62)', margin: '0 0 14px' }}>{t.excerpt}</p>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<p style={{ fontSize: 12.5, lineHeight: 1.9, color: 'rgba(7,29,73,0.62)', margin: '0 0 14px', flexGrow: 1 }}>{t.excerpt}</p>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 'auto' }}>
<a
href={MSC_URL}
target="_blank"
@ -89,8 +90,8 @@ export default function FactoryReportsScroll() {
</a>
</div>
{/* compact 4-up row */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }} className="max-lg:!grid-cols-2 max-sm:!grid-cols-1">
{/* compact 3-up row, centered */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, maxWidth: 960, margin: '0 auto' }} className="max-lg:!grid-cols-2 max-sm:!grid-cols-1">
{reports.map((r) => (
<ScrollAnimation key={r.id} direction={r.dir}>
<ReportCard r={r} lang={lang} />

View File

@ -7,7 +7,7 @@ const flashReport = reports.find(r => r.type === 'flash')!
const PANEL_API =
(import.meta as ImportMeta & { env?: Record<string, string> }).env
?.VITE_PANEL_API || 'http://localhost:3001'
?.VITE_PANEL_API || ''
type PriceRow = { label: string; value: string; delta: string; up: boolean }

View File

@ -218,7 +218,7 @@ export default function LatestReportsSection() {
<div style={{ marginBottom: '40px' }}>
<SectionBanner
label="آخرین گزارش‌ها"
link={{ text: 'مشاهده آرشیو کامل ←', to: '/reports' }}
link={{ text: 'مشاهده آرشیو کامل ←', to: '/archive' }}
/>
</div>

View File

@ -1,9 +1,21 @@
import { useState, useEffect } from 'react'
import Globe from '@/components/ui/globe'
import StrategicEvents from '@/components/ui/StrategicEvents'
import { useLang } from '@/context/LangContext'
export default function MapEventsSection() {
const { lang } = useLang()
const [globeSize, setGlobeSize] = useState(560)
useEffect(() => {
const update = () => {
const w = window.innerWidth
setGlobeSize(w < 768 ? Math.min(w - 32, 340) : 560)
}
update()
window.addEventListener('resize', update)
return () => window.removeEventListener('resize', update)
}, [])
return (
<section dir={lang === 'fa' ? 'rtl' : 'ltr'} style={{ background: 'var(--paper)', borderBottom: '1px solid var(--rule-thin)', overflow: 'hidden' }}>
@ -16,8 +28,8 @@ export default function MapEventsSection() {
</div>
{/* ── Globe — LEFT, no text ── */}
<div style={{ flex: '6 6 0', minWidth: 0, overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px 16px' }}>
<Globe size={560} />
<div style={{ flex: '6 6 0', minWidth: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px 16px' }}>
<Globe size={globeSize} />
</div>
</div>

View File

@ -46,7 +46,6 @@ export default function RadarTechSection({ only }: { only?: 'radar' | 'approach'
const t = T[lang]
const showRadar = only !== 'approach'
const showApproach = only !== 'radar'
return (
<section dir={lang === 'fa' ? 'rtl' : 'ltr'} style={{ background: 'var(--paper-2)' }}>
<div className="max-w-7xl mx-auto px-12 max-md:px-5">

View File

@ -295,7 +295,7 @@ export default function ReportsCarouselSection() {
</div>
<Link
to="/reports"
to="/archive"
style={{
fontSize: 11,
fontWeight: 700,

View File

@ -7,7 +7,7 @@ type RiskItem = { quote: string; quoteEn?: string; name: string; nameEn?: string
const PANEL_API =
(import.meta as ImportMeta & { env?: Record<string, string> }).env
?.VITE_PANEL_API || 'http://localhost:3001'
?.VITE_PANEL_API || ''
/* ─── Risk data (fallback when panel is offline) ─────────── */
const fallbackRiskItems: RiskItem[] = [

View File

@ -36,7 +36,7 @@ const FEATURES: Feat[] = [
},
},
{
img: '/hero%202.webp', imgSide: 'left', imgPos: 'center center',
img: '/hero2.webp', imgSide: 'left', imgPos: '100% center',
fa: {
kicker: 'فناوری صنعتی',
title: 'داده، تحلیل، آینده‌نگاری',
@ -53,7 +53,7 @@ const FEATURES: Feat[] = [
},
},
{
img: '/hero%203.webp', imgSide: 'right', imgPos: 'center center',
img: '/hero3.webp', imgSide: 'right', imgPos: '100% center',
fa: {
kicker: 'ژئواکونومی',
title: 'آزمون استراتژی در آینده‌ها',

View File

@ -272,7 +272,7 @@ export default function VideocastPodcastSection({ only }: { only?: 'video' | 'po
alignItems: 'center',
gap: 16,
flexDirection: lang === 'fa' ? 'row-reverse' : 'row',
width: '100%'
minWidth: 0,
}}>
{/* Play button */}
<button
@ -298,11 +298,11 @@ export default function VideocastPodcastSection({ only }: { only?: 'video' | 'po
{/* Waveform container */}
<div style={{
flex: 1,
minWidth: 0,
display: 'flex',
alignItems: 'center',
gap: 10,
direction: 'ltr',
width: '100%'
}}>
<span style={{ fontSize: 11, color: 'rgba(3, 35, 64, 0.6)', fontWeight: 700, flexShrink: 0 }}>۰۲:۳۱</span>
<div style={{

View File

@ -0,0 +1,339 @@
import { useState } from 'react'
import { useLang } from '@/context/LangContext'
import { Clock, Calendar, BookOpen, PlayCircle, ArrowLeft, ArrowRight } from 'lucide-react'
import { podcasts as PODCASTS, videos as VIDEOS } from '@/data/media'
const NAVY = '#032340'
const CARD = '#0a2540'
const GOLD = '#CD9E53'
const T = {
fa: {
otherEps: 'سایر اپیزودها',
archive: 'آرشیو اپیزودها',
listen: 'بشنوید',
videocast: 'ویدیوکست اندیشکده',
allVids: 'همه ویدیوها',
otherVids: 'سایر ویدیوها',
watch: 'ببینید',
},
en: {
otherEps: 'Other Episodes',
archive: 'Episode Archive',
listen: 'Listen',
videocast: 'Institute Videocast',
allVids: 'All Videos',
otherVids: 'Other Videos',
watch: 'Watch',
},
}
/* ─── episode card (vertical, centered) ─── */
function EpRow({ pod, active, onClick, lang }: {
pod: typeof PODCASTS[0]; active: boolean; onClick: () => void; lang: 'fa' | 'en'
}) {
return (
<button onClick={onClick} style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8,
width: '100%', background: 'none', border: 'none',
borderBottom: '1px solid rgba(255,255,255,0.06)',
padding: '12px 4px', cursor: 'pointer', fontFamily: 'inherit',
transition: 'opacity .15s',
}}>
{/* thumbnail */}
<div style={{
width: '100%', aspectRatio: '1/1', borderRadius: 12, overflow: 'hidden',
border: active ? `2px solid ${GOLD}` : '2px solid rgba(255,255,255,0.12)',
transition: 'border-color .2s', flexShrink: 0,
}}>
<img src={pod.cover} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
</div>
{/* title */}
<div style={{
fontSize: 12, fontWeight: 800,
color: active ? GOLD : '#fff',
lineHeight: 1.4, textAlign: 'center',
display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
transition: 'color .2s', width: '100%',
direction: lang === 'fa' ? 'rtl' : 'ltr',
}}>{pod[lang].title}</div>
{/* date */}
<span style={{ fontSize: 10, color: 'rgba(255,255,255,0.3)', fontWeight: 600, direction: 'ltr' }}>
{pod.date}
</span>
</button>
)
}
/* ─── white book cover ─── */
function BookCard({ pod, lang }: { pod: typeof PODCASTS[0]; lang: 'fa' | 'en' }) {
const epNum = pod.epNum.replace(/[^0-9۰-۹]/g, '')
return (
<div style={{
background: '#f4efe6', borderRadius: 20, padding: '18px 16px 16px',
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10,
width: 200, flexShrink: 0,
boxShadow: '0 20px 48px -16px rgba(0,0,0,0.5)',
}}>
<div style={{ fontSize: 18, fontWeight: 900, color: GOLD, lineHeight: 1 }}>{epNum}</div>
<div style={{ width: '100%', aspectRatio: '1/1', borderRadius: 10, overflow: 'hidden', boxShadow: '0 4px 14px -4px rgba(0,0,0,0.3)' }}>
<img src={pod.cover} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
</div>
<div style={{ textAlign: 'center', width: '100%' }}>
<div style={{ fontSize: 12.5, fontWeight: 900, color: NAVY, lineHeight: 1.4, marginBottom: 4 }}>{pod[lang].title}</div>
<div style={{ fontSize: 10.5, color: 'rgba(3,35,64,0.5)', fontWeight: 600 }}>{pod[lang].guest}</div>
</div>
<div style={{ width: 30, height: 30, borderRadius: 8, background: 'rgba(3,35,64,0.08)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<BookOpen size={15} color={NAVY} strokeWidth={1.8} />
</div>
</div>
)
}
/* ─── video list row ─── */
function VidRow({ vid, lang }: { vid: typeof VIDEOS[0]; lang: 'fa' | 'en' }) {
const isFa = lang === 'fa'
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '10px 0', borderTop: '1px solid rgba(3,35,64,0.07)',
flexDirection: isFa ? 'row-reverse' : 'row', cursor: 'pointer',
}}>
<div style={{ flexShrink: 0, width: 82, height: 54, borderRadius: 9, overflow: 'hidden', position: 'relative' }}>
<img src={vid.thumb} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
<div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ width: 0, height: 0, borderTop: '6px solid transparent', borderBottom: '6px solid transparent', borderLeft: '10px solid rgba(255,255,255,0.9)', marginLeft: 2 }} />
</div>
</div>
<div style={{ flex: 1, minWidth: 0, textAlign: isFa ? 'right' : 'left' }}>
<div style={{
fontSize: 12.5, fontWeight: 800, color: NAVY, lineHeight: 1.5, marginBottom: 3,
display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
}}>{vid[lang].title}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, direction: 'ltr', justifyContent: isFa ? 'flex-end' : 'flex-start' }}>
<span style={{ fontSize: 10.5, color: 'rgba(3,35,64,0.4)', fontWeight: 600 }}>{vid.date}</span>
<span style={{ fontSize: 10.5, color: GOLD, fontWeight: 700 }}>{vid.duration}</span>
</div>
</div>
</div>
)
}
/* ══ MAIN ══ */
export default function Podcast() {
const { lang } = useLang()
const t = T[lang]
const isFa = lang === 'fa'
const [active, setActive] = useState(0)
const pod = PODCASTS[active]
const epNum = pod.epNum.replace(/[^0-9۰-۹]/g, '')
return (
<div dir={isFa ? 'rtl' : 'ltr'} style={{ background: 'var(--paper)', minHeight: '100vh', color: NAVY, paddingBottom: 72 }}>
<div className="max-w-7xl mx-auto px-8 max-md:px-4" style={{ paddingTop: 40 }}>
{/* ══ PODCAST HERO CARD (dark) ══ */}
<div style={{
background: CARD,
borderRadius: 24,
padding: 24,
marginBottom: 24,
display: 'grid',
gridTemplateColumns: '220px 1fr 220px',
gap: 0,
alignItems: 'stretch',
position: 'relative',
overflow: 'hidden',
minHeight: 460,
}} className="max-lg:!grid-cols-1 max-lg:!gap-6 max-lg:!min-h-0">
{/* dot pattern */}
<div aria-hidden style={{
position: 'absolute',
top: 0, left: isFa ? 'auto' : 0, right: isFa ? 0 : 'auto',
width: 160, height: 160, opacity: 0.06, pointerEvents: 'none',
backgroundImage: 'radial-gradient(circle, #fff 1.5px, transparent 1.5px)',
backgroundSize: '16px 16px',
}} />
{/* COL 1 — episode list */}
<div style={{
order: isFa ? 3 : 1,
borderRight: isFa ? 'none' : '1px solid rgba(255,255,255,0.07)',
borderLeft: isFa ? '1px solid rgba(255,255,255,0.07)' : 'none',
paddingRight: isFa ? 0 : 20,
paddingLeft: isFa ? 20 : 0,
display: 'flex', flexDirection: 'column',
}}>
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
marginBottom: 12, direction: isFa ? 'rtl' : 'ltr',
}}>
<span style={{ fontSize: 13, fontWeight: 900, color: '#fff' }}>{t.otherEps}</span>
<button style={{
display: 'inline-flex', alignItems: 'center', gap: 4, direction: 'ltr',
background: 'rgba(255,255,255,0.07)', border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 20, padding: '3px 10px', cursor: 'pointer',
color: 'rgba(255,255,255,0.5)', fontSize: 10.5, fontWeight: 700, fontFamily: 'inherit',
}}>
{isFa ? <ArrowRight size={10} /> : <ArrowLeft size={10} />}
{t.archive}
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
{PODCASTS.map((p, i) => (
<EpRow key={p.id} pod={p} active={active === i} onClick={() => setActive(i)} lang={lang} />
))}
</div>
</div>
{/* COL 2 — center */}
<div style={{
order: 2,
display: 'flex', flexDirection: 'column', alignItems: 'flex-start',
justifyContent: 'center', gap: 16, padding: '0 28px',
textAlign: isFa ? 'right' : 'left',
direction: isFa ? 'rtl' : 'ltr',
}} className="max-lg:!px-0">
{/* meta */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12, fontSize: 11.5, color: 'rgba(255,255,255,0.38)', fontWeight: 600, direction: 'ltr' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
<Calendar size={11} strokeWidth={2} />{pod.date}
</span>
<span style={{ color: 'rgba(255,255,255,0.15)' }}></span>
<span style={{ color: GOLD, fontWeight: 900 }}>#{epNum}</span>
</div>
{/* title */}
<h1 style={{
fontSize: 'clamp(16px,1.6vw,22px)', fontWeight: 900, color: '#fff',
lineHeight: 1.4, letterSpacing: '-0.2px', margin: 0,
}}>{pod[lang].title}</h1>
{/* guest */}
<div style={{ fontSize: 12, color: GOLD, fontWeight: 700 }}>{pod[lang].guest}</div>
{/* body excerpt */}
<p style={{
fontSize: 12.5, lineHeight: 1.9, color: 'rgba(255,255,255,0.5)',
margin: 0, textAlign: 'justify',
display: '-webkit-box', WebkitLineClamp: 4, WebkitBoxOrient: 'vertical', overflow: 'hidden',
}}>{pod[lang].body}</p>
{/* listen + duration */}
<div style={{ display: 'flex', alignItems: 'center', gap: 14, direction: 'ltr', marginTop: 4 }}>
<button style={{
display: 'inline-flex', alignItems: 'center', gap: 7,
background: GOLD, border: 'none', borderRadius: 28,
padding: '10px 24px', cursor: 'pointer', color: NAVY,
fontSize: 13, fontWeight: 900, fontFamily: 'inherit',
boxShadow: `0 8px 24px -8px ${GOLD}88`,
transition: 'transform .18s',
}}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.transform = 'scale(1.05)' }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = 'scale(1)' }}
>
{isFa ? <ArrowRight size={13} /> : <ArrowLeft size={13} />}
{t.listen}
</button>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 12, color: 'rgba(255,255,255,0.4)', fontWeight: 700 }}>
<Clock size={13} color={GOLD} strokeWidth={2} />{pod.duration}
</span>
</div>
</div>
{/* COL 3 — book cover */}
<div style={{
order: isFa ? 1 : 3,
borderLeft: isFa ? 'none' : '1px solid rgba(255,255,255,0.07)',
borderRight: isFa ? '1px solid rgba(255,255,255,0.07)' : 'none',
paddingLeft: isFa ? 0 : 20,
paddingRight: isFa ? 20 : 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<BookCard pod={pod} lang={lang} />
</div>
</div>
{/* ══ VIDEOCAST CARD (light) ══ */}
<div style={{
background: '#fff',
border: '1px solid rgba(3,35,64,0.08)',
borderRadius: 24,
padding: 24,
boxShadow: '0 4px 24px -8px rgba(3,35,64,0.08)',
}}>
{/* header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20, direction: isFa ? 'rtl' : 'ltr' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexDirection: isFa ? 'row-reverse' : 'row' }}>
<div style={{ width: 34, height: 34, borderRadius: 10, background: 'rgba(3,35,64,0.06)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<PlayCircle size={17} color={GOLD} strokeWidth={1.8} />
</div>
<span style={{ fontSize: 15, fontWeight: 900, color: NAVY }}>{t.videocast}</span>
</div>
<button style={{
display: 'inline-flex', alignItems: 'center', gap: 6, direction: 'ltr',
background: 'none', border: 'none', cursor: 'pointer', color: GOLD,
fontSize: 12, fontWeight: 700, fontFamily: 'inherit',
}}>
{isFa ? <ArrowRight size={12} /> : <ArrowLeft size={12} />}{t.allVids}
</button>
</div>
{/* grid: featured (left) + list (right) */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24 }} className="max-lg:!grid-cols-1">
{/* featured video */}
<div style={{ cursor: 'pointer' }}>
<div style={{ position: 'relative', borderRadius: 14, overflow: 'hidden', aspectRatio: '16/9', marginBottom: 14 }}>
<img src={VIDEOS[0].thumb} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,35,64,0.55) 0%, transparent 55%)' }} />
<button aria-label="play" style={{
position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)',
width: 52, height: 52, borderRadius: '50%',
background: `${GOLD}ee`, border: 'none', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: `0 8px 24px -8px ${GOLD}88`,
}}>
<div style={{ width: 0, height: 0, borderTop: '10px solid transparent', borderBottom: '10px solid transparent', borderLeft: `17px solid ${NAVY}`, marginLeft: 3 }} />
</button>
<div style={{ position: 'absolute', bottom: 10, left: isFa ? 'auto' : 12, right: isFa ? 12 : 'auto' }}>
<span style={{ fontSize: 10, fontWeight: 800, color: GOLD, background: 'rgba(3,35,64,0.75)', padding: '3px 8px', borderRadius: 6 }}>{VIDEOS[0].ep}</span>
</div>
</div>
<div style={{ textAlign: isFa ? 'right' : 'left' }}>
<div style={{ fontSize: 15, fontWeight: 900, color: NAVY, lineHeight: 1.5, marginBottom: 8 }}>{VIDEOS[0][lang].title}</div>
<div style={{ fontSize: 12, color: 'rgba(3,35,64,0.5)', marginBottom: 12 }}>{VIDEOS[0][lang].guest}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, direction: 'ltr', justifyContent: isFa ? 'flex-end' : 'flex-start' }}>
<button style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
background: GOLD, border: 'none', borderRadius: 20,
padding: '8px 20px', cursor: 'pointer', color: NAVY,
fontSize: 12, fontWeight: 900, fontFamily: 'inherit',
}}>
{isFa ? <ArrowRight size={12} /> : <ArrowLeft size={12} />}{t.watch}
</button>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 11, color: 'rgba(3,35,64,0.4)', fontWeight: 600 }}>
<Clock size={11} color={GOLD} strokeWidth={2} />{VIDEOS[0].duration}
</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 11, color: 'rgba(3,35,64,0.4)', fontWeight: 600 }}>
<Calendar size={11} strokeWidth={2} />{VIDEOS[0].date}
</span>
</div>
</div>
</div>
{/* video list */}
<div>
<div style={{ fontSize: 11, fontWeight: 800, color: 'rgba(3,35,64,0.3)', marginBottom: 2, textAlign: isFa ? 'right' : 'left' }}>
{t.otherVids}
</div>
{VIDEOS.slice(1).map(v => <VidRow key={v.id} vid={v} lang={lang} />)}
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,525 @@
import React, { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuth, authFetch } from '@/context/AuthContext'
import { useLang } from '@/context/LangContext'
const GOLD = '#CD9E53'
const NAVY = '#032340'
type Tab = 'info' | 'password' | 'content'
interface Purchase {
id: number
article_id: string
title: string
category: string
purchased_at: string
file_url?: string
}
export default function ProfilePage() {
const { lang } = useLang()
const { member, token, logout } = useAuth()
const navigate = useNavigate()
const [tab, setTab] = useState<Tab>('info')
useEffect(() => {
if (!token) navigate('/login')
}, [token, navigate])
if (!member || !token) return null
const initials = member.name.split(' ').map(w => w[0]).join('').slice(0, 2)
const tabs: { key: Tab; label: string }[] = [
{ key: 'info', label: lang === 'fa' ? 'اطلاعات من' : 'My Info' },
{ key: 'password', label: lang === 'fa' ? 'تغییر رمز' : 'Password' },
{ key: 'content', label: lang === 'fa' ? 'مطالب من' : 'My Content' },
]
return (
<div style={{ minHeight: '100vh', background: '#f4f5f7', direction: 'rtl' }}>
{/* ── Hero Banner ── */}
<div style={{
background: `linear-gradient(135deg, ${NAVY} 0%, #043560 60%, #021828 100%)`,
paddingTop: 80, paddingBottom: 0, position: 'relative', overflow: 'hidden',
}}>
{/* grid texture */}
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: 0.04 }} preserveAspectRatio="xMidYMid slice">
<defs><pattern id="pg" width="44" height="44" patternUnits="userSpaceOnUse">
<path d="M 44 0 L 0 0 0 44" fill="none" stroke={GOLD} strokeWidth="1"/>
</pattern></defs>
<rect width="100%" height="100%" fill="url(#pg)" />
</svg>
{/* decorative ring */}
<svg style={{ position: 'absolute', left: -80, top: -80, opacity: 0.06, pointerEvents: 'none' }} width="360" height="360" viewBox="0 0 360 360">
{[1,2,3].map(i => <circle key={i} cx="180" cy="180" r={60 + i * 50} fill="none" stroke={GOLD} strokeWidth="1"/>)}
</svg>
<div style={{ maxWidth: 860, margin: '0 auto', padding: '0 1.5rem' }}>
{/* profile card - overlaps the banner */}
<div style={{
background: '#fff',
borderRadius: '20px 20px 0 0',
padding: '2rem 2rem 0',
boxShadow: '0 -4px 40px rgba(0,0,0,0.12)',
position: 'relative',
}}>
{/* avatar + info row */}
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '1.5rem', paddingBottom: '1.5rem', borderBottom: '1px solid #f0f0f0' }}>
{/* avatar */}
<div style={{ position: 'relative', flexShrink: 0, marginTop: -44 }}>
<div style={{
width: 88, height: 88, borderRadius: '50%',
background: `linear-gradient(135deg, ${GOLD} 0%, #e8b96a 100%)`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: '1.9rem', fontWeight: 800, color: NAVY,
border: '4px solid #fff', boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
letterSpacing: -1,
}}>
{initials}
</div>
<div style={{
position: 'absolute', bottom: 4, right: 4,
width: 16, height: 16, borderRadius: '50%',
background: '#22c55e', border: '2.5px solid #fff',
}}/>
</div>
{/* name / email */}
<div style={{ flex: 1, paddingBottom: '0.25rem' }}>
<h1 style={{ margin: '0 0 0.25rem', color: NAVY, fontSize: '1.35rem', fontWeight: 800 }}>{member.name}</h1>
<p style={{ margin: 0, color: '#888', fontSize: '0.875rem', direction: 'ltr', textAlign: 'right' }}>{member.email}</p>
</div>
{/* logout */}
<button
onClick={() => { logout(); navigate('/') }}
style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '0.5rem 1rem', borderRadius: 8,
border: '1.5px solid #e5e5e5', background: '#fff',
color: '#888', fontSize: '0.82rem', fontWeight: 600,
cursor: 'pointer', transition: 'all 0.18s', flexShrink: 0,
marginBottom: '0.25rem',
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#c0392b'; e.currentTarget.style.color = '#c0392b' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = '#e5e5e5'; e.currentTarget.style.color = '#888' }}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>
</svg>
{lang === 'fa' ? 'خروج' : 'Logout'}
</button>
</div>
{/* tabs */}
<div style={{ display: 'flex', gap: 0, marginTop: 0 }}>
{tabs.map(t => (
<button key={t.key} onClick={() => setTab(t.key)} style={{
padding: '1rem 1.5rem', background: 'none', border: 'none',
borderBottom: tab === t.key ? `2.5px solid ${GOLD}` : '2.5px solid transparent',
color: tab === t.key ? NAVY : '#aaa',
fontWeight: tab === t.key ? 700 : 500,
fontSize: '0.9rem', cursor: 'pointer', transition: 'all 0.18s',
whiteSpace: 'nowrap', fontFamily: 'inherit',
}}>
{t.label}
</button>
))}
</div>
</div>
</div>
</div>
{/* ── Tab Content ── */}
<div style={{ maxWidth: 860, margin: '0 auto', padding: '1.5rem 1.5rem 4rem' }}>
{tab === 'info' && <InfoTab member={member} token={token} lang={lang} />}
{tab === 'password' && <PasswordTab token={token} lang={lang} />}
{tab === 'content' && <ContentTab token={token} lang={lang} />}
</div>
</div>
)
}
/*
Info Tab
*/
function InfoTab({ member, token, lang, }: {
member: { name: string; phone?: string; email: string }
token: string
lang: 'fa' | 'en'
}) {
const { updateMember } = useAuth()
const [name, setName] = useState(member.name)
const [phone, setPhone] = useState(member.phone ?? '')
const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
async function save(e: React.FormEvent) {
e.preventDefault()
setStatus('saving')
try {
const res = await authFetch(token, '/api/members/me', { method: 'PUT', body: JSON.stringify({ name, phone }) })
if (!res.ok) throw new Error()
updateMember({ name, phone })
setStatus('saved')
setTimeout(() => setStatus('idle'), 2500)
} catch { setStatus('error') }
}
return (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.25rem', alignItems: 'start' }}
className="max-md:grid-cols-1">
{/* form card */}
<div style={cardStyle}>
<SectionTitle icon={
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={GOLD} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
} title={lang === 'fa' ? 'اطلاعات شخصی' : 'Personal Info'} />
<form onSubmit={save} style={{ display: 'flex', flexDirection: 'column', gap: '1.1rem', marginTop: '1.25rem' }}>
<FormField label={lang === 'fa' ? 'نام و نام خانوادگی' : 'Full Name'}>
<FancyInput value={name} onChange={e => setName(e.target.value)} required />
</FormField>
<FormField label={lang === 'fa' ? 'شماره تماس' : 'Phone'}>
<FancyInput value={phone} onChange={e => setPhone(e.target.value)} dir="ltr" placeholder="+98..." />
</FormField>
<FormField label={lang === 'fa' ? 'ایمیل' : 'Email'}>
<FancyInput value={member.email} disabled dir="ltr"
style={{ background: '#f7f7f7', color: '#aaa', cursor: 'not-allowed' }} />
</FormField>
{status === 'error' && <ErrorMsg>{lang === 'fa' ? 'خطا در ذخیره' : 'Save failed'}</ErrorMsg>}
<button type="submit" disabled={status === 'saving'} style={submitBtn(status === 'saving')}>
{status === 'saved' ? (lang === 'fa' ? 'ذخیره شد ✓' : 'Saved ✓')
: status === 'saving' ? '...'
: (lang === 'fa' ? 'ذخیره تغییرات' : 'Save Changes')}
</button>
</form>
</div>
{/* account info card */}
<div style={cardStyle}>
<SectionTitle icon={
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={GOLD} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
} title={lang === 'fa' ? 'وضعیت حساب' : 'Account Status'} />
<div style={{ marginTop: '1.25rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{[
{ label: lang === 'fa' ? 'نوع حساب' : 'Account Type', value: lang === 'fa' ? 'عضو عادی' : 'Member' },
{ label: lang === 'fa' ? 'وضعیت' : 'Status',
value: <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, color: '#22c55e', fontWeight: 700, fontSize: '0.85rem' }}>
<span style={{ width: 7, height: 7, borderRadius: '50%', background: '#22c55e', display: 'inline-block' }}/>
{lang === 'fa' ? 'فعال' : 'Active'}
</span>
},
].map(({ label, value }) => (
<div key={label} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0.85rem 1rem', background: '#f9f9f9', borderRadius: 10 }}>
<span style={{ color: '#888', fontSize: '0.85rem' }}>{label}</span>
<span style={{ color: NAVY, fontWeight: 600, fontSize: '0.875rem' }}>{value}</span>
</div>
))}
</div>
</div>
</div>
)
}
/*
Password Tab
*/
function PasswordTab({ token, lang }: { token: string; lang: 'fa' | 'en' }) {
const [current, setCurrent] = useState('')
const [next, setNext] = useState('')
const [confirm, setConfirm] = useState('')
const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
const [errMsg, setErrMsg] = useState('')
async function save(e: React.FormEvent) {
e.preventDefault()
if (next !== confirm) { setErrMsg(lang === 'fa' ? 'رمزها یکسان نیستند' : 'Passwords do not match'); return }
setStatus('saving'); setErrMsg('')
try {
const res = await authFetch(token, '/api/members/me/password', { method: 'PUT', body: JSON.stringify({ currentPassword: current, newPassword: next }) })
const data = await res.json()
if (!res.ok) throw new Error(data.error)
setStatus('saved'); setCurrent(''); setNext(''); setConfirm('')
setTimeout(() => setStatus('idle'), 2500)
} catch (err: unknown) {
setErrMsg(err instanceof Error ? err.message : lang === 'fa' ? 'خطا' : 'Error')
setStatus('error')
}
}
return (
<div style={{ maxWidth: 480 }}>
<div style={cardStyle}>
<SectionTitle icon={
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={GOLD} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
} title={lang === 'fa' ? 'تغییر رمز عبور' : 'Change Password'} />
<form onSubmit={save} style={{ display: 'flex', flexDirection: 'column', gap: '1.1rem', marginTop: '1.25rem' }}>
<FormField label={lang === 'fa' ? 'رمز فعلی' : 'Current Password'}>
<FancyInput type="password" value={current} onChange={e => setCurrent(e.target.value)} required dir="ltr" />
</FormField>
<FormField label={lang === 'fa' ? 'رمز جدید' : 'New Password'}>
<FancyInput type="password" value={next} onChange={e => setNext(e.target.value)} required minLength={6} dir="ltr" />
</FormField>
<FormField label={lang === 'fa' ? 'تکرار رمز جدید' : 'Confirm New Password'}>
<FancyInput type="password" value={confirm} onChange={e => setConfirm(e.target.value)} required dir="ltr" />
</FormField>
{errMsg && <ErrorMsg>{errMsg}</ErrorMsg>}
<button type="submit" disabled={status === 'saving'} style={submitBtn(status === 'saving')}>
{status === 'saved' ? (lang === 'fa' ? 'رمز تغییر یافت ✓' : 'Changed ✓')
: status === 'saving' ? '...'
: (lang === 'fa' ? 'تغییر رمز' : 'Change Password')}
</button>
</form>
</div>
{/* tip */}
<div style={{ marginTop: '1rem', padding: '1rem 1.25rem', background: `${GOLD}11`, border: `1px solid ${GOLD}33`, borderRadius: 10, display: 'flex', gap: 10, alignItems: 'flex-start' }}>
<svg style={{ flexShrink: 0, marginTop: 1 }} width="15" height="15" viewBox="0 0 24 24" fill="none" stroke={GOLD} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<p style={{ margin: 0, color: '#7a6535', fontSize: '0.8rem', lineHeight: 1.7 }}>
{lang === 'fa' ? 'رمز قوی شامل حروف بزرگ، اعداد و نمادها باشد. حداقل ۶ کاراکتر.' : 'Use uppercase letters, numbers and symbols for a strong password.'}
</p>
</div>
</div>
)
}
/*
Content Tab
*/
function ContentTab({ token, lang }: { token: string; lang: 'fa' | 'en' }) {
const [items, setItems] = useState<Purchase[]>([])
const [loading, setLoading] = useState(true)
const [err, setErr] = useState('')
useEffect(() => {
authFetch(token, '/api/members/me/purchases')
.then(r => r.json())
.then(data => { setItems(Array.isArray(data) ? data : []); setLoading(false) })
.catch(() => { setErr(lang === 'fa' ? 'خطا در بارگذاری' : 'Load error'); setLoading(false) })
}, [token, lang])
const BASE = import.meta.env.VITE_PANEL_API ?? ''
const categoryColors: Record<string, string> = {
'گزارش': '#3b82f6', 'تحلیل': '#8b5cf6', 'رادار': '#10b981',
'بازار': '#f59e0b', 'ویژه': '#ef4444',
}
return (
<div>
{/* stats bar */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', marginBottom: '1.5rem' }}>
{[
{ label: lang === 'fa' ? 'مطالب خریداری‌شده' : 'Purchased', value: loading ? '—' : items.length.toString(), icon: '📄' },
{ label: lang === 'fa' ? 'قابل دانلود' : 'Downloads', value: loading ? '—' : items.filter(i => i.file_url).length.toString(), icon: '⬇️' },
{ label: lang === 'fa' ? 'دسته‌بندی' : 'Categories', value: loading ? '—' : new Set(items.map(i => i.category).filter(Boolean)).size.toString(), icon: '🗂️' },
].map(stat => (
<div key={stat.label} style={{ ...cardStyle, padding: '1.25rem', textAlign: 'center' }}>
<div style={{ fontSize: '1.5rem', marginBottom: '0.5rem' }}>{stat.icon}</div>
<div style={{ color: NAVY, fontSize: '1.6rem', fontWeight: 800 }}>{stat.value}</div>
<div style={{ color: '#999', fontSize: '0.75rem', marginTop: 2 }}>{stat.label}</div>
</div>
))}
</div>
<div style={cardStyle}>
<SectionTitle icon={
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={GOLD} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
</svg>
} title={lang === 'fa' ? 'مطالب من' : 'My Content'} />
<div style={{ marginTop: '1.25rem' }}>
{loading && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{[1, 2, 3].map(i => (
<div key={i} style={{ height: 72, borderRadius: 12, background: 'linear-gradient(90deg, #f0f0f0 25%, #f8f8f8 50%, #f0f0f0 75%)', backgroundSize: '200% 100%', animation: 'shimmer 1.4s infinite' }}/>
))}
</div>
)}
{!loading && err && <ErrorMsg>{err}</ErrorMsg>}
{!loading && !err && items.length === 0 && (
<div style={{ textAlign: 'center', padding: '3.5rem 1rem', color: '#ccc' }}>
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.2" style={{ marginBottom: '1rem' }}>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>
</svg>
<p style={{ margin: '0 0 0.5rem', fontSize: '1rem', color: '#aaa', fontWeight: 600 }}>
{lang === 'fa' ? 'هنوز مطلبی خریداری نکرده‌اید' : 'No content purchased yet'}
</p>
<p style={{ margin: 0, fontSize: '0.82rem', color: '#ccc' }}>
{lang === 'fa' ? 'گزارش‌ها و تحلیل‌های اختصاصی را از بخش گزارش‌ها تهیه کنید' : 'Purchase reports and analyses from the reports section'}
</p>
</div>
)}
{!loading && items.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{items.map(item => {
const cat = item.category
const catColor = categoryColors[cat] ?? '#6b7280'
const dateStr = new Date(item.purchased_at).toLocaleDateString(lang === 'fa' ? 'fa-IR' : 'en-US', { year: 'numeric', month: 'long', day: 'numeric' })
return (
<div key={item.id} style={{
display: 'flex', alignItems: 'center', gap: '1rem',
padding: '1rem 1.25rem', borderRadius: 12,
border: '1.5px solid #f0f0f0', background: '#fafafa',
transition: 'all 0.18s',
}}
onMouseEnter={e => { e.currentTarget.style.background = '#fff'; e.currentTarget.style.borderColor = `${GOLD}55`; e.currentTarget.style.boxShadow = `0 2px 12px ${GOLD}15` }}
onMouseLeave={e => { e.currentTarget.style.background = '#fafafa'; e.currentTarget.style.borderColor = '#f0f0f0'; e.currentTarget.style.boxShadow = 'none' }}
>
{/* icon */}
<div style={{
width: 44, height: 44, borderRadius: 10, flexShrink: 0,
background: `${catColor}15`, border: `1.5px solid ${catColor}30`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={catColor} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
</svg>
</div>
{/* text */}
<div style={{ flex: 1, minWidth: 0 }}>
<p style={{ margin: '0 0 0.3rem', fontWeight: 700, color: NAVY, fontSize: '0.92rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{item.title || item.article_id}
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem', flexWrap: 'wrap' }}>
{cat && (
<span style={{ background: `${catColor}18`, color: catColor, borderRadius: 20, padding: '0.15rem 0.6rem', fontSize: '0.72rem', fontWeight: 700 }}>
{cat}
</span>
)}
<span style={{ color: '#bbb', fontSize: '0.75rem' }}>{dateStr}</span>
</div>
</div>
{/* download */}
{item.file_url && (
<a
href={`${BASE}/api/members/me/purchases/${item.id}/download?token=${token}`}
target="_blank" rel="noreferrer"
style={{
display: 'flex', alignItems: 'center', gap: 5,
padding: '0.5rem 1rem', borderRadius: 8, flexShrink: 0,
background: NAVY, color: '#fff', textDecoration: 'none',
fontSize: '0.78rem', fontWeight: 700, transition: 'all 0.18s',
}}
onMouseEnter={e => { e.currentTarget.style.background = GOLD; e.currentTarget.style.color = NAVY }}
onMouseLeave={e => { e.currentTarget.style.background = NAVY; e.currentTarget.style.color = '#fff' }}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
</svg>
{lang === 'fa' ? 'دانلود' : 'Download'}
</a>
)}
</div>
)
})}
</div>
)}
</div>
</div>
<style>{`
@keyframes shimmer {
0% { background-position: 200% 0 }
100% { background-position: -200% 0 }
}
`}</style>
</div>
)
}
/*
Shared primitives
*/
const cardStyle: React.CSSProperties = {
background: '#fff', borderRadius: 16,
padding: '1.5rem', border: '1px solid #ebebeb',
boxShadow: '0 1px 6px rgba(0,0,0,0.04)',
}
function SectionTitle({ icon, title }: { icon: React.ReactNode; title: string }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: 30, height: 30, borderRadius: 8, background: `${GOLD}18`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{icon}
</div>
<h3 style={{ margin: 0, color: NAVY, fontSize: '0.95rem', fontWeight: 700 }}>{title}</h3>
</div>
)
}
function FormField({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<label style={{ display: 'block', color: '#555', fontSize: '0.8rem', fontWeight: 600, marginBottom: 6 }}>{label}</label>
{children}
</div>
)
}
function FancyInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
const [focused, setFocused] = useState(false)
return (
<input
{...props}
onFocus={e => { setFocused(true); props.onFocus?.(e) }}
onBlur={e => { setFocused(false); props.onBlur?.(e) }}
style={{
width: '100%', padding: '0.78rem 1rem', borderRadius: 10,
border: `1.5px solid ${focused ? GOLD : '#e5e5e5'}`,
background: props.disabled ? '#f7f7f7' : '#fff',
color: '#1a1a1a', fontSize: '0.9rem', outline: 'none',
boxSizing: 'border-box', transition: 'border-color 0.18s',
fontFamily: 'inherit',
...props.style,
}}
/>
)
}
function ErrorMsg({ children }: { children: React.ReactNode }) {
return (
<div style={{ background: '#fff0f0', border: '1px solid #ffd0d0', borderRadius: 8, padding: '0.65rem 1rem', color: '#c0392b', fontSize: '0.83rem' }}>
{children}
</div>
)
}
function submitBtn(disabled: boolean): React.CSSProperties {
return {
padding: '0.8rem 2rem', borderRadius: 10, border: 'none',
background: disabled ? '#e5e5e5' : GOLD,
color: disabled ? '#aaa' : NAVY,
fontWeight: 700, fontSize: '0.9rem',
cursor: disabled ? 'not-allowed' : 'pointer',
alignSelf: 'flex-start', transition: 'all 0.18s',
boxShadow: disabled ? 'none' : `0 4px 16px ${GOLD}44`,
fontFamily: 'inherit',
}
}

View File

@ -275,7 +275,7 @@ function MembershipForm({ lang }: { lang: 'fa' | 'en' }) {
async function submit() {
if (!name || !phone) return
setStatus('sending')
const PANEL_API = import.meta.env.VITE_PANEL_API || 'http://localhost:3001'
const PANEL_API = import.meta.env.VITE_PANEL_API || ''
try {
const res = await fetch(`${PANEL_API}/api/contact`, {
method: 'POST',

View File

@ -1,6 +1,7 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { motion, AnimatePresence } from 'framer-motion'
import { BarChart3, Cpu, Leaf, Globe, Zap, ChevronLeft, ChevronRight } from 'lucide-react'
import { BarChart3, Cpu, Leaf, Globe, Zap, ChevronLeft, ChevronRight, X, LogIn, UserPlus, AlertTriangle, Calendar, User } from 'lucide-react'
import { useLang } from '@/context/LangContext'
import { RADAR_ITEMS, type RadarItem, type RadarCategory } from '@/content/radar'
@ -16,14 +17,260 @@ const CATEGORY_ICON: Record<RadarCategory, typeof BarChart3> = {
energy: Zap,
}
const CATEGORY_LABEL: Record<RadarCategory, { fa: string; en: string }> = {
market: { fa: 'بازار', en: 'Market' },
tech: { fa: 'فناوری', en: 'Technology' },
commodity: { fa: 'کامودیتی', en: 'Commodity' },
geo: { fa: 'ژئوپلیتیک', en: 'Geopolitics' },
energy: { fa: 'انرژی', en: 'Energy' },
}
const T = {
fa: { overline: 'FORESIGHT & INTELLIGENCE', heading: 'رادار آینده', sub: 'رصد سیگنال‌های راهبردی و روندهای نوظهور صنعت فولاد جهان', readMore: 'ادامه مطلب' },
en: { overline: 'FORESIGHT & INTELLIGENCE', heading: 'Future Radar', sub: 'Monitoring strategic signals and emerging trends in the global steel industry', readMore: 'Read more' },
fa: {
overline: 'FORESIGHT & INTELLIGENCE', heading: 'رادار آینده',
sub: 'رصد سیگنال‌های راهبردی و روندهای نوظهور صنعت فولاد جهان',
readMore: 'ادامه مطلب',
analysis: 'تحلیل راهبردی',
disclaimer: 'این تحلیل توسط هوش مصنوعی تولید شده و جنبه راهنمایی دارد.',
blurTitle: 'برای مطالعه کامل این تحلیل، عضو شوید',
blurSub: 'دسترسی کامل به تمام گزارش‌ها و تحلیل‌های رادار آینده',
login: 'ورود', signup: 'عضویت',
author: 'نویسنده',
},
en: {
overline: 'FORESIGHT & INTELLIGENCE', heading: 'Future Radar',
sub: 'Monitoring strategic signals and emerging trends in the global steel industry',
readMore: 'Read more',
analysis: 'Strategic Analysis',
disclaimer: 'This analysis was generated with AI assistance and is for guidance purposes.',
blurTitle: 'Become a member to read the full analysis',
blurSub: 'Full access to all Future Radar reports and analyses',
login: 'Login', signup: 'Sign Up',
author: 'Author',
},
}
/* ─── membership gate (blur overlay) ─── */
function MembershipGate({ lang }: { lang: 'fa' | 'en' }) {
const t = T[lang]
return (
<div style={{
position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', gap: 16,
background: 'linear-gradient(to bottom, rgba(250,247,244,0) 0%, rgba(250,247,244,0.85) 30%, rgba(250,247,244,0.97) 60%)',
padding: '0 32px 32px',
}}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 16, fontWeight: 800, color: NAVY, marginBottom: 8 }}>{t.blurTitle}</div>
<div style={{ fontSize: 13, color: 'rgba(7,29,73,0.6)', lineHeight: 1.7 }}>{t.blurSub}</div>
</div>
<div style={{ display: 'flex', gap: 10, direction: 'ltr' }}>
<a
href="/membership"
style={{
display: 'inline-flex', alignItems: 'center', gap: 7,
padding: '10px 22px', borderRadius: 10, fontSize: 13, fontWeight: 700,
background: NAVY, color: '#fff', textDecoration: 'none',
}}
>
<LogIn size={15} />
{t.login}
</a>
<a
href="/membership"
style={{
display: 'inline-flex', alignItems: 'center', gap: 7,
padding: '10px 22px', borderRadius: 10, fontSize: 13, fontWeight: 700,
background: GOLD, color: NAVY, textDecoration: 'none',
}}
>
<UserPlus size={15} />
{t.signup}
</a>
</div>
</div>
)
}
/* ─── radar item modal ─── */
export function RadarItemModal({ item, lang, onClose }: { item: RadarItem; lang: 'fa' | 'en'; onClose: () => void }) {
const t = T[lang]
const content = item[lang]
const isLoggedIn = (() => { try { return localStorage.getItem('sf_logged_in') === '1' } catch { return false } })()
const Icon = CATEGORY_ICON[item.category]
const catLabel = CATEGORY_LABEL[item.category][lang]
const dir = lang === 'fa' ? 'rtl' : 'ltr'
const bodyParagraphs = content.body ?? []
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
style={{
position: 'fixed', inset: 0, zIndex: 1000,
background: 'rgba(3,35,64,0.65)', backdropFilter: 'blur(4px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: '20px 16px',
}}
>
<motion.div
initial={{ opacity: 0, y: 32, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 32, scale: 0.96 }}
transition={{ duration: 0.38, ease: [0.22, 1, 0.36, 1] }}
onClick={e => e.stopPropagation()}
dir={dir}
style={{
background: '#faf7f4', borderRadius: 20, width: '100%', maxWidth: 720,
maxHeight: '90vh', overflowY: 'auto', position: 'relative',
boxShadow: '0 40px 100px -20px rgba(3,35,64,0.5)',
}}
>
{/* cover image */}
<div style={{ position: 'relative', width: '100%', height: 280 }}>
<img
src={item.img}
alt={content.title}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', borderRadius: '20px 20px 0 0' }}
/>
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,35,64,0.6) 0%, transparent 50%)', borderRadius: '20px 20px 0 0' }} />
{/* category tag on the image */}
<div style={{
position: 'absolute', bottom: 16,
...(lang === 'fa' ? { right: 20 } : { left: 20 }),
display: 'flex', alignItems: 'center', gap: 6,
background: GOLD, borderRadius: 8, padding: '5px 12px',
direction: 'ltr',
}}>
<Icon size={13} color={NAVY} />
<span style={{ fontSize: 11, fontWeight: 800, color: NAVY }}>{catLabel}</span>
</div>
</div>
{/* close button */}
<button
onClick={onClose}
style={{
position: 'absolute', top: 16,
...(lang === 'fa' ? { left: 16 } : { right: 16 }),
width: 36, height: 36, borderRadius: '50%', border: 'none',
background: 'rgba(3,35,64,0.7)', color: '#fff', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
backdropFilter: 'blur(4px)',
}}
>
<X size={18} />
</button>
{/* content area */}
<div style={{ padding: '28px 32px 36px' }}>
{/* title */}
<h2 style={{ fontSize: 'clamp(18px,2.2vw,24px)', fontWeight: 900, color: NAVY, lineHeight: 1.4, margin: '0 0 16px' }}>
{content.title}
</h2>
{/* meta row: author + date — direction:ltr so icon is physically before text */}
<div style={{ display: 'flex', gap: 20, marginBottom: 24, flexWrap: 'wrap', direction: 'ltr', justifyContent: lang === 'fa' ? 'flex-end' : 'flex-start' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'rgba(7,29,73,0.55)', fontSize: 12, fontWeight: 600 }}>
<User size={14} color={GOLD} />
<span>{item.author.name}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'rgba(7,29,73,0.55)', fontSize: 12, fontWeight: 600 }}>
<Calendar size={14} color={GOLD} />
<span>{content.date}</span>
</div>
</div>
{/* body paragraphs */}
<div style={{ position: 'relative' }}>
{bodyParagraphs.length > 0 ? (
<>
{/* first paragraph always visible */}
<p style={{ fontSize: 14, lineHeight: 2, color: 'rgba(7,29,73,0.8)', margin: '0 0 16px' }}>
{bodyParagraphs[0]}
</p>
{/* rest: visible if logged in, blurred if not */}
{bodyParagraphs.length > 1 && (
<div style={{ position: 'relative' }}>
<div style={{
filter: isLoggedIn ? 'none' : 'blur(7px)',
opacity: isLoggedIn ? 1 : 0.55,
maxHeight: isLoggedIn ? 'none' : 340,
overflow: isLoggedIn ? 'visible' : 'hidden',
WebkitMaskImage: isLoggedIn ? 'none' : 'linear-gradient(to bottom, black 0%, transparent 90%)',
maskImage: isLoggedIn ? 'none' : 'linear-gradient(to bottom, black 0%, transparent 90%)',
userSelect: isLoggedIn ? 'auto' : 'none',
transition: 'filter 0.3s',
}}>
{bodyParagraphs.slice(1).map((p, i) => (
<p key={i} style={{ fontSize: 14, lineHeight: 2, color: 'rgba(7,29,73,0.8)', margin: '0 0 16px' }}>{p}</p>
))}
</div>
{!isLoggedIn && <MembershipGate lang={lang} />}
</div>
)}
</>
) : (
<p style={{ fontSize: 14, lineHeight: 2, color: 'rgba(7,29,73,0.8)', margin: '0 0 16px' }}>
{content.excerpt}
</p>
)}
</div>
{/* strategic analysis box (only if logged in) */}
{isLoggedIn && content.analysis && (
<div style={{
marginTop: 28, borderRadius: 14, padding: '20px 22px',
background: `${NAVY}08`, border: `1.5px solid ${NAVY}15`,
}}>
<div style={{ fontSize: 11, fontWeight: 800, letterSpacing: '2px', color: GOLD, marginBottom: 10 }}>{t.analysis}</div>
<p style={{ fontSize: 13, lineHeight: 1.9, color: NAVY, margin: 0 }}>{content.analysis}</p>
</div>
)}
{/* AI disclaimer */}
{isLoggedIn && (
<div style={{
marginTop: 20, display: 'flex', alignItems: 'flex-start', gap: 8,
direction: 'ltr', justifyContent: lang === 'fa' ? 'flex-end' : 'flex-start',
}}>
<AlertTriangle size={13} color={GOLD} style={{ flexShrink: 0, marginTop: 2 }} />
<span style={{ fontSize: 11, color: 'rgba(7,29,73,0.45)', lineHeight: 1.6 }}>{t.disclaimer}</span>
</div>
)}
{/* close button at bottom */}
{isLoggedIn && (
<div style={{ marginTop: 28, display: 'flex', justifyContent: 'center' }}>
<button
onClick={onClose}
style={{
padding: '10px 32px', borderRadius: 10, border: 'none', cursor: 'pointer',
background: NAVY, color: '#fff', fontSize: 13, fontWeight: 700, fontFamily: 'inherit',
display: 'flex', alignItems: 'center', gap: 8, direction: 'ltr',
}}
>
<X size={14} />
{lang === 'fa' ? 'بستن' : 'Close'}
</button>
</div>
)}
</div>
</motion.div>
</motion.div>
</AnimatePresence>
)
}
/* ─── one card: gold bar (default) → navy author panel (hover) ─── */
export function RadarCard({ item, lang }: { item: RadarItem; lang: 'fa' | 'en' }) {
export function RadarCard({ item, lang, onClick }: { item: RadarItem; lang: 'fa' | 'en'; onClick?: (item: RadarItem) => void }) {
const [hover, setHover] = useState(false)
const navigate = useNavigate()
const t = item[lang]
const Icon = CATEGORY_ICON[item.category]
@ -31,9 +278,10 @@ export function RadarCard({ item, lang }: { item: RadarItem; lang: 'fa' | 'en' }
<motion.article
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onClick={() => onClick ? onClick(item) : navigate(`/radar/post/${item.id}`)}
style={{
position: 'relative', borderRadius: 16, overflow: 'hidden', cursor: 'pointer',
aspectRatio: '3 / 3.5', background: NAVY,
aspectRatio: '3 / 2.4', background: NAVY,
boxShadow: hover ? '0 30px 70px -30px rgba(7,29,73,0.45)' : '0 16px 40px -24px rgba(7,29,73,0.30)',
}}
animate={{ y: hover ? -4 : 0 }}
@ -67,7 +315,6 @@ export function RadarCard({ item, lang }: { item: RadarItem; lang: 'fa' | 'en' }
display: 'flex', alignItems: 'center', gap: 12, direction: lang === 'fa' ? 'rtl' : 'ltr',
}}
>
{/* icon on the leading (right in RTL) side of the bar */}
<div style={{ flexShrink: 0, width: 40, height: 40, borderRadius: '50%', background: 'rgba(7,29,73,0.10)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Icon size={20} color={NAVY} strokeWidth={2} />
</div>
@ -113,7 +360,6 @@ export function RadarCard({ item, lang }: { item: RadarItem; lang: 'fa' | 'en' }
/* ─── pagination ─── */
function Pagination({ page, pages, onGo }: { page: number; pages: number; onGo: (p: number) => void }) {
if (pages <= 1) return null
// show: first, current-ish, last, with ellipsis
const nums: (number | '…')[] = []
for (let i = 1; i <= pages; i++) {
if (i === 1 || i === pages || Math.abs(i - page) <= 1) nums.push(i)
@ -153,6 +399,7 @@ export default function Radar() {
const { lang } = useLang()
const t = T[lang]
const [page, setPage] = useState(1)
const [selectedItem, setSelectedItem] = useState<RadarItem | null>(null)
const pages = Math.ceil(RADAR_ITEMS.length / PER_PAGE)
const start = (page - 1) * PER_PAGE
@ -185,12 +432,17 @@ export default function Radar() {
{/* card grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 22 }} className="max-lg:!grid-cols-2 max-sm:!grid-cols-1">
{visible.map((item) => (
<RadarCard key={item.id} item={item} lang={lang} />
<RadarCard key={item.id} item={item} lang={lang} onClick={setSelectedItem} />
))}
</div>
<Pagination page={page} pages={pages} onGo={go} />
</div>
{/* modal */}
{selectedItem && (
<RadarItemModal item={selectedItem} lang={lang} onClose={() => setSelectedItem(null)} />
)}
</div>
)
}

View File

@ -1,5 +1,5 @@
import { useState } from 'react'
import { Link, useParams, Navigate } from 'react-router-dom'
import { Link, useParams, Navigate, useNavigate } from 'react-router-dom'
import { motion } from 'framer-motion'
import { ArrowLeft, Download, Calendar, ChevronLeft, ChevronRight } from 'lucide-react'
import { useLang } from '@/context/LangContext'
@ -50,7 +50,7 @@ function Breadcrumb({ label, lang }: { label: string; lang: 'fa' | 'en' }) {
}
/* ─── a "latest" mini-card (image on the trailing side, text + CTA) ─── */
function MiniCard({ img, title, date, lang }: { img: string; title: string; date: string; lang: 'fa' | 'en' }) {
function MiniCard({ img, title, date, lang, onClick }: { img: string; title: string; date: string; lang: 'fa' | 'en'; onClick?: () => void }) {
const cta = lang === 'fa' ? 'مشاهده تحلیل' : 'View analysis'
return (
<article style={{ display: 'flex', gap: 16, direction: lang === 'fa' ? 'rtl' : 'ltr', alignItems: 'stretch' }}>
@ -71,7 +71,7 @@ function MiniCard({ img, title, date, lang }: { img: string; title: string; date
: 'Beyond a strategic mineral, lithium is now the main lever of power in reshaping the economic order ...'}
</p>
<div style={{ marginTop: 'auto', display: 'flex', justifyContent: lang === 'fa' ? 'flex-start' : 'flex-start' }}>
<button style={{
<button onClick={onClick} style={{
display: 'inline-flex', alignItems: 'center', gap: 8, direction: 'ltr',
background: GOLD, color: NAVY, border: 'none', borderRadius: 8,
padding: '8px 16px', fontFamily: 'inherit', fontSize: 12, fontWeight: 700, cursor: 'pointer',
@ -86,9 +86,9 @@ function MiniCard({ img, title, date, lang }: { img: string; title: string; date
}
/* ─── big featured card (right column of the hero) ─── */
function FeaturedCard({ img, title, date, lang }: { img: string; title: string; date: string; lang: 'fa' | 'en' }) {
function FeaturedCard({ img, title, date, lang, onClick }: { img: string; title: string; date: string; lang: 'fa' | 'en'; onClick?: () => void }) {
return (
<article style={{ position: 'relative', borderRadius: 16, overflow: 'hidden', height: '100%', minHeight: 360 }} className="max-md:!min-h-[260px]">
<article onClick={onClick} style={{ position: 'relative', borderRadius: 16, overflow: 'hidden', height: '100%', minHeight: 360, cursor: 'pointer' }} className="max-md:!min-h-[260px]">
<img src={img} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,35,64,0.92) 0%, rgba(3,35,64,0.45) 45%, transparent 75%)' }} />
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: 28, direction: lang === 'fa' ? 'rtl' : 'ltr', textAlign: lang === 'fa' ? 'right' : 'left' }} className="max-md:!p-5">
@ -103,12 +103,13 @@ function FeaturedCard({ img, title, date, lang }: { img: string; title: string;
}
/* ─── grid card (gold framed) ─── */
function GridCard({ img, title, excerpt, date, lang }: { img: string; title: string; excerpt: string; date: string; lang: 'fa' | 'en' }) {
function GridCard({ img, title, excerpt, date, lang, onClick }: { img: string; title: string; excerpt: string; date: string; lang: 'fa' | 'en'; onClick?: () => void }) {
const [hover, setHover] = useState(false)
return (
<motion.article
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onClick={onClick}
animate={{ y: hover ? -4 : 0 }}
transition={{ duration: 0.3, ease: EASE }}
style={{
@ -127,12 +128,12 @@ function GridCard({ img, title, excerpt, date, lang }: { img: string; title: str
src={img} alt=""
animate={{ scale: hover ? 1.06 : 1 }}
transition={{ duration: 0.6, ease: EASE }}
style={{ width: '100%', aspectRatio: '16 / 10', objectFit: 'cover', display: 'block' }}
style={{ width: '100%', aspectRatio: '16 / 8', objectFit: 'cover', display: 'block' }}
/>
</div>
<div style={{ padding: '14px 16px 12px', display: 'flex', flexDirection: 'column', flex: 1, textAlign: lang === 'fa' ? 'right' : 'left' }}>
<h3 style={{ fontSize: 14, fontWeight: 800, color: NAVY, lineHeight: 1.55, margin: '0 0 6px', overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical' }}>{title}</h3>
<p style={{ fontSize: 12, lineHeight: 1.8, color: 'rgba(7,29,73,0.55)', margin: '0 0 12px', overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical' }}>{excerpt}</p>
<div style={{ padding: '10px 14px 10px', display: 'flex', flexDirection: 'column', flex: 1, textAlign: lang === 'fa' ? 'right' : 'left' }}>
<h3 style={{ fontSize: 13, fontWeight: 800, color: NAVY, lineHeight: 1.5, margin: '0 0 4px', overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical' }}>{title}</h3>
<p style={{ fontSize: 11.5, lineHeight: 1.7, color: 'rgba(7,29,73,0.55)', margin: '0 0 8px', overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical' }}>{excerpt}</p>
{/* footer: date (trailing/right) + arrow (physical left) */}
<div style={{ marginTop: 'auto', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 11, color: 'rgba(7,29,73,0.5)', fontWeight: 600 }}>
@ -268,6 +269,7 @@ export default function RadarCategory() {
const { lang } = useLang()
const { category } = useParams<{ category: string }>()
const [page, setPage] = useState(1)
const navigate = useNavigate()
const cfg = category ? RADAR_CATEGORY_PAGES[category as RadarSlug] : undefined
if (!cfg) return <Navigate to="/radar" replace />
@ -308,13 +310,13 @@ export default function RadarCategory() {
<Reveal>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, marginBottom: 40 }} className="max-lg:!grid-cols-1 max-md:!gap-4 max-md:!mb-7">
{/* RIGHT (first in RTL): big featured */}
<FeaturedCard img={c.featured.img} title={c.featured.title} date={c.featured.date} lang={lang} />
<FeaturedCard img={c.featured.img} title={c.featured.title} date={c.featured.date} lang={lang} onClick={() => navigate(`/radar/post/${seed[0]?.id ?? 'r1'}`)} />
{/* LEFT (second in RTL): latest box */}
<div style={{ background: '#efe9e1', borderRadius: 16, padding: 24, display: 'flex', flexDirection: 'column' }} className="max-md:!p-4">
<h2 style={{ fontSize: 16, fontWeight: 900, color: GOLD, margin: '0 0 18px', textAlign: lang === 'fa' ? 'right' : 'left' }}>{c.latestHeading}</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 20, flex: 1, justifyContent: 'space-around' }}>
{latest.map((item) => (
<MiniCard key={item.id} img={item.img} title={item[lang].title} date={item[lang].date} lang={lang} />
{latest.map((item, idx) => (
<MiniCard key={item.id} img={item.img} title={item[lang].title} date={item[lang].date} lang={lang} onClick={() => navigate(`/radar/post/${seed[idx]?.id ?? seed[0]?.id ?? 'r1'}`)} />
))}
</div>
</div>
@ -323,9 +325,9 @@ export default function RadarCategory() {
{/* ── FIRST GRID (4) ── */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 18 }} className="max-lg:!grid-cols-2 max-sm:!grid-cols-1">
{visible.slice(0, 4).map((item) => (
{visible.slice(0, 4).map((item, idx) => (
<Reveal key={item.id}>
<GridCard img={item.img} title={item[lang].title} excerpt={item[lang].excerpt} date={item[lang].date} lang={lang} />
<GridCard img={item.img} title={item[lang].title} excerpt={item[lang].excerpt} date={item[lang].date} lang={lang} onClick={() => navigate(`/radar/post/${seed[idx % seed.length].id}`)} />
</Reveal>
))}
</div>
@ -337,9 +339,9 @@ export default function RadarCategory() {
{/* ── SECOND GRID (rest) ── */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 18 }} className="max-lg:!grid-cols-2 max-sm:!grid-cols-1">
{visible.slice(4).map((item) => (
{visible.slice(4).map((item, idx) => (
<Reveal key={item.id}>
<GridCard img={item.img} title={item[lang].title} excerpt={item[lang].excerpt} date={item[lang].date} lang={lang} />
<GridCard img={item.img} title={item[lang].title} excerpt={item[lang].excerpt} date={item[lang].date} lang={lang} onClick={() => navigate(`/radar/post/${seed[(idx + 4) % seed.length].id}`)} />
</Reveal>
))}
</div>

View File

@ -0,0 +1,232 @@
import { useState } from 'react'
import { useParams, Navigate, Link } from 'react-router-dom'
import AuthPage from '@/pages/Auth/AuthPage'
import { motion } from 'framer-motion'
import { Calendar, Tag, UserPlus, LogIn, Send, Share2, Globe, Mail, ArrowLeft } from 'lucide-react'
import { useLang } from '@/context/LangContext'
import { RADAR_ITEMS, type RadarItem, type RadarCategory } from '@/content/radar'
const NAVY = '#032340'
const GOLD = '#CD9E53'
const EASE = [0.22, 1, 0.36, 1] as const
const CATEGORY_LABEL: Record<RadarCategory, { fa: string; en: string }> = {
market: { fa: 'بازار', en: 'Market' },
tech: { fa: 'فناوری', en: 'Technology' },
commodity: { fa: 'کامودیتی', en: 'Commodity' },
geo: { fa: 'ژئوپلیتیک', en: 'Geopolitics' },
energy: { fa: 'انرژی', en: 'Energy' },
}
/* ─── membership gate ─── */
function MembershipGate({ lang, onLogin }: { lang: 'fa' | 'en'; onLogin: () => void }) {
const isFa = lang === 'fa'
const copy = isFa
? 'ما در اندیشکده فولاد آینده، خروجی‌های راهبردی خود را برای اعضای شبکه تخصصی‌مان تدوین می‌کنیم. با عضویت در این اندیشکده، می‌توانید به متن کامل گزارش‌ها، سناریوهای آینده‌پژوهی و تحلیل‌های سطح‌بالا دسترسی داشته باشید.'
: 'At the Future Steel Institute we craft our strategic outputs for members of our expert network. Membership unlocks full reports, foresight scenarios and high-level analysis.'
return (
<div style={{
position: 'relative', background: '#fff', borderRadius: 18, padding: 'clamp(20px,3vw,34px)',
boxShadow: '0 30px 70px -30px rgba(7,29,73,0.35)', border: '1px solid rgba(7,29,73,0.06)',
maxWidth: 720, margin: '0 auto', direction: isFa ? 'rtl' : 'ltr',
}}>
<p style={{ fontSize: 14.5, lineHeight: 2.1, color: 'rgba(7,29,73,0.78)', margin: '0 0 22px', textAlign: 'justify' }}>{copy}</p>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 10, direction: 'ltr' }}>
{[Send, Share2, Globe, Mail].map((Icon, i) => (
<a key={i} href="#" aria-label="social" style={{ width: 36, height: 36, borderRadius: '50%', background: NAVY, color: GOLD, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
<Icon size={16} />
</a>
))}
</div>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<button onClick={onLogin} style={{
display: 'inline-flex', alignItems: 'center', gap: 8, direction: 'ltr',
background: 'transparent', color: NAVY, border: `1.5px solid ${NAVY}`, borderRadius: 10,
padding: '12px 26px', fontFamily: 'inherit', fontSize: 14, fontWeight: 800, cursor: 'pointer',
}}>
<LogIn size={16} strokeWidth={2.5} />
<span>{isFa ? 'ورود' : 'Log in'}</span>
</button>
<Link to="/membership" style={{
display: 'inline-flex', alignItems: 'center', gap: 8, direction: 'ltr',
background: GOLD, color: NAVY, borderRadius: 10, padding: '12px 26px',
fontSize: 14, fontWeight: 800, textDecoration: 'none',
}}>
<UserPlus size={16} strokeWidth={2.5} />
<span>{isFa ? 'ثبت نام' : 'Sign up'}</span>
</Link>
</div>
</div>
</div>
)
}
/* ─── related sidebar ─── */
function RelatedSidebar({ items, currentId, lang }: { items: RadarItem[]; currentId: string; lang: 'fa' | 'en' }) {
const related = items.filter(r => r.id !== currentId).slice(0, 3)
return (
<aside style={{ position: 'sticky', top: 88, alignSelf: 'flex-start' }} className="max-lg:!static">
<div style={{ fontSize: 11, fontWeight: 800, letterSpacing: '2px', color: GOLD, marginBottom: 16 }}>
{lang === 'fa' ? 'سیگنال‌های مرتبط' : 'RELATED SIGNALS'}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{related.map(r => (
<Link key={r.id} to={`/radar/post/${r.id}`} style={{ textDecoration: 'none' }}>
<article style={{
background: '#fff', borderRadius: 14, overflow: 'hidden', cursor: 'pointer',
border: '1px solid rgba(205,158,83,0.3)', borderBottom: `3px solid ${GOLD}`,
transition: 'transform 180ms, box-shadow 180ms',
}}
onMouseEnter={e => { const d = e.currentTarget; d.style.transform = 'translateY(-3px)'; d.style.boxShadow = '0 16px 34px -22px rgba(7,29,73,0.42)' }}
onMouseLeave={e => { const d = e.currentTarget; d.style.transform = 'translateY(0)'; d.style.boxShadow = 'none' }}
>
<img src={r.img} alt="" style={{ width: '100%', aspectRatio: '16/10', objectFit: 'cover', display: 'block' }} />
<div style={{ padding: '12px 14px', textAlign: lang === 'fa' ? 'right' : 'left' }}>
<h4 style={{ fontSize: 13, fontWeight: 800, color: NAVY, lineHeight: 1.6, margin: '0 0 6px' }}>{r[lang].title}</h4>
<p style={{ fontSize: 11.5, color: 'rgba(7,29,73,0.55)', lineHeight: 1.7, margin: '0 0 10px' }}>{r[lang].excerpt}</p>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ width: 28, height: 28, borderRadius: '50%', background: 'rgba(205,158,83,0.15)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
<ArrowLeft size={14} color={GOLD} strokeWidth={2.5} className="no-rtl-flip" />
</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 10.5, color: 'rgba(7,29,73,0.5)', fontWeight: 600 }}>
<Calendar size={11} color={GOLD} />{r[lang].date}
</span>
</div>
</div>
</article>
</Link>
))}
</div>
</aside>
)
}
/* ─── main ─── */
export default function RadarPost() {
const { lang } = useLang()
const { id } = useParams<{ id: string }>()
const isFa = lang === 'fa'
const item = RADAR_ITEMS.find(r => r.id === id)
if (!item) return <Navigate to="/radar" replace />
const content = item[lang]
const catLabel = CATEGORY_LABEL[item.category][lang]
const [loggedIn, setLoggedIn] = useState(() => {
try { return localStorage.getItem('sf_logged_in') === '1' } catch { return false }
})
const [showAuth, setShowAuth] = useState(false)
const handleAuthSuccess = () => {
try { localStorage.setItem('sf_logged_in', '1') } catch { /* ignore */ }
setLoggedIn(true)
setShowAuth(false)
}
const body = content.body ?? []
const firstPara = body[0]
const restParas = body.slice(1)
return (
<div dir={isFa ? 'rtl' : 'ltr'} style={{ background: '#fff', minHeight: '80vh' }}>
{/* ── COVER ── */}
<div style={{
position: 'relative', width: '100vw',
marginInline: 'calc(50% - 50vw)',
height: 'clamp(420px, 92vh, 1000px)',
borderRadius: 40, overflow: 'hidden',
}} className="max-md:!rounded-3xl">
<img src={item.img} alt={content.title} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,35,64,0.6) 0%, transparent 50%)' }} />
</div>
{showAuth && (
<AuthPage defaultMode="register" onSuccess={handleAuthSuccess} onClose={() => setShowAuth(false)} />
)}
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 28, paddingBottom: 72 }}>
{/* ── BODY GRID ── */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 300px', gap: 40 }} className="max-lg:!grid-cols-1 max-lg:!gap-8">
{/* MAIN ARTICLE */}
<article style={{ minWidth: 0 }}>
{/* date */}
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'rgba(7,29,73,0.55)', fontWeight: 600, marginBottom: 12 }}>
<Calendar size={14} color={GOLD} /> {content.date}
</div>
{/* title */}
<h1 style={{ fontSize: 'clamp(22px,2.6vw,34px)', fontWeight: 900, color: NAVY, lineHeight: 1.5, letterSpacing: '-0.5px', margin: '0 0 12px' }}>
{content.title}
</h1>
{/* category + author */}
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 26, flexWrap: 'wrap' }}>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 13, fontWeight: 700, color: GOLD }}>
<Tag size={15} /> {catLabel}
</div>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
<img src={item.author.avatar} alt={item.author.name} style={{ width: 28, height: 28, borderRadius: '50%', objectFit: 'cover' }} />
<span style={{ fontSize: 13, fontWeight: 700, color: NAVY }}>{item.author.name}</span>
</div>
</div>
{/* body */}
{loggedIn ? (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.4, ease: EASE }}>
{body.map((p, i) => (
<p key={i} style={{ fontSize: 15.5, lineHeight: 2.2, color: NAVY, margin: '0 0 18px', textAlign: 'justify', fontWeight: 500 }}>{p}</p>
))}
{/* strategic analysis box */}
{content.analysis && (
<div style={{ margin: '32px 0', borderRadius: 16, padding: '22px 24px', background: `${NAVY}08`, border: `1.5px solid ${NAVY}18` }}>
<div style={{ fontSize: 10, fontWeight: 800, letterSpacing: '2.5px', color: GOLD, marginBottom: 12 }}>
{isFa ? 'تحلیل راهبردی' : 'STRATEGIC ANALYSIS'}
</div>
<p style={{ fontSize: 14.5, lineHeight: 2, color: NAVY, margin: 0 }}>{content.analysis}</p>
</div>
)}
{/* AI disclaimer */}
<p style={{ fontSize: 12, color: 'rgba(7,29,73,0.4)', marginTop: 8, lineHeight: 1.7 }}>
{isFa
? '⚠ این تحلیل با کمک هوش مصنوعی تهیه شده و جنبه راهنمایی دارد.'
: '⚠ This analysis was prepared with AI assistance and is for guidance purposes.'}
</p>
</motion.div>
) : (
<>
{firstPara && (
<p style={{ fontSize: 15.5, lineHeight: 2.2, color: NAVY, margin: '0 0 18px', textAlign: 'justify', fontWeight: 500 }}>{firstPara}</p>
)}
<div style={{ position: 'relative' }}>
<div aria-hidden style={{
filter: 'blur(7px)', opacity: 0.55, maxHeight: 340, overflow: 'hidden',
pointerEvents: 'none', userSelect: 'none',
WebkitMaskImage: 'linear-gradient(to bottom, #000 0%, #000 28%, transparent 92%)',
maskImage: 'linear-gradient(to bottom, #000 0%, #000 28%, transparent 92%)',
}}>
{restParas.map((p, i) => (
<p key={i} style={{ fontSize: 15.5, lineHeight: 2.2, color: NAVY, margin: '0 0 18px', textAlign: 'justify', fontWeight: 500 }}>{p}</p>
))}
</div>
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'flex-start', justifyContent: 'center', paddingTop: 36 }}>
<MembershipGate lang={lang} onLogin={() => setShowAuth(true)} />
</div>
</div>
</>
)}
</article>
{/* RELATED SIDEBAR */}
<RelatedSidebar items={RADAR_ITEMS} currentId={item.id} lang={lang} />
</div>
</div>
</div>
)
}

View File

@ -103,7 +103,7 @@ export default function ReportDetail() {
const report = reports.find((r) => r.id === id)
if (!report) return <Navigate to="/reports" replace />
if (!report) return <Navigate to="/archive" replace />
return (
<div
@ -125,14 +125,14 @@ export default function ReportDetail() {
}}
>
<Link
to="/reports"
to="/archive"
style={{ fontSize: 11, color: 'var(--ink-5)', textDecoration: 'none' }}
>
گزارشها
</Link>
<ChevronRight size={10} style={{ color: 'var(--ink-5)', transform: 'scaleX(-1)' }} />
<Link
to={`/reports?category=${report.category}`}
to={`/archive?category=${report.category}`}
style={{ fontSize: 11, color: 'var(--ink-5)', textDecoration: 'none' }}
>
{report.category}

View File

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

View File

@ -15,6 +15,16 @@ export default defineConfig({
},
server: {
allowedHosts: ALLOWED_TUNNEL_HOSTS,
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
'/uploads': {
target: 'http://localhost:3001',
changeOrigin: true,
},
},
},
preview: {
allowedHosts: ALLOWED_TUNNEL_HOSTS,