blockchain_view.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. const { div, h2, h3, p, section, button, form, a, input, span, pre, table, tr, td, strong } = require("../server/node_modules/hyperaxe");
  2. const { template, i18n } = require("../views/main_views");
  3. const moment = require("../server/node_modules/moment");
  4. const FILTER_LABELS = {
  5. votes: i18n.typeVotes, vote: i18n.typeVote, recent: i18n.recent, all: i18n.all,
  6. mine: i18n.mine, tombstone: i18n.typeTombstone, logs: i18n.typeLog || 'LOGS', pixelia: i18n.typePixelia,
  7. curriculum: i18n.typeCurriculum, document: i18n.typeDocument, bookmark: i18n.typeBookmark,
  8. feed: i18n.typeFeed, event: i18n.typeEvent, task: i18n.typeTask, report: i18n.typeReport,
  9. image: i18n.typeImage, audio: i18n.typeAudio, video: i18n.typeVideo, post: i18n.typePost,
  10. forum: i18n.typeForum, about: i18n.typeAbout, contact: i18n.typeContact, pub: i18n.typePub,
  11. transfer: i18n.typeTransfer, market: i18n.typeMarket, job: i18n.typeJob, tribe: i18n.typeTribe,
  12. project: i18n.typeProject, banking: i18n.typeBanking, bankWallet: i18n.typeBankWallet, bankClaim: i18n.typeBankClaim,
  13. aiExchange: i18n.typeAiExchange, parliament: i18n.typeParliament, courts: i18n.typeCourts,
  14. map: i18n.typeMap, shop: i18n.typeShop, shopProduct: i18n.typeShopProduct || 'Shop Product',
  15. pad: i18n.typePad || 'PAD', chat: i18n.typeChat || 'CHAT', gameScore: i18n.typeGameScore || 'GAME SCORE',
  16. calendar: i18n.typeCalendar || 'CALENDAR', torrent: i18n.typeTorrent
  17. };
  18. const BASE_FILTERS = ['recent', 'all', 'mine', 'tombstone', 'logs'];
  19. const CAT_BLOCK1 = ['votes', 'event', 'task', 'report', 'calendar', 'parliament', 'courts'];
  20. const CAT_BLOCK2 = ['pub', 'tribe', 'about', 'contact', 'curriculum', 'vote', 'aiExchange'];
  21. const CAT_BLOCK3 = ['banking', 'job', 'market', 'project', 'transfer', 'feed', 'post', 'pixelia', 'shop', 'gameScore'];
  22. const CAT_BLOCK4 = ['forum', 'pad', 'chat', 'bookmark', 'image', 'video', 'audio', 'document', 'map', 'torrent'];
  23. const SEARCH_FIELDS = ['author','id','from','to'];
  24. const formatCarbon = (bytes) => {
  25. const n = Number(bytes) || 0;
  26. if (!n) return '0 µg CO₂';
  27. const grams = (n / (1024 * 1024)) * 0.095;
  28. if (grams >= 1) return `${grams.toFixed(2)} g CO₂`;
  29. const mg = grams * 1000;
  30. if (mg >= 1) return `${mg.toFixed(2)} mg CO₂`;
  31. const ug = mg * 1000;
  32. return `${ug.toFixed(2)} µg CO₂`;
  33. };
  34. const hiddenSearchInputs = (search) =>
  35. SEARCH_FIELDS.map(k => {
  36. const v = String(search?.[k] ?? '').trim();
  37. return v ? input({ type: 'hidden', name: k, value: v }) : null;
  38. }).filter(Boolean);
  39. const toDatetimeLocal = (s) => {
  40. const raw = String(s || '').trim();
  41. if (!raw) return '';
  42. const ts = new Date(raw).getTime();
  43. if (!Number.isFinite(ts)) return '';
  44. return moment(ts).format('YYYY-MM-DDTHH:mm');
  45. };
  46. const toQueryString = (filter, search = {}) => {
  47. const parts = [];
  48. const f = String(filter || '').trim();
  49. if (f) parts.push(`filter=${encodeURIComponent(f)}`);
  50. for (const k of SEARCH_FIELDS) {
  51. const v = String(search?.[k] ?? '').trim();
  52. if (v) parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
  53. }
  54. return parts.length ? `?${parts.join('&')}` : '';
  55. };
  56. const filterBlocks = (blocks, filter, userId) => {
  57. if (filter === 'recent') return blocks.filter(b => Date.now() - b.ts < 24*60*60*1000);
  58. if (filter === 'mine') return blocks.filter(b => b.author === userId);
  59. if (filter === 'all') return blocks;
  60. if (filter === 'banking') return blocks.filter(b => b.type === 'bankWallet' || b.type === 'bankClaim');
  61. if (filter === 'parliament') {
  62. const pset = new Set(['parliamentTerm','parliamentProposal','parliamentLaw','parliamentCandidature','parliamentRevocation']);
  63. return blocks.filter(b => pset.has(b.type));
  64. }
  65. if (filter === 'courts') {
  66. const cset = new Set(['courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote']);
  67. return blocks.filter(b => cset.has(b.type));
  68. }
  69. if (filter === 'shop') return blocks.filter(b => b.type === 'shop' || b.type === 'shopProduct');
  70. if (filter === 'logs') return blocks.filter(b => b.type === 'log' && b.author === userId);
  71. return blocks.filter(b => b.type === filter);
  72. };
  73. const computeStats = (blocks) => {
  74. const arr = Array.isArray(blocks) ? blocks : [];
  75. const byType = new Map();
  76. const byAuthor = new Map();
  77. for (const b of arr) {
  78. if (!b || !b.type) continue;
  79. byType.set(b.type, (byType.get(b.type) || 0) + 1);
  80. if (b.author) byAuthor.set(b.author, (byAuthor.get(b.author) || 0) + 1);
  81. }
  82. const typeBreakdown = Array.from(byType.entries())
  83. .map(([type, count]) => ({ type, count }))
  84. .sort((a, b) => b.count - a.count);
  85. const topAuthors = Array.from(byAuthor.entries())
  86. .map(([author, count]) => ({ author, count }))
  87. .sort((a, b) => b.count - a.count)
  88. .slice(0, 5);
  89. return { total: arr.length, typeBreakdown, topAuthors };
  90. };
  91. const renderStatsPanel = (stats, currentFilter, search) => {
  92. if (!stats || stats.total === 0) return null;
  93. const topTypes = stats.typeBreakdown.slice(0, 10);
  94. return div({ class: 'blockchain-stats' },
  95. div({ class: 'tags-header' },
  96. h3(`${i18n.blockchainStatsTitle || 'Stats'} (${stats.total})`)
  97. ),
  98. topTypes.length
  99. ? div({ class: 'blockchain-stats-types' },
  100. h3({ class: 'blockchain-stats-subtitle' }, i18n.blockchainStatsTypeBreakdown || 'Type breakdown'),
  101. div({ class: 'mode-buttons-cols' },
  102. topTypes.map(({ type, count }) =>
  103. form({ method: 'GET', action: '/blockexplorer' },
  104. input({ type: 'hidden', name: 'filter', value: type }),
  105. ...hiddenSearchInputs(search),
  106. button({
  107. type: 'submit',
  108. class: currentFilter === type ? 'filter-btn active' : 'filter-btn'
  109. }, `${(FILTER_LABELS[type] || type).toUpperCase()} (${count})`)
  110. )
  111. )
  112. )
  113. )
  114. : null,
  115. stats.topAuthors.length
  116. ? div({ class: 'blockchain-stats-authors' },
  117. h3({ class: 'blockchain-stats-subtitle' }, i18n.blockchainStatsTopAuthors || 'Top authors'),
  118. table({ class: 'block-info-table' },
  119. tr(
  120. td({ class: 'card-label' }, i18n.blockchainBlockAuthor),
  121. td({ class: 'card-label' }, i18n.blockchainStatsCount || 'Blocks')
  122. ),
  123. ...stats.topAuthors.map(({ author, count }) =>
  124. tr(
  125. td(a({ href: `/blockexplorer?filter=all&author=${encodeURIComponent(author)}`, class: 'user-link block-author' }, author)),
  126. td(String(count))
  127. )
  128. )
  129. )
  130. )
  131. : null
  132. );
  133. };
  134. const generateFilterButtons = (filters, currentFilter, action, search = {}) =>
  135. div({ class: 'mode-buttons-cols' },
  136. filters.map(mode =>
  137. form({ method: 'GET', action },
  138. input({ type: 'hidden', name: 'filter', value: mode }),
  139. ...hiddenSearchInputs(search),
  140. button({
  141. type: 'submit',
  142. class: currentFilter === mode ? 'filter-btn active' : 'filter-btn'
  143. }, (FILTER_LABELS[mode]||mode).toUpperCase())
  144. )
  145. )
  146. );
  147. const getViewDetailsAction = (type, block) => {
  148. if (block && block.content && typeof block.content.encryptedPayload === 'string') return null;
  149. switch (type) {
  150. case 'votes': return `/votes/${encodeURIComponent(block.id)}`;
  151. case 'transfer': return `/transfers/${encodeURIComponent(block.id)}`;
  152. case 'pixelia': return `/pixelia`;
  153. case 'tribe': return `/tribe/${encodeURIComponent(block.id)}`;
  154. case 'curriculum': return `/inhabitant/${encodeURIComponent(block.author)}`;
  155. case 'image': return `/images/${encodeURIComponent(block.id)}`;
  156. case 'audio': return `/audios/${encodeURIComponent(block.id)}`;
  157. case 'video': return `/videos/${encodeURIComponent(block.id)}`;
  158. case 'forum': return `/forum/${encodeURIComponent(block.content?.key||block.id)}`;
  159. case 'document': return `/documents/${encodeURIComponent(block.id)}`;
  160. case 'bookmark': return `/bookmarks/${encodeURIComponent(block.id)}`;
  161. case 'event': return `/events/${encodeURIComponent(block.id)}`;
  162. case 'task': return `/tasks/${encodeURIComponent(block.id)}`;
  163. case 'about': return `/author/${encodeURIComponent(block.author)}`;
  164. case 'post': return `/thread/${encodeURIComponent(block.id)}#${encodeURIComponent(block.id)}`;
  165. case 'vote': return `/thread/${encodeURIComponent(block.content.vote.link)}#${encodeURIComponent(block.content.vote.link)}`;
  166. case 'contact': return `/inhabitants`;
  167. case 'pub': return `/invites`;
  168. case 'market': return `/market/${encodeURIComponent(block.id)}`;
  169. case 'job': return `/jobs/${encodeURIComponent(block.id)}`;
  170. case 'project': return `/projects/${encodeURIComponent(block.id)}`;
  171. case 'report': return `/reports/${encodeURIComponent(block.id)}`;
  172. case 'calendar': return `/calendars/${encodeURIComponent(block.id)}`;
  173. case 'bankWallet': return `/wallet`;
  174. case 'bankClaim': return `/banking${block.content?.epochId ? `/epoch/${encodeURIComponent(block.content.epochId)}` : ''}`;
  175. case 'parliamentTerm': return `/parliament`;
  176. case 'parliamentProposal': return `/parliament`;
  177. case 'parliamentLaw': return `/parliament`;
  178. case 'parliamentCandidature': return `/parliament`;
  179. case 'parliamentRevocation': return `/parliament`;
  180. case 'courtsCase': return `/courts`;
  181. case 'courtsEvidence': return `/courts`;
  182. case 'courtsAnswer': return `/courts`;
  183. case 'courtsVerdict': return `/courts`;
  184. case 'courtsSettlement': return `/courts`;
  185. case 'courtsSettlementProposal': return `/courts`;
  186. case 'courtsSettlementAccepted': return `/courts`;
  187. case 'courtsNomination': return `/courts`;
  188. case 'courtsNominationVote': return `/courts`;
  189. case 'map': return `/maps/${encodeURIComponent(block.id)}`;
  190. case 'torrent': return `/torrents/${encodeURIComponent(block.id)}`;
  191. case 'mapMarker': return block.content?.mapId ? `/maps/${encodeURIComponent(block.content.mapId)}` : `/maps`;
  192. case 'shop': return `/shops/${encodeURIComponent(block.id)}`;
  193. case 'shopProduct': return `/shops/product/${encodeURIComponent(block.id)}`;
  194. case 'pad': return `/pads/${encodeURIComponent(block.id)}`;
  195. case 'chat': return `/chats/${encodeURIComponent(block.id)}`;
  196. case 'gameScore': return `/games?filter=scoring`;
  197. case 'log': return `/logs/view/${encodeURIComponent(block.id)}`;
  198. case 'calendarDate':
  199. case 'calendarNote': return block.content?.calendarId ? `/calendars/${encodeURIComponent(block.content.calendarId)}` : `/calendars`;
  200. case 'padEntry': return block.content?.padId ? `/pads/${encodeURIComponent(block.content.padId)}` : `/pads`;
  201. case 'chatMessage': return block.content?.roomId ? `/chats/${encodeURIComponent(block.content.roomId)}` : `/chats`;
  202. default: return null;
  203. }
  204. };
  205. const TYPE_COLORS = {
  206. post:'#3498db', vote:'#9b59b6', votes:'#9b59b6', about:'#1abc9c', contact:'#16a085',
  207. pub:'#2ecc71', tribe:'#e67e22', event:'#e74c3c', task:'#f39c12', report:'#c0392b',
  208. image:'#2980b9', audio:'#8e44ad', video:'#d35400', document:'#27ae60', bookmark:'#f1c40f',
  209. forum:'#1abc9c', feed:'#95a5a6', transfer:'#e74c3c', market:'#e67e22', job:'#3498db',
  210. project:'#2ecc71', banking:'#f39c12', bankWallet:'#f39c12', bankClaim:'#f39c12',
  211. pixelia:'#9b59b6', curriculum:'#1abc9c', aiExchange:'#3498db', tombstone:'#7f8c8d',
  212. parliamentTerm:'#8e44ad', parliamentProposal:'#8e44ad', parliamentLaw:'#8e44ad',
  213. parliamentCandidature:'#8e44ad', parliamentRevocation:'#8e44ad',
  214. courtsCase:'#c0392b', courtsEvidence:'#c0392b', courtsAnswer:'#c0392b',
  215. courtsVerdict:'#c0392b', courtsSettlement:'#c0392b', courtsNomination:'#c0392b',
  216. map:'#27ae60', mapMarker:'#27ae60',
  217. shop:'#e67e22', shopProduct:'#e67e22',
  218. pad:'#2ecc71', chat:'#3498db', gameScore:'#f39c12',
  219. calendar:'#e74c3c'
  220. };
  221. const renderBlockDiagram = (blocks, qs) => {
  222. const last2 = blocks.slice(0, 2);
  223. if (!last2.length) return null;
  224. return div({ class: 'block-diagram-section' },
  225. h3({ class: 'block-diagram-title' }, i18n.blockchainLatestDatagram || 'Latest Datagram'),
  226. ...last2.map(block => {
  227. const ts = moment(block.ts).format('YYYY-MM-DD HH:mm:ss');
  228. const typeLabel = (FILTER_LABELS[block.type] || block.type).toUpperCase();
  229. const color = TYPE_COLORS[block.type] || '#95a5a6';
  230. const shortId = block.id.length > 20 ? block.id.slice(0, 10) + '…' + block.id.slice(-8) : block.id;
  231. const shortAuthor = block.author.length > 20 ? block.author.slice(0, 10) + '…' + block.author.slice(-8) : block.author;
  232. const contentKeys = Object.keys(block.content || {}).filter(k => k !== 'type').join(', ');
  233. const flags = [
  234. block.isTombstoned ? 'TOMBSTONED' : null,
  235. block.isReplaced ? 'REPLACED' : null,
  236. block.content?.replaces ? 'EDIT' : null
  237. ].filter(Boolean).join(' | ') || '—';
  238. const datagramQs = qs ? `${qs}&view=datagram` : '?view=datagram';
  239. const typeClass = `bd-type-${String(block.type || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '-')}`;
  240. return a({ href: `/blockexplorer/block/${encodeURIComponent(block.id)}${datagramQs}`, class: 'block-diagram-link' },
  241. div({ class: `block-diagram ${typeClass}` },
  242. div({ class: 'block-diagram-ruler' },
  243. span('0'), span('4'), span('8'), span('16'), span('24'), span('31')
  244. ),
  245. div({ class: 'block-diagram-grid' },
  246. div({ class: 'block-diagram-cell bd-seq' },
  247. span({ class: 'bd-label' }, 'SEQ:'),
  248. span({ class: 'bd-value' }, String(block.content?.sequence || '—'))
  249. ),
  250. div({ class: 'block-diagram-cell bd-type' },
  251. span({ class: 'bd-label' }, 'TYPE:'),
  252. span({ class: 'bd-value' }, typeLabel)
  253. ),
  254. div({ class: 'block-diagram-cell bd-ts' },
  255. span({ class: 'bd-label' }, 'TIMESTAMP:'),
  256. span({ class: 'bd-value' }, ts)
  257. ),
  258. div({ class: 'block-diagram-cell bd-id' },
  259. span({ class: 'bd-label' }, 'BLOCK ID:'),
  260. span({ class: 'bd-value' }, shortId)
  261. ),
  262. div({ class: 'block-diagram-cell bd-author' },
  263. span({ class: 'bd-label' }, 'AUTHOR:'),
  264. span({ class: 'bd-value' }, shortAuthor)
  265. ),
  266. div({ class: 'block-diagram-cell bd-flags' },
  267. span({ class: 'bd-label' }, 'FLAGS:'),
  268. span({ class: 'bd-value' }, flags)
  269. ),
  270. div({ class: 'block-diagram-cell bd-ctype' },
  271. span({ class: 'bd-label' }, 'CONTENT.TYPE:'),
  272. span({ class: 'bd-value' }, block.content?.type || '—')
  273. ),
  274. div({ class: 'block-diagram-cell bd-data' },
  275. span({ class: 'bd-label' }, 'CONTENT:'),
  276. span({ class: 'bd-value' }, contentKeys || '—')
  277. )
  278. )
  279. )
  280. );
  281. })
  282. );
  283. };
  284. const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, viewMode = 'block', restricted = false) => {
  285. if (!block) {
  286. return template(
  287. i18n.blockchain,
  288. section(
  289. div({ class: 'tags-header' },
  290. h2(i18n.blockchain),
  291. p(i18n.blockchainDescription)
  292. ),
  293. p(i18n.blockchainNoBlocks || 'No blocks')
  294. )
  295. );
  296. }
  297. const qs = toQueryString(filter, search);
  298. const isDatagram = viewMode === 'datagram';
  299. const blockContent = restricted
  300. ? div(
  301. div({ class: 'block-single' },
  302. div({ class: 'block-row block-row--meta' },
  303. span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockID}:`),
  304. span({ class: 'blockchain-card-value' }, block.id)
  305. ),
  306. div({ class: 'block-row block-row--meta' },
  307. span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockTimestamp}:`),
  308. span({ class: 'blockchain-card-value' }, moment(block.ts).format('YYYY-MM-DDTHH:mm:ss.SSSZ')),
  309. span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockType}:`),
  310. span({ class: 'blockchain-card-value' }, (FILTER_LABELS[block.type]||block.type).toUpperCase())
  311. )
  312. ),
  313. div({ class: 'block-row block-row--content' },
  314. p({ class: 'access-denied-msg' }, i18n.blockAccessRestricted)
  315. )
  316. )
  317. : isDatagram
  318. ? renderBlockDiagram([block], qs)
  319. : div(
  320. div({ class: 'block-single' },
  321. div({ class: 'block-row block-row--meta' },
  322. span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockID}:`),
  323. span({ class: 'blockchain-card-value' }, block.id)
  324. ),
  325. div({ class: 'block-row block-row--meta' },
  326. span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockTimestamp}:`),
  327. span({ class: 'blockchain-card-value' }, moment(block.ts).format('YYYY-MM-DDTHH:mm:ss.SSSZ')),
  328. span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockType}:`),
  329. span({ class: 'blockchain-card-value' }, (FILTER_LABELS[block.type]||block.type).toUpperCase())
  330. ),
  331. div({ class: 'block-row block-row--meta' },
  332. span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockCarbon || 'Carbon footprint'}:`),
  333. span({ class: 'blockchain-card-value' }, formatCarbon(block.size))
  334. ),
  335. div({ class: 'block-row block-row--meta block-row--meta-spaced' },
  336. a({ href:`/author/${encodeURIComponent(block.author)}`, class:'block-author user-link' }, block.author)
  337. )
  338. ),
  339. div({ class:'block-row block-row--content' },
  340. div({ class:'block-content-preview' },
  341. block.content && typeof block.content.encryptedPayload === 'string'
  342. ? div({ class: 'encrypted-payload-box' },
  343. p({ class: 'encrypted-label' }, `[${i18n.bxEncrypted || 'ENCRYPTED'}]`),
  344. p({ class: 'encrypted-hex-label' }, i18n.bxEncryptedHexLabel || 'Ciphertext (preview)'),
  345. pre({ class: 'json-content' }, String(block.content.encryptedPayload).slice(0, 128) + (String(block.content.encryptedPayload).length > 128 ? '…' : ''))
  346. )
  347. : pre({ class:'json-content' }, JSON.stringify(block.content,null,2))
  348. )
  349. )
  350. );
  351. return template(
  352. i18n.blockchain,
  353. section(
  354. div({ class: 'tags-header' },
  355. h2(i18n.blockchain),
  356. p(i18n.blockchainDescription)
  357. ),
  358. div({ class: 'mode-buttons-row' },
  359. div({ class: 'filter-column' },
  360. generateFilterButtons(BASE_FILTERS, filter, '/blockexplorer', search)
  361. ),
  362. div({ class: 'filter-column' },
  363. generateFilterButtons(CAT_BLOCK1, filter, '/blockexplorer', search),
  364. generateFilterButtons(CAT_BLOCK2, filter, '/blockexplorer', search)
  365. ),
  366. div({ class: 'filter-column' },
  367. generateFilterButtons(CAT_BLOCK3, filter, '/blockexplorer', search),
  368. generateFilterButtons(CAT_BLOCK4, filter, '/blockexplorer', search)
  369. )
  370. ),
  371. blockContent,
  372. div({ class:'block-row block-row--back' },
  373. form({ method:'GET', action:'/blockexplorer' },
  374. input({ type: 'hidden', name: 'filter', value: filter }),
  375. ...hiddenSearchInputs(search),
  376. button({ type:'submit', class:'filter-btn' }, `← ${i18n.blockchainBack}`)
  377. ),
  378. !block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
  379. form({ method:'GET', action:getViewDetailsAction(block.type, block) },
  380. button({ type:'submit', class:'filter-btn' }, i18n.visitContent)
  381. )
  382. : (block.isTombstoned || block.isReplaced) ?
  383. div({ class: 'deleted-label' },
  384. i18n.blockchainContentDeleted || "This content has been deleted."
  385. )
  386. : null
  387. )
  388. )
  389. );
  390. };
  391. const renderBlockchainView = (blocks, filter, userId, search = {}) => {
  392. const s = search || {};
  393. const authorVal = String(s.author || '');
  394. const idVal = String(s.id || '');
  395. const fromVal = toDatetimeLocal(s.from);
  396. const toVal = toDatetimeLocal(s.to);
  397. const shown = filterBlocks(blocks, filter, userId);
  398. const qs = toQueryString(filter, s);
  399. return template(
  400. i18n.blockchain,
  401. section(
  402. div({ class:'tags-header' },
  403. h2(i18n.blockchain),
  404. p(i18n.blockchainDescription)
  405. ),
  406. div({ class:'mode-buttons-row' },
  407. div({ class: 'filter-column' },
  408. generateFilterButtons(BASE_FILTERS, filter, '/blockexplorer', s)
  409. ),
  410. div({ class: 'filter-column' },
  411. generateFilterButtons(CAT_BLOCK1, filter, '/blockexplorer', s),
  412. generateFilterButtons(CAT_BLOCK2, filter, '/blockexplorer', s)
  413. ),
  414. div({ class: 'filter-column' },
  415. generateFilterButtons(CAT_BLOCK3, filter, '/blockexplorer', s),
  416. generateFilterButtons(CAT_BLOCK4, filter, '/blockexplorer', s)
  417. )
  418. ),
  419. div({ class: 'blockexplorer-search' },
  420. form({ method: 'GET', action: '/blockexplorer', class: 'blockexplorer-search-form' },
  421. input({ type: 'hidden', name: 'filter', value: filter }),
  422. div({ class: 'blockexplorer-search-row' },
  423. div({ class: 'blockexplorer-search-pair' },
  424. input({ type: 'text', name: 'id', value: idVal, placeholder: i18n.blockchainBlockID, class: 'blockexplorer-search-input' }),
  425. input({ type: 'text', name: 'author', value: authorVal, placeholder: i18n.courtsJudgeIdPh, class: 'blockexplorer-search-input' })
  426. ),
  427. div({ class: 'blockexplorer-search-dates' },
  428. input({ type: 'datetime-local', name: 'from', value: fromVal, class: 'blockexplorer-search-input' }),
  429. input({ type: 'datetime-local', name: 'to', value: toVal, class: 'blockexplorer-search-input' })
  430. ),
  431. div({ class: 'blockexplorer-search-actions' },
  432. button({ type: 'submit', class: 'filter-box__button' }, i18n.searchSubmit)
  433. )
  434. )
  435. )
  436. ),
  437. renderBlockDiagram(shown, qs),
  438. h2({ class: 'block-diagram-title' }, 'Blockchain Blocks'),
  439. shown.length === 0
  440. ? div(p(i18n.blockchainNoBlocks))
  441. : shown
  442. .sort((a,b)=>{
  443. const ta = a.type==='market'&&a.content.updatedAt
  444. ? new Date(a.content.updatedAt).getTime()
  445. : a.ts;
  446. const tb = b.type==='market'&&b.content.updatedAt
  447. ? new Date(b.content.updatedAt).getTime()
  448. : b.ts;
  449. return tb - ta;
  450. })
  451. .map(block=>
  452. div({ class:'block' },
  453. div({ class:'block-buttons' },
  454. block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}`, class:'btn-singleview', title:i18n.blockchainDetails }, '⦿'),
  455. block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram || 'Datagram' }, '⊞'),
  456. !block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
  457. form({ method:'GET', action:getViewDetailsAction(block.type, block) },
  458. button({ type:'submit', class:'filter-btn' }, i18n.visitContent)
  459. )
  460. : (block.isTombstoned || block.isReplaced) ?
  461. div({ class: 'deleted-label' },
  462. i18n.blockchainContentDeleted || "This content has been deleted."
  463. )
  464. : null
  465. ),
  466. div({ class:'block-row block-row--meta' },
  467. table({ class:'block-info-table' },
  468. tr(td({ class:'card-label' }, i18n.blockchainBlockTimestamp), td({ class:'card-value' }, moment(block.ts).format('YYYY-MM-DDTHH:mm:ss.SSSZ'))),
  469. tr(td({ class:'card-label' }, i18n.blockchainBlockID), td({ class:'card-value' }, block.id)),
  470. tr(td({ class:'card-label' }, i18n.blockchainBlockType), td({ class:'card-value' }, (FILTER_LABELS[block.type]||block.type).toUpperCase())),
  471. tr(td({ class:'card-label' }, i18n.blockchainBlockAuthor), td({ class:'card-value' }, a({ href:`/author/${encodeURIComponent(block.author)}`, class:'block-author user-link' }, block.author))),
  472. tr(td({ class:'card-label' }, i18n.blockchainBlockCarbon || 'Carbon footprint'), td({ class:'card-value' }, formatCarbon(block.size)))
  473. )
  474. )
  475. )
  476. )
  477. )
  478. );
  479. };
  480. module.exports = { renderBlockchainView, renderSingleBlockView, computeStats };