blockchain_view.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. const { div, h2, p, section, button, form, a, input, span, pre, table, tr, td } = 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, 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
  14. };
  15. const BASE_FILTERS = ['recent', 'all', 'mine', 'tombstone'];
  16. const CAT_BLOCK1 = ['votes', 'event', 'task', 'report', 'parliament'];
  17. const CAT_BLOCK2 = ['pub', 'tribe', 'about', 'contact', 'curriculum', 'vote', 'aiExchange'];
  18. const CAT_BLOCK3 = ['banking', 'job', 'market', 'project', 'transfer', 'feed', 'post', 'pixelia'];
  19. const CAT_BLOCK4 = ['forum', 'bookmark', 'image', 'video', 'audio', 'document'];
  20. const filterBlocks = (blocks, filter, userId) => {
  21. if (filter === 'recent') return blocks.filter(b => Date.now() - b.ts < 24*60*60*1000);
  22. if (filter === 'mine') return blocks.filter(b => b.author === userId);
  23. if (filter === 'all') return blocks;
  24. if (filter === 'banking') return blocks.filter(b => b.type === 'bankWallet' || b.type === 'bankClaim');
  25. if (filter === 'parliament') {
  26. const pset = new Set(['parliamentTerm','parliamentProposal','parliamentLaw','parliamentCandidature','parliamentRevocation']);
  27. return blocks.filter(b => pset.has(b.type));
  28. }
  29. return blocks.filter(b => b.type === filter);
  30. };
  31. const generateFilterButtons = (filters, currentFilter, action) =>
  32. div({ class: 'mode-buttons-cols' },
  33. filters.map(mode =>
  34. form({ method: 'GET', action },
  35. input({ type: 'hidden', name: 'filter', value: mode }),
  36. button({
  37. type: 'submit',
  38. class: currentFilter === mode ? 'filter-btn active' : 'filter-btn'
  39. }, (FILTER_LABELS[mode]||mode).toUpperCase())
  40. )
  41. )
  42. );
  43. const getViewDetailsAction = (type, block) => {
  44. switch (type) {
  45. case 'votes': return `/votes/${encodeURIComponent(block.id)}`;
  46. case 'transfer': return `/transfers/${encodeURIComponent(block.id)}`;
  47. case 'pixelia': return `/pixelia`;
  48. case 'tribe': return `/tribe/${encodeURIComponent(block.id)}`;
  49. case 'curriculum': return `/inhabitant/${encodeURIComponent(block.author)}`;
  50. case 'image': return `/images/${encodeURIComponent(block.id)}`;
  51. case 'audio': return `/audios/${encodeURIComponent(block.id)}`;
  52. case 'video': return `/videos/${encodeURIComponent(block.id)}`;
  53. case 'forum': return `/forum/${encodeURIComponent(block.content?.key||block.id)}`;
  54. case 'document': return `/documents/${encodeURIComponent(block.id)}`;
  55. case 'bookmark': return `/bookmarks/${encodeURIComponent(block.id)}`;
  56. case 'event': return `/events/${encodeURIComponent(block.id)}`;
  57. case 'task': return `/tasks/${encodeURIComponent(block.id)}`;
  58. case 'about': return `/author/${encodeURIComponent(block.author)}`;
  59. case 'post': return `/thread/${encodeURIComponent(block.id)}#${encodeURIComponent(block.id)}`;
  60. case 'vote': return `/thread/${encodeURIComponent(block.content.vote.link)}#${encodeURIComponent(block.content.vote.link)}`;
  61. case 'contact': return `/inhabitants`;
  62. case 'pub': return `/invites`;
  63. case 'market': return `/market/${encodeURIComponent(block.id)}`;
  64. case 'job': return `/jobs/${encodeURIComponent(block.id)}`;
  65. case 'project': return `/projects/${encodeURIComponent(block.id)}`;
  66. case 'report': return `/reports/${encodeURIComponent(block.id)}`;
  67. case 'bankWallet': return `/wallet`;
  68. case 'bankClaim': return `/banking${block.content?.epochId ? `/epoch/${encodeURIComponent(block.content.epochId)}` : ''}`;
  69. case 'parliamentTerm': return `/parliament`;
  70. case 'parliamentProposal': return `/parliament`;
  71. case 'parliamentLaw': return `/parliament`;
  72. case 'parliamentCandidature': return `/parliament`;
  73. case 'parliamentRevocation': return `/parliament`;
  74. default: return null;
  75. }
  76. };
  77. const renderSingleBlockView = (block, filter) =>
  78. template(
  79. i18n.blockchain,
  80. section(
  81. div({ class: 'tags-header' },
  82. h2(i18n.blockchain),
  83. p(i18n.blockchainDescription)
  84. ),
  85. div({ class: 'mode-buttons-row' },
  86. div({ style: 'display:flex;flex-direction:column;gap:8px;' },
  87. generateFilterButtons(BASE_FILTERS, filter, '/blockexplorer')
  88. ),
  89. div({ style: 'display:flex;flex-direction:column;gap:8px;' },
  90. generateFilterButtons(CAT_BLOCK1, filter, '/blockexplorer'),
  91. generateFilterButtons(CAT_BLOCK2, filter, '/blockexplorer')
  92. ),
  93. div({ style: 'display:flex;flex-direction:column;gap:8px;' },
  94. generateFilterButtons(CAT_BLOCK3, filter, '/blockexplorer'),
  95. generateFilterButtons(CAT_BLOCK4, filter, '/blockexplorer')
  96. )
  97. ),
  98. div({ class: 'block-single' },
  99. div({ class: 'block-row block-row--meta' },
  100. span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockID}:`),
  101. span({ class: 'blockchain-card-value' }, block.id)
  102. ),
  103. div({ class: 'block-row block-row--meta' },
  104. span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockTimestamp}:`),
  105. span({ class: 'blockchain-card-value' }, moment(block.ts).format('YYYY-MM-DDTHH:mm:ss.SSSZ')),
  106. span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockType}:`),
  107. span({ class: 'blockchain-card-value' }, (FILTER_LABELS[block.type]||block.type).toUpperCase())
  108. ),
  109. div({ class: 'block-row block-row--meta', style:'margin-top:8px;' },
  110. a({ href:`/author/${encodeURIComponent(block.author)}`, class:'block-author user-link' }, block.author)
  111. )
  112. ),
  113. div({ class:'block-row block-row--content' },
  114. div({ class:'block-content-preview' },
  115. pre({ class:'json-content' }, JSON.stringify(block.content,null,2))
  116. )
  117. ),
  118. div({ class:'block-row block-row--back' },
  119. form({ method:'GET', action:'/blockexplorer' },
  120. button({ type:'submit', class:'filter-btn' }, `← ${i18n.blockchainBack}`)
  121. ),
  122. !block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
  123. form({ method:'GET', action:getViewDetailsAction(block.type, block) },
  124. button({ type:'submit', class:'filter-btn' }, i18n.visitContent)
  125. )
  126. : (block.isTombstoned || block.isReplaced) ?
  127. div({ class: 'deleted-label', style: 'color:#b00;font-weight:bold;margin-top:8px;' },
  128. i18n.blockchainContentDeleted || "This content has been deleted."
  129. )
  130. : null
  131. )
  132. )
  133. );
  134. const renderBlockchainView = (blocks, filter, userId) =>
  135. template(
  136. i18n.blockchain,
  137. section(
  138. div({ class:'tags-header' },
  139. h2(i18n.blockchain),
  140. p(i18n.blockchainDescription)
  141. ),
  142. div({ class:'mode-buttons-row' },
  143. div({ style:'display:flex;flex-direction:column;gap:8px;' },
  144. generateFilterButtons(BASE_FILTERS,filter,'/blockexplorer')
  145. ),
  146. div({ style:'display:flex;flex-direction:column;gap:8px;' },
  147. generateFilterButtons(CAT_BLOCK1,filter,'/blockexplorer'),
  148. generateFilterButtons(CAT_BLOCK2,filter,'/blockexplorer')
  149. ),
  150. div({ style:'display:flex;flex-direction:column;gap:8px;' },
  151. generateFilterButtons(CAT_BLOCK3,filter,'/blockexplorer'),
  152. generateFilterButtons(CAT_BLOCK4,filter,'/blockexplorer')
  153. )
  154. ),
  155. filterBlocks(blocks,filter,userId).length===0
  156. ? div(p(i18n.blockchainNoBlocks))
  157. : filterBlocks(blocks,filter,userId)
  158. .sort((a,b)=>{
  159. const ta = a.type==='market'&&a.content.updatedAt
  160. ? new Date(a.content.updatedAt).getTime()
  161. : a.ts;
  162. const tb = b.type==='market'&&b.content.updatedAt
  163. ? new Date(b.content.updatedAt).getTime()
  164. : b.ts;
  165. return tb - ta;
  166. })
  167. .map(block=>
  168. div({ class:'block' },
  169. div({ class:'block-buttons' },
  170. a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}`, class:'btn-singleview', title:i18n.blockchainDetails },'⦿'),
  171. !block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
  172. form({ method:'GET', action:getViewDetailsAction(block.type, block) },
  173. button({ type:'submit', class:'filter-btn' }, i18n.visitContent)
  174. )
  175. : (block.isTombstoned || block.isReplaced) ?
  176. div({ class: 'deleted-label', style: 'color:#b00;font-weight:bold;margin-top:8px;' },
  177. i18n.blockchainContentDeleted || "This content has been deleted."
  178. )
  179. : null
  180. ),
  181. div({ class:'block-row block-row--meta' },
  182. table({ class:'block-info-table' },
  183. tr(td({ class:'card-label' }, i18n.blockchainBlockTimestamp), td({ class:'card-value' }, moment(block.ts).format('YYYY-MM-DDTHH:mm:ss.SSSZ'))),
  184. tr(td({ class:'card-label' }, i18n.blockchainBlockID), td({ class:'card-value' }, block.id)),
  185. tr(td({ class:'card-label' }, i18n.blockchainBlockType), td({ class:'card-value' }, (FILTER_LABELS[block.type]||block.type).toUpperCase())),
  186. tr(td({ class:'card-label' }, i18n.blockchainBlockAuthor), td({ class:'card-value' }, a({ href:`/author/${encodeURIComponent(block.author)}`, class:'block-author user-link' }, block.author)))
  187. )
  188. )
  189. )
  190. )
  191. )
  192. );
  193. module.exports = { renderBlockchainView, renderSingleBlockView };