stats_view.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. const { div, h2, p, section, button, form, input, ul, li, a, h3, span, strong, table, tr, td, th } = require("../server/node_modules/hyperaxe");
  2. const { template, i18n } = require('./main_views');
  3. Object.assign(i18n, {
  4. statsChat: "Chats",
  5. statsChatMessage: "Chat messages",
  6. statsPad: "Pads",
  7. statsPadEntry: "Pad entries",
  8. statsGameScore: "Game scores",
  9. statsParliamentCandidature: "Parliament candidatures",
  10. statsParliamentTerm: "Parliament terms",
  11. statsParliamentProposal: "Parliament proposals",
  12. statsParliamentRevocation: "Parliament revocations",
  13. statsParliamentLaw: "Parliament laws",
  14. statsCourtsCase: "Court cases",
  15. statsCourtsEvidence: "Court evidence",
  16. statsCourtsAnswer: "Court answers",
  17. statsCourtsVerdict: "Court verdicts",
  18. statsCourtsSettlement: "Court settlements",
  19. statsCourtsSettlementProposal: "Settlement proposals",
  20. statsCourtsSettlementAccepted: "Settlements accepted",
  21. statsCourtsNomination: "Judge nominations",
  22. statsCourtsNominationVote: "Nomination votes"
  23. });
  24. const C = (stats, t) => Number((stats && stats.content && stats.content[t]) || 0);
  25. const O = (stats, t) => Number((stats && stats.opinions && stats.opinions[t]) || 0);
  26. exports.statsView = (stats, filter) => {
  27. const title = i18n.statsTitle;
  28. const description = i18n.statsDescription;
  29. const modes = ['ALL', 'MINE', 'TOMBSTONE'];
  30. const types = [
  31. 'bookmark', 'event', 'task', 'votes', 'report', 'feed', 'project',
  32. 'image', 'torrent', 'audio', 'video', 'document', 'transfer', 'post', 'tribe',
  33. 'market', 'forum', 'job', 'aiExchange', 'map', 'shop', 'shopProduct',
  34. 'chat', 'chatMessage', 'pad', 'padEntry', 'gameScore', 'calendar', 'calendarDate', 'calendarNote',
  35. 'parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw',
  36. 'courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote'
  37. ];
  38. const labels = {
  39. bookmark: i18n.statsBookmark,
  40. event: i18n.statsEvent,
  41. task: i18n.statsTask,
  42. votes: i18n.statsVotes,
  43. report: i18n.statsReport,
  44. feed: i18n.statsFeed,
  45. project: i18n.statsProject,
  46. image: i18n.statsImage,
  47. torrent: i18n.statsTorrent,
  48. audio: i18n.statsAudio,
  49. video: i18n.statsVideo,
  50. document: i18n.statsDocument,
  51. transfer: i18n.statsTransfer,
  52. post: i18n.statsPost,
  53. tribe: i18n.statsTribe,
  54. market: i18n.statsMarket,
  55. forum: i18n.statsForum,
  56. job: i18n.statsJob,
  57. aiExchange: i18n.statsAiExchange,
  58. map: i18n.statsMap,
  59. shop: i18n.statsShop,
  60. shopProduct: i18n.statsShopProduct,
  61. chat: i18n.statsChat,
  62. chatMessage: i18n.statsChatMessage,
  63. pad: i18n.statsPad,
  64. padEntry: i18n.statsPadEntry,
  65. gameScore: i18n.statsGameScore,
  66. calendar: i18n.statsCalendar,
  67. calendarDate: i18n.statsCalendarDate,
  68. calendarNote: i18n.statsCalendarNote,
  69. parliamentCandidature: i18n.statsParliamentCandidature,
  70. parliamentTerm: i18n.statsParliamentTerm,
  71. parliamentProposal: i18n.statsParliamentProposal,
  72. parliamentRevocation: i18n.statsParliamentRevocation,
  73. parliamentLaw: i18n.statsParliamentLaw,
  74. courtsCase: i18n.statsCourtsCase,
  75. courtsEvidence: i18n.statsCourtsEvidence,
  76. courtsAnswer: i18n.statsCourtsAnswer,
  77. courtsVerdict: i18n.statsCourtsVerdict,
  78. courtsSettlement: i18n.statsCourtsSettlement,
  79. courtsSettlementProposal: i18n.statsCourtsSettlementProposal,
  80. courtsSettlementAccepted: i18n.statsCourtsSettlementAccepted,
  81. courtsNomination: i18n.statsCourtsNomination,
  82. courtsNominationVote: i18n.statsCourtsNominationVote
  83. };
  84. const totalContent = types.filter(t => t !== 'karmaScore').reduce((sum, t) => sum + C(stats, t), 0);
  85. const totalOpinions = types.reduce((sum, t) => sum + O(stats, t), 0);
  86. const blockStyle = 'padding:16px;border:1px solid #ddd;border-radius:8px;margin-bottom:24px;';
  87. const headerStyle = 'background-color:#f8f9fa; padding:24px; border-radius:8px; border:1px solid #e0e0e0; box-shadow:0 2px 8px rgba(0,0,0,0.1);';
  88. return template(
  89. title,
  90. section(
  91. div({ class: 'tags-header' },
  92. h2(title),
  93. p(description)
  94. ),
  95. div({ class: 'mode-buttons', style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:16px;margin-bottom:24px;' },
  96. modes.map(m =>
  97. form({ method: 'GET', action: '/stats' },
  98. input({ type: 'hidden', name: 'filter', value: m }),
  99. button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, i18n[m + 'Button'])
  100. )
  101. )
  102. ),
  103. section(
  104. div({ style: headerStyle },
  105. h3({ style: 'font-size:18px; color:#555; margin:8px 0;' }, `${i18n.statsCreatedAt}: `, span({ style: 'color:#888;' }, stats.createdAt)),
  106. h3({ style: 'font-size:18px; color:#555; margin:8px 0; font-weight:600;' },
  107. a({ class: "user-link", href: `/author/${encodeURIComponent(stats.id)}`, style: 'color:#007bff; text-decoration:none;' }, stats.id)
  108. ),
  109. div({ style: 'margin-bottom:16px;' },
  110. ul({ style: 'list-style-type:none; padding:0; margin:0;' },
  111. li({ style: 'font-size:18px; color:#555; margin:8px 0;' }, `${i18n.statsBlobsSize}: `, span({ style: 'color:#888;' }, stats.statsBlobsSize)),
  112. li({ style: 'font-size:18px; color:#555; margin:8px 0;' }, `${i18n.statsBlockchainSize}: `, span({ style: 'color:#888;' }, stats.statsBlockchainSize)),
  113. li({ style: 'font-size:18px; color:#555; margin:8px 0;' }, strong(`${i18n.statsSize}: `, span({ style: 'color:#888;' }, span({ style: 'color:#555;' }, stats.folderSize))))
  114. )
  115. )
  116. ),
  117. div({ class: "stats-karma-block" }, h3(`${i18n.bankingUserEngagementScore}: ${C(stats, 'karmaScore')}`)),
  118. div({ style: headerStyle },
  119. h3(i18n.statsCarbonFootprintTitle || 'Carbon Footprint'),
  120. (() => {
  121. const parseSize = (s) => {
  122. if (!s) return 0;
  123. const m = String(s).match(/([\d.]+)\s*(GB|MB|KB|B)/i);
  124. if (!m) return 0;
  125. const v = parseFloat(m[1]);
  126. const u = m[2].toUpperCase();
  127. if (u === 'GB') return v * 1024;
  128. if (u === 'MB') return v;
  129. if (u === 'KB') return v / 1024;
  130. return v / (1024 * 1024);
  131. };
  132. const blobsMB = parseSize(stats.statsBlobsSize);
  133. const chainMB = parseSize(stats.statsBlockchainSize);
  134. const totalMB = blobsMB + chainMB;
  135. const kWhPerMB = 0.0002;
  136. const gCO2PerKWh = 475;
  137. const networkCO2 = parseFloat((totalMB * kWhPerMB * gCO2PerKWh).toFixed(2));
  138. const inhabitants = stats.usersKPIs?.totalInhabitants || stats.inhabitants || 1;
  139. const userCO2 = parseFloat((networkCO2 / Math.max(1, inhabitants)).toFixed(2));
  140. const maxAnnualCO2 = 500;
  141. if (filter === 'MINE') {
  142. const pct = networkCO2 > 0 ? Math.min(100, (userCO2 / networkCO2) * 100).toFixed(1) : '0.0';
  143. return div({ class: 'carbon-chart' },
  144. div({ class: 'carbon-bar-label' },
  145. span(i18n.statsCarbonUser || 'Your footprint'),
  146. span(`${userCO2} g CO₂`)
  147. ),
  148. div({ class: 'carbon-bar-track' },
  149. div({ class: 'carbon-bar-fill carbon-bar-mine', style: `width:${pct}%;` })
  150. ),
  151. div({ class: 'carbon-bar-label' },
  152. span(i18n.statsCarbonNetwork || 'Network total'),
  153. span(`${networkCO2} g CO₂`)
  154. ),
  155. div({ class: 'carbon-bar-track' },
  156. div({ class: 'carbon-bar-fill carbon-bar-network', style: 'width:100%;' })
  157. ),
  158. p({ class: 'carbon-bar-note' }, strong(`${pct}%`), ` ${i18n.statsCarbonOfNetwork || 'of network total'}`),
  159. p({ class: 'carbon-bar-formula' }, 'Based on local data storage weight ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)'))
  160. );
  161. }
  162. if (filter === 'TOMBSTONE') {
  163. const tombCount = stats.tombstoneKPIs?.networkTombstoneCount || 0;
  164. const avgTombBytes = 500;
  165. const tombMB = (tombCount * avgTombBytes) / (1024 * 1024);
  166. const tombCO2 = parseFloat((tombMB * kWhPerMB * gCO2PerKWh).toFixed(4));
  167. const tombPct = networkCO2 > 0 ? Math.min(100, (tombCO2 / networkCO2) * 100).toFixed(1) : '0.0';
  168. return div({ class: 'carbon-chart' },
  169. div({ class: 'carbon-bar-label' },
  170. span(i18n.statsCarbonTombstone || 'Tombstoning footprint'),
  171. span(`${tombCO2} g CO₂`)
  172. ),
  173. div({ class: 'carbon-bar-track' },
  174. div({ class: 'carbon-bar-fill carbon-bar-mine', style: `width:${tombPct}%;` })
  175. ),
  176. div({ class: 'carbon-bar-label' },
  177. span(i18n.statsCarbonNetwork || 'Network total'),
  178. span(`${networkCO2} g CO₂`)
  179. ),
  180. div({ class: 'carbon-bar-track' },
  181. div({ class: 'carbon-bar-fill carbon-bar-network', style: 'width:100%;' })
  182. ),
  183. p({ class: 'carbon-bar-note' }, strong(`${tombPct}%`), ` ${i18n.statsCarbonOfNetwork || 'of network total'} (${tombCount} tombstones × ~${avgTombBytes} bytes)`),
  184. p({ class: 'carbon-bar-formula' }, 'Based on estimated tombstone message size ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)'))
  185. );
  186. }
  187. const pct = Math.min(100, (networkCO2 / maxAnnualCO2) * 100).toFixed(1);
  188. return div({ class: 'carbon-chart' },
  189. div({ class: 'carbon-bar-label' },
  190. span(i18n.statsCarbonNetwork || 'Network footprint'),
  191. span(`${networkCO2} g CO₂`)
  192. ),
  193. div({ class: 'carbon-bar-track' },
  194. div({ class: 'carbon-bar-fill carbon-bar-network', style: `width:${pct}%;` })
  195. ),
  196. div({ class: 'carbon-bar-label' },
  197. span(i18n.statsCarbonMaxAnnual || 'Annual max estimate'),
  198. span(`${maxAnnualCO2} g CO₂`)
  199. ),
  200. div({ class: 'carbon-bar-track' },
  201. div({ class: 'carbon-bar-fill carbon-bar-max', style: 'width:100%;' })
  202. ),
  203. p({ class: 'carbon-bar-note' }, strong(`${pct}%`), ` ${i18n.statsCarbonOfEstMax || 'of estimated max capacity'}`),
  204. p({ class: 'carbon-bar-formula' }, 'Based on local data storage weight ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)'))
  205. );
  206. })()
  207. ),
  208. div({ style: headerStyle },
  209. h3({ style: 'font-size:18px; color:#555; margin:8px 0; font-weight:600;' }, i18n.statsBankingTitle),
  210. ul({ style: 'list-style-type:none; padding:0; margin:0;' },
  211. li({ style: 'font-size:18px; color:#555; margin:8px 0;' }, `${i18n.statsEcoWalletLabel}: `, a({ href: '/wallet', style: 'color:#007bff; text-decoration:none; word-break:break-all;' }, stats?.banking?.myAddress || i18n.statsEcoWalletNotConfigured)),
  212. li({ style: 'font-size:18px; color:#555; margin:8px 0;' }, `${i18n.statsTotalEcoAddresses}: `, span({ style: 'color:#888;' }, String(stats?.banking?.totalAddresses || 0)))
  213. )
  214. ),
  215. div({ style: headerStyle },
  216. h3({ style: 'font-size:18px; color:#555; margin:8px 0; font-weight:600;' }, i18n.statsAITraining),
  217. ul({ style: 'list-style-type:none; padding:0; margin:0;' },
  218. li({ style: 'font-size:18px; color:#555; margin:8px 0;' }, `${i18n.statsAIExchanges}: `, span({ style: 'color:#888;' }, String(C(stats, 'aiExchange') || 0)))
  219. )
  220. ),
  221. div({ style: headerStyle }, h3(`${i18n.statsPUBs}: ${String(stats.pubsCount || 0)}`)),
  222. filter === 'ALL'
  223. ? div({ class: 'stats-container' }, [
  224. div({ style: blockStyle },
  225. h2(i18n.statsActivity7d),
  226. table({ style: 'width:100%; border-collapse: collapse;' },
  227. tr(th(i18n.day), th(i18n.messages)),
  228. ...(Array.isArray(stats.activity?.daily7) ? stats.activity.daily7 : []).map(row => tr(td(row.day), td(String(row.count))))
  229. ),
  230. p(`${i18n.statsActivity7dTotal}: ${stats.activity?.daily7Total || 0}`),
  231. p(`${i18n.statsActivity30dTotal}: ${stats.activity?.daily30Total || 0}`)
  232. ),
  233. div({ style: blockStyle },
  234. h2(`${i18n.statsDiscoveredTribes}: ${stats.allTribesPublic.length}`),
  235. table({ style: 'width:100%; border-collapse: collapse; margin-top: 8px;' },
  236. ...stats.allTribesPublic.map(t => tr(td(a({ href: `/tribe/${encodeURIComponent(t.id)}`, class: 'tribe-link' }, t.name))))
  237. )
  238. ),
  239. div({ style: blockStyle },
  240. h2(`${i18n.statsPrivateDiscoveredTribes}: ${stats.tribePrivateCount || 0}`)
  241. ),
  242. div({ style: blockStyle }, h2(`${i18n.statsUsersTitle}: ${stats.usersKPIs?.totalInhabitants || stats.inhabitants || 0}`)),
  243. div({ style: blockStyle }, h2(`${i18n.statsDiscoveredForum}: ${C(stats, 'forum')}`)),
  244. div({ style: blockStyle }, h2(`${i18n.statsDiscoveredTransfer}: ${C(stats, 'transfer')}`)),
  245. div({ style: blockStyle },
  246. h2(i18n.statsMarketTitle),
  247. ul([
  248. li(`${i18n.statsMarketTotal}: ${stats.marketKPIs?.total || 0}`),
  249. li(`${i18n.statsMarketForSale}: ${stats.marketKPIs?.forSale || 0}`),
  250. li(`${i18n.statsMarketReserved}: ${stats.marketKPIs?.reserved || 0}`),
  251. li(`${i18n.statsMarketClosed}: ${stats.marketKPIs?.closed || 0}`),
  252. li(`${i18n.statsMarketSold}: ${stats.marketKPIs?.sold || 0}`),
  253. li(`${i18n.statsMarketRevenue}: ${((stats.marketKPIs?.revenueECO || 0)).toFixed(6)} ECO`),
  254. li(`${i18n.statsMarketAvgSoldPrice}: ${((stats.marketKPIs?.avgSoldPrice || 0)).toFixed(6)} ECO`)
  255. ])
  256. ),
  257. div({ style: blockStyle },
  258. h2(i18n.statsProjectsTitle),
  259. ul([
  260. li(`${i18n.statsProjectsTotal}: ${stats.projectsKPIs?.total || 0}`),
  261. li(`${i18n.statsProjectsActive}: ${stats.projectsKPIs?.active || 0}`),
  262. li(`${i18n.statsProjectsCompleted}: ${stats.projectsKPIs?.completed || 0}`),
  263. li(`${i18n.statsProjectsPaused}: ${stats.projectsKPIs?.paused || 0}`),
  264. li(`${i18n.statsProjectsCancelled}: ${stats.projectsKPIs?.cancelled || 0}`),
  265. li(`${i18n.statsProjectsGoalTotal}: ${(stats.projectsKPIs?.ecoGoalTotal || 0)} ECO`),
  266. li(`${i18n.statsProjectsPledgedTotal}: ${(stats.projectsKPIs?.ecoPledgedTotal || 0)} ECO`),
  267. li(`${i18n.statsProjectsSuccessRate}: ${((stats.projectsKPIs?.successRate || 0)).toFixed(1)}%`),
  268. li(`${i18n.statsProjectsAvgProgress}: ${((stats.projectsKPIs?.avgProgress || 0)).toFixed(1)}%`),
  269. li(`${i18n.statsProjectsMedianProgress}: ${((stats.projectsKPIs?.medianProgress || 0)).toFixed(1)}%`),
  270. li(`${i18n.statsProjectsActiveFundingAvg}: ${((stats.projectsKPIs?.activeFundingAvg || 0)).toFixed(1)}%`)
  271. ])
  272. ),
  273. div({ style: blockStyle },
  274. h2(`${i18n.statsNetworkOpinions}: ${totalOpinions}`),
  275. ul(types.map(t => O(stats, t) > 0 ? li(`${labels[t]}: ${O(stats, t)}`) : null).filter(Boolean))
  276. ),
  277. div({ style: blockStyle },
  278. h2(`${i18n.statsNetworkContent}: ${totalContent}`),
  279. ul(
  280. types.filter(t => t !== 'karmaScore' && t !== 'shopProduct' && t !== 'padEntry' && t !== 'chatMessage').map(t => {
  281. if (C(stats, t) <= 0) return null;
  282. if (t === 'shop') return li(
  283. span(`${labels[t]}: ${C(stats, t)}`),
  284. ul([li(`${labels.shopProduct}: ${C(stats, 'shopProduct')}`)])
  285. );
  286. if (t === 'pad') return li(
  287. span(`${labels[t]}: ${C(stats, t)}`),
  288. ul([li(`${labels.padEntry}: ${C(stats, 'padEntry')}`)])
  289. );
  290. if (t === 'chat') return li(
  291. span(`${labels[t]}: ${C(stats, t)}`),
  292. ul([li(`${labels.chatMessage}: ${C(stats, 'chatMessage')}`)])
  293. );
  294. if (t !== 'tribe') return li(`${labels[t]}: ${C(stats, t)}`);
  295. return li(
  296. span(`${labels[t]}: ${C(stats, t)}`),
  297. ul([
  298. li(`${i18n.statsPublic}: ${stats.tribePublicCount || 0}`),
  299. li(`${i18n.statsPrivate}: ${stats.tribePrivateCount || 0}`)
  300. ])
  301. );
  302. }).filter(Boolean)
  303. )
  304. )
  305. ])
  306. : filter === 'MINE'
  307. ? div({ class: 'stats-container' }, [
  308. div({ style: blockStyle },
  309. h2(i18n.statsActivity7d),
  310. table({ style: 'width:100%; border-collapse: collapse;' },
  311. tr(th(i18n.day), th(i18n.messages)),
  312. ...(Array.isArray(stats.activity?.daily7) ? stats.activity.daily7 : []).map(row => tr(td(row.day), td(String(row.count))))
  313. ),
  314. p(`${i18n.statsActivity7dTotal}: ${stats.activity?.daily7Total || 0}`),
  315. p(`${i18n.statsActivity30dTotal}: ${stats.activity?.daily30Total || 0}`)
  316. ),
  317. div({ style: blockStyle },
  318. h2(`${i18n.statsDiscoveredTribes}: ${stats.memberTribesDetailed.length}`),
  319. table({ style: 'width:100%; border-collapse: collapse; margin-top: 8px;' },
  320. ...stats.memberTribesDetailed.map(t => tr(td(a({ href: `/tribe/${encodeURIComponent(t.id)}`, class: 'tribe-link' }, t.name))))
  321. )
  322. ),
  323. Array.isArray(stats.myPrivateTribesDetailed) && stats.myPrivateTribesDetailed.length
  324. ? div({ style: blockStyle },
  325. h2(`${i18n.statsPrivateDiscoveredTribes}: ${stats.myPrivateTribesDetailed.length}`),
  326. table({ style: 'width:100%; border-collapse: collapse; margin-top: 8px;' },
  327. ...stats.myPrivateTribesDetailed.map(tp => tr(td(a({ href: `/tribe/${encodeURIComponent(tp.id)}`, class: 'tribe-link' }, tp.name))))
  328. )
  329. )
  330. : null,
  331. div({ style: blockStyle }, h2(`${i18n.statsYourForum}: ${C(stats, 'forum')}`)),
  332. div({ style: blockStyle }, h2(`${i18n.statsYourTransfer}: ${C(stats, 'transfer')}`)),
  333. div({ style: blockStyle },
  334. h2(i18n.statsMarketTitle),
  335. ul([
  336. li(`${i18n.statsMarketTotal}: ${stats.marketKPIs?.total || 0}`),
  337. li(`${i18n.statsMarketForSale}: ${stats.marketKPIs?.forSale || 0}`),
  338. li(`${i18n.statsMarketReserved}: ${stats.marketKPIs?.reserved || 0}`),
  339. li(`${i18n.statsMarketClosed}: ${stats.marketKPIs?.closed || 0}`),
  340. li(`${i18n.statsMarketSold}: ${stats.marketKPIs?.sold || 0}`),
  341. li(`${i18n.statsMarketRevenue}: ${((stats.marketKPIs?.revenueECO || 0)).toFixed(6)} ECO`),
  342. li(`${i18n.statsMarketAvgSoldPrice}: ${((stats.marketKPIs?.avgSoldPrice || 0)).toFixed(6)} ECO`)
  343. ])
  344. ),
  345. div({ style: blockStyle },
  346. h2(i18n.statsProjectsTitle),
  347. ul([
  348. li(`${i18n.statsProjectsTotal}: ${stats.projectsKPIs?.total || 0}`),
  349. li(`${i18n.statsProjectsActive}: ${stats.projectsKPIs?.active || 0}`),
  350. li(`${i18n.statsProjectsCompleted}: ${stats.projectsKPIs?.completed || 0}`),
  351. li(`${i18n.statsProjectsPaused}: ${stats.projectsKPIs?.paused || 0}`),
  352. li(`${i18n.statsProjectsCancelled}: ${stats.projectsKPIs?.cancelled || 0}`),
  353. li(`${i18n.statsProjectsGoalTotal}: ${(stats.projectsKPIs?.ecoGoalTotal || 0)} ECO`),
  354. li(`${i18n.statsProjectsPledgedTotal}: ${(stats.projectsKPIs?.ecoPledgedTotal || 0)} ECO`),
  355. li(`${i18n.statsProjectsSuccessRate}: ${((stats.projectsKPIs?.successRate || 0)).toFixed(1)}%`),
  356. li(`${i18n.statsProjectsAvgProgress}: ${((stats.projectsKPIs?.avgProgress || 0)).toFixed(1)}%`),
  357. li(`${i18n.statsProjectsMedianProgress}: ${((stats.projectsKPIs?.medianProgress || 0)).toFixed(1)}%`),
  358. li(`${i18n.statsProjectsActiveFundingAvg}: ${((stats.projectsKPIs?.activeFundingAvg || 0)).toFixed(1)}%`)
  359. ])
  360. ),
  361. div({ style: blockStyle },
  362. h2(`${i18n.statsYourOpinions}: ${totalOpinions}`),
  363. ul(types.map(t => O(stats, t) > 0 ? li(`${labels[t]}: ${O(stats, t)}`) : null).filter(Boolean))
  364. ),
  365. div({ style: blockStyle },
  366. h2(`${i18n.statsYourContent}: ${totalContent}`),
  367. ul(
  368. types.filter(t => t !== 'karmaScore' && t !== 'shopProduct').map(t => {
  369. if (C(stats, t) <= 0) return null;
  370. if (t === 'shop') return li(
  371. span(`${labels[t]}: ${C(stats, t)}`),
  372. ul([li(`${labels.shopProduct}: ${C(stats, 'shopProduct')}`)])
  373. );
  374. if (t !== 'tribe') return li(`${labels[t]}: ${C(stats, t)}`);
  375. return li(
  376. span(`${labels[t]}: ${C(stats, t)}`),
  377. ul([
  378. li(`${i18n.statsPublic}: ${stats.tribePublicCount || 0}`),
  379. li(`${i18n.statsPrivate}: ${stats.tribePrivateCount || 0}`),
  380. ...(Array.isArray(stats.myPrivateTribesDetailed) && stats.myPrivateTribesDetailed.length
  381. ? [
  382. li(i18n.statsPrivateDiscoveredTribes),
  383. ...stats.myPrivateTribesDetailed.map(tp =>
  384. li(a({ href: `/tribe/${encodeURIComponent(tp.id)}`, class: 'tribe-link' }, tp.name))
  385. )
  386. ]
  387. : [])
  388. ])
  389. );
  390. }).filter(Boolean)
  391. )
  392. )
  393. ])
  394. : div({ class: 'stats-container' }, [
  395. div({ style: blockStyle },
  396. h2(`${i18n.TOMBSTONEButton}: ${stats.userTombstoneCount}`),
  397. h2(`${i18n.statsTombstoneRatio.toUpperCase()}: ${((stats.tombstoneKPIs?.ratio || 0)).toFixed(2)}%`)
  398. )
  399. ])
  400. )
  401. )
  402. );
  403. };