Bilingual site (FA/EN), redesigned sections, new pages
- Full FA/EN toggle via LangContext across all pages and sections - New pages: Radar, Technology, Pulse, Membership - Home restructured: Hero → SnapshotBar → 5 chapters - FactoryBentoSection: Mobarakeh Steel monthly reports (bento grid) - SteelNewsletterSection: bilingual newsletter + factory list - Section labels: Persian numerals, editorial rule style - Header: LTR in EN mode, bilingual nav - Footer: updated copyright and tagline - Typography: consistent h2 sizes across sections - BentoGrid: larger cards (p-8, 18px title) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
38956bf77a
commit
60bf240cf5
|
|
@ -8,6 +8,7 @@
|
||||||
"name": "andishkade-foolad",
|
"name": "andishkade-foolad",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tabler/icons-react": "^3.44.0",
|
||||||
"@tanstack/react-query": "^5.100.14",
|
"@tanstack/react-query": "^5.100.14",
|
||||||
"apexcharts": "^5.13.0",
|
"apexcharts": "^5.13.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|
@ -852,6 +853,32 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tabler/icons": {
|
||||||
|
"version": "3.44.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.44.0.tgz",
|
||||||
|
"integrity": "sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/codecalm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tabler/icons-react": {
|
||||||
|
"version": "3.44.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.44.0.tgz",
|
||||||
|
"integrity": "sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tabler/icons": "3.44.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/codecalm"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tabler/icons-react": "^3.44.0",
|
||||||
"@tanstack/react-query": "^5.100.14",
|
"@tanstack/react-query": "^5.100.14",
|
||||||
"apexcharts": "^5.13.0",
|
"apexcharts": "^5.13.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|
|
||||||
75
panel/db.js
75
panel/db.js
|
|
@ -40,8 +40,83 @@ db.exec(`
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_articles_created ON articles(created_at DESC);
|
CREATE INDEX IF NOT EXISTS idx_articles_created ON articles(created_at DESC);
|
||||||
CREATE INDEX IF NOT EXISTS idx_articles_featured ON articles(featured);
|
CREATE INDEX IF NOT EXISTS idx_articles_featured ON articles(featured);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS risk_signals (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
quote TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
date TEXT,
|
||||||
|
level TEXT NOT NULL CHECK (level IN ('critical','high','medium','low','opportunity')),
|
||||||
|
sort_order INTEGER DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_risks_sort ON risk_signals(sort_order, created_at);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS prices (
|
||||||
|
symbol TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
value REAL,
|
||||||
|
unit TEXT,
|
||||||
|
change_value REAL,
|
||||||
|
change_pct REAL,
|
||||||
|
source_url TEXT,
|
||||||
|
fetched_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
export function rowToPrice(row) {
|
||||||
|
if (!row) return null;
|
||||||
|
return {
|
||||||
|
symbol: row.symbol,
|
||||||
|
name: row.name,
|
||||||
|
value: row.value,
|
||||||
|
unit: row.unit,
|
||||||
|
changeValue: row.change_value,
|
||||||
|
changePct: row.change_pct,
|
||||||
|
sourceUrl: row.source_url,
|
||||||
|
fetchedAt: row.fetched_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function upsertPrice(p) {
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO prices (symbol, name, value, unit, change_value, change_pct, source_url, fetched_at)
|
||||||
|
VALUES (@symbol, @name, @value, @unit, @change_value, @change_pct, @source_url, datetime('now'))
|
||||||
|
ON CONFLICT(symbol) DO UPDATE SET
|
||||||
|
name = excluded.name,
|
||||||
|
value = excluded.value,
|
||||||
|
unit = excluded.unit,
|
||||||
|
change_value = excluded.change_value,
|
||||||
|
change_pct = excluded.change_pct,
|
||||||
|
source_url = excluded.source_url,
|
||||||
|
fetched_at = excluded.fetched_at
|
||||||
|
`).run({
|
||||||
|
symbol: p.symbol,
|
||||||
|
name: p.name,
|
||||||
|
value: p.value ?? null,
|
||||||
|
unit: p.unit ?? null,
|
||||||
|
change_value: p.changeValue ?? null,
|
||||||
|
change_pct: p.changePct ?? null,
|
||||||
|
source_url: p.sourceUrl ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rowToRiskSignal(row) {
|
||||||
|
if (!row) return null;
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
quote: row.quote,
|
||||||
|
name: row.name,
|
||||||
|
date: row.date,
|
||||||
|
level: row.level,
|
||||||
|
sortOrder: row.sort_order,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
updatedAt: row.updated_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function rowToArticle(row) {
|
export function rowToArticle(row) {
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"better-sqlite3": "^11.3.0",
|
"better-sqlite3": "^11.3.0",
|
||||||
|
"cheerio": "^1.2.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
|
|
@ -141,6 +142,12 @@
|
||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/boolbase": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
|
@ -226,6 +233,48 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cheerio": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cheerio-select": "^2.1.0",
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.2.2",
|
||||||
|
"encoding-sniffer": "^0.2.1",
|
||||||
|
"htmlparser2": "^10.1.0",
|
||||||
|
"parse5": "^7.3.0",
|
||||||
|
"parse5-htmlparser2-tree-adapter": "^7.1.0",
|
||||||
|
"parse5-parser-stream": "^7.1.2",
|
||||||
|
"undici": "^7.19.0",
|
||||||
|
"whatwg-mimetype": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cheerio-select": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-select": "^5.1.0",
|
||||||
|
"css-what": "^6.1.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chownr": {
|
"node_modules/chownr": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
|
|
@ -306,6 +355,34 @@
|
||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-select": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-what": "^6.1.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"nth-check": "^2.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-what": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
|
@ -367,6 +444,61 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.6.1",
|
"version": "16.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
|
|
@ -417,6 +549,31 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/encoding-sniffer": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
|
"whatwg-encoding": "^3.1.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/encoding-sniffer/node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/end-of-stream": {
|
"node_modules/end-of-stream": {
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
|
|
@ -426,6 +583,18 @@
|
||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
|
@ -662,6 +831,37 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/htmlparser2": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.2.2",
|
||||||
|
"entities": "^7.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/entities": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
|
@ -1010,6 +1210,18 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nth-check": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
|
@ -1052,6 +1264,55 @@
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse5": {
|
||||||
|
"version": "7.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||||
|
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5-htmlparser2-tree-adapter": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"parse5": "^7.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5-parser-stream": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"parse5": "^7.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5/node_modules/entities": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
|
@ -1527,6 +1788,15 @@
|
||||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "7.26.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.26.0.tgz",
|
||||||
|
"integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
|
@ -1560,6 +1830,40 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/whatwg-encoding": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||||
|
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrappy": {
|
"node_modules/wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"better-sqlite3": "^11.3.0",
|
"better-sqlite3": "^11.3.0",
|
||||||
|
"cheerio": "^1.2.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
|
|
|
||||||
|
|
@ -71,21 +71,40 @@ function renderLogin(errorMsg = '') {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderList() {
|
function topbarHtml(active, primaryLabel) {
|
||||||
root.innerHTML = `
|
return `
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div class="brand">پنل اندیشکده فولاد</div>
|
<div class="brand">پنل اندیشکده فولاد</div>
|
||||||
|
<nav class="tabs">
|
||||||
|
<button data-tab="articles" class="${active==='articles'?'tab-active':''}">مطالب</button>
|
||||||
|
<button data-tab="risks" class="${active==='risks'?'tab-active':''}">ریسکها</button>
|
||||||
|
</nav>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="newBtn" class="primary">+ مطلب جدید</button>
|
<button id="newBtn" class="primary">${primaryLabel}</button>
|
||||||
<button id="logoutBtn" class="ghost">خروج</button>
|
<button id="logoutBtn" class="ghost">خروج</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
function wireTabs() {
|
||||||
|
document.querySelectorAll('.tabs button').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
if (btn.dataset.tab === 'articles') renderList();
|
||||||
|
else if (btn.dataset.tab === 'risks') renderRisksList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$('#logoutBtn').addEventListener('click', () => { clearToken(); renderLogin(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderList() {
|
||||||
|
root.innerHTML = `
|
||||||
|
${topbarHtml('articles', '+ مطلب جدید')}
|
||||||
<main class="page">
|
<main class="page">
|
||||||
<h2>مطالب</h2>
|
<h2>مطالب</h2>
|
||||||
<div id="list" class="list"><p class="muted">در حال بارگذاری…</p></div>
|
<div id="list" class="list"><p class="muted">در حال بارگذاری…</p></div>
|
||||||
</main>
|
</main>
|
||||||
`;
|
`;
|
||||||
$('#logoutBtn').addEventListener('click', () => { clearToken(); renderLogin(); });
|
wireTabs();
|
||||||
$('#newBtn').addEventListener('click', () => renderEditor(null));
|
$('#newBtn').addEventListener('click', () => renderEditor(null));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -290,5 +309,130 @@ function escapeHtml(s) {
|
||||||
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RISK_LEVELS = [
|
||||||
|
{ value: 'critical', label: 'بحرانی' },
|
||||||
|
{ value: 'high', label: 'بالا' },
|
||||||
|
{ value: 'medium', label: 'متوسط' },
|
||||||
|
{ value: 'low', label: 'پایین' },
|
||||||
|
{ value: 'opportunity', label: 'فرصت' },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function renderRisksList() {
|
||||||
|
root.innerHTML = `
|
||||||
|
${topbarHtml('risks', '+ سیگنال جدید')}
|
||||||
|
<main class="page">
|
||||||
|
<h2>سیگنالهای ریسک</h2>
|
||||||
|
<div id="list" class="list"><p class="muted">در حال بارگذاری…</p></div>
|
||||||
|
</main>
|
||||||
|
`;
|
||||||
|
wireTabs();
|
||||||
|
$('#newBtn').addEventListener('click', () => renderRiskEditor(null));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const items = await api('/api/risks?limit=200');
|
||||||
|
if (!items.length) {
|
||||||
|
$('#list').innerHTML = `<p class="muted">هنوز سیگنالی ثبت نشده است.</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const labelOf = (lvl) => (RISK_LEVELS.find(r => r.value === lvl)?.label) || lvl;
|
||||||
|
$('#list').innerHTML = items.map(r => `
|
||||||
|
<article class="row risk-row" data-id="${r.id}">
|
||||||
|
<div class="row-main">
|
||||||
|
<div class="row-meta">
|
||||||
|
<span class="pill level-${r.level}">${labelOf(r.level)}</span>
|
||||||
|
<strong>${r.name}</strong>
|
||||||
|
${r.date ? `<span class="muted">${r.date}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
<p class="muted clamp-2">${r.quote}</p>
|
||||||
|
</div>
|
||||||
|
<div class="row-actions">
|
||||||
|
<button data-action="edit">ویرایش</button>
|
||||||
|
<button data-action="delete" class="danger">حذف</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
$('#list').addEventListener('click', async (e) => {
|
||||||
|
const btn = e.target.closest('button[data-action]');
|
||||||
|
if (!btn) return;
|
||||||
|
const id = btn.closest('.row').dataset.id;
|
||||||
|
if (btn.dataset.action === 'edit') {
|
||||||
|
const r = await api(`/api/risks/${id}`);
|
||||||
|
renderRiskEditor(r);
|
||||||
|
} else if (btn.dataset.action === 'delete') {
|
||||||
|
if (!confirm('این سیگنال حذف شود؟')) return;
|
||||||
|
await api(`/api/risks/${id}`, { method: 'DELETE' });
|
||||||
|
renderRisksList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
$('#list').innerHTML = `<p class="error">خطا در بارگذاری سیگنالها.</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRiskEditor(risk) {
|
||||||
|
const r = risk || { quote: '', name: '', date: '', level: 'high', sortOrder: 0 };
|
||||||
|
const isEdit = !!risk;
|
||||||
|
|
||||||
|
root.innerHTML = `
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="brand">${isEdit ? 'ویرایش سیگنال ریسک' : 'سیگنال ریسک جدید'}</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="backBtn" class="ghost">← بازگشت</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="page editor">
|
||||||
|
<form id="editorForm" class="grid">
|
||||||
|
<div class="col span-2">
|
||||||
|
<label>عنوان / منبع
|
||||||
|
<input name="name" value="${escapeAttr(r.name)}" required />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label>سطح
|
||||||
|
<select name="level">
|
||||||
|
${RISK_LEVELS.map(l => `<option value="${l.value}" ${l.value===r.level?'selected':''}>${l.label}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>تاریخ / افق (شمسی)
|
||||||
|
<input name="date" value="${escapeAttr(r.date)}" />
|
||||||
|
</label>
|
||||||
|
<label>ترتیب نمایش (عدد کوچکتر اول)
|
||||||
|
<input name="sortOrder" type="number" value="${r.sortOrder || 0}" />
|
||||||
|
</label>
|
||||||
|
<div class="col span-2">
|
||||||
|
<label>متن سیگنال
|
||||||
|
<textarea name="quote" rows="6" required>${escapeHtml(r.quote)}</textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col span-2 row-end">
|
||||||
|
<button type="submit" class="primary">${isEdit ? 'ذخیره تغییرات' : 'ایجاد'}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$('#backBtn').addEventListener('click', () => renderRisksList());
|
||||||
|
|
||||||
|
$('#editorForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const fd = new FormData(e.target);
|
||||||
|
const payload = {
|
||||||
|
name: fd.get('name'),
|
||||||
|
quote: fd.get('quote'),
|
||||||
|
date: fd.get('date') || null,
|
||||||
|
level: fd.get('level'),
|
||||||
|
sortOrder: Number(fd.get('sortOrder') || 0),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
if (isEdit) await api(`/api/risks/${risk.id}`, { method: 'PUT', body: payload });
|
||||||
|
else await api('/api/risks', { method: 'POST', body: payload });
|
||||||
|
renderRisksList();
|
||||||
|
} catch (err) {
|
||||||
|
alert('ذخیره ناموفق بود: ' + err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (getToken()) renderList();
|
if (getToken()) renderList();
|
||||||
else renderLogin();
|
else renderLogin();
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,40 @@ textarea { resize: vertical; min-height: 80px; }
|
||||||
.topbar .brand { font-weight: 900; font-size: 15px; letter-spacing: -.3px; }
|
.topbar .brand { font-weight: 900; font-size: 15px; letter-spacing: -.3px; }
|
||||||
.topbar .actions { display: flex; gap: 10px; }
|
.topbar .actions { display: flex; gap: 10px; }
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.tabs button {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 8px 18px;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.tabs button:hover {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.tabs button.tab-active {
|
||||||
|
color: var(--ink);
|
||||||
|
border-bottom-color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-row { grid-template-columns: 1fr auto; }
|
||||||
|
.risk-row .row-meta strong { font-size: 13px; }
|
||||||
|
|
||||||
|
.pill.level-critical { background: #7f1d1d; color: white; }
|
||||||
|
.pill.level-high { background: #b91c1c; color: white; }
|
||||||
|
.pill.level-medium { background: #92400e; color: white; }
|
||||||
|
.pill.level-low { background: #166534; color: white; }
|
||||||
|
.pill.level-opportunity { background: #1e40af; color: white; }
|
||||||
|
|
||||||
.page { max-width: 1100px; margin: 0 auto; padding: 32px; }
|
.page { max-width: 1100px; margin: 0 auto; padding: 32px; }
|
||||||
.page h2 { font-size: 22px; letter-spacing: -.5px; margin: 0 0 24px; }
|
.page h2 { font-size: 22px; letter-spacing: -.5px; margin: 0 0 24px; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import { db, upsertPrice } from './db.js';
|
||||||
|
|
||||||
|
const SOURCES = [
|
||||||
|
{ url: 'https://www.tgju.org/basemetal', kind: 'basemetal', defaultUnit: 'دلار/تن' },
|
||||||
|
{ url: 'https://www.tgju.org/gold-global', kind: 'gold', defaultUnit: 'دلار/اونس' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const USER_AGENT =
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
|
||||||
|
'(KHTML, like Gecko) Chrome/125.0 Safari/537.36';
|
||||||
|
|
||||||
|
async function fetchPage(url, timeoutMs = 20000) {
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
signal: ctrl.signal,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'fa,en;q=0.8',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
||||||
|
return await res.text();
|
||||||
|
} finally {
|
||||||
|
clearTimeout(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Persian/Arabic digits to ASCII, drop thousand separators, keep sign/decimal
|
||||||
|
function toNumber(raw) {
|
||||||
|
if (raw == null) return null;
|
||||||
|
const ascii = String(raw)
|
||||||
|
.replace(/[۰-۹]/g, (d) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d)))
|
||||||
|
.replace(/[٠-٩]/g, (d) => String('٠١٢٣٤٥٦٧٨٩'.indexOf(d)))
|
||||||
|
.replace(/[,،\s]/g, '')
|
||||||
|
.replace(/[()٪%]/g, '')
|
||||||
|
.replace(/[^\d.+\-]/g, '');
|
||||||
|
if (!ascii || ascii === '+' || ascii === '-' || ascii === '.') return null;
|
||||||
|
const n = parseFloat(ascii);
|
||||||
|
return Number.isFinite(n) ? n : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the TGJU change cell — known formats:
|
||||||
|
// "(0.26%) 12.38" → pct=0.26, value=12.38
|
||||||
|
// "(-0.5%) -55" → pct=-0.5, value=-55
|
||||||
|
// "0" or "" → pct=0, value=0
|
||||||
|
function parseChangeCell(raw) {
|
||||||
|
if (!raw) return { changePct: null, changeValue: null };
|
||||||
|
const text = raw.trim();
|
||||||
|
if (!text || text === '0' || text === '۰' || text === '٠') {
|
||||||
|
return { changePct: 0, changeValue: 0 };
|
||||||
|
}
|
||||||
|
let changePct = null;
|
||||||
|
let changeValue = null;
|
||||||
|
const pctMatch = text.match(/\(\s*(-?[\d.,۰-۹٠-٩]+)\s*%\s*\)/);
|
||||||
|
if (pctMatch) changePct = toNumber(pctMatch[1]);
|
||||||
|
// Remove the parenthesised pct, what's left should be the absolute change
|
||||||
|
const withoutPct = text.replace(/\([^)]*\)/g, '').trim();
|
||||||
|
if (withoutPct) changeValue = toNumber(withoutPct);
|
||||||
|
return { changePct, changeValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePrices(html, sourceUrl, defaultUnit) {
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const byName = new Map();
|
||||||
|
|
||||||
|
$('tr[data-market-row], tr[data-market-nameslug]').each((idx, tr) => {
|
||||||
|
const $tr = $(tr);
|
||||||
|
const symbolAttr = $tr.attr('data-market-row') || $tr.attr('data-market-nameslug') || `row-${idx}`;
|
||||||
|
|
||||||
|
// Name lives in <th> for these tables (not the first <td>).
|
||||||
|
let name = $tr.find('th').first().text().trim();
|
||||||
|
if (!name) name = $tr.find('a').first().text().trim();
|
||||||
|
name = name.replace(/\s+/g, ' ').trim();
|
||||||
|
if (!name || /^(عنوان|بازار|نام)$/.test(name)) return;
|
||||||
|
if (name.length > 60) return;
|
||||||
|
|
||||||
|
// Price: prefer the row-level data-price attr (always clean numeric),
|
||||||
|
// else first <td>'s text.
|
||||||
|
const dataPrice = $tr.attr('data-price');
|
||||||
|
let value = toNumber(dataPrice);
|
||||||
|
if (value === null) {
|
||||||
|
const firstTd = $tr.find('td').first().text().trim();
|
||||||
|
value = toNumber(firstTd);
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(value) || value === 0) return;
|
||||||
|
|
||||||
|
// Change: TGJU puts the entire change expression in the SECOND <td>
|
||||||
|
// ("(0.26%) 12.38" format). Earlier rows of the page may not have it,
|
||||||
|
// so we tolerate missing data.
|
||||||
|
const changeCellRaw = $tr.find('td').eq(1).text().trim();
|
||||||
|
const { changePct, changeValue } = parseChangeCell(changeCellRaw);
|
||||||
|
|
||||||
|
// Dedup by visible name; prefer non-generic symbols.
|
||||||
|
const existing = byName.get(name);
|
||||||
|
const isGeneric = /^general[_-]?\d+$/i.test(symbolAttr);
|
||||||
|
if (existing) {
|
||||||
|
const existingIsGeneric = /^general[_-]?\d+$/i.test(existing.symbol);
|
||||||
|
if (!existingIsGeneric || isGeneric) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byName.set(name, {
|
||||||
|
symbol: symbolAttr,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
unit: defaultUnit,
|
||||||
|
changeValue,
|
||||||
|
changePct,
|
||||||
|
sourceUrl,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(byName.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scrapeOnce() {
|
||||||
|
// Collect results before mutating DB — if ALL sources fail, keep old data.
|
||||||
|
const successful = [];
|
||||||
|
for (const src of SOURCES) {
|
||||||
|
try {
|
||||||
|
const html = await fetchPage(src.url);
|
||||||
|
const items = parsePrices(html, src.url, src.defaultUnit);
|
||||||
|
successful.push({ src, items });
|
||||||
|
console.log(`[scraper] ${src.kind}: ${items.length} prices parsed`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[scraper] ${src.kind} failed: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (successful.length === 0) {
|
||||||
|
console.log('[scraper] all sources failed — keeping previous data');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// At least one source returned — replace the rows from those sources only.
|
||||||
|
// This way a temporary failure of one page doesn't wipe the other's data.
|
||||||
|
for (const { src, items } of successful) {
|
||||||
|
db.prepare('DELETE FROM prices WHERE source_url = ?').run(src.url);
|
||||||
|
for (const it of items) upsertPrice(it);
|
||||||
|
}
|
||||||
|
return successful.reduce((s, x) => s + x.items.length, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let interval = null;
|
||||||
|
export function startScraperLoop(intervalMs = 2 * 60 * 1000) {
|
||||||
|
if (interval) return;
|
||||||
|
setTimeout(() => { scrapeOnce().catch((e) => console.error('[scraper] initial:', e)); }, 1500);
|
||||||
|
interval = setInterval(() => {
|
||||||
|
scrapeOnce().catch((e) => console.error('[scraper] tick:', e));
|
||||||
|
}, intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopScraperLoop() {
|
||||||
|
if (interval) { clearInterval(interval); interval = null; }
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,8 @@ import { nanoid } from 'nanoid';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { db, rowToArticle } from './db.js';
|
import { db, rowToArticle, rowToRiskSignal, rowToPrice } from './db.js';
|
||||||
|
import { startScraperLoop, scrapeOnce } from './scraper.js';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const PORT = Number(process.env.PORT || 3001);
|
const PORT = Number(process.env.PORT || 3001);
|
||||||
|
|
@ -158,8 +159,94 @@ app.delete('/api/articles/:id', authRequired, (req, res) => {
|
||||||
res.status(204).end();
|
res.status(204).end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- risk signals ----------
|
||||||
|
const RISK_FIELDS = `id, quote, name, date, level, sort_order, created_at, updated_at`;
|
||||||
|
|
||||||
|
app.get('/api/risks', (req, res) => {
|
||||||
|
const limit = Math.min(Number(req.query.limit) || 100, 500);
|
||||||
|
const rows = db
|
||||||
|
.prepare(`SELECT ${RISK_FIELDS} FROM risk_signals ORDER BY sort_order ASC, created_at DESC LIMIT ?`)
|
||||||
|
.all(limit);
|
||||||
|
res.json(rows.map(rowToRiskSignal));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/risks/:id', (req, res) => {
|
||||||
|
const row = db.prepare(`SELECT ${RISK_FIELDS} FROM risk_signals WHERE id = ?`).get(req.params.id);
|
||||||
|
if (!row) return res.status(404).json({ error: 'not_found' });
|
||||||
|
res.json(rowToRiskSignal(row));
|
||||||
|
});
|
||||||
|
|
||||||
|
const ALLOWED_LEVELS = new Set(['critical', 'high', 'medium', 'low', 'opportunity']);
|
||||||
|
function normalizeRiskBody(b) {
|
||||||
|
const level = ALLOWED_LEVELS.has(b.level) ? b.level : null;
|
||||||
|
return {
|
||||||
|
quote: String(b.quote || '').trim(),
|
||||||
|
name: String(b.name || '').trim(),
|
||||||
|
date: b.date || null,
|
||||||
|
level,
|
||||||
|
sort_order: Number(b.sortOrder) || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
app.post('/api/risks', authRequired, (req, res) => {
|
||||||
|
const r = normalizeRiskBody(req.body || {});
|
||||||
|
if (!r.quote || !r.name || !r.level) return res.status(400).json({ error: 'quote_name_level_required' });
|
||||||
|
const id = nanoid(14);
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO risk_signals (id, quote, name, date, level, sort_order)
|
||||||
|
VALUES (@id, @quote, @name, @date, @level, @sort_order)
|
||||||
|
`).run({ id, ...r });
|
||||||
|
const row = db.prepare(`SELECT ${RISK_FIELDS} FROM risk_signals WHERE id = ?`).get(id);
|
||||||
|
res.status(201).json(rowToRiskSignal(row));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/risks/:id', authRequired, (req, res) => {
|
||||||
|
const existing = db.prepare('SELECT id FROM risk_signals WHERE id = ?').get(req.params.id);
|
||||||
|
if (!existing) return res.status(404).json({ error: 'not_found' });
|
||||||
|
const r = normalizeRiskBody(req.body || {});
|
||||||
|
if (!r.quote || !r.name || !r.level) return res.status(400).json({ error: 'quote_name_level_required' });
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE risk_signals SET
|
||||||
|
quote = @quote, name = @name, date = @date, level = @level,
|
||||||
|
sort_order = @sort_order, updated_at = datetime('now')
|
||||||
|
WHERE id = @id
|
||||||
|
`).run({ id: req.params.id, ...r });
|
||||||
|
const row = db.prepare(`SELECT ${RISK_FIELDS} FROM risk_signals WHERE id = ?`).get(req.params.id);
|
||||||
|
res.json(rowToRiskSignal(row));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/risks/:id', authRequired, (req, res) => {
|
||||||
|
const info = db.prepare('DELETE FROM risk_signals WHERE id = ?').run(req.params.id);
|
||||||
|
if (info.changes === 0) return res.status(404).json({ error: 'not_found' });
|
||||||
|
res.status(204).end();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- prices (scraped from tgju.org every 2 min) ----------
|
||||||
|
app.get('/api/prices', (_req, res) => {
|
||||||
|
const rows = db
|
||||||
|
.prepare('SELECT symbol, name, value, unit, change_value, change_pct, source_url, fetched_at FROM prices ORDER BY name')
|
||||||
|
.all();
|
||||||
|
res.json(rows.map(rowToPrice));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/prices/refresh', authRequired, async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const n = await scrapeOnce();
|
||||||
|
res.json({ ok: true, scraped: n });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/health', (_req, res) => res.json({ ok: true }));
|
app.get('/api/health', (_req, res) => res.json({ ok: true }));
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Panel running at ${PUBLIC_ORIGIN}`);
|
console.log(`Panel running at ${PUBLIC_ORIGIN}`);
|
||||||
|
const intervalMs = Number(process.env.SCRAPER_INTERVAL_MS) || 2 * 60 * 1000;
|
||||||
|
if (process.env.SCRAPER_DISABLED !== '1') {
|
||||||
|
startScraperLoop(intervalMs);
|
||||||
|
console.log(`[scraper] tgju.org polling every ${intervalMs / 1000}s`);
|
||||||
|
} else {
|
||||||
|
console.log('[scraper] disabled via SCRAPER_DISABLED=1');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ function SocialIcon({ label }: { label: string }) {
|
||||||
function FooterLink({ href, label }: { href: string; label: string }) {
|
function FooterLink({ href, label }: { href: string; label: string }) {
|
||||||
const [hovered, setHovered] = useState(false)
|
const [hovered, setHovered] = useState(false)
|
||||||
return (
|
return (
|
||||||
<li style={{ marginBottom: 10 }}>
|
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
|
@ -41,45 +40,19 @@ function FooterLink({ href, label }: { href: string; label: string }) {
|
||||||
transition: 'color 150ms',
|
transition: 'color 150ms',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLUMNS = [
|
const FOOTER_LINKS = [
|
||||||
{
|
{ label: 'توضیح اندیشکده', href: '#' },
|
||||||
heading: 'گزارشها',
|
|
||||||
links: [
|
|
||||||
{ label: 'گزارشهای فصلی', href: '#' },
|
|
||||||
{ label: 'Flash Reports', href: '#' },
|
|
||||||
{ label: 'گزارشهای ریسک', href: '#' },
|
|
||||||
{ label: 'گزارش رایگان', href: '#' },
|
|
||||||
{ label: 'آرشیو کامل', href: '#' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: 'اندیشکده',
|
|
||||||
links: [
|
|
||||||
{ label: 'درباره ما', href: '#' },
|
{ label: 'درباره ما', href: '#' },
|
||||||
{ label: 'تیم تحریریه', href: '#' },
|
|
||||||
{ label: 'اشتراک سازمانی', href: '#' },
|
|
||||||
{ label: 'همکاری با ما', href: '#' },
|
{ label: 'همکاری با ما', href: '#' },
|
||||||
{ label: 'تماس', href: '#' },
|
{ label: 'تماس با ما', href: '#' },
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: 'منابع',
|
|
||||||
links: [
|
|
||||||
{ label: 'داشبورد بازار', href: '#' },
|
|
||||||
{ label: 'رویدادها', href: '#' },
|
|
||||||
{ label: 'اسکنر جهانی', href: '#' },
|
|
||||||
{ label: 'خبرنامه', href: '#' },
|
|
||||||
{ label: 'درگاه پرداخت', href: '#' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
|
|
@ -88,44 +61,22 @@ export function Footer() {
|
||||||
dir="rtl"
|
dir="rtl"
|
||||||
style={{ backgroundColor: 'var(--ink)', color: 'rgba(255,255,255,0.6)' }}
|
style={{ backgroundColor: 'var(--ink)', color: 'rgba(255,255,255,0.6)' }}
|
||||||
>
|
>
|
||||||
{/* TOP GRID */}
|
{/* TOP SECTION */}
|
||||||
<div style={{ padding: '64px 0' }} className="max-w-7xl mx-auto w-full px-12 max-md:px-5">
|
<div style={{ padding: '56px 0 48px' }} className="max-w-7xl mx-auto w-full px-12 max-md:px-5">
|
||||||
<div
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: 40 }}>
|
||||||
style={{
|
|
||||||
display: 'grid',
|
{/* Brand + description */}
|
||||||
gridTemplateColumns: '2fr 1px 1fr 1px 1fr 1px 1fr',
|
<div style={{ maxWidth: 360 }}>
|
||||||
gap: 0,
|
<div style={{ fontSize: 22, fontWeight: 900, color: '#fff', lineHeight: 1.2, marginBottom: 4 }}>
|
||||||
}}
|
|
||||||
className="mq-stack max-md:grid-cols-1 max-md:gap-10"
|
|
||||||
>
|
|
||||||
{/* Brand */}
|
|
||||||
<div style={{ paddingLeft: 0, paddingRight: 48 }} className="max-md:pr-0">
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: 22, fontWeight: 900, color: '#fff',
|
|
||||||
lineHeight: 1.2, marginBottom: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
اندیشکده فولاد آینده
|
اندیشکده فولاد آینده
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div style={{ fontSize: 10, letterSpacing: 3, color: 'rgba(255,255,255,0.3)', textTransform: 'uppercase', marginBottom: 20, direction: 'ltr', textAlign: 'right' }}>
|
||||||
style={{
|
|
||||||
fontSize: 10, letterSpacing: 3,
|
|
||||||
color: 'rgba(255,255,255,0.3)', textTransform: 'uppercase',
|
|
||||||
marginBottom: 20, direction: 'ltr', textAlign: 'right',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
FUTURE STEEL POLICY INSTITUTE
|
FUTURE STEEL POLICY INSTITUTE
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: 13, lineHeight: 1.75, color: 'rgba(255,255,255,0.5)', marginBottom: 20, maxWidth: 300 }}>
|
<p style={{ fontSize: 13, lineHeight: 1.8, color: 'rgba(255,255,255,0.5)', marginBottom: 24 }}>
|
||||||
مرکز مستقل تحلیل راهبردی و سیاستگذاری صنعت فولاد و فلزات کشور
|
مرکز مستقل تحلیل راهبردی و سیاستگذاری صنعت فولاد و فلزات کشور — زیر نظر شرکت فولاد مبارکه
|
||||||
</p>
|
</p>
|
||||||
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.4)', lineHeight: 2, marginBottom: 20 }}>
|
<div style={{ display: 'flex', gap: 6 }}>
|
||||||
<div>تهران · خیابان ولیعصر</div>
|
|
||||||
<div>info@iransteel.ir</div>
|
|
||||||
<div dir="ltr" style={{ textAlign: 'right' }}>۰۲۱–۸۸۱۲۳۴۵۶</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
|
||||||
<SocialIcon label="in" />
|
<SocialIcon label="in" />
|
||||||
<SocialIcon label="tg" />
|
<SocialIcon label="tg" />
|
||||||
<SocialIcon label="tw" />
|
<SocialIcon label="tw" />
|
||||||
|
|
@ -133,67 +84,24 @@ export function Footer() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dividers + link columns */}
|
{/* 4 links */}
|
||||||
{COLUMNS.map((col, i) => (
|
<nav aria-label="پیوندهای پایین" style={{ display: 'flex', gap: 32, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
<>
|
{FOOTER_LINKS.map(link => (
|
||||||
<div
|
|
||||||
key={`div-${i}`}
|
|
||||||
aria-hidden="true"
|
|
||||||
style={{ background: 'rgba(255,255,255,0.08)', width: 1 }}
|
|
||||||
className="max-md:hidden"
|
|
||||||
/>
|
|
||||||
<div key={col.heading} style={{ padding: '0 36px' }} className="max-md:p-0">
|
|
||||||
<h5
|
|
||||||
style={{
|
|
||||||
fontSize: 9, fontWeight: 700, letterSpacing: 3,
|
|
||||||
textTransform: 'uppercase', color: 'rgba(255,255,255,0.35)',
|
|
||||||
margin: '0 0 16px', paddingBottom: 10,
|
|
||||||
borderBottom: '1px solid rgba(255,255,255,0.1)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{col.heading}
|
|
||||||
</h5>
|
|
||||||
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
|
|
||||||
{col.links.map(link => (
|
|
||||||
<FooterLink key={link.label} href={link.href} label={link.label} />
|
<FooterLink key={link.label} href={link.href} label={link.label} />
|
||||||
))}
|
))}
|
||||||
</ul>
|
</nav>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* BOTTOM ROW */}
|
{/* BOTTOM ROW */}
|
||||||
|
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)' }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{ padding: '22px 0', fontSize: 13, color: 'rgba(255,255,255,0.85)', lineHeight: 1.8, textAlign: 'center', fontWeight: 500 }}
|
||||||
borderTop: '1px solid rgba(255,255,255,0.08)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: '20px 48px',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
fontSize: 10,
|
|
||||||
color: 'rgba(255,255,255,0.25)',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: 12,
|
|
||||||
}}
|
|
||||||
className="max-w-7xl mx-auto w-full px-12 max-md:px-5"
|
className="max-w-7xl mx-auto w-full px-12 max-md:px-5"
|
||||||
>
|
>
|
||||||
<span>© ۱۴۰۳ اندیشکده فولاد آینده — تمام حقوق محفوظ است</span>
|
<div>تمامی حقوق این وب سایت محفوظ و متعلق به شرکت فولاد مبارکه است.</div>
|
||||||
<div style={{ display: 'flex', gap: 18 }}>
|
<div style={{ color: 'rgba(255,255,255,0.55)', marginTop: 6, fontSize: 12 }}>
|
||||||
{['حریم خصوصی', 'شرایط استفاده', 'درگاه پرداخت', 'نقشه سایت'].map(l => (
|
با افتخار قدرت گرفته از دیدوان؛ چشم همیشه باز مدیران
|
||||||
<a
|
|
||||||
key={l}
|
|
||||||
href="#"
|
|
||||||
style={{ color: 'rgba(255,255,255,0.25)', textDecoration: 'none' }}
|
|
||||||
>
|
|
||||||
{l}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type { CSSProperties } from 'react'
|
||||||
import { NavLink } from 'react-router-dom'
|
import { NavLink } from 'react-router-dom'
|
||||||
import { Search, Menu, X } from 'lucide-react'
|
import { Search, Menu, X } from 'lucide-react'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { useLang } from '@/context/LangContext'
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────
|
/* ─────────────────────────────────────────────────────────
|
||||||
DESIGN TOKENS
|
DESIGN TOKENS
|
||||||
|
|
@ -21,17 +22,21 @@ const T = {
|
||||||
/* ─────────────────────────────────────────────────────────
|
/* ─────────────────────────────────────────────────────────
|
||||||
DATA
|
DATA
|
||||||
───────────────────────────────────────────────────────── */
|
───────────────────────────────────────────────────────── */
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS: { fa: string; en: string; to: string }[] = [
|
||||||
{ label: 'گزارشها', to: '/reports' },
|
{ fa: 'خانه', en: 'Home', to: '/' },
|
||||||
{ label: 'گزارش ویژه', to: '/special' },
|
{ fa: 'رادار آینده', en: 'Future Radar', to: '/radar' },
|
||||||
{ label: 'ریسکها', to: '/risks' },
|
{ fa: 'مرز فناوری', en: 'Tech Frontier', to: '/technology' },
|
||||||
{ label: 'رویدادها', to: '/events' },
|
{ fa: 'نبض صنعت', en: 'Industry Pulse', to: '/pulse' },
|
||||||
{ label: 'اسکنر جهانی', to: '/scanner' },
|
{ fa: 'رویدادها', en: 'Events', to: '/events' },
|
||||||
{ label: 'تیم تحریریه', to: '/team' },
|
{ fa: 'عضویت', en: 'Membership', to: '/membership' },
|
||||||
{ label: 'آرشیو', to: '/archive' },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const META_LEFT = ['درباره ما', 'کارشناسان', 'همکاری', 'رسانه']
|
const META_LEFT: { fa: string; en: string }[] = [
|
||||||
|
{ fa: 'درباره ما', en: 'About' },
|
||||||
|
{ fa: 'کارشناسان', en: 'Experts' },
|
||||||
|
{ fa: 'همکاری', en: 'Careers' },
|
||||||
|
{ fa: 'رسانه', en: 'Press' },
|
||||||
|
]
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────
|
/* ─────────────────────────────────────────────────────────
|
||||||
NAV LINK (used in main + drawer)
|
NAV LINK (used in main + drawer)
|
||||||
|
|
@ -103,6 +108,7 @@ export default function Header() {
|
||||||
const [searchOpen, setSearchOpen] = useState(false)
|
const [searchOpen, setSearchOpen] = useState(false)
|
||||||
const [scrolled, setScrolled] = useState(false)
|
const [scrolled, setScrolled] = useState(false)
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const { lang, toggle: toggleLang } = useLang()
|
||||||
|
|
||||||
/* scroll listener — toggles glass + collapses meta strip */
|
/* scroll listener — toggles glass + collapses meta strip */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -142,7 +148,7 @@ export default function Header() {
|
||||||
backdropFilter: scrolled ? 'blur(18px) saturate(160%)' : 'none',
|
backdropFilter: scrolled ? 'blur(18px) saturate(160%)' : 'none',
|
||||||
WebkitBackdropFilter: scrolled ? 'blur(18px) saturate(160%)' : 'none',
|
WebkitBackdropFilter: scrolled ? 'blur(18px) saturate(160%)' : 'none',
|
||||||
borderBottom: scrolled ? '1px solid rgba(26,23,18,0.08)' : '1px solid transparent',
|
borderBottom: scrolled ? '1px solid rgba(26,23,18,0.08)' : '1px solid transparent',
|
||||||
direction: 'rtl',
|
direction: lang === 'fa' ? 'rtl' : 'ltr',
|
||||||
transition: 'background 220ms ease, backdrop-filter 220ms ease, border-color 220ms ease',
|
transition: 'background 220ms ease, backdrop-filter 220ms ease, border-color 220ms ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -172,14 +178,14 @@ export default function Header() {
|
||||||
>
|
>
|
||||||
{/* Right: date */}
|
{/* Right: date */}
|
||||||
<span style={{ letterSpacing: '0.3px' }}>
|
<span style={{ letterSpacing: '0.3px' }}>
|
||||||
شنبه ۲۱ بهمن ۱۴۰۳ · دوره دوازدهم
|
{lang === 'fa' ? 'شنبه ۲۱ بهمن ۱۴۰۳ · دوره دوازدهم' : 'Sat 10 Feb 2025 · Vol. XII'}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Left: meta links + login */}
|
{/* Left: meta links + login + lang toggle */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
|
||||||
{META_LEFT.map((item) => (
|
{META_LEFT.map((item) => (
|
||||||
<a
|
<a
|
||||||
key={item}
|
key={item.fa}
|
||||||
href="#"
|
href="#"
|
||||||
style={{
|
style={{
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
|
|
@ -190,7 +196,7 @@ export default function Header() {
|
||||||
onMouseEnter={e => (e.currentTarget.style.color = 'var(--paper)')}
|
onMouseEnter={e => (e.currentTarget.style.color = 'var(--paper)')}
|
||||||
onMouseLeave={e => (e.currentTarget.style.color = 'rgba(244,239,231,0.7)')}
|
onMouseLeave={e => (e.currentTarget.style.color = 'rgba(244,239,231,0.7)')}
|
||||||
>
|
>
|
||||||
{item}
|
{lang === 'fa' ? item.fa : item.en}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
<span style={{ color: 'rgba(244,239,231,0.2)' }}>·</span>
|
<span style={{ color: 'rgba(244,239,231,0.2)' }}>·</span>
|
||||||
|
|
@ -203,18 +209,26 @@ export default function Header() {
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
ورود
|
{lang === 'fa' ? 'ورود' : 'Login'}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<button
|
||||||
href="#"
|
onClick={toggleLang}
|
||||||
style={{
|
style={{
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: 'rgba(244,239,231,0.7)',
|
color: 'rgba(244,239,231,0.7)',
|
||||||
textDecoration: 'none',
|
background: 'transparent',
|
||||||
|
border: '1px solid rgba(244,239,231,0.2)',
|
||||||
|
padding: '2px 8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
letterSpacing: '0.5px',
|
||||||
|
transition: 'color 120ms, border-color 120ms',
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={e => { e.currentTarget.style.color = 'var(--paper)'; e.currentTarget.style.borderColor = 'rgba(244,239,231,0.5)' }}
|
||||||
|
onMouseLeave={e => { e.currentTarget.style.color = 'rgba(244,239,231,0.7)'; e.currentTarget.style.borderColor = 'rgba(244,239,231,0.2)' }}
|
||||||
>
|
>
|
||||||
EN
|
{lang === 'fa' ? 'EN' : 'FA'}
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -290,7 +304,7 @@ export default function Header() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{NAV_ITEMS.map((item) => (
|
{NAV_ITEMS.map((item) => (
|
||||||
<MainNavItem key={item.to} to={item.to} label={item.label} />
|
<MainNavItem key={item.to} to={item.to} label={lang === 'fa' ? item.fa : item.en} />
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
@ -467,7 +481,7 @@ export default function Header() {
|
||||||
<DrawerNavItem
|
<DrawerNavItem
|
||||||
key={item.to}
|
key={item.to}
|
||||||
to={item.to}
|
to={item.to}
|
||||||
label={item.label}
|
label={lang === 'fa' ? item.fa : item.en}
|
||||||
onClose={() => setMobileOpen(false)}
|
onClose={() => setMobileOpen(false)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -484,10 +498,10 @@ export default function Header() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RedCtaButton style={{ width: '100%', padding: '10px 16px' }}>
|
<RedCtaButton style={{ width: '100%', padding: '10px 16px' }}>
|
||||||
اشتراک سازمانی
|
{lang === 'fa' ? 'اشتراک سازمانی' : 'Membership'}
|
||||||
</RedCtaButton>
|
</RedCtaButton>
|
||||||
<OutlineButton style={{ width: '100%', padding: '10px 16px' }}>
|
<OutlineButton style={{ width: '100%', padding: '10px 16px' }}>
|
||||||
ورود
|
{lang === 'fa' ? 'ورود' : 'Login'}
|
||||||
</OutlineButton>
|
</OutlineButton>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -499,11 +513,11 @@ export default function Header() {
|
||||||
>
|
>
|
||||||
{META_LEFT.map((item) => (
|
{META_LEFT.map((item) => (
|
||||||
<a
|
<a
|
||||||
key={item}
|
key={item.fa}
|
||||||
href="#"
|
href="#"
|
||||||
style={{ fontSize: 11, color: T.ink4, textDecoration: 'none' }}
|
style={{ fontSize: 11, color: T.ink4, textDecoration: 'none' }}
|
||||||
>
|
>
|
||||||
{item}
|
{lang === 'fa' ? item.fa : item.en}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,45 @@
|
||||||
import { marketPrices } from '@/data/market';
|
import { useEffect, useState } from 'react';
|
||||||
import type { MarketPrice } from '@/data/market';
|
import { marketPrices as fallbackPrices } from '@/data/market';
|
||||||
|
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';
|
||||||
|
|
||||||
|
type ApiPrice = {
|
||||||
|
symbol: string;
|
||||||
|
name: string;
|
||||||
|
value: number | null;
|
||||||
|
unit: string | null;
|
||||||
|
changeValue: number | null;
|
||||||
|
changePct: number | null;
|
||||||
|
sourceUrl: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function defaultUnitFor(sourceUrl: string | null): string {
|
||||||
|
if (!sourceUrl) return '';
|
||||||
|
if (sourceUrl.includes('gold-global')) return 'دلار/اونس';
|
||||||
|
if (sourceUrl.includes('basemetal')) return 'دلار/تن';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiToMarketPrices(items: ApiPrice[]): MarketPrice[] {
|
||||||
|
return items
|
||||||
|
.filter((p) => typeof p.value === 'number' && p.value > 0)
|
||||||
|
.map((p): MarketPrice => {
|
||||||
|
const pct = p.changePct ?? 0;
|
||||||
|
const trend: PriceTrend = pct > 0 ? 'up' : pct < 0 ? 'down' : 'flat';
|
||||||
|
return {
|
||||||
|
id: p.symbol,
|
||||||
|
name: p.name,
|
||||||
|
value: p.value as number,
|
||||||
|
unit: p.unit || defaultUnitFor(p.sourceUrl),
|
||||||
|
change: p.changeValue ?? 0,
|
||||||
|
changePercent: pct,
|
||||||
|
trend,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function formatValue(item: MarketPrice): string {
|
function formatValue(item: MarketPrice): string {
|
||||||
return item.value.toLocaleString('fa-IR');
|
return item.value.toLocaleString('fa-IR');
|
||||||
|
|
@ -70,6 +110,25 @@ function TickerItem({ item }: { item: MarketPrice }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MarketStrip() {
|
export function MarketStrip() {
|
||||||
|
const [marketPrices, setMarketPrices] = useState<MarketPrice[]>(fallbackPrices);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
const load = () => {
|
||||||
|
fetch(`${PANEL_API}/api/prices`)
|
||||||
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
|
.then((data: ApiPrice[] | null) => {
|
||||||
|
if (cancelled || !Array.isArray(data) || data.length === 0) return;
|
||||||
|
const mapped = apiToMarketPrices(data);
|
||||||
|
if (mapped.length > 0) setMarketPrices(mapped);
|
||||||
|
})
|
||||||
|
.catch(() => { /* keep fallback */ });
|
||||||
|
};
|
||||||
|
load();
|
||||||
|
const id = setInterval(load, 2 * 60 * 1000);
|
||||||
|
return () => { cancelled = true; clearInterval(id); };
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@ import { Outlet } from 'react-router-dom'
|
||||||
import Header from './Header'
|
import Header from './Header'
|
||||||
import { Footer } from './Footer'
|
import { Footer } from './Footer'
|
||||||
import { MarketStrip } from './MarketStrip'
|
import { MarketStrip } from './MarketStrip'
|
||||||
|
import { LangProvider } from '@/context/LangContext'
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
return (
|
return (
|
||||||
|
<LangProvider>
|
||||||
<div style={{ minHeight: '100dvh', display: 'flex', flexDirection: 'column' }}>
|
<div style={{ minHeight: '100dvh', display: 'flex', flexDirection: 'column' }}>
|
||||||
<Header />
|
<Header />
|
||||||
<MarketStrip />
|
<MarketStrip />
|
||||||
|
|
@ -13,5 +15,6 @@ export default function RootLayout() {
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
</LangProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ import Archive from '@/pages/Archive/Archive'
|
||||||
import Special from '@/pages/Special/Special'
|
import Special from '@/pages/Special/Special'
|
||||||
import Risks from '@/pages/Risks/Risks'
|
import Risks from '@/pages/Risks/Risks'
|
||||||
import Scanner from '@/pages/Scanner/Scanner'
|
import Scanner from '@/pages/Scanner/Scanner'
|
||||||
|
import Radar from '@/pages/Radar/Radar'
|
||||||
|
import Technology from '@/pages/Technology/Technology'
|
||||||
|
import Pulse from '@/pages/Pulse/Pulse'
|
||||||
|
import Membership from '@/pages/Membership/Membership'
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
|
|
@ -28,6 +32,10 @@ export const router = createBrowserRouter([
|
||||||
{ path: 'special', element: <Special /> },
|
{ path: 'special', element: <Special /> },
|
||||||
{ path: 'risks', element: <Risks /> },
|
{ path: 'risks', element: <Risks /> },
|
||||||
{ path: 'scanner', element: <Scanner /> },
|
{ path: 'scanner', element: <Scanner /> },
|
||||||
|
{ path: 'radar', element: <Radar /> },
|
||||||
|
{ path: 'technology', element: <Technology /> },
|
||||||
|
{ path: 'pulse', element: <Pulse /> },
|
||||||
|
{ path: 'membership', element: <Membership /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export function BentoGridItem({
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex flex-col bg-[var(--paper)] p-6 transition-colors duration-200 hover:bg-[var(--paper-2)]',
|
'group relative flex flex-col bg-[var(--paper)] p-8 transition-colors duration-200 hover:bg-[var(--paper-2)]',
|
||||||
onClick && 'cursor-pointer',
|
onClick && 'cursor-pointer',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
@ -102,9 +102,9 @@ export function BentoGridItem({
|
||||||
|
|
||||||
<h3
|
<h3
|
||||||
style={{
|
style={{
|
||||||
fontSize: 15,
|
fontSize: 18,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
letterSpacing: '-0.3px',
|
letterSpacing: '-0.4px',
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
color: 'var(--ink)',
|
color: 'var(--ink)',
|
||||||
}}
|
}}
|
||||||
|
|
@ -115,11 +115,11 @@ export function BentoGridItem({
|
||||||
{description && (
|
{description && (
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 14,
|
||||||
lineHeight: 1.75,
|
lineHeight: 1.8,
|
||||||
color: 'var(--ink-3)',
|
color: 'var(--ink-3)',
|
||||||
fontWeight: 300,
|
fontWeight: 400,
|
||||||
marginTop: 2,
|
marginTop: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{description}
|
{description}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { createContext, useContext, useState } from 'react'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
type Lang = 'fa' | 'en'
|
||||||
|
type LangCtx = { lang: Lang; toggle: () => void }
|
||||||
|
|
||||||
|
const LangContext = createContext<LangCtx>({ lang: 'fa', toggle: () => {} })
|
||||||
|
|
||||||
|
export function LangProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [lang, setLang] = useState<Lang>('fa')
|
||||||
|
const toggle = () => setLang(l => (l === 'fa' ? 'en' : 'fa'))
|
||||||
|
return <LangContext.Provider value={{ lang, toggle }}>{children}</LangContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLang() {
|
||||||
|
return useContext(LangContext)
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,9 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { events } from '@/data/events'
|
import { events } from '@/data/events'
|
||||||
import type { EventType } from '@/data/events'
|
import type { EventType } from '@/data/events'
|
||||||
|
import { useLang } from '@/context/LangContext'
|
||||||
|
|
||||||
/* ─── constants ──────────────────────────────────────────── */
|
type FilterValue = 'all' | EventType
|
||||||
type FilterValue = 'همه' | EventType
|
|
||||||
|
|
||||||
const FILTERS: { label: string; value: FilterValue }[] = [
|
|
||||||
{ label: 'همه', value: 'همه' },
|
|
||||||
{ label: 'کنگره', value: 'conference' },
|
|
||||||
{ label: 'نمایشگاه', value: 'exhibition' },
|
|
||||||
{ label: 'سمینار', value: 'seminar' },
|
|
||||||
{ label: 'جهانی', value: 'international' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const TYPE_LABELS: Record<EventType, string> = {
|
|
||||||
conference: 'کنگره',
|
|
||||||
exhibition: 'نمایشگاه',
|
|
||||||
seminar: 'سمینار',
|
|
||||||
international: 'جهانی',
|
|
||||||
}
|
|
||||||
|
|
||||||
const TYPE_BORDER_COLORS: Record<EventType, string> = {
|
const TYPE_BORDER_COLORS: Record<EventType, string> = {
|
||||||
conference: 'var(--ink)',
|
conference: 'var(--ink)',
|
||||||
|
|
@ -27,174 +12,77 @@ const TYPE_BORDER_COLORS: Record<EventType, string> = {
|
||||||
international: 'var(--red)',
|
international: 'var(--red)',
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── event card ─────────────────────────────────────────── */
|
const TYPE_LABELS = {
|
||||||
function EventCard({ event }: { event: (typeof events)[0] }) {
|
fa: { conference: 'کنگره', exhibition: 'نمایشگاه', seminar: 'سمینار', international: 'جهانی' },
|
||||||
|
en: { conference: 'Conference', exhibition: 'Exhibition', seminar: 'Seminar', international: 'International' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILTERS = {
|
||||||
|
fa: [
|
||||||
|
{ label: 'همه', value: 'all' as FilterValue },
|
||||||
|
{ label: 'کنگره', value: 'conference' as FilterValue },
|
||||||
|
{ label: 'نمایشگاه', value: 'exhibition' as FilterValue },
|
||||||
|
{ label: 'سمینار', value: 'seminar' as FilterValue },
|
||||||
|
{ label: 'جهانی', value: 'international' as FilterValue },
|
||||||
|
],
|
||||||
|
en: [
|
||||||
|
{ label: 'All', value: 'all' as FilterValue },
|
||||||
|
{ label: 'Conference', value: 'conference' as FilterValue },
|
||||||
|
{ label: 'Exhibition', value: 'exhibition' as FilterValue },
|
||||||
|
{ label: 'Seminar', value: 'seminar' as FilterValue },
|
||||||
|
{ label: 'International', value: 'international' as FilterValue },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
fa: { heading: 'رویدادهای پیشرو', year: '۱۴۰۳–۱۴۰۴', empty: 'رویدادی در این دستهبندی یافت نشد' },
|
||||||
|
en: { heading: 'Upcoming Events', year: '1403–1404', empty: 'No events found in this category' },
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventCard({ event, lang }: { event: (typeof events)[0]; lang: 'fa' | 'en' }) {
|
||||||
const [hovered, setHovered] = useState(false)
|
const [hovered, setHovered] = useState(false)
|
||||||
const borderColor = TYPE_BORDER_COLORS[event.type]
|
const borderColor = TYPE_BORDER_COLORS[event.type]
|
||||||
|
const typeLabels = TYPE_LABELS[lang]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
style={{
|
style={{ border: '1px solid var(--rule-thin)', background: hovered ? 'var(--paper-2)' : 'var(--paper)', transition: 'background 140ms', display: 'flex', flexDirection: 'column' }}
|
||||||
border: '1px solid var(--rule-thin)',
|
|
||||||
background: hovered ? 'var(--paper-2)' : 'var(--paper)',
|
|
||||||
transition: 'background 140ms',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 0,
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
>
|
>
|
||||||
{/* date + type header */}
|
|
||||||
<div style={{ display: 'flex', borderBottom: '2px solid var(--ink)' }}>
|
<div style={{ display: 'flex', borderBottom: '2px solid var(--ink)' }}>
|
||||||
{/* date block */}
|
<div style={{ padding: '18px 24px', borderLeft: '1px solid var(--rule-thin)', minWidth: 100, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', background: 'var(--paper)', flexShrink: 0 }}>
|
||||||
<div
|
<span style={{ fontSize: 52, fontWeight: 900, lineHeight: 1, color: 'var(--ink)', letterSpacing: '-2px', display: 'block' }}>
|
||||||
style={{
|
{event.date.toLocaleString(lang === 'fa' ? 'fa-IR' : 'en-US')}
|
||||||
padding: '18px 24px',
|
|
||||||
borderLeft: '1px solid var(--rule-thin)',
|
|
||||||
minWidth: 100,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
background: 'var(--paper)',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 52,
|
|
||||||
fontWeight: 900,
|
|
||||||
lineHeight: 1,
|
|
||||||
color: 'var(--ink)',
|
|
||||||
letterSpacing: '-2px',
|
|
||||||
display: 'block',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{event.date.toLocaleString('fa-IR')}
|
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span style={{ fontSize: 11, fontWeight: 500, color: 'var(--ink-4)', marginTop: 4, display: 'block', whiteSpace: 'nowrap' }}>
|
||||||
style={{
|
{event.month} {event.year.toLocaleString(lang === 'fa' ? 'fa-IR' : 'en-US')}
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: 500,
|
|
||||||
color: 'var(--ink-4)',
|
|
||||||
marginTop: 4,
|
|
||||||
display: 'block',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{event.month} {event.year.toLocaleString('fa-IR')}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ flex: 1, padding: '18px 20px', display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 8 }}>
|
||||||
{/* type badge + title area top */}
|
<span style={{ display: 'inline-block', alignSelf: 'flex-start', fontSize: 10, fontWeight: 700, letterSpacing: '1.2px', color: borderColor, border: `1px solid ${borderColor}`, padding: '2px 8px' }}>
|
||||||
<div
|
{typeLabels[event.type]}
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
padding: '18px 20px',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: 'inline-block',
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: '1.2px',
|
|
||||||
color: borderColor,
|
|
||||||
border: `1px solid ${borderColor}`,
|
|
||||||
padding: '2px 8px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{TYPE_LABELS[event.type]}
|
|
||||||
</span>
|
</span>
|
||||||
|
<h3 style={{ fontSize: 16, fontWeight: 800, lineHeight: 1.45, color: 'var(--ink)', letterSpacing: '-0.3px' }}>{event.title}</h3>
|
||||||
<h3
|
|
||||||
style={{
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 800,
|
|
||||||
lineHeight: 1.45,
|
|
||||||
color: 'var(--ink)',
|
|
||||||
letterSpacing: '-0.3px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{event.title}
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* body */}
|
|
||||||
<div style={{ padding: '16px 20px', flex: 1, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
<div style={{ padding: '16px 20px', flex: 1, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||||
{/* location */}
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span
|
<span style={{ width: 4, height: 4, background: 'var(--ink-4)', flexShrink: 0, display: 'inline-block' }} />
|
||||||
style={{
|
<span style={{ fontSize: 12, color: 'var(--ink-4)', fontWeight: 400 }}>{event.location}، {event.city}</span>
|
||||||
width: 4,
|
</div>
|
||||||
height: 4,
|
<p style={{ fontSize: 13, fontWeight: 300, color: 'var(--ink-3)', lineHeight: 1.8 }}>{event.description}</p>
|
||||||
background: 'var(--ink-4)',
|
|
||||||
flexShrink: 0,
|
|
||||||
display: 'inline-block',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span style={{ fontSize: 12, color: 'var(--ink-4)', fontWeight: 400 }}>
|
|
||||||
{event.location}، {event.city}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* description */}
|
<div style={{ borderTop: '1px solid var(--rule-thin)', padding: '12px 20px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: 300,
|
|
||||||
color: 'var(--ink-3)',
|
|
||||||
lineHeight: 1.8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{event.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* footer */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
borderTop: '1px solid var(--rule-thin)',
|
|
||||||
padding: '12px 20px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{event.registrationOpen ? (
|
{event.registrationOpen ? (
|
||||||
<a
|
<a href="#" style={{ fontSize: 13, fontWeight: 700, color: 'var(--red)', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
href="#"
|
{lang === 'fa' ? 'ثبتنام' : 'Register'}<span style={{ fontSize: 15 }}>←</span>
|
||||||
style={{
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: 'var(--red)',
|
|
||||||
textDecoration: 'none',
|
|
||||||
letterSpacing: '-0.2px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
ثبتنام
|
|
||||||
<span style={{ fontSize: 15 }}>←</span>
|
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span style={{ fontSize: 11, fontWeight: 500, color: 'var(--ink-5)' }}>
|
||||||
style={{
|
{lang === 'fa' ? 'بهزودی' : 'Coming Soon'}
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: 500,
|
|
||||||
color: 'var(--ink-5)',
|
|
||||||
letterSpacing: '0.5px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
بهزودی
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -202,120 +90,39 @@ function EventCard({ event }: { event: (typeof events)[0] }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── main component ─────────────────────────────────────── */
|
|
||||||
export default function Events() {
|
export default function Events() {
|
||||||
const [activeFilter, setActiveFilter] = useState<FilterValue>('همه')
|
const { lang } = useLang()
|
||||||
|
const t = T[lang]
|
||||||
|
const filters = FILTERS[lang]
|
||||||
|
const [activeFilter, setActiveFilter] = useState<FilterValue>('all')
|
||||||
|
|
||||||
const filtered = activeFilter === 'همه'
|
const filtered = activeFilter === 'all' ? events : events.filter(e => e.type === activeFilter)
|
||||||
? events
|
|
||||||
: events.filter((e) => e.type === activeFilter)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={{ maxWidth: 1280, margin: '0 auto', padding: '64px 48px', direction: lang === 'fa' ? 'rtl' : 'ltr' }}>
|
||||||
style={{
|
<div style={{ display: 'flex', alignItems: 'stretch', borderBottom: '3px solid var(--ink)', marginBottom: 36 }}>
|
||||||
maxWidth: 1280,
|
<div style={{ background: 'var(--ink)', padding: '14px 28px', display: 'flex', alignItems: 'center' }}>
|
||||||
margin: '0 auto',
|
<h1 style={{ fontSize: 24, fontWeight: 900, color: 'var(--paper)', letterSpacing: '-0.5px', whiteSpace: 'nowrap' }}>{t.heading}</h1>
|
||||||
padding: '64px 48px',
|
|
||||||
direction: 'rtl',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* ─── Page title banner ─────────────────────────────── */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'stretch',
|
|
||||||
borderBottom: '3px solid var(--ink)',
|
|
||||||
marginBottom: 36,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
background: 'var(--ink)',
|
|
||||||
padding: '14px 28px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
style={{
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 900,
|
|
||||||
color: 'var(--paper)',
|
|
||||||
letterSpacing: '-0.5px',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
رویدادهای پیشرو
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', padding: '14px 0 14px 4px' }}>
|
||||||
<div
|
<span style={{ fontSize: 13, fontWeight: 500, color: 'var(--ink-4)' }}>{t.year}</span>
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '14px 0 14px 4px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: 500,
|
|
||||||
color: 'var(--ink-4)',
|
|
||||||
letterSpacing: '0.5px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
۱۴۰۳–۱۴۰۴
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ─── Filter tabs ───────────────────────────────────── */}
|
<div style={{ display: 'flex', gap: 0, borderBottom: '1px solid var(--rule-thin)', marginBottom: 40, overflowX: 'auto', scrollbarWidth: 'none' }}>
|
||||||
<div
|
{filters.map(f => {
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: 0,
|
|
||||||
borderBottom: '1px solid var(--rule-thin)',
|
|
||||||
marginBottom: 40,
|
|
||||||
overflowX: 'auto',
|
|
||||||
scrollbarWidth: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{FILTERS.map((f) => {
|
|
||||||
const isActive = activeFilter === f.value
|
const isActive = activeFilter === f.value
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={f.value}
|
key={f.value}
|
||||||
onClick={() => setActiveFilter(f.value)}
|
onClick={() => setActiveFilter(f.value)}
|
||||||
style={{
|
style={{ padding: '10px 24px', fontSize: 13, fontWeight: isActive ? 700 : 500, border: 'none', background: isActive ? 'var(--ink)' : 'transparent', color: isActive ? 'var(--paper)' : 'var(--ink-4)', cursor: 'pointer', fontFamily: 'inherit', whiteSpace: 'nowrap', flexShrink: 0, transition: 'all 120ms', marginBottom: -1, outline: 'none' }}
|
||||||
padding: '10px 24px',
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: isActive ? 700 : 500,
|
|
||||||
border: 'none',
|
|
||||||
borderBottom: isActive ? '2px solid transparent' : '2px solid transparent',
|
|
||||||
background: isActive ? 'var(--ink)' : 'transparent',
|
|
||||||
color: isActive ? 'var(--paper)' : 'var(--ink-4)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontFamily: 'inherit',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
flexShrink: 0,
|
|
||||||
transition: 'all 120ms',
|
|
||||||
position: 'relative',
|
|
||||||
marginBottom: -1,
|
|
||||||
outline: 'none',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{f.label}
|
{f.label}
|
||||||
{f.value !== 'همه' && (
|
{f.value !== 'all' && (
|
||||||
<span
|
<span style={{ marginLeft: 6, fontSize: 10, color: isActive ? 'rgba(244,239,231,0.6)' : 'var(--ink-6)' }}>
|
||||||
style={{
|
({events.filter(e => e.type === f.value).length})
|
||||||
marginRight: 6,
|
|
||||||
fontSize: 10,
|
|
||||||
color: isActive ? 'rgba(244,239,231,0.6)' : 'var(--ink-6)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
({events.filter((e) => e.type === f.value).length})
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -323,45 +130,18 @@ export default function Events() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ─── Events grid ───────────────────────────────────── */}
|
<div className="events-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 1, background: 'var(--rule-thin)' }}>
|
||||||
<div
|
{filtered.map(event => <EventCard key={event.id} event={event} lang={lang} />)}
|
||||||
className="events-grid"
|
{filtered.length % 2 !== 0 && <div style={{ background: 'var(--paper)' }} />}
|
||||||
style={{
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
||||||
gap: 1,
|
|
||||||
background: 'var(--rule-thin)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filtered.map((event) => (
|
|
||||||
<EventCard key={event.id} event={event} />
|
|
||||||
))}
|
|
||||||
{/* fill odd trailing cell */}
|
|
||||||
{filtered.length % 2 !== 0 && (
|
|
||||||
<div style={{ background: 'var(--paper)' }} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filtered.length === 0 && (
|
{filtered.length === 0 && (
|
||||||
<div
|
<div style={{ padding: '80px 0', textAlign: 'center', color: 'var(--ink-5)' }}>
|
||||||
style={{
|
<p style={{ fontSize: 15, fontWeight: 300 }}>{t.empty}</p>
|
||||||
padding: '80px 0',
|
|
||||||
textAlign: 'center',
|
|
||||||
color: 'var(--ink-5)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p style={{ fontSize: 15, fontWeight: 300 }}>رویدادی در این دستهبندی یافت نشد</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* responsive */}
|
<style>{`@media (max-width: 767px) { .events-grid { grid-template-columns: 1fr !important; } }`}</style>
|
||||||
<style>{`
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.events-grid {
|
|
||||||
grid-template-columns: 1fr !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,106 +1,37 @@
|
||||||
import HeroSection from './sections/HeroSection'
|
import HeroSection from './sections/HeroSection'
|
||||||
import SnapshotBar from './sections/SnapshotBar'
|
import SnapshotBar from './sections/SnapshotBar'
|
||||||
import GlobalScannerSection from './sections/GlobalScannerSection'
|
import GlobalScannerSection from './sections/GlobalScannerSection'
|
||||||
import ReportsCarouselSection from './sections/ReportsCarouselSection'
|
import SteelNewsletterSection from './sections/SteelNewsletterSection'
|
||||||
import LatestNewsBentoSection from './sections/LatestNewsBentoSection'
|
|
||||||
import EventsSection from './sections/EventsSection'
|
import EventsSection from './sections/EventsSection'
|
||||||
|
import FactoryBentoSection from './sections/FactoryBentoSection'
|
||||||
import RiskSection from './sections/RiskSection'
|
import RiskSection from './sections/RiskSection'
|
||||||
import TeamSection from './sections/TeamSection'
|
import { useLang } from '@/context/LangContext'
|
||||||
import PlansSection from './sections/PlansSection'
|
|
||||||
import PartnersSection from './sections/PartnersSection'
|
|
||||||
import NewsletterSection from './sections/NewsletterSection'
|
|
||||||
|
|
||||||
/* ─── Section chapter marker ─────────────────────────────
|
/* ─── Section chapter marker ─────────────────────────────
|
||||||
A thin full-bleed dark bar that stamps the start of every
|
A thin full-bleed dark bar that stamps the start of every
|
||||||
major section — creates instant, unmistakable boundaries.
|
major section — creates instant, unmistakable boundaries.
|
||||||
──────────────────────────────────────────────────────── */
|
──────────────────────────────────────────────────────── */
|
||||||
/* Minimal chapter mark — thin strip, matches next section's bg, no duplicate title */
|
function SectionLabel({ n, bg = 'paper' }: { n: string; bg?: 'paper' | 'paper-2' }) {
|
||||||
function SectionLabel({
|
|
||||||
n,
|
|
||||||
bg = 'paper',
|
|
||||||
}: {
|
|
||||||
n: string
|
|
||||||
/** matches next section bg so there's no color seam */
|
|
||||||
bg?: 'paper' | 'paper-2'
|
|
||||||
title?: string // ignored — editorial h2 inside each section owns the title
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={{ background: `var(--${bg})`, paddingTop: 44 }}>
|
||||||
style={{
|
<div className="max-w-7xl mx-auto px-12 max-md:px-5">
|
||||||
background: `var(--${bg})`,
|
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||||
borderTop: '1px solid var(--rule-thin)',
|
<div style={{
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="max-w-7xl mx-auto px-12 max-md:px-5"
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
gap: 16,
|
|
||||||
paddingTop: 32,
|
|
||||||
paddingBottom: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Left (visual end in RTL): chapter index */}
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 48,
|
|
||||||
fontWeight: 900,
|
|
||||||
color: 'var(--red)',
|
|
||||||
letterSpacing: '-2px',
|
|
||||||
lineHeight: 0.85,
|
|
||||||
direction: 'ltr',
|
|
||||||
fontVariantNumeric: 'tabular-nums',
|
|
||||||
}}
|
|
||||||
className="max-md:text-[32px]"
|
|
||||||
>
|
|
||||||
{n}
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 2,
|
|
||||||
lineHeight: 1.2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 9,
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: '2.5px',
|
|
||||||
color: 'var(--ink-5)',
|
|
||||||
direction: 'ltr',
|
|
||||||
fontFamily: 'ui-monospace, monospace',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
CHAPTER
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: 600,
|
fontWeight: 800,
|
||||||
letterSpacing: '0.5px',
|
color: 'var(--red)',
|
||||||
color: 'var(--ink-4)',
|
letterSpacing: '2px',
|
||||||
}}
|
fontFamily: 'ui-monospace, monospace',
|
||||||
>
|
direction: 'ltr',
|
||||||
بخش {n}
|
border: '1.5px solid var(--red)',
|
||||||
</span>
|
padding: '3px 9px',
|
||||||
|
flexShrink: 0,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
}}>
|
||||||
|
{n}
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ flex: 1, height: 1, background: 'var(--rule-thin)' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right (visual start RTL): thin rule fills */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
height: 1,
|
|
||||||
background: 'var(--rule-thin)',
|
|
||||||
maxWidth: 280,
|
|
||||||
}}
|
|
||||||
className="max-md:hidden"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -108,48 +39,34 @@ function SectionLabel({
|
||||||
|
|
||||||
/* ─── Home page ──────────────────────────────────────── */
|
/* ─── Home page ──────────────────────────────────────── */
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const { lang } = useLang()
|
||||||
return (
|
return (
|
||||||
<div>
|
<div dir={lang === 'fa' ? 'rtl' : 'ltr'}>
|
||||||
{/* ── 00 · Hero ─────────────────────────────────── */}
|
{/* ── 00 · Hero ─────────────────────────────────── */}
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
|
|
||||||
{/* ── Ticker ────────────────────────────────────── */}
|
{/* ── Ticker ────────────────────────────────────── */}
|
||||||
<SnapshotBar />
|
<SnapshotBar />
|
||||||
|
|
||||||
{/* ── 01 · اسکنر جهانی · bg: paper ─────────────── */}
|
{/* ── 01 · اسکنر جهانی ──────────────────────────── */}
|
||||||
<SectionLabel n="01" bg="paper" />
|
<SectionLabel n="۰۱" bg="paper" />
|
||||||
<GlobalScannerSection />
|
<GlobalScannerSection />
|
||||||
|
|
||||||
{/* ── 02 · تحلیلها · bg: paper-2 ──────────────── */}
|
{/* ── 02 · خبرنامه فولاد ────────────────────────── */}
|
||||||
<SectionLabel n="02" bg="paper-2" />
|
<SectionLabel n="۰۲" bg="paper-2" />
|
||||||
<ReportsCarouselSection />
|
<SteelNewsletterSection />
|
||||||
|
|
||||||
{/* ── 03 · اخبار · bg: paper ────────────────────── */}
|
{/* ── 03 · آخرین رویدادها ───────────────────────── */}
|
||||||
<SectionLabel n="03" bg="paper" />
|
<SectionLabel n="۰۳" bg="paper-2" />
|
||||||
<LatestNewsBentoSection />
|
|
||||||
|
|
||||||
{/* ── 04 · رویدادها · bg: paper-2 ──────────────── */}
|
|
||||||
<SectionLabel n="04" bg="paper-2" />
|
|
||||||
<EventsSection />
|
<EventsSection />
|
||||||
|
|
||||||
{/* ── 05 · ریسک · bg: paper ─────────────────────── */}
|
{/* ── 04 · گزارش کارخانهها ─────────────────────── */}
|
||||||
<SectionLabel n="05" bg="paper" />
|
<SectionLabel n="۰۴" bg="paper-2" />
|
||||||
|
<FactoryBentoSection />
|
||||||
|
|
||||||
|
{/* ── 05 · نقشه ریسک ────────────────────────────── */}
|
||||||
|
<SectionLabel n="۰۵" bg="paper" />
|
||||||
<RiskSection />
|
<RiskSection />
|
||||||
|
|
||||||
{/* ── 06 · تیم · bg: paper-2 ────────────────────── */}
|
|
||||||
<SectionLabel n="06" bg="paper-2" />
|
|
||||||
<TeamSection />
|
|
||||||
|
|
||||||
{/* ── 07 · اشتراک · bg: paper ───────────────────── */}
|
|
||||||
<SectionLabel n="07" bg="paper" />
|
|
||||||
<PlansSection />
|
|
||||||
|
|
||||||
{/* ── 08 · همراهان · bg: paper-2 ────────────────── */}
|
|
||||||
<SectionLabel n="08" bg="paper-2" />
|
|
||||||
<PartnersSection />
|
|
||||||
|
|
||||||
{/* ── خبرنامه (last, no label) ───────────────────── */}
|
|
||||||
<NewsletterSection />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,32 @@
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { events } from '@/data/events'
|
import { events } from '@/data/events'
|
||||||
import type { Event } from '@/data/events'
|
import type { Event } from '@/data/events'
|
||||||
|
import { useLang } from '@/context/LangContext'
|
||||||
|
|
||||||
const typeLabel: Record<Event['type'], string> = {
|
const typeLabel: Record<Event['type'], { fa: string; en: string }> = {
|
||||||
conference: 'کنگره',
|
conference: { fa: 'کنگره', en: 'Conference' },
|
||||||
exhibition: 'نمایشگاه',
|
exhibition: { fa: 'نمایشگاه', en: 'Exhibition' },
|
||||||
seminar: 'سمینار',
|
seminar: { fa: 'سمینار', en: 'Seminar' },
|
||||||
international: 'جهانی',
|
international: { fa: 'جهانی', en: 'International' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
fa: {
|
||||||
|
overline: 'تقویم رویداد · ۱۴۰۳–۱۴۰۴',
|
||||||
|
heading: 'رویدادهای پیشرو',
|
||||||
|
sub: 'همایشها، نمایشگاهها و نشستهای تخصصی صنعت فولاد در ماههای آینده — برای حضور یا حمایت رسانهای.',
|
||||||
|
all: 'تقویم کامل ←',
|
||||||
|
register: 'ثبتنام ←',
|
||||||
|
soon: 'بهزودی',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
overline: 'EVENT CALENDAR · 1403–1404',
|
||||||
|
heading: 'Upcoming Events',
|
||||||
|
sub: 'Conferences, exhibitions and specialized steel industry meetings — for attendance or media sponsorship.',
|
||||||
|
all: 'Full Calendar →',
|
||||||
|
register: 'Register →',
|
||||||
|
soon: 'Coming Soon',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeBorderColor: Record<Event['type'], string> = {
|
const typeBorderColor: Record<Event['type'], string> = {
|
||||||
|
|
@ -24,6 +44,8 @@ const typeTextColor: Record<Event['type'], string> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EventsSection() {
|
export default function EventsSection() {
|
||||||
|
const { lang } = useLang()
|
||||||
|
const t = T[lang]
|
||||||
const displayed = events.slice(0, 4)
|
const displayed = events.slice(0, 4)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -46,18 +68,18 @@ export default function EventsSection() {
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 18 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 18 }}>
|
||||||
<div style={{ width: 32, height: 2, background: 'var(--red)' }} />
|
<div style={{ width: 32, height: 2, background: 'var(--red)' }} />
|
||||||
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>
|
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>
|
||||||
تقویم رویداد · ۱۴۰۳–۱۴۰۴
|
{t.overline}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 style={{ fontSize: 'clamp(28px, 3.6vw, 48px)', fontWeight: 900, letterSpacing: '-1.2px', color: 'var(--ink)', lineHeight: 1.1, marginBottom: 12 }}>
|
<h2 style={{ fontSize: 'clamp(22px, 2.5vw, 32px)', fontWeight: 900, letterSpacing: '-0.5px', color: 'var(--ink)', lineHeight: 1.2, marginBottom: 12 }}>
|
||||||
رویدادهای پیشرو
|
{t.heading}
|
||||||
</h2>
|
</h2>
|
||||||
<p style={{ fontSize: 14, lineHeight: 1.7, color: 'var(--ink-4)', maxWidth: 560 }}>
|
<p style={{ fontSize: 14, lineHeight: 1.7, color: 'var(--ink-4)', maxWidth: 560 }}>
|
||||||
همایشها، نمایشگاهها و نشستهای تخصصی صنعت فولاد در ماههای آینده — برای حضور یا حمایت رسانهای.
|
{t.sub}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/events" style={{ fontSize: 11, fontWeight: 700, letterSpacing: '1.5px', textTransform: 'uppercase', color: 'var(--red)', textDecoration: 'none', whiteSpace: 'nowrap', paddingBottom: 4 }}>
|
<Link to="/events" style={{ fontSize: 11, fontWeight: 700, letterSpacing: '1.5px', textTransform: 'uppercase', color: 'var(--red)', textDecoration: 'none', whiteSpace: 'nowrap', paddingBottom: 4 }}>
|
||||||
تقویم کامل ←
|
{t.all}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -97,7 +119,7 @@ export default function EventsSection() {
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{typeLabel[event.type]}
|
{typeLabel[event.type][lang]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -110,11 +132,11 @@ export default function EventsSection() {
|
||||||
{/* CTA */}
|
{/* CTA */}
|
||||||
{event.registrationOpen ? (
|
{event.registrationOpen ? (
|
||||||
<Link to="#" style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', color: 'var(--red)', textDecoration: 'none', cursor: 'pointer' }}>
|
<Link to="#" style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', color: 'var(--red)', textDecoration: 'none', cursor: 'pointer' }}>
|
||||||
ثبتنام ←
|
{t.register}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ fontSize: 10, color: 'var(--ink-5)', textTransform: 'uppercase', fontWeight: 700 }}>
|
<span style={{ fontSize: 10, color: 'var(--ink-5)', textTransform: 'uppercase', fontWeight: 700 }}>
|
||||||
بهزودی
|
{t.soon}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
import { BentoGrid, BentoGridItem } from '@/components/ui/bento-grid'
|
||||||
|
import { useLang } from '@/context/LangContext'
|
||||||
|
|
||||||
|
type FactoryReport = {
|
||||||
|
publisher: string
|
||||||
|
publisherEn: string
|
||||||
|
title: string
|
||||||
|
titleEn: string
|
||||||
|
description: string
|
||||||
|
descriptionEn: string
|
||||||
|
date: string
|
||||||
|
dateEn: string
|
||||||
|
accentColor: string
|
||||||
|
imgBg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const REPORTS: FactoryReport[] = [
|
||||||
|
{
|
||||||
|
publisher: 'فولاد مبارکه', publisherEn: 'Mobarakeh Steel',
|
||||||
|
title: 'تحلیل عملکرد تولید و صادرات — بهمن ۱۴۰۳',
|
||||||
|
titleEn: 'Production & Export Performance — Feb 2025',
|
||||||
|
description: 'بررسی آمار تولید ماهانه، مقایسه با بودجه سالانه و تحلیل روند صادرات فولاد تخت در بازارهای منطقهای',
|
||||||
|
descriptionEn: 'Monthly production figures, annual budget comparison, and flat steel export trend analysis across regional markets.',
|
||||||
|
date: 'بهمن ۱۴۰۳', dateEn: 'Feb 2025',
|
||||||
|
accentColor: 'var(--red)',
|
||||||
|
imgBg: 'linear-gradient(135deg, rgba(180,20,20,0.13) 0%, rgba(180,20,20,0.04) 100%)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
publisher: 'فولاد مبارکه', publisherEn: 'Mobarakeh Steel',
|
||||||
|
title: 'وضعیت بازار و قیمتگذاری — دی ۱۴۰۳',
|
||||||
|
titleEn: 'Market Conditions & Pricing — Jan 2025',
|
||||||
|
description: 'تحلیل نوسانات قیمت فولاد تخت در بازار داخلی، بررسی اثر نرخ ارز و چشمانداز بازار در فصل زمستان',
|
||||||
|
descriptionEn: 'Flat steel price fluctuations in the domestic market, exchange rate impact analysis, and winter market outlook.',
|
||||||
|
date: 'دی ۱۴۰۳', dateEn: 'Jan 2025',
|
||||||
|
accentColor: 'var(--red)',
|
||||||
|
imgBg: 'linear-gradient(135deg, rgba(180,20,20,0.11) 0%, rgba(180,20,20,0.03) 100%)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
publisher: 'فولاد مبارکه', publisherEn: 'Mobarakeh Steel',
|
||||||
|
title: 'گزارش پایداری و محیط زیست — آذر ۱۴۰۳',
|
||||||
|
titleEn: 'Sustainability & Environment Report — Dec 2024',
|
||||||
|
description: 'شاخصهای زیستمحیطی ماهانه؛ مصرف انرژی، کاهش انتشار کربن و عملکرد سیستم بازیافت پساب صنعتی',
|
||||||
|
descriptionEn: 'Monthly environmental KPIs: energy consumption, carbon emission reductions, and industrial wastewater recycling performance.',
|
||||||
|
date: 'آذر ۱۴۰۳', dateEn: 'Dec 2024',
|
||||||
|
accentColor: 'var(--red)',
|
||||||
|
imgBg: 'linear-gradient(135deg, rgba(180,20,20,0.09) 0%, rgba(180,20,20,0.03) 100%)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
publisher: 'فولاد مبارکه', publisherEn: 'Mobarakeh Steel',
|
||||||
|
title: 'تحلیل نیروی انسانی و بهرهوری — آبان ۱۴۰۳',
|
||||||
|
titleEn: 'Workforce & Productivity Analysis — Nov 2024',
|
||||||
|
description: 'بررسی شاخصهای بهرهوری نیروی کار، آموزشهای ماهانه و آمار ایمنی شغلی و حوادث محیط کار',
|
||||||
|
descriptionEn: 'Workforce productivity indicators, monthly training hours, occupational safety statistics, and workplace incident rates.',
|
||||||
|
date: 'آبان ۱۴۰۳', dateEn: 'Nov 2024',
|
||||||
|
accentColor: 'var(--red)',
|
||||||
|
imgBg: 'linear-gradient(135deg, rgba(180,20,20,0.13) 0%, rgba(180,20,20,0.04) 100%)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
publisher: 'فولاد مبارکه', publisherEn: 'Mobarakeh Steel',
|
||||||
|
title: 'عملکرد مالی و سرمایهگذاری — مهر ۱۴۰۳',
|
||||||
|
titleEn: 'Financial Performance & Investment — Oct 2024',
|
||||||
|
description: 'خلاصه عملکرد مالی، پروژههای جاری سرمایهگذاری و پیشرفت طرح توسعه تا پایان مهرماه ۱۴۰۳',
|
||||||
|
descriptionEn: 'Financial performance summary, active investment projects, and development plan progress through end of October 2024.',
|
||||||
|
date: 'مهر ۱۴۰۳', dateEn: 'Oct 2024',
|
||||||
|
accentColor: 'var(--red)',
|
||||||
|
imgBg: 'linear-gradient(135deg, rgba(180,20,20,0.10) 0%, rgba(180,20,20,0.03) 100%)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
publisher: 'فولاد مبارکه', publisherEn: 'Mobarakeh Steel',
|
||||||
|
title: 'فناوری و نوآوری در فرآیند تولید — شهریور ۱۴۰۳',
|
||||||
|
titleEn: 'Technology & Innovation in Production — Sep 2024',
|
||||||
|
description: 'گزارش پیشرفت پروژههای دیجیتالسازی، بهرهگیری از هوش مصنوعی در کنترل کیفیت و اتوماسیون خط تولید',
|
||||||
|
descriptionEn: 'Digitalization project progress, AI adoption in quality control, and production line automation milestones.',
|
||||||
|
date: 'شهریور ۱۴۰۳', dateEn: 'Sep 2024',
|
||||||
|
accentColor: 'var(--red)',
|
||||||
|
imgBg: 'linear-gradient(135deg, rgba(180,20,20,0.12) 0%, rgba(180,20,20,0.04) 100%)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
publisher: 'فولاد مبارکه', publisherEn: 'Mobarakeh Steel',
|
||||||
|
title: 'زنجیره تأمین و مواد اولیه — مرداد ۱۴۰۳',
|
||||||
|
titleEn: 'Supply Chain & Raw Materials — Aug 2024',
|
||||||
|
description: 'تحلیل وضعیت تأمین سنگآهن، آهن اسفنجی و کُک؛ بررسی ریسکهای زنجیره تأمین در فصل تابستان',
|
||||||
|
descriptionEn: 'Iron ore, DRI, and coke supply status; supply chain risk assessment for the summer season.',
|
||||||
|
date: 'مرداد ۱۴۰۳', dateEn: 'Aug 2024',
|
||||||
|
accentColor: 'var(--red)',
|
||||||
|
imgBg: 'linear-gradient(135deg, rgba(180,20,20,0.08) 0%, rgba(180,20,20,0.03) 100%)',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
fa: { overline: 'MOBARAKEH STEEL', heading: 'ماهنامه تحلیلی کارخانه', all: 'مشاهده همه ←' },
|
||||||
|
en: { overline: 'MOBARAKEH STEEL', heading: 'Factory Monthly Analysis', all: 'View All →' },
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReportCover({ report, lang }: { report: FactoryReport; lang: 'fa' | 'en' }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%', minHeight: 160, background: report.imgBg,
|
||||||
|
borderBottom: `2px solid ${report.accentColor}33`,
|
||||||
|
display: 'flex', flexDirection: 'column', justifyContent: 'flex-end',
|
||||||
|
padding: '16px 20px', position: 'relative', overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', top: '50%', left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
fontSize: 52, fontWeight: 900, letterSpacing: '-3px',
|
||||||
|
color: report.accentColor, opacity: 0.07, whiteSpace: 'nowrap',
|
||||||
|
userSelect: 'none', direction: 'ltr',
|
||||||
|
}}>
|
||||||
|
{report.publisherEn.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
display: 'inline-flex', alignSelf: 'flex-start',
|
||||||
|
background: report.accentColor, color: '#fff',
|
||||||
|
fontSize: 9, fontWeight: 700, letterSpacing: '1.5px',
|
||||||
|
padding: '4px 10px', textTransform: 'uppercase',
|
||||||
|
}}>
|
||||||
|
{lang === 'fa' ? report.publisher : report.publisherEn}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FactoryBentoSection() {
|
||||||
|
const { lang } = useLang()
|
||||||
|
const t = T[lang]
|
||||||
|
|
||||||
|
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" style={{ paddingTop: 48, paddingBottom: 64 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 28, flexWrap: 'wrap', gap: 12 }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)', marginBottom: 6 }}>{t.overline}</div>
|
||||||
|
<h2 style={{ fontSize: 'clamp(22px,2.5vw,32px)', fontWeight: 900, color: 'var(--ink)', letterSpacing: '-0.5px' }}>{t.heading}</h2>
|
||||||
|
</div>
|
||||||
|
<a href="#" style={{ fontSize: 12, fontWeight: 700, color: 'var(--red)', textDecoration: 'none' }}>{t.all}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BentoGrid>
|
||||||
|
{REPORTS.map((report, i) => (
|
||||||
|
<BentoGridItem
|
||||||
|
key={i}
|
||||||
|
header={<ReportCover report={report} lang={lang} />}
|
||||||
|
tag={`${lang === 'fa' ? report.date : report.dateEn} · ${lang === 'fa' ? 'ماهنامه تحلیلی کارخانه' : 'Monthly Factory Analysis'}`}
|
||||||
|
tagColor={report.accentColor}
|
||||||
|
title={lang === 'fa' ? report.title : report.titleEn}
|
||||||
|
description={lang === 'fa' ? report.description : report.descriptionEn}
|
||||||
|
accentLine
|
||||||
|
className={i === 3 || i === 6 ? 'md:col-span-2' : ''}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</BentoGrid>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,29 @@
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import WorldMap from '@/components/ui/world-map'
|
import WorldMap from '@/components/ui/world-map'
|
||||||
|
import { useLang } from '@/context/LangContext'
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
fa: {
|
||||||
|
h2a: 'تمام رویدادهای فولادی دنیا', h2b: ' را رصد میکنیم',
|
||||||
|
sub: 'از بورس دالیان چین تا گمرک بصره عراق، از تصمیمات کمیته فولاد OECD تا ظرفیتسازیهای هند — هر سیگنالی که بر رقابتپذیری ایران اثر میگذارد، پیش از بقیه به دست شما میرسد.',
|
||||||
|
stats: [
|
||||||
|
{ n: '۱۸۰+', label: 'بازار رصدشده', sub: 'در ۵ قاره' },
|
||||||
|
{ n: '۷/۲۴', label: 'پایش لحظهای', sub: 'قیمت و حجم' },
|
||||||
|
{ n: '۴۰+', label: 'منبع داده', sub: 'بورسها و گمرکها' },
|
||||||
|
{ n: '۱۵', label: 'کشور رقیب', sub: 'زیر ذرهبین' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
h2a: 'Every Global Steel Event,', h2b: ' Monitored',
|
||||||
|
sub: "From the Dalian Commodity Exchange to the Basra customs gate, from OECD Steel Committee decisions to India's capacity build-up — every signal affecting Iran's competitiveness reaches you first.",
|
||||||
|
stats: [
|
||||||
|
{ n: '180+', label: 'Markets Tracked', sub: 'Across 5 continents' },
|
||||||
|
{ n: '24/7', label: 'Live Monitoring', sub: 'Price & volume' },
|
||||||
|
{ n: '40+', label: 'Data Sources', sub: 'Exchanges & customs' },
|
||||||
|
{ n: '15', label: 'Rival Countries', sub: 'Under the lens' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Steel trade routes ─────────────────────────────────────────────
|
/* ─── Steel trade routes ─────────────────────────────────────────────
|
||||||
Arcs show geopolitically relevant steel flows affecting Iran's market
|
Arcs show geopolitically relevant steel flows affecting Iran's market
|
||||||
|
|
@ -48,6 +72,8 @@ const ROUTES = [
|
||||||
|
|
||||||
/* ─── Section ────────────────────────────────────────── */
|
/* ─── Section ────────────────────────────────────────── */
|
||||||
export default function GlobalScannerSection() {
|
export default function GlobalScannerSection() {
|
||||||
|
const { lang } = useLang()
|
||||||
|
const t = T[lang]
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -62,14 +88,14 @@ export default function GlobalScannerSection() {
|
||||||
>
|
>
|
||||||
{/* Headline */}
|
{/* Headline */}
|
||||||
<h2 style={{
|
<h2 style={{
|
||||||
fontSize: 'clamp(28px, 3.5vw, 52px)',
|
fontSize: 'clamp(22px, 2.5vw, 32px)',
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
lineHeight: 1.1,
|
lineHeight: 1.1,
|
||||||
letterSpacing: '-1.5px',
|
letterSpacing: '-1.5px',
|
||||||
color: 'var(--ink)',
|
color: 'var(--ink)',
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
}}>
|
}}>
|
||||||
تمام رویدادهای فولادی دنیا{' '}
|
{t.h2a}{' '}
|
||||||
<motion.span
|
<motion.span
|
||||||
style={{ color: 'var(--ink-6)', display: 'inline' }}
|
style={{ color: 'var(--ink-6)', display: 'inline' }}
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
|
|
@ -77,7 +103,7 @@ export default function GlobalScannerSection() {
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.8, delay: 0.3 }}
|
transition={{ duration: 0.8, delay: 0.3 }}
|
||||||
>
|
>
|
||||||
را رصد میکنیم
|
{t.h2b}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|
@ -90,9 +116,7 @@ export default function GlobalScannerSection() {
|
||||||
maxWidth: 600,
|
maxWidth: 600,
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
}}>
|
}}>
|
||||||
از بورس دالیان چین تا گمرک بصره عراق، از تصمیمات کمیته فولاد OECD
|
{t.sub}
|
||||||
تا ظرفیتسازیهای هند — هر سیگنالی که بر رقابتپذیری ایران اثر
|
|
||||||
میگذارد، پیش از بقیه به دست شما میرسد.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -113,12 +137,7 @@ export default function GlobalScannerSection() {
|
||||||
}}
|
}}
|
||||||
className="grid grid-cols-4 max-md:grid-cols-2 max-sm:grid-cols-1"
|
className="grid grid-cols-4 max-md:grid-cols-2 max-sm:grid-cols-1"
|
||||||
>
|
>
|
||||||
{[
|
{t.stats.map((s, i) => (
|
||||||
{ n: '۱۸۰+', label: 'بازار رصدشده', sub: 'در ۵ قاره' },
|
|
||||||
{ n: '۷/۲۴', label: 'پایش لحظهای', sub: 'قیمت و حجم' },
|
|
||||||
{ n: '۴۰+', label: 'منبع داده', sub: 'بورسها و گمرکها' },
|
|
||||||
{ n: '۱۵', label: 'کشور رقیب', sub: 'زیر ذرهبین' },
|
|
||||||
].map((s, i) => (
|
|
||||||
<motion.div
|
<motion.div
|
||||||
key={i}
|
key={i}
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
|
|
||||||
|
|
@ -1,424 +1,122 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { reports } from '@/data/reports'
|
import { useLang } from '@/context/LangContext'
|
||||||
|
|
||||||
const featured = reports.find(r => r.featured)!
|
const T = {
|
||||||
|
fa: {
|
||||||
const STATS = [
|
h1a: 'جایی که آینده', h1b: ' به تصمیم ', h1c: 'تبدیل میشود',
|
||||||
{ n: '۴', label: 'سناریو محتمل' },
|
sub: 'تحلیل روندها، فناوریها و سناریوهای آینده صنعت فولاد',
|
||||||
{ n: '۱۵', label: 'کشور رقیب' },
|
cta1: 'گزارش ویژه ←', cta2: 'نبض صنعت',
|
||||||
{ n: '۸۴', label: 'صفحه تحلیلی' },
|
video: 'Short film · Future of Steel',
|
||||||
{ n: '۱۲', label: 'توصیه سیاستی' },
|
},
|
||||||
]
|
en: {
|
||||||
|
h1a: 'Where the Future', h1b: ' Becomes ', h1c: 'Decision',
|
||||||
|
sub: 'Analyzing trends, technologies and future scenarios of the global steel industry',
|
||||||
|
cta1: 'Special Report →', cta2: 'Industry Pulse',
|
||||||
|
video: 'Short film · Future of Steel',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export default function HeroSection() {
|
export default function HeroSection() {
|
||||||
const [buyHov, setBuyHov] = useState(false)
|
const { lang } = useLang()
|
||||||
const [freeHov, setFreeHov] = useState(false)
|
const t = T[lang]
|
||||||
|
const [repHov, setRepHov] = useState(false)
|
||||||
|
const [pulseHov, setPulseHov] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
background: 'var(--paper)',
|
background: 'var(--ink)',
|
||||||
borderBottom: '3px solid var(--ink)',
|
borderBottom: '3px solid var(--red)',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
minHeight: '80vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Subtle grid texture */}
|
{/* Dark overlay */}
|
||||||
<div
|
<div aria-hidden="true" style={{ position: 'absolute', inset: 0, background: 'linear-gradient(135deg, rgba(26,23,18,0.92) 0%, rgba(26,23,18,0.70) 100%)', zIndex: 1 }} />
|
||||||
aria-hidden="true"
|
{/* Grid texture */}
|
||||||
style={{
|
<div aria-hidden="true" style={{ position: 'absolute', inset: 0, backgroundImage: 'linear-gradient(to right, rgba(244,239,231,0.03) 1px, transparent 1px), linear-gradient(to bottom, rgba(244,239,231,0.03) 1px, transparent 1px)', backgroundSize: '80px 80px', pointerEvents: 'none', zIndex: 2 }} />
|
||||||
position: 'absolute',
|
{/* Red glow */}
|
||||||
inset: 0,
|
<div aria-hidden="true" style={{ position: 'absolute', inset: 0, backgroundImage: 'radial-gradient(ellipse 80% 60% at 60% 50%, rgba(180,20,20,0.12) 0%, transparent 70%)', zIndex: 1 }} />
|
||||||
backgroundImage: `
|
|
||||||
linear-gradient(to right, rgba(26,23,18,0.04) 1px, transparent 1px),
|
|
||||||
linear-gradient(to bottom, rgba(26,23,18,0.04) 1px, transparent 1px)
|
|
||||||
`,
|
|
||||||
backgroundSize: '64px 64px',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="relative max-w-7xl mx-auto px-12 max-md:px-5"
|
className="relative max-w-7xl mx-auto px-12 max-md:px-5"
|
||||||
style={{ paddingTop: 56, paddingBottom: 64 }}
|
style={{ paddingTop: 80, paddingBottom: 88, position: 'relative', zIndex: 3 }}
|
||||||
>
|
>
|
||||||
{/* ─── Top meta row ─────────────────────────── */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
paddingBottom: 24,
|
|
||||||
borderBottom: '1px solid var(--rule-thin)',
|
|
||||||
marginBottom: 48,
|
|
||||||
}}
|
|
||||||
className="max-md:flex-wrap max-md:gap-3 max-md:mb-8"
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
|
||||||
<div style={{
|
|
||||||
background: 'var(--red)',
|
|
||||||
color: 'white',
|
|
||||||
fontSize: 9,
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: '2.5px',
|
|
||||||
padding: '5px 12px',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
}}>
|
|
||||||
گزارش ویژه · شماره ۰۱
|
|
||||||
</div>
|
|
||||||
<span style={{
|
|
||||||
fontSize: 10,
|
|
||||||
color: 'var(--ink-5)',
|
|
||||||
letterSpacing: '1.5px',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
fontWeight: 600,
|
|
||||||
}}>
|
|
||||||
{featured.publishDate} · {featured.category}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 10,
|
|
||||||
color: 'var(--ink-5)',
|
|
||||||
fontWeight: 600,
|
|
||||||
letterSpacing: '1.5px',
|
|
||||||
fontFamily: 'ui-monospace, monospace',
|
|
||||||
direction: 'ltr',
|
|
||||||
}}
|
|
||||||
className="max-md:hidden"
|
|
||||||
>
|
|
||||||
ISSN 2783-XXXX · VOL. XII
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ─── Main grid — 8/12 + 4/12 ─────────────── */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: '1fr 320px',
|
|
||||||
gap: 56,
|
|
||||||
alignItems: 'start',
|
|
||||||
}}
|
|
||||||
className="mq-stack max-md:grid-cols-1 max-md:gap-10"
|
|
||||||
>
|
|
||||||
{/* ═══ Left col ═══ */}
|
|
||||||
<div>
|
|
||||||
{/* Overline */}
|
{/* Overline */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 8 }}
|
initial={{ opacity: 0, y: 8 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.4 }}
|
transition={{ duration: 0.4 }}
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 28 }}
|
style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 32 }}
|
||||||
>
|
>
|
||||||
<div style={{ width: 40, height: 2, background: 'var(--red)' }} />
|
<div style={{ width: 40, height: 2, background: 'var(--red)' }} />
|
||||||
<span style={{
|
<span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'rgba(244,239,231,0.5)' }}>
|
||||||
fontSize: 11,
|
FUTURE STEEL POLICY INSTITUTE
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: '3px',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
color: 'var(--ink-4)',
|
|
||||||
}}>
|
|
||||||
تحلیل سناریو · راهبردی
|
|
||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Massive editorial headline */}
|
{/* Headline */}
|
||||||
<motion.h1
|
<motion.h1
|
||||||
initial={{ opacity: 0, y: 16 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.55, delay: 0.08 }}
|
transition={{ duration: 0.6, delay: 0.08 }}
|
||||||
style={{
|
style={{ fontSize: 'clamp(36px, 5.5vw, 80px)', fontWeight: 900, lineHeight: 1.05, letterSpacing: '-2px', color: 'var(--paper)', marginBottom: 28, maxWidth: 820 }}
|
||||||
fontSize: 'clamp(34px, 5.2vw, 76px)',
|
|
||||||
fontWeight: 900,
|
|
||||||
lineHeight: 1.05,
|
|
||||||
letterSpacing: '-1.5px',
|
|
||||||
color: 'var(--ink)',
|
|
||||||
marginBottom: 28,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{(() => {
|
{t.h1a}
|
||||||
const [main, sub] = featured.title.split(':')
|
<span style={{ color: 'var(--red)' }}>{t.h1b}</span>
|
||||||
return (
|
{t.h1c}
|
||||||
<>
|
|
||||||
<span style={{ display: 'block' }}>{main}</span>
|
|
||||||
{sub && (
|
|
||||||
<span style={{
|
|
||||||
display: 'block',
|
|
||||||
color: 'var(--red)',
|
|
||||||
fontWeight: 900,
|
|
||||||
marginTop: 8,
|
|
||||||
}}>
|
|
||||||
{sub.trim()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
|
|
||||||
{/* Lede */}
|
{/* Subtitle */}
|
||||||
<motion.p
|
<motion.p
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.5, delay: 0.18 }}
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
style={{
|
style={{ fontSize: 'clamp(15px, 1.3vw, 19px)', lineHeight: 1.75, fontWeight: 400, color: 'rgba(244,239,231,0.6)', maxWidth: 560, marginBottom: 48, borderRight: lang === 'fa' ? '2px solid var(--red)' : 'none', borderLeft: lang === 'en' ? '2px solid var(--red)' : 'none', paddingRight: lang === 'fa' ? 18 : 0, paddingLeft: lang === 'en' ? 18 : 0 }}
|
||||||
fontSize: 'clamp(15px, 1.2vw, 18px)',
|
|
||||||
lineHeight: 1.7,
|
|
||||||
fontWeight: 400,
|
|
||||||
color: 'var(--ink-3)',
|
|
||||||
maxWidth: 620,
|
|
||||||
marginBottom: 36,
|
|
||||||
borderRight: '2px solid var(--red)',
|
|
||||||
paddingRight: 16,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
مدلسازی کمی بر اساس دادههای تجارت ۱۵ کشور رقیب — از بدبینانه تا خوشبینانه،
|
{t.sub}
|
||||||
با توصیههای سیاستی مستقیم برای وزارت صمت و سازمان بنادر.
|
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
{/* Stats inline row */}
|
{/* CTAs */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 8 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.4, delay: 0.25 }}
|
transition={{ duration: 0.4, delay: 0.32 }}
|
||||||
style={{
|
style={{ display: 'flex', gap: 14, flexWrap: 'wrap' }}
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
|
||||||
gap: 0,
|
|
||||||
borderTop: '1px solid var(--rule-thin)',
|
|
||||||
borderBottom: '1px solid var(--rule-thin)',
|
|
||||||
marginBottom: 36,
|
|
||||||
}}
|
|
||||||
className="mq-stack-2 max-md:grid-cols-2"
|
|
||||||
>
|
>
|
||||||
{STATS.map((s, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
style={{
|
|
||||||
padding: '16px 12px 16px 0',
|
|
||||||
borderRight: i < STATS.length - 1 ? '1px solid var(--rule-thin)' : 'none',
|
|
||||||
}}
|
|
||||||
className={i < 2 ? 'max-md:border-b max-md:border-[var(--rule-thin)]' : ''}
|
|
||||||
>
|
|
||||||
<div style={{
|
|
||||||
fontSize: 'clamp(24px, 2.8vw, 36px)',
|
|
||||||
fontWeight: 900,
|
|
||||||
letterSpacing: '-1px',
|
|
||||||
color: 'var(--ink)',
|
|
||||||
lineHeight: 1,
|
|
||||||
fontVariantNumeric: 'tabular-nums',
|
|
||||||
}}>
|
|
||||||
{s.n}
|
|
||||||
</div>
|
|
||||||
<div style={{
|
|
||||||
fontSize: 10,
|
|
||||||
color: 'var(--ink-5)',
|
|
||||||
fontWeight: 600,
|
|
||||||
letterSpacing: '0.5px',
|
|
||||||
marginTop: 6,
|
|
||||||
}}>
|
|
||||||
{s.label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Author + CTAs */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 8 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.4, delay: 0.35 }}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 24,
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
||||||
<div style={{
|
|
||||||
width: 40, height: 40,
|
|
||||||
background: 'var(--ink)',
|
|
||||||
color: 'var(--paper)',
|
|
||||||
fontSize: 14, fontWeight: 900,
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
flexShrink: 0,
|
|
||||||
borderRadius: '50%',
|
|
||||||
}}>
|
|
||||||
{featured.authorInitial}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.2 }}>
|
|
||||||
{featured.author}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 10, color: 'var(--ink-5)', lineHeight: 1.3, marginTop: 2 }}>
|
|
||||||
{featured.authorRole}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-sm:hidden" style={{ width: 1, height: 36, background: 'var(--rule-thin)' }} />
|
|
||||||
|
|
||||||
<div className="flex gap-2.5 max-sm:flex-col max-sm:w-full">
|
|
||||||
<button
|
<button
|
||||||
onMouseEnter={() => setBuyHov(true)}
|
onMouseEnter={() => setRepHov(true)}
|
||||||
onMouseLeave={() => setBuyHov(false)}
|
onMouseLeave={() => setRepHov(false)}
|
||||||
style={{
|
style={{ background: repHov ? 'var(--red)' : 'var(--paper)', color: repHov ? 'var(--paper)' : 'var(--ink)', border: 'none', padding: '14px 32px', fontSize: 13, fontWeight: 700, fontFamily: 'Vazir, sans-serif', cursor: 'pointer', transition: 'background 160ms, color 160ms', whiteSpace: 'nowrap', letterSpacing: '0.3px' }}
|
||||||
background: buyHov ? 'var(--red)' : 'var(--ink)',
|
|
||||||
color: 'var(--paper)',
|
|
||||||
border: 'none',
|
|
||||||
padding: '12px 28px',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 700,
|
|
||||||
fontFamily: 'Vazir, sans-serif',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'background 160ms',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
letterSpacing: '0.3px',
|
|
||||||
}}
|
|
||||||
className="max-sm:w-full"
|
|
||||||
>
|
>
|
||||||
خرید گزارش · {featured.price.toLocaleString('fa-IR')} تومان
|
{t.cta1}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onMouseEnter={() => setFreeHov(true)}
|
onMouseEnter={() => setPulseHov(true)}
|
||||||
onMouseLeave={() => setFreeHov(false)}
|
onMouseLeave={() => setPulseHov(false)}
|
||||||
style={{
|
style={{ background: 'transparent', color: 'rgba(244,239,231,0.8)', border: `1px solid ${pulseHov ? 'rgba(244,239,231,0.7)' : 'rgba(244,239,231,0.3)'}`, padding: '14px 32px', fontSize: 13, fontWeight: 600, fontFamily: 'Vazir, sans-serif', cursor: 'pointer', transition: 'border-color 160ms', whiteSpace: 'nowrap' }}
|
||||||
background: freeHov ? 'var(--ink)' : 'transparent',
|
|
||||||
color: freeHov ? 'var(--paper)' : 'var(--ink)',
|
|
||||||
border: '1px solid var(--ink)',
|
|
||||||
padding: '12px 24px',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
fontFamily: 'Vazir, sans-serif',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'background 160ms, color 160ms',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
className="max-sm:w-full"
|
|
||||||
>
|
>
|
||||||
دانلود خلاصه رایگان
|
{t.cta2}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ═══ Right col: dossier mini-cover — frosted glass over ink ═══ */}
|
{/* Video placeholder badge */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 24 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.55, delay: 0.4 }}
|
transition={{ duration: 0.4, delay: 0.5 }}
|
||||||
style={{
|
className="max-md:hidden"
|
||||||
position: 'relative',
|
style={{ position: 'absolute', bottom: 88, left: 48, display: 'flex', alignItems: 'center', gap: 10, color: 'rgba(244,239,231,0.35)', fontSize: 10, letterSpacing: '2px', textTransform: 'uppercase', direction: 'ltr' }}
|
||||||
background: 'rgba(26,23,18,0.78)',
|
|
||||||
backdropFilter: 'blur(24px) saturate(170%)',
|
|
||||||
WebkitBackdropFilter: 'blur(24px) saturate(170%)',
|
|
||||||
border: '1px solid rgba(244,239,231,0.08)',
|
|
||||||
color: 'var(--paper)',
|
|
||||||
padding: '36px 30px',
|
|
||||||
boxShadow: '0 24px 60px rgba(26,23,18,0.22), inset 0 1px 0 rgba(244,239,231,0.05)',
|
|
||||||
transform: 'rotate(0.6deg)',
|
|
||||||
}}
|
|
||||||
className="max-md:rotate-0"
|
|
||||||
>
|
>
|
||||||
<div style={{
|
<div style={{ width: 36, height: 36, border: '1px solid rgba(244,239,231,0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14 }}>▶</div>
|
||||||
position: 'absolute',
|
Short film · Future of Steel
|
||||||
top: 0, right: 0, left: 0,
|
|
||||||
height: 4,
|
|
||||||
background: 'var(--red)',
|
|
||||||
}} />
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 18,
|
|
||||||
left: 22,
|
|
||||||
fontSize: 9,
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: '2px',
|
|
||||||
color: 'rgba(244,239,231,0.4)',
|
|
||||||
direction: 'ltr',
|
|
||||||
fontFamily: 'ui-monospace, monospace',
|
|
||||||
}}>
|
|
||||||
Nº 01 / 2025
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: '3px',
|
|
||||||
color: 'rgba(244,239,231,0.4)',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
marginBottom: 24,
|
|
||||||
marginTop: 8,
|
|
||||||
}}>
|
|
||||||
DOSSIER
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
fontSize: 120,
|
|
||||||
fontWeight: 900,
|
|
||||||
lineHeight: 0.85,
|
|
||||||
letterSpacing: '-6px',
|
|
||||||
color: 'var(--red)',
|
|
||||||
direction: 'ltr',
|
|
||||||
fontVariantNumeric: 'tabular-nums',
|
|
||||||
marginBottom: 12,
|
|
||||||
}}>
|
|
||||||
۰۴
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: 'rgba(244,239,231,0.85)',
|
|
||||||
lineHeight: 1.4,
|
|
||||||
marginBottom: 4,
|
|
||||||
}}>
|
|
||||||
سناریو راهبردی
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
fontSize: 11,
|
|
||||||
color: 'rgba(244,239,231,0.5)',
|
|
||||||
lineHeight: 1.5,
|
|
||||||
marginBottom: 28,
|
|
||||||
}}>
|
|
||||||
برای آینده فولاد ایران در بازار جهانی ۱۴۰۴
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul style={{
|
|
||||||
listStyle: 'none',
|
|
||||||
padding: 0,
|
|
||||||
margin: 0,
|
|
||||||
borderTop: '1px solid rgba(244,239,231,0.1)',
|
|
||||||
paddingTop: 16,
|
|
||||||
}}>
|
|
||||||
{[
|
|
||||||
{ k: 'صفحات', v: `${featured.pages.toLocaleString('fa-IR')} صفحه` },
|
|
||||||
{ k: 'نوع', v: 'گزارش راهبردی' },
|
|
||||||
{ k: 'انتشار', v: featured.publishDate },
|
|
||||||
{ k: 'مخاطب', v: 'سیاستگذار · مدیر ارشد' },
|
|
||||||
].map((row) => (
|
|
||||||
<li
|
|
||||||
key={row.k}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '8px 0',
|
|
||||||
fontSize: 11,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ color: 'rgba(244,239,231,0.5)' }}>{row.k}</span>
|
|
||||||
<span style={{ color: 'rgba(244,239,231,0.9)', fontWeight: 600 }}>{row.v}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,58 @@
|
||||||
import type { CSSProperties } from 'react'
|
import { useEffect, useState, type CSSProperties } from 'react'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { reports } from '@/data/reports'
|
import { reports } from '@/data/reports'
|
||||||
|
|
||||||
const featured = reports.find(r => r.featured)!
|
const featured = reports.find(r => r.featured)!
|
||||||
const flashReport = reports.find(r => r.type === 'flash')!
|
const flashReport = reports.find(r => r.type === 'flash')!
|
||||||
|
|
||||||
/* ─── Price snapshot ────────────────────────────────────── */
|
const PANEL_API =
|
||||||
const PRICES = [
|
(import.meta as ImportMeta & { env?: Record<string, string> }).env
|
||||||
|
?.VITE_PANEL_API || 'http://localhost:3001'
|
||||||
|
|
||||||
|
type PriceRow = { label: string; value: string; delta: string; up: boolean }
|
||||||
|
|
||||||
|
const FALLBACK_PRICES: PriceRow[] = [
|
||||||
{ label: 'HRC آسیا', value: '۵۹۰', delta: '+۳.۲٪', up: true },
|
{ label: 'HRC آسیا', value: '۵۹۰', delta: '+۳.۲٪', up: true },
|
||||||
{ label: 'میلگرد ایران', value: '۲۱,۸۰۰', delta: '+۰.۸٪', up: true },
|
{ label: 'میلگرد ایران', value: '۲۱,۸۰۰', delta: '+۰.۸٪', up: true },
|
||||||
{ label: 'سنگآهن دالیان', value: '۱۰۸', delta: '+۱۸٪', up: true },
|
{ label: 'سنگآهن دالیان', value: '۱۰۸', delta: '+۱۸٪', up: true },
|
||||||
{ label: 'HRC اروپا', value: '۶۲۴', delta: '−۱.۴٪', up: false },
|
{ label: 'HRC اروپا', value: '۶۲۴', delta: '−۱.۴٪', up: false },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
type ApiPrice = {
|
||||||
|
symbol: string
|
||||||
|
name: string
|
||||||
|
value: number | null
|
||||||
|
changePct: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPersianDigits(s: string): string {
|
||||||
|
return s.replace(/[0-9]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[Number(d)])
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(n: number): string {
|
||||||
|
const fixed = Math.abs(n) >= 1000
|
||||||
|
? n.toLocaleString('en-US')
|
||||||
|
: n.toFixed(Math.abs(n) >= 10 ? 0 : 2)
|
||||||
|
return toPersianDigits(fixed)
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapApiToRows(items: ApiPrice[]): PriceRow[] {
|
||||||
|
return items
|
||||||
|
.filter((p) => typeof p.value === 'number')
|
||||||
|
.slice(0, 4)
|
||||||
|
.map((p) => {
|
||||||
|
const pct = p.changePct ?? 0
|
||||||
|
const sign = pct > 0 ? '+' : pct < 0 ? '−' : ''
|
||||||
|
const magnitude = toPersianDigits(Math.abs(pct).toFixed(1))
|
||||||
|
return {
|
||||||
|
label: p.name,
|
||||||
|
value: formatNumber(p.value as number),
|
||||||
|
delta: `${sign}${magnitude}٪`,
|
||||||
|
up: pct >= 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Shared card style (NO gridColumn here — set via className) */
|
/* ─── Shared card style (NO gridColumn here — set via className) */
|
||||||
const CARD: CSSProperties = {
|
const CARD: CSSProperties = {
|
||||||
background: 'var(--paper)',
|
background: 'var(--paper)',
|
||||||
|
|
@ -50,6 +90,25 @@ function Accent({ color = 'var(--red)' }: { color?: string }) {
|
||||||
|
|
||||||
/* ─── Section ────────────────────────────────────────────── */
|
/* ─── Section ────────────────────────────────────────────── */
|
||||||
export default function LatestNewsBentoSection() {
|
export default function LatestNewsBentoSection() {
|
||||||
|
const [prices, setPrices] = useState<PriceRow[]>(FALLBACK_PRICES)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
const load = () => {
|
||||||
|
fetch(`${PANEL_API}/api/prices`)
|
||||||
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
|
.then((data: ApiPrice[] | null) => {
|
||||||
|
if (cancelled || !Array.isArray(data) || data.length === 0) return
|
||||||
|
const rows = mapApiToRows(data)
|
||||||
|
if (rows.length > 0) setPrices(rows)
|
||||||
|
})
|
||||||
|
.catch(() => { /* keep fallback */ })
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
const id = setInterval(load, 2 * 60 * 1000)
|
||||||
|
return () => { cancelled = true; clearInterval(id) }
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section style={{ background: 'var(--paper)' }}>
|
<section style={{ background: 'var(--paper)' }}>
|
||||||
|
|
||||||
|
|
@ -116,7 +175,7 @@ export default function LatestNewsBentoSection() {
|
||||||
آخرین نرخهای بازار
|
آخرین نرخهای بازار
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||||
{PRICES.map(p => (
|
{prices.map(p => (
|
||||||
<div key={p.label} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '9px 0', borderBottom: '1px solid var(--rule-thin)' }}>
|
<div key={p.label} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '9px 0', borderBottom: '1px solid var(--rule-thin)' }}>
|
||||||
<span style={{ fontSize: 11, color: 'var(--ink-4)', fontWeight: 500 }}>{p.label}</span>
|
<span style={{ fontSize: 11, color: 'var(--ink-4)', fontWeight: 500 }}>{p.label}</span>
|
||||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,16 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
import { useLang } from '@/context/LangContext'
|
||||||
|
|
||||||
/* ─── Risk data ──────────────────────────────────────────── */
|
type RiskLevel = 'critical' | 'high' | 'medium' | 'low' | 'opportunity'
|
||||||
const riskItems = [
|
type RiskItem = { quote: string; name: string; date: string; level: RiskLevel }
|
||||||
|
|
||||||
|
const PANEL_API =
|
||||||
|
(import.meta as ImportMeta & { env?: Record<string, string> }).env
|
||||||
|
?.VITE_PANEL_API || 'http://localhost:3001'
|
||||||
|
|
||||||
|
/* ─── Risk data (fallback when panel is offline) ─────────── */
|
||||||
|
const fallbackRiskItems: RiskItem[] = [
|
||||||
{
|
{
|
||||||
quote: 'چین در نیمه اول ۲۰۲۴ رکورد ۵۴ میلیون تن صادرات فولاد را شکست. این موج صادراتی قیمت بازارهای آسیای غربی را ۱۲٪ کاهش داده و مستقیماً سهم ایران در عراق را تهدید میکند.',
|
quote: 'چین در نیمه اول ۲۰۲۴ رکورد ۵۴ میلیون تن صادرات فولاد را شکست. این موج صادراتی قیمت بازارهای آسیای غربی را ۱۲٪ کاهش داده و مستقیماً سهم ایران در عراق را تهدید میکند.',
|
||||||
name: 'بازار جهانی — چین',
|
name: 'بازار جهانی — چین',
|
||||||
|
|
@ -65,15 +74,58 @@ const riskItems = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const LEVEL = {
|
const LEVEL = {
|
||||||
|
fa: {
|
||||||
critical: { dot: '#7f1d1d', label: 'بحرانی', bg: 'rgba(127,29,29,0.08)' },
|
critical: { dot: '#7f1d1d', label: 'بحرانی', bg: 'rgba(127,29,29,0.08)' },
|
||||||
high: { dot: '#9b1c1c', label: 'بالا', bg: 'rgba(155,28,28,0.07)' },
|
high: { dot: '#9b1c1c', label: 'بالا', bg: 'rgba(155,28,28,0.07)' },
|
||||||
medium: { dot: '#92400e', label: 'متوسط', bg: 'rgba(146,64,14,0.07)' },
|
medium: { dot: '#92400e', label: 'متوسط', bg: 'rgba(146,64,14,0.07)' },
|
||||||
low: { dot: '#166534', label: 'پایین', bg: 'rgba(22,101,52,0.07)' },
|
low: { dot: '#166534', label: 'پایین', bg: 'rgba(22,101,52,0.07)' },
|
||||||
opportunity: { dot: '#1e40af', label: 'فرصت', bg: 'rgba(30,64,175,0.07)' },
|
opportunity: { dot: '#1e40af', label: 'فرصت', bg: 'rgba(30,64,175,0.07)' },
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
critical: { dot: '#7f1d1d', label: 'Critical', bg: 'rgba(127,29,29,0.08)' },
|
||||||
|
high: { dot: '#9b1c1c', label: 'High', bg: 'rgba(155,28,28,0.07)' },
|
||||||
|
medium: { dot: '#92400e', label: 'Medium', bg: 'rgba(146,64,14,0.07)' },
|
||||||
|
low: { dot: '#166534', label: 'Low', bg: 'rgba(22,101,52,0.07)' },
|
||||||
|
opportunity: { dot: '#1e40af', label: 'Opportunity', bg: 'rgba(30,64,175,0.07)' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
fa: {
|
||||||
|
overline: (n: number) => `پایش لحظهای · ${n.toLocaleString('fa-IR')} سیگنال`,
|
||||||
|
heading: 'نقشه ریسک صنعت',
|
||||||
|
sub: 'ریسکهای ژئوپلیتیک، مقرراتی و بازار که در ماههای اخیر پایش شدهاند — با سطحبندی از بحرانی تا فرصت.',
|
||||||
|
all: 'همه ریسکها ←',
|
||||||
|
legend: ['بحرانی', 'بالا', 'متوسط', 'پایین', 'فرصت'],
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
overline: (n: number) => `LIVE MONITORING · ${n} SIGNALS`,
|
||||||
|
heading: 'Industry Risk Map',
|
||||||
|
sub: 'Geopolitical, regulatory, and market risks monitored in recent months — rated from critical to opportunity.',
|
||||||
|
all: 'All Risks →',
|
||||||
|
legend: ['Critical', 'High', 'Medium', 'Low', 'Opportunity'],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Section ────────────────────────────────────────────── */
|
/* ─── Section ────────────────────────────────────────────── */
|
||||||
export default function RiskSection() {
|
export default function RiskSection() {
|
||||||
|
const { lang } = useLang()
|
||||||
|
const t = T[lang]
|
||||||
|
const levelCfg = LEVEL[lang]
|
||||||
|
const [riskItems, setRiskItems] = useState<RiskItem[]>(fallbackRiskItems)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
fetch(`${PANEL_API}/api/risks?limit=200`)
|
||||||
|
.then(r => r.ok ? r.json() : null)
|
||||||
|
.then((data: RiskItem[] | null) => {
|
||||||
|
if (cancelled || !Array.isArray(data) || data.length === 0) return
|
||||||
|
setRiskItems(data)
|
||||||
|
})
|
||||||
|
.catch(() => { /* keep fallback */ })
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [])
|
||||||
|
|
||||||
/* Duplicate items for seamless infinite loop (handled in JSX, not effect, to
|
/* Duplicate items for seamless infinite loop (handled in JSX, not effect, to
|
||||||
stay immune to StrictMode double-invocation that would otherwise clone twice). */
|
stay immune to StrictMode double-invocation that would otherwise clone twice). */
|
||||||
const loopItems = [...riskItems, ...riskItems]
|
const loopItems = [...riskItems, ...riskItems]
|
||||||
|
|
@ -98,31 +150,26 @@ export default function RiskSection() {
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 18 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 18 }}>
|
||||||
<div style={{ width: 32, height: 2, background: 'var(--red)' }} />
|
<div style={{ width: 32, height: 2, background: 'var(--red)' }} />
|
||||||
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>
|
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>
|
||||||
پایش لحظهای · {riskItems.length.toLocaleString('fa-IR')} سیگنال
|
{t.overline(riskItems.length)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 style={{ fontSize: 'clamp(28px, 3.6vw, 48px)', fontWeight: 900, letterSpacing: '-1.2px', color: 'var(--ink)', lineHeight: 1.1, marginBottom: 12 }}>
|
<h2 style={{ fontSize: 'clamp(22px, 2.5vw, 32px)', fontWeight: 900, letterSpacing: '-0.5px', color: 'var(--ink)', lineHeight: 1.2, marginBottom: 12 }}>
|
||||||
نقشه ریسک صنعت
|
{t.heading}
|
||||||
</h2>
|
</h2>
|
||||||
<p style={{ fontSize: 14, lineHeight: 1.7, color: 'var(--ink-4)', maxWidth: 580 }}>
|
<p style={{ fontSize: 14, lineHeight: 1.7, color: 'var(--ink-4)', maxWidth: 580 }}>
|
||||||
ریسکهای ژئوپلیتیک، مقرراتی و بازار که در ماههای اخیر پایش شدهاند —
|
{t.sub}
|
||||||
با سطحبندی از بحرانی تا فرصت.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/risks" style={{ fontSize: 11, fontWeight: 700, letterSpacing: '1.5px', textTransform: 'uppercase', color: 'var(--red)', textDecoration: 'none', whiteSpace: 'nowrap', paddingBottom: 4 }}>
|
<Link to="/risks" style={{ fontSize: 11, fontWeight: 700, letterSpacing: '1.5px', textTransform: 'uppercase', color: 'var(--red)', textDecoration: 'none', whiteSpace: 'nowrap', paddingBottom: 4 }}>
|
||||||
همه ریسکها ←
|
{t.all}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Legend row */}
|
{/* Legend row */}
|
||||||
<div style={{ display: 'flex', gap: 22, flexWrap: 'wrap', marginBottom: 8 }}>
|
<div style={{ display: 'flex', gap: 22, flexWrap: 'wrap', marginBottom: 8 }}>
|
||||||
{[
|
{(['critical','high','medium','low','opportunity'] as const).map((key, i) => ({
|
||||||
{ color: '#7f1d1d', label: 'بحرانی' },
|
color: levelCfg[key].dot, label: t.legend[i],
|
||||||
{ color: '#9b1c1c', label: 'بالا' },
|
})).map(l => (
|
||||||
{ color: '#92400e', label: 'متوسط' },
|
|
||||||
{ color: '#166534', label: 'پایین' },
|
|
||||||
{ color: '#1e40af', label: 'فرصت' },
|
|
||||||
].map(l => (
|
|
||||||
<div key={l.label} style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
|
<div key={l.label} style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
|
||||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: l.color }} />
|
<div style={{ width: 8, height: 8, borderRadius: '50%', background: l.color }} />
|
||||||
<span style={{ fontSize: 12, color: 'var(--ink-4)', fontWeight: 600 }}>{l.label}</span>
|
<span style={{ fontSize: 12, color: 'var(--ink-4)', fontWeight: 600 }}>{l.label}</span>
|
||||||
|
|
@ -160,7 +207,7 @@ export default function RiskSection() {
|
||||||
onMouseLeave={e => (e.currentTarget.style.animationPlayState = 'running')}
|
onMouseLeave={e => (e.currentTarget.style.animationPlayState = 'running')}
|
||||||
>
|
>
|
||||||
{loopItems.map((item, idx) => {
|
{loopItems.map((item, idx) => {
|
||||||
const cfg = LEVEL[item.level]
|
const cfg = levelCfg[item.level]
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={idx}
|
key={idx}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useLang } from '@/context/LangContext'
|
||||||
|
|
||||||
|
const ISSUES = {
|
||||||
|
fa: [
|
||||||
|
{ num: '۱۲', title: 'فولاد و تحریمهای جدید: راهکارهای صادراتی ۱۴۰۴', date: 'بهمن ۱۴۰۳' },
|
||||||
|
{ num: '۱۱', title: 'قیمتگذاری فولاد در بازار داخلی: چالشها و فرصتها', date: 'دی ۱۴۰۳' },
|
||||||
|
{ num: '۱۰', title: 'آمار تولید فولاد ایران در ۹ ماهه ۱۴۰۳', date: 'آذر ۱۴۰۳' },
|
||||||
|
],
|
||||||
|
en: [
|
||||||
|
{ num: '12', title: 'Steel & New Sanctions: Export Strategies for 1404', date: 'Feb 2025' },
|
||||||
|
{ num: '11', title: 'Steel Pricing in the Domestic Market: Challenges & Opportunities', date: 'Jan 2025' },
|
||||||
|
{ num: '10', title: "Iran Steel Production Stats — First 9 Months of 1403", date: 'Dec 2024' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const FACTORIES = {
|
||||||
|
fa: [
|
||||||
|
{ name: 'فولاد مبارکه', capacity: '۷.۸ میلیون تن', location: 'اصفهان', type: 'فولاد تخت' },
|
||||||
|
{ name: 'ذوبآهن اصفهان', capacity: '۴.۲ میلیون تن', location: 'اصفهان', type: 'مقاطع سنگین' },
|
||||||
|
{ name: 'فولاد خوزستان', capacity: '۳.۲ میلیون تن', location: 'اهواز', type: 'بیلت و شمش' },
|
||||||
|
{ name: 'فولاد هرمزگان', capacity: '۱.۵ میلیون تن', location: 'هرمزگان', type: 'اسلب' },
|
||||||
|
],
|
||||||
|
en: [
|
||||||
|
{ name: 'Mobarakeh Steel', capacity: '7.8 Mt/yr', location: 'Isfahan', type: 'Flat Steel' },
|
||||||
|
{ name: 'Isfahan Steel', capacity: '4.2 Mt/yr', location: 'Isfahan', type: 'Heavy Sections' },
|
||||||
|
{ name: 'Khuzestan Steel', capacity: '3.2 Mt/yr', location: 'Ahvaz', type: 'Billet & Slab' },
|
||||||
|
{ name: 'Hormozgan Steel', capacity: '1.5 Mt/yr', location: 'Hormozgan', type: 'Slab' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
fa: {
|
||||||
|
overline: 'STEEL NEWSLETTER', heading: 'خبرنامه فولاد', archive: 'آرشیو کامل ←',
|
||||||
|
issueLabel: 'خبرنامه ماهانه', placeholder: 'ایمیل خود را وارد کنید...',
|
||||||
|
subscribe: 'عضویت در خبرنامه', done: '✓ ثبت شد',
|
||||||
|
overlinePlants: 'MAJOR PLANTS', headingPlants: 'کارخانههای بزرگ فولاد ایران',
|
||||||
|
allPlants: 'مشاهده همه ←', capacity: 'ظرفیت سالانه',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
overline: 'STEEL NEWSLETTER', heading: 'Steel Newsletter', archive: 'Full Archive →',
|
||||||
|
issueLabel: 'Monthly Issue', placeholder: 'Enter your email...',
|
||||||
|
subscribe: 'Subscribe', done: '✓ Subscribed',
|
||||||
|
overlinePlants: 'MAJOR PLANTS', headingPlants: "Iran's Major Steel Plants",
|
||||||
|
allPlants: 'View All →', capacity: 'Annual capacity',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SteelNewsletterSection() {
|
||||||
|
const { lang } = useLang()
|
||||||
|
const t = T[lang]
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [done, setDone] = useState(false)
|
||||||
|
|
||||||
|
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" style={{ paddingTop: 56, paddingBottom: 64 }}>
|
||||||
|
|
||||||
|
{/* Newsletter issues */}
|
||||||
|
<div style={{ marginBottom: 64 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 24, flexWrap: 'wrap', gap: 12 }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)', marginBottom: 6 }}>{t.overline}</div>
|
||||||
|
<h2 style={{ fontSize: 'clamp(20px,2.5vw,28px)', fontWeight: 900, color: 'var(--ink)', letterSpacing: '-0.5px' }}>{t.heading}</h2>
|
||||||
|
</div>
|
||||||
|
<a href="#" style={{ fontSize: 12, fontWeight: 700, color: 'var(--red)', textDecoration: 'none' }}>{t.archive}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px,1fr))', gap: 1, background: 'var(--rule-thin)', marginBottom: 24 }}>
|
||||||
|
{ISSUES[lang].map((issue, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.35, delay: i * 0.07 }}
|
||||||
|
style={{ background: 'var(--paper)', padding: '22px 20px', cursor: 'pointer', display: 'flex', gap: 16, alignItems: 'flex-start' }}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: 36, fontWeight: 900, color: 'var(--red)', letterSpacing: '-2px', lineHeight: 1, flexShrink: 0, direction: 'ltr', fontVariantNumeric: 'tabular-nums' }}>
|
||||||
|
{issue.num}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 9, fontWeight: 700, color: 'var(--ink-4)', letterSpacing: '1.5px', marginBottom: 6, textTransform: 'uppercase' }}>{t.issueLabel}</div>
|
||||||
|
<h3 style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.5, marginBottom: 8 }}>{issue.title}</h3>
|
||||||
|
<div style={{ fontSize: 10, color: 'var(--ink-5)' }}>{issue.date}</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subscribe bar */}
|
||||||
|
<div style={{ display: 'flex', maxWidth: 480, border: '1px solid var(--ink)' }}>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
placeholder={t.placeholder}
|
||||||
|
disabled={done}
|
||||||
|
style={{ flex: 1, border: 'none', padding: '11px 14px', fontSize: 13, fontFamily: 'Vazir, sans-serif', background: 'var(--paper)', color: 'var(--ink)', outline: 'none', direction: lang === 'fa' ? 'rtl' : 'ltr' }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => { if (email) setDone(true) }}
|
||||||
|
disabled={done}
|
||||||
|
style={{ background: done ? 'var(--ink)' : 'var(--red)', color: 'var(--paper)', border: 'none', padding: '11px 20px', fontSize: 12, fontWeight: 700, fontFamily: 'Vazir, sans-serif', cursor: done ? 'default' : 'pointer', whiteSpace: 'nowrap', flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
{done ? t.done : t.subscribe}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Major plants */}
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)', marginBottom: 6 }}>{t.overlinePlants}</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 24, flexWrap: 'wrap', gap: 12 }}>
|
||||||
|
<h2 style={{ fontSize: 'clamp(20px,2.5vw,28px)', fontWeight: 900, color: 'var(--ink)', letterSpacing: '-0.5px' }}>{t.headingPlants}</h2>
|
||||||
|
<a href="#" style={{ fontSize: 12, fontWeight: 700, color: 'var(--red)', textDecoration: 'none' }}>{t.allPlants}</a>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(210px,1fr))', gap: 1, background: 'var(--rule-thin)' }}>
|
||||||
|
{FACTORIES[lang].map((f, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
initial={{ opacity: 0, y: 8 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.35, delay: i * 0.06 }}
|
||||||
|
style={{ background: 'var(--paper)', padding: '20px 18px', position: 'relative' }}
|
||||||
|
>
|
||||||
|
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, height: 2, background: 'var(--ink)' }} />
|
||||||
|
<div style={{ fontSize: 14, fontWeight: 800, color: 'var(--ink)', marginBottom: 8, lineHeight: 1.3 }}>{f.name}</div>
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--ink-4)', marginBottom: 2 }}>{f.location} · {f.type}</div>
|
||||||
|
<div style={{ fontSize: 20, fontWeight: 900, color: 'var(--red)', letterSpacing: '-0.5px', marginTop: 10 }}>{f.capacity}</div>
|
||||||
|
<div style={{ fontSize: 9, color: 'var(--ink-5)', fontWeight: 600, marginTop: 2 }}>{t.capacity}</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
|
const REPORTS = [
|
||||||
|
{
|
||||||
|
year: '۱۴۰۲',
|
||||||
|
title: 'گزارش پایداری فولاد مبارکه',
|
||||||
|
tags: ['کاهش کربن', 'مصرف آب', 'ایمنی شغلی'],
|
||||||
|
summary: 'کاهش ۱۸٪ انتشار CO₂ نسبت به سال پایه، بازیافت ۹۴٪ پساب صنعتی',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: '۱۴۰۲',
|
||||||
|
title: 'گزارش مسئولیت اجتماعی ذوبآهن اصفهان',
|
||||||
|
tags: ['جامعه محلی', 'آموزش', 'محیط زیست'],
|
||||||
|
summary: 'سرمایهگذاری ۲۴۰ میلیارد تومانی در پروژههای اجتماعی منطقه',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
year: '۱۴۰۱',
|
||||||
|
title: 'استراتژی خنثیسازی کربن صنعت فولاد ایران',
|
||||||
|
tags: ['ESG', 'هیدروژن سبز', 'نقشه راه'],
|
||||||
|
summary: 'نقشه راه کاهش انتشار تا ۴۵٪ تا سال ۱۴۱۰',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const MSTID_STATS = [
|
||||||
|
{ label: 'شاخص پایششده', value: '۵۰۰+' },
|
||||||
|
{ label: 'کارخانه عضو', value: '۴۸' },
|
||||||
|
{ label: 'بهروزرسانی داده', value: 'روزانه' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function SustainabilitySection() {
|
||||||
|
return (
|
||||||
|
<section dir="rtl" style={{ background: 'var(--paper)' }}>
|
||||||
|
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 56, paddingBottom: 64 }}>
|
||||||
|
|
||||||
|
{/* گزارشهای پایداری */}
|
||||||
|
<div style={{ marginBottom: 56 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 24, flexWrap: 'wrap', gap: 12 }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)', marginBottom: 6 }}>SUSTAINABILITY</div>
|
||||||
|
<h2 style={{ fontSize: 'clamp(20px,2.5vw,28px)', fontWeight: 900, color: 'var(--ink)', letterSpacing: '-0.5px' }}>گزارشهای پایداری</h2>
|
||||||
|
</div>
|
||||||
|
<a href="#" style={{ fontSize: 12, fontWeight: 700, color: 'var(--red)', textDecoration: 'none' }}>همه گزارشها ←</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(270px,1fr))', gap: 1, background: 'var(--rule-thin)' }}>
|
||||||
|
{REPORTS.map((r, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.4, delay: i * 0.07 }}
|
||||||
|
style={{ background: 'var(--paper)', padding: '24px 22px', position: 'relative', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, height: 3, background: '#2d6a4f' }} />
|
||||||
|
<div style={{ fontSize: 9, fontWeight: 700, color: '#2d6a4f', letterSpacing: '1.5px', marginBottom: 10, textTransform: 'uppercase' }}>
|
||||||
|
گزارش پایداری · {r.year}
|
||||||
|
</div>
|
||||||
|
<h3 style={{ fontSize: 14, fontWeight: 800, color: 'var(--ink)', lineHeight: 1.45, marginBottom: 10 }}>{r.title}</h3>
|
||||||
|
<p style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.65, marginBottom: 14 }}>{r.summary}</p>
|
||||||
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||||
|
{r.tags.map(tag => (
|
||||||
|
<span key={tag} style={{ fontSize: 9, fontWeight: 700, background: 'rgba(45,106,79,0.1)', color: '#2d6a4f', padding: '3px 8px' }}>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* MSTid.com banner */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
style={{ background: 'var(--ink)', color: 'var(--paper)', padding: '36px 32px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 24 }}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'rgba(244,239,231,0.4)', marginBottom: 8 }}>POWERED BY</div>
|
||||||
|
<div style={{ fontSize: 28, fontWeight: 900, letterSpacing: '-1px', color: 'var(--paper)', marginBottom: 8, direction: 'ltr' }}>MSTid.com</div>
|
||||||
|
<div style={{ fontSize: 13, color: 'rgba(244,239,231,0.6)', lineHeight: 1.65, maxWidth: 420 }}>
|
||||||
|
مرجع جامع آمار و اطلاعات صنعت فولاد — دادههای لحظهای از کارخانهها، بازارها و شاخصهای اقتصادی
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 28, flexWrap: 'wrap' }}>
|
||||||
|
{MSTID_STATS.map(s => (
|
||||||
|
<div key={s.label} style={{ textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: 28, fontWeight: 900, color: 'var(--red)', letterSpacing: '-1px', lineHeight: 1 }}>{s.value}</div>
|
||||||
|
<div style={{ fontSize: 10, color: 'rgba(244,239,231,0.5)', marginTop: 4, fontWeight: 600 }}>{s.label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="https://mstid.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ background: 'var(--red)', color: 'var(--paper)', padding: '12px 28px', fontSize: 13, fontWeight: 700, textDecoration: 'none', whiteSpace: 'nowrap', display: 'inline-block', flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
ورود به سامانه ←
|
||||||
|
</a>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useLang } from '@/context/LangContext'
|
||||||
|
|
||||||
|
const PLANS = {
|
||||||
|
fa: [
|
||||||
|
{
|
||||||
|
id: 'basic', highlight: false,
|
||||||
|
title: 'پایه', subtitle: 'دسترسی به گزارشهای عمومی',
|
||||||
|
price: '۲,۵۰۰,۰۰۰', period: 'تومان / سال',
|
||||||
|
features: ['دسترسی به گزارشهای رایگان', 'خبرنامه ماهانه', 'دعوت به رویدادهای عمومی'],
|
||||||
|
cta: 'شروع رایگان', selected: '✓ انتخاب شد', recommended: 'پیشنهادی',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pro', highlight: true,
|
||||||
|
title: 'سازمانی', subtitle: 'مناسب شرکتهای فولادی',
|
||||||
|
price: '۱۸,۰۰۰,۰۰۰', period: 'تومان / سال',
|
||||||
|
features: ['دسترسی کامل به نبض صنعت', 'تمام گزارشهای تحلیلی', 'Flash Reports هفتگی', 'پشتیبانی اختصاصی', 'تا ۵ کاربر'],
|
||||||
|
cta: 'شروع عضویت', selected: '✓ انتخاب شد', recommended: 'پیشنهادی',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'enterprise', highlight: false,
|
||||||
|
title: 'ویژه', subtitle: 'برای سازمانهای بزرگ',
|
||||||
|
price: 'تماس بگیرید', period: '',
|
||||||
|
features: ['همه امکانات سازمانی', 'کاربران نامحدود', 'API داده', 'گزارشهای سفارشی', 'نشستهای تحلیلی اختصاصی'],
|
||||||
|
cta: 'تماس با ما', selected: '✓ انتخاب شد', recommended: 'پیشنهادی',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
en: [
|
||||||
|
{
|
||||||
|
id: 'basic', highlight: false,
|
||||||
|
title: 'Basic', subtitle: 'Access to public reports',
|
||||||
|
price: '2,500,000', period: 'IRR / year',
|
||||||
|
features: ['Access to free reports', 'Monthly newsletter', 'Invitation to public events'],
|
||||||
|
cta: 'Get Started', selected: '✓ Selected', recommended: 'Recommended',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pro', highlight: true,
|
||||||
|
title: 'Professional', subtitle: 'For steel companies',
|
||||||
|
price: '18,000,000', period: 'IRR / year',
|
||||||
|
features: ['Full access to Industry Pulse', 'All analytical reports', 'Weekly Flash Reports', 'Dedicated support', 'Up to 5 users'],
|
||||||
|
cta: 'Start Membership', selected: '✓ Selected', recommended: 'Recommended',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'enterprise', highlight: false,
|
||||||
|
title: 'Enterprise', subtitle: 'For large organizations',
|
||||||
|
price: 'Contact Us', period: '',
|
||||||
|
features: ['All Professional features', 'Unlimited users', 'Data API', 'Custom reports', 'Dedicated analytical sessions'],
|
||||||
|
cta: 'Contact Us', selected: '✓ Selected', recommended: 'Recommended',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
fa: {
|
||||||
|
overline: 'MEMBERSHIP', heading: 'عضویت',
|
||||||
|
sub: 'به جامعه تخصصی اندیشکده فولاد آینده بپیوندید و به دادهها و تحلیلهای اختصاصی دسترسی داشته باشید',
|
||||||
|
contactHeading: 'تکمیل فرآیند عضویت',
|
||||||
|
contactSub: 'برای پرداخت و فعالسازی حساب، با تیم ما تماس بگیرید یا ایمیل ارسال کنید.',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
overline: 'MEMBERSHIP', heading: 'Membership',
|
||||||
|
sub: 'Join the specialized community of the Future Steel Policy Institute and gain access to exclusive data and analysis',
|
||||||
|
contactHeading: 'Complete Membership Process',
|
||||||
|
contactSub: 'To complete payment and activate your account, contact our team or send an email.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Membership() {
|
||||||
|
const { lang } = useLang()
|
||||||
|
const t = T[lang]
|
||||||
|
const plans = PLANS[lang]
|
||||||
|
const [selected, setSelected] = useState<string | null>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div dir={lang === 'fa' ? 'rtl' : 'ltr'} style={{ background: 'var(--paper)', minHeight: '80vh' }}>
|
||||||
|
<div style={{ borderBottom: '3px solid var(--ink)' }}>
|
||||||
|
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 48, paddingBottom: 40 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
|
||||||
|
<div style={{ width: 36, height: 2, background: 'var(--red)' }} />
|
||||||
|
<span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>{t.overline}</span>
|
||||||
|
</div>
|
||||||
|
<h1 style={{ fontSize: 'clamp(28px,4vw,52px)', fontWeight: 900, letterSpacing: '-1.5px', color: 'var(--ink)', marginBottom: 12 }}>{t.heading}</h1>
|
||||||
|
<p style={{ fontSize: 15, color: 'var(--ink-3)', lineHeight: 1.7, maxWidth: 560 }}>{t.sub}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 56, paddingBottom: 72 }}>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 1, background: 'var(--rule-thin)', marginBottom: 56 }}>
|
||||||
|
{plans.map((plan, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={plan.id}
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.4, delay: i * 0.08 }}
|
||||||
|
style={{ background: plan.highlight ? 'var(--ink)' : 'var(--paper)', color: plan.highlight ? 'var(--paper)' : 'var(--ink)', padding: '36px 28px', position: 'relative', display: 'flex', flexDirection: 'column' }}
|
||||||
|
>
|
||||||
|
{plan.highlight && <div style={{ position: 'absolute', top: 0, right: 0, left: 0, height: 4, background: 'var(--red)' }} />}
|
||||||
|
{plan.highlight && (
|
||||||
|
<div style={{ position: 'absolute', top: 16, left: 20, fontSize: 9, fontWeight: 700, letterSpacing: '2px', color: 'var(--red)', textTransform: 'uppercase' }}>
|
||||||
|
{plan.recommended}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ fontSize: 11, fontWeight: 700, color: plan.highlight ? 'rgba(244,239,231,0.5)' : 'var(--ink-4)', letterSpacing: '1.5px', textTransform: 'uppercase', marginBottom: 8 }}>{plan.title}</div>
|
||||||
|
<div style={{ fontSize: 13, color: plan.highlight ? 'rgba(244,239,231,0.6)' : 'var(--ink-3)', marginBottom: 24 }}>{plan.subtitle}</div>
|
||||||
|
<div style={{ marginBottom: 28 }}>
|
||||||
|
<span style={{ fontSize: plan.price === 'تماس بگیرید' || plan.price === 'Contact Us' ? 18 : 24, fontWeight: 900, letterSpacing: '-0.5px' }}>{plan.price}</span>
|
||||||
|
{plan.period && <span style={{ fontSize: 11, color: plan.highlight ? 'rgba(244,239,231,0.5)' : 'var(--ink-4)', marginRight: lang === 'fa' ? 6 : 0, marginLeft: lang === 'en' ? 6 : 0 }}>{plan.period}</span>}
|
||||||
|
</div>
|
||||||
|
<ul style={{ listStyle: 'none', padding: 0, margin: '0 0 28px', flex: 1, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||||
|
{plan.features.map(f => (
|
||||||
|
<li key={f} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: plan.highlight ? 'rgba(244,239,231,0.8)' : 'var(--ink-3)' }}>
|
||||||
|
<span style={{ color: 'var(--red)', fontWeight: 900, fontSize: 13, flexShrink: 0 }}>✓</span>
|
||||||
|
{f}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelected(plan.id)}
|
||||||
|
style={{ background: selected === plan.id ? 'var(--red)' : plan.highlight ? 'var(--paper)' : 'var(--ink)', color: plan.highlight && selected !== plan.id ? 'var(--ink)' : 'var(--paper)', border: 'none', padding: '12px 0', fontSize: 13, fontWeight: 700, fontFamily: 'Vazir, sans-serif', cursor: 'pointer', width: '100%', transition: 'background 150ms', letterSpacing: '0.3px' }}
|
||||||
|
>
|
||||||
|
{selected === plan.id ? plan.selected : plan.cta}
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selected && selected !== 'enterprise' && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
style={{ border: '2px solid var(--ink)', padding: '32px 28px', maxWidth: 480, position: 'relative' }}
|
||||||
|
>
|
||||||
|
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, height: 4, background: 'var(--red)' }} />
|
||||||
|
<h3 style={{ fontSize: 16, fontWeight: 800, color: 'var(--ink)', marginBottom: 10 }}>{t.contactHeading}</h3>
|
||||||
|
<p style={{ fontSize: 13, color: 'var(--ink-3)', lineHeight: 1.75, marginBottom: 20 }}>{t.contactSub}</p>
|
||||||
|
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink)' }}>info@iransteel.ir</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useLang } from '@/context/LangContext'
|
||||||
|
|
||||||
|
const STATS = {
|
||||||
|
fa: [
|
||||||
|
{ label: 'تولید فولاد خام', value: '۳۱.۲', unit: 'میلیون تن', delta: '+۴.۱٪' },
|
||||||
|
{ label: 'صادرات فولاد', value: '۸.۷', unit: 'میلیون تن', delta: '+۱.۸٪' },
|
||||||
|
{ label: 'واردات سنگآهن', value: '۱۲.۴', unit: 'میلیون تن', delta: '-۲.۳٪' },
|
||||||
|
{ label: 'ظرفیت نصبشده', value: '۳۸', unit: 'میلیون تن', delta: '—' },
|
||||||
|
],
|
||||||
|
en: [
|
||||||
|
{ label: 'Crude Steel Production', value: '31.2', unit: 'Million tons', delta: '+4.1%' },
|
||||||
|
{ label: 'Steel Exports', value: '8.7', unit: 'Million tons', delta: '+1.8%' },
|
||||||
|
{ label: 'Iron Ore Imports', value: '12.4', unit: 'Million tons', delta: '-2.3%' },
|
||||||
|
{ label: 'Installed Capacity', value: '38', unit: 'Million tons', delta: '—' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
fa: {
|
||||||
|
overline: 'INDUSTRY PULSE', heading: 'نبض صنعت',
|
||||||
|
sub: 'سامانه جامع آمار و اطلاعات صنعت فولاد ایران — دادههای لحظهای تولید، صادرات، واردات و قیمتها',
|
||||||
|
statsLabel: 'آمار کلیدی صنعت · ۱۴۰۳',
|
||||||
|
gateAccess: 'دسترسی اعضا',
|
||||||
|
gateHeading: 'دسترسی کامل به سامانه نبض صنعت',
|
||||||
|
gateSub: 'اعضای اندیشکده به پایگاه داده جامع فولاد ایران شامل آمار لحظهای تولید، صادرات، واردات، قیمتهای داخلی و بینالمللی و ۵۰۰+ شاخص صنعتی دسترسی دارند.',
|
||||||
|
features: [
|
||||||
|
'دادههای لحظهای تولید کارخانههای بزرگ',
|
||||||
|
'آمار صادرات تفکیکشده بر اساس کشور مقصد',
|
||||||
|
'نمودارهای تعاملی و قابل دانلود',
|
||||||
|
'گزارشهای تحلیلی ماهانه اختصاصی',
|
||||||
|
],
|
||||||
|
cta: 'درخواست عضویت سازمانی ←',
|
||||||
|
ctaDone: '✓ درخواست شما ثبت شد. تیم ما ظرف ۴۸ ساعت با شما تماس میگیرد.',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
overline: 'INDUSTRY PULSE', heading: 'Industry Pulse',
|
||||||
|
sub: 'Comprehensive statistics and data platform for Iran\'s steel industry — live production, export, import, and price data',
|
||||||
|
statsLabel: 'KEY INDUSTRY STATS · 1403',
|
||||||
|
gateAccess: 'MEMBERS ONLY',
|
||||||
|
gateHeading: 'Full Access to Industry Pulse',
|
||||||
|
gateSub: 'Institute members have access to Iran\'s comprehensive steel database including live production stats, export/import data, domestic and international prices, and 500+ industrial indicators.',
|
||||||
|
features: [
|
||||||
|
'Real-time production data from major plants',
|
||||||
|
'Export statistics broken down by destination country',
|
||||||
|
'Interactive downloadable charts',
|
||||||
|
'Exclusive monthly analytical reports',
|
||||||
|
],
|
||||||
|
cta: 'Request Organizational Membership →',
|
||||||
|
ctaDone: '✓ Your request has been received. Our team will contact you within 48 hours.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Pulse() {
|
||||||
|
const { lang } = useLang()
|
||||||
|
const t = T[lang]
|
||||||
|
const stats = STATS[lang]
|
||||||
|
const [requested, setRequested] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div dir={lang === 'fa' ? 'rtl' : 'ltr'} style={{ background: 'var(--paper)', minHeight: '80vh' }}>
|
||||||
|
<div style={{ borderBottom: '3px solid var(--ink)' }}>
|
||||||
|
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 48, paddingBottom: 40 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
|
||||||
|
<div style={{ width: 36, height: 2, background: 'var(--red)' }} />
|
||||||
|
<span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>{t.overline}</span>
|
||||||
|
</div>
|
||||||
|
<h1 style={{ fontSize: 'clamp(28px,4vw,52px)', fontWeight: 900, letterSpacing: '-1.5px', color: 'var(--ink)', marginBottom: 12 }}>{t.heading}</h1>
|
||||||
|
<p style={{ fontSize: 15, color: 'var(--ink-3)', lineHeight: 1.7, maxWidth: 600 }}>{t.sub}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 48, paddingBottom: 64 }}>
|
||||||
|
<div style={{ marginBottom: 48 }}>
|
||||||
|
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '2px', color: 'var(--ink-4)', textTransform: 'uppercase', marginBottom: 20 }}>
|
||||||
|
{t.statsLabel}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 1, background: 'var(--rule-thin)' }}>
|
||||||
|
{stats.map((s, i) => (
|
||||||
|
<div key={i} style={{ background: 'var(--paper)', padding: '24px 20px', filter: i >= 2 ? 'blur(5px)' : 'none', userSelect: i >= 2 ? 'none' : 'auto', pointerEvents: i >= 2 ? 'none' : 'auto' }}>
|
||||||
|
<div style={{ fontSize: 32, fontWeight: 900, letterSpacing: '-1px', color: 'var(--ink)', lineHeight: 1, marginBottom: 6 }}>{s.value}</div>
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--ink-5)', marginBottom: 4 }}>{s.unit}</div>
|
||||||
|
<div style={{ fontSize: 10, fontWeight: 700, color: 'var(--ink-3)' }}>{s.label}</div>
|
||||||
|
<div style={{ fontSize: 10, fontWeight: 700, color: s.delta.startsWith('+') ? '#2d6a4f' : s.delta.startsWith('-') ? 'var(--red)' : 'var(--ink-5)', marginTop: 6 }}>{s.delta}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
style={{ border: '2px solid var(--ink)', padding: '40px 36px', maxWidth: 600, position: 'relative' }}
|
||||||
|
>
|
||||||
|
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, height: 4, background: 'var(--red)' }} />
|
||||||
|
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '2.5px', textTransform: 'uppercase', color: 'var(--red)', marginBottom: 16 }}>{t.gateAccess}</div>
|
||||||
|
<h2 style={{ fontSize: 20, fontWeight: 900, color: 'var(--ink)', marginBottom: 12, lineHeight: 1.3 }}>{t.gateHeading}</h2>
|
||||||
|
<p style={{ fontSize: 13, color: 'var(--ink-3)', lineHeight: 1.75, marginBottom: 24 }}>{t.gateSub}</p>
|
||||||
|
<ul style={{ listStyle: 'none', padding: 0, margin: '0 0 28px', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
{t.features.map(item => (
|
||||||
|
<li key={item} style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 13, color: 'var(--ink)' }}>
|
||||||
|
<span style={{ color: 'var(--red)', fontWeight: 900, fontSize: 14 }}>✓</span>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{!requested ? (
|
||||||
|
<button
|
||||||
|
onClick={() => setRequested(true)}
|
||||||
|
style={{ background: 'var(--ink)', color: 'var(--paper)', border: 'none', padding: '13px 32px', fontSize: 13, fontWeight: 700, fontFamily: 'Vazir, sans-serif', cursor: 'pointer', letterSpacing: '0.3px', transition: 'background 150ms' }}
|
||||||
|
>
|
||||||
|
{t.cta}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div style={{ background: 'rgba(26,23,18,0.05)', padding: '14px 20px', fontSize: 13, color: 'var(--ink)', fontWeight: 600 }}>
|
||||||
|
{t.ctaDone}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useLang } from '@/context/LangContext'
|
||||||
|
|
||||||
|
const SIGNALS = {
|
||||||
|
fa: [
|
||||||
|
{ tag: 'چین', title: 'صادرات فولاد چین به بالاترین رکورد ۸ ساله رسید', date: 'بهمن ۱۴۰۳', level: 'critical' },
|
||||||
|
{ tag: 'اروپا', title: 'CBAM اروپا از فروردین ۱۴۰۴ اجرایی میشود', date: 'دی ۱۴۰۳', level: 'high' },
|
||||||
|
{ tag: 'هند', title: 'هند ظرفیت تولید فولاد خود را تا ۲۰۳۰ دو برابر میکند', date: 'آذر ۱۴۰۳', level: 'medium' },
|
||||||
|
{ tag: 'سنگآهن', title: 'قیمت سنگآهن دالیان ۱۸٪ جهش کرد', date: 'بهمن ۱۴۰۳', level: 'high' },
|
||||||
|
{ tag: 'انرژی', title: 'هزینه برق صنایع فولاد ۳۲٪ افزایش یافت', date: 'دی ۱۴۰۳', level: 'high' },
|
||||||
|
{ tag: 'فناوری', title: 'اولین کارخانه فولاد هیدروژنی اروپا وارد مرحله تولید شد', date: 'آذر ۱۴۰۳', level: 'medium' },
|
||||||
|
],
|
||||||
|
en: [
|
||||||
|
{ tag: 'China', title: "China's Steel Exports Hit an 8-Year Record High", date: 'Feb 2025', level: 'critical' },
|
||||||
|
{ tag: 'Europe', title: "Europe's CBAM Mechanism Takes Effect from Spring 2025", date: 'Jan 2025', level: 'high' },
|
||||||
|
{ tag: 'India', title: 'India to Double Steel Production Capacity by 2030', date: 'Dec 2024', level: 'medium' },
|
||||||
|
{ tag: 'Iron Ore', title: 'Dalian Iron Ore Price Surges 18%', date: 'Feb 2025', level: 'high' },
|
||||||
|
{ tag: 'Energy', title: 'Steel Industry Electricity Costs Rise 32%', date: 'Jan 2025', level: 'high' },
|
||||||
|
{ tag: 'Technology', title: "Europe's First Hydrogen Steel Plant Enters Production", date: 'Dec 2024', level: 'medium' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEVEL_COLOR: Record<string, string> = {
|
||||||
|
critical: '#7f1d1d',
|
||||||
|
high: 'var(--red)',
|
||||||
|
medium: '#b45309',
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEVEL_LABEL = {
|
||||||
|
fa: { critical: 'بحرانی', high: 'بالا', medium: 'متوسط' },
|
||||||
|
en: { critical: 'Critical', high: 'High', medium: 'Medium' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
fa: { overline: 'FUTURE RADAR', heading: 'رادار آینده', sub: 'رصد سیگنالهای جهانی و منطقهای با اثر مستقیم بر صنعت فولاد ایران' },
|
||||||
|
en: { overline: 'FUTURE RADAR', heading: 'Future Radar', sub: 'Monitoring global and regional signals with direct impact on Iran\'s steel industry' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Radar() {
|
||||||
|
const { lang } = useLang()
|
||||||
|
const t = T[lang]
|
||||||
|
const signals = SIGNALS[lang]
|
||||||
|
const levelLabel = LEVEL_LABEL[lang]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div dir={lang === 'fa' ? 'rtl' : 'ltr'} style={{ background: 'var(--paper)', minHeight: '80vh' }}>
|
||||||
|
<div style={{ borderBottom: '3px solid var(--ink)' }}>
|
||||||
|
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 48, paddingBottom: 40 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
|
||||||
|
<div style={{ width: 36, height: 2, background: 'var(--red)' }} />
|
||||||
|
<span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>{t.overline}</span>
|
||||||
|
</div>
|
||||||
|
<h1 style={{ fontSize: 'clamp(28px,4vw,52px)', fontWeight: 900, letterSpacing: '-1.5px', color: 'var(--ink)', marginBottom: 12 }}>{t.heading}</h1>
|
||||||
|
<p style={{ fontSize: 15, color: 'var(--ink-3)', lineHeight: 1.7, maxWidth: 560 }}>{t.sub}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 48, paddingBottom: 64 }}>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 1, background: 'var(--rule-thin)' }}>
|
||||||
|
{signals.map((s, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.4, delay: i * 0.06 }}
|
||||||
|
style={{ background: 'var(--paper)', padding: '28px 24px', position: 'relative' }}
|
||||||
|
>
|
||||||
|
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, height: 3, background: LEVEL_COLOR[s.level] }} />
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>
|
||||||
|
<span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '1.5px', background: 'var(--ink)', color: 'var(--paper)', padding: '3px 8px' }}>{s.tag}</span>
|
||||||
|
<span style={{ fontSize: 9, fontWeight: 700, color: LEVEL_COLOR[s.level], letterSpacing: '1px' }}>{levelLabel[s.level as keyof typeof levelLabel]}</span>
|
||||||
|
</div>
|
||||||
|
<h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--ink)', lineHeight: 1.55, marginBottom: 16 }}>{s.title}</h3>
|
||||||
|
<div style={{ fontSize: 10, color: 'var(--ink-5)', fontWeight: 600 }}>{s.date}</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useLang } from '@/context/LangContext'
|
||||||
|
|
||||||
|
const TECHS = {
|
||||||
|
fa: [
|
||||||
|
{ num: '۰۱', title: 'فولاد هیدروژنی (DRI-H₂)', tag: 'انرژی پاک', summary: 'جایگزینی کک با هیدروژن سبز در کورههای احیای مستقیم — مسیر صنعت فولاد به سمت خنثیسازی کربن' },
|
||||||
|
{ num: '۰۲', title: 'دیجیتالسازی و AI در تولید', tag: 'هوش مصنوعی', summary: 'کاربرد یادگیری ماشین در بهینهسازی فرآیند ذوب، پیشبینی نقص کیفی و کاهش ضایعات' },
|
||||||
|
{ num: '۰۳', title: 'الکترولیز فولاد (MOE)', tag: 'فناوری نوین', summary: 'تولید فولاد از طریق الکترولیز اکسید آهن با برق تجدیدپذیر — فناوری نسل بعدی' },
|
||||||
|
{ num: '۰۴', title: 'بازیافت و اقتصاد چرخشی', tag: 'پایداری', summary: 'افزایش نرخ بازیافت ضایعات فولادی و کاهش وابستگی به سنگآهن خام' },
|
||||||
|
{ num: '۰۵', title: 'کوره قوس الکتریکی نسل جدید EAF', tag: 'تجهیزات', summary: 'EAF با راندمان انرژی ۴۰٪ بالاتر از نسل فعلی — مناسب برای شبکههای ناپایدار' },
|
||||||
|
{ num: '۰۶', title: 'توأمسازی CCS با تولید فولاد', tag: 'محیط زیست', summary: 'ترکیب کربنگیری با فرآیندهای موجود — راهحل میانمدت برای کاهش انتشار' },
|
||||||
|
],
|
||||||
|
en: [
|
||||||
|
{ num: '01', title: 'Hydrogen Steel (DRI-H₂)', tag: 'Clean Energy', summary: 'Replacing coke with green hydrogen in direct reduction furnaces — the steel industry\'s path to carbon neutrality' },
|
||||||
|
{ num: '02', title: 'Digitalization & AI in Production', tag: 'Artificial Intelligence', summary: 'Machine learning applied to smelting optimization, quality defect prediction, and waste reduction' },
|
||||||
|
{ num: '03', title: 'Molten Oxide Electrolysis (MOE)', tag: 'Emerging Tech', summary: 'Producing steel via electrolysis of iron oxide using renewable electricity — next-generation technology' },
|
||||||
|
{ num: '04', title: 'Recycling & Circular Economy', tag: 'Sustainability', summary: 'Increasing steel scrap recycling rates and reducing dependence on raw iron ore' },
|
||||||
|
{ num: '05', title: 'New-Generation Electric Arc Furnace', tag: 'Equipment', summary: 'EAF with 40% higher energy efficiency than current generation — optimized for unstable power grids' },
|
||||||
|
{ num: '06', title: 'CCS Integration with Steel Production',tag: 'Environment', summary: 'Combining carbon capture with existing processes — a medium-term solution for emission reduction' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
fa: { overline: 'TECH FRONTIER', heading: 'مرز فناوری', sub: 'فناوریهای نوظهور در صنعت فولاد جهان و افق کاربرد آنها در ایران' },
|
||||||
|
en: { overline: 'TECH FRONTIER', heading: 'Tech Frontier', sub: 'Emerging technologies in the global steel industry and their application horizon in Iran' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Technology() {
|
||||||
|
const { lang } = useLang()
|
||||||
|
const t = T[lang]
|
||||||
|
const techs = TECHS[lang]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div dir={lang === 'fa' ? 'rtl' : 'ltr'} style={{ background: 'var(--paper)', minHeight: '80vh' }}>
|
||||||
|
<div style={{ borderBottom: '3px solid var(--ink)' }}>
|
||||||
|
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 48, paddingBottom: 40 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
|
||||||
|
<div style={{ width: 36, height: 2, background: 'var(--red)' }} />
|
||||||
|
<span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '3px', textTransform: 'uppercase', color: 'var(--ink-4)' }}>{t.overline}</span>
|
||||||
|
</div>
|
||||||
|
<h1 style={{ fontSize: 'clamp(28px,4vw,52px)', fontWeight: 900, letterSpacing: '-1.5px', color: 'var(--ink)', marginBottom: 12 }}>{t.heading}</h1>
|
||||||
|
<p style={{ fontSize: 15, color: 'var(--ink-3)', lineHeight: 1.7, maxWidth: 560 }}>{t.sub}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-12 max-md:px-5" style={{ paddingTop: 48, paddingBottom: 64 }}>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 1, background: 'var(--rule-thin)' }}>
|
||||||
|
{techs.map((tech, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.4, delay: i * 0.06 }}
|
||||||
|
style={{ background: 'var(--paper)', padding: '28px 24px', position: 'relative' }}
|
||||||
|
>
|
||||||
|
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, height: 3, background: 'var(--ink)' }} />
|
||||||
|
<div style={{ fontSize: 48, fontWeight: 900, letterSpacing: '-3px', color: 'rgba(26,23,18,0.06)', lineHeight: 1, marginBottom: 10, direction: 'ltr' }}>{tech.num}</div>
|
||||||
|
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '1.5px', color: 'var(--red)', marginBottom: 10, textTransform: 'uppercase' }}>{tech.tag}</div>
|
||||||
|
<h3 style={{ fontSize: 15, fontWeight: 800, color: 'var(--ink)', lineHeight: 1.45, marginBottom: 12 }}>{tech.title}</h3>
|
||||||
|
<p style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.7 }}>{tech.summary}</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue