parliament_view.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939
  1. const { form, button, div, h2, p, section, input, label, br, a, span, table, thead, tbody, tr, th, td, textarea, select, option, ul, li, img } = require('../server/node_modules/hyperaxe');
  2. const moment = require("../server/node_modules/moment");
  3. const { template, i18n, userLink} = require('./main_views');
  4. const TERM_DAYS = 60;
  5. const fmt = (d) => moment(d).format('YYYY-MM-DD HH:mm:ss');
  6. const timeLeft = (end) => {
  7. const diff = moment(end).diff(moment());
  8. if (diff <= 0) return '0d 00:00:00';
  9. const dur = moment.duration(diff);
  10. const d = Math.floor(dur.asDays());
  11. const h = String(dur.hours()).padStart(2,'0');
  12. const m = String(dur.minutes()).padStart(2,'0');
  13. const s = String(dur.seconds()).padStart(2,'0');
  14. return `${d}d ${h}:${m}:${s}`;
  15. };
  16. const reqVotes = (method, total) => {
  17. const m = String(method || '').toUpperCase();
  18. if (m === 'DEMOCRACY' || m === 'ANARCHY') return Math.floor(Number(total || 0) / 2) + 1;
  19. if (m === 'MAJORITY') return Math.ceil(Number(total || 0) * 0.8);
  20. if (m === 'MINORITY') return Math.ceil(Number(total || 0) * 0.2);
  21. return 0;
  22. };
  23. const showVoteMetrics = (method) => {
  24. const m = String(method || '').toUpperCase();
  25. return !(m === 'DICTATORSHIP' || m === 'KARMATOCRACY');
  26. };
  27. const applyEl = (fn, attrs, kids) => fn.apply(null, [attrs || {}].concat(kids || []));
  28. const methodImageSrc = (method) => `assets/images/${String(method || '').toUpperCase().toLowerCase()}.png`;
  29. const MethodBadge = (method) => {
  30. const m = String(method || '').toUpperCase();
  31. const labelTxt = String(i18n[`parliamentMethod${m}`] || m).toUpperCase();
  32. return span(
  33. { class: 'method-badge' },
  34. labelTxt,
  35. br(),br(),
  36. img({ src: methodImageSrc(m), alt: labelTxt, class: 'method-badge__icon' })
  37. );
  38. };
  39. const MethodHero = (method) => {
  40. const m = String(method || '').toUpperCase();
  41. const labelTxt = String(i18n[`parliamentMethod${m}`] || m).toUpperCase();
  42. return span(
  43. { class: 'method-hero' },
  44. labelTxt,
  45. br(),br(),
  46. img({ src: methodImageSrc(m), alt: labelTxt, class: 'method-hero__icon' })
  47. );
  48. };
  49. const KPI = (labelTxt, value) =>
  50. div({ class: 'kpi' },
  51. span({ class: 'kpi__label' }, labelTxt),
  52. span({ class: 'kpi__value' }, value)
  53. );
  54. const CycleInfo = (start, end, labels = {
  55. since: i18n.parliamentLegSince,
  56. end: i18n.parliamentLegEnd,
  57. remaining: i18n.parliamentTimeRemaining
  58. }) =>
  59. div({ class: 'cycle-info' },
  60. KPI((labels.since + ': ').toUpperCase(), fmt(start)),
  61. KPI((labels.end + ': ').toUpperCase(), fmt(end)),
  62. KPI((labels.remaining + ': ').toUpperCase(), timeLeft(end))
  63. );
  64. const Tabs = (active) =>
  65. div(
  66. { class: 'filters' },
  67. form(
  68. { method: 'GET', action: '/parliament' },
  69. ['government', 'candidatures', 'proposals', 'laws', 'revocations', 'historical', 'leaders', 'rules'].map(f =>
  70. button({ type: 'submit', name: 'filter', value: f, class: active === f ? 'filter-btn active' : 'filter-btn' }, i18n[`parliamentFilter${f.charAt(0).toUpperCase()+f.slice(1)}`])
  71. )
  72. )
  73. );
  74. const GovHeader = (g) => {
  75. const termStart = g && g.since ? g.since : moment().toISOString();
  76. const termEnd = g && g.end ? g.end : moment(termStart).add(TERM_DAYS, 'days').toISOString();
  77. const methodKeyRaw = g && g.method ? String(g.method) : 'ANARCHY';
  78. const methodKey = methodKeyRaw.toUpperCase();
  79. const i18nMeth = i18n[`parliamentMethod${methodKey}`];
  80. const methodLabel = (i18nMeth && String(i18nMeth).trim() ? String(i18nMeth) : methodKey).toUpperCase();
  81. const isAnarchy = methodKey === 'ANARCHY';
  82. const population = String(Number(g.inhabitantsTotal || 0));
  83. const votesReceivedNum = Number.isFinite(Number(g.votesReceived)) ? Number(g.votesReceived) : 0;
  84. const totalVotesNum = Number.isFinite(Number(g.totalVotes)) ? Number(g.totalVotes) : 0;
  85. const votesDisplay = `${votesReceivedNum} (${totalVotesNum})`;
  86. return div(
  87. { class: 'cycle-info' },
  88. div({ class: 'kpi' },
  89. span({ class: 'kpi__label' }, (i18n.parliamentLegSince + ': ').toUpperCase()),
  90. span({ class: 'kpi__value' }, fmt(termStart))
  91. ),
  92. div({ class: 'kpi' },
  93. span({ class: 'kpi__label' }, (i18n.parliamentLegEnd + ': ').toUpperCase()),
  94. span({ class: 'kpi__value' }, fmt(termEnd))
  95. ),
  96. div({ class: 'kpi' },
  97. span({ class: 'kpi__label' }, (i18n.parliamentTimeRemaining + ': ').toUpperCase()),
  98. span({ class: 'kpi__value' }, timeLeft(termEnd))
  99. ),
  100. div({ class: 'kpi' },
  101. span({ class: 'kpi__label' }, (i18n.parliamentPopulation + ': ').toUpperCase()),
  102. span({ class: 'kpi__value' }, population)
  103. ),
  104. div({ class: 'kpi' },
  105. span({ class: 'kpi__label' }, (i18n.parliamentGovMethod + ': ').toUpperCase()),
  106. span({ class: 'kpi__value' }, methodLabel)
  107. ),
  108. !isAnarchy
  109. ? div({ class: 'kpi' },
  110. span({ class: 'kpi__label' }, (i18n.parliamentVotesReceived + ': ').toUpperCase()),
  111. span({ class: 'kpi__value' }, votesDisplay)
  112. )
  113. : null
  114. );
  115. };
  116. const GovernmentCard = (g, meta) => {
  117. const termStart = g && g.since ? g.since : moment().toISOString();
  118. const termEnd = g && g.end ? g.end : moment(termStart).add(TERM_DAYS, 'days').toISOString();
  119. const actorLabel =
  120. g.powerType === 'tribe'
  121. ? (i18n.parliamentActorInPowerTribe || i18n.parliamentActorInPower || 'TRIBE RULING')
  122. : (i18n.parliamentActorInPowerInhabitant || i18n.parliamentActorInPower || 'INHABITANT RULING');
  123. const methodKeyRaw = g && g.method ? String(g.method) : 'ANARCHY';
  124. const methodKey = methodKeyRaw.toUpperCase();
  125. const i18nMeth = i18n[`parliamentMethod${methodKey}`];
  126. const methodLabel = (i18nMeth && String(i18nMeth).trim() ? String(i18nMeth) : methodKey).toUpperCase();
  127. const actorLink =
  128. g.powerType === 'tribe'
  129. ? a({ class: 'user-link', href: `/tribe/${encodeURIComponent(g.powerId)}` }, g.powerTitle || g.powerId)
  130. : userLink(g.powerId, g.powerTitle);
  131. const actorBio = meta && meta.bio ? meta.bio : '';
  132. const memberIds = Array.isArray(g.membersList) ? g.membersList : (Array.isArray(g.members) ? g.members : []);
  133. const membersRow =
  134. g.powerType === 'tribe'
  135. ? tr(
  136. { class: 'parliament-members-row' },
  137. td(
  138. { colspan: 2 },
  139. div(
  140. span({ class: 'card-label' }, (i18n.parliamentMembers + ': ').toUpperCase()),
  141. memberIds && memberIds.length
  142. ? ul({ class: 'parliament-members-list' }, ...memberIds.map(id => li(userLink(id))))
  143. : span({ class: 'card-value' }, String(g.members || 0))
  144. )
  145. )
  146. )
  147. : null;
  148. return div(
  149. { class: 'card' },
  150. h2(i18n.parliamentGovernmentCard),
  151. GovHeader(g),
  152. div({ class: 'method-image-centered' }, img({ src: methodImageSrc(methodKey), alt: methodLabel })),
  153. div(
  154. { class: 'table-wrap' },
  155. applyEl(table, { class: 'table table--centered gov-overview' }, [
  156. thead(tr(
  157. th(i18n.parliamentGovMethod),
  158. th(i18n.parliamentPoliciesProposal || 'LAWS PROPOSAL'),
  159. th(i18n.parliamentPoliciesApproved || 'LAWS APPROVED'),
  160. th(i18n.parliamentPoliciesDeclined || 'LAWS DECLINED'),
  161. th(i18n.parliamentPoliciesDiscarded || 'LAWS DISCARDED'),
  162. th(i18n.parliamentPoliciesRevocated || 'LAWS REVOCATED'),
  163. th(i18n.parliamentEfficiency || '% EFFICIENCY')
  164. )),
  165. tbody(tr(
  166. td(methodLabel),
  167. td(String(g.proposed || 0)),
  168. td(String(g.approved || 0)),
  169. td(String(g.declined || 0)),
  170. td(String(g.discarded || 0)),
  171. td(String(g.revocated || 0)),
  172. td(`${String(g.efficiency || 0)} %`)
  173. ))
  174. ])
  175. ),
  176. (g.powerType === 'tribe' || g.powerType === 'inhabitant')
  177. ? div(
  178. { class: 'table-wrap mt-2' },
  179. applyEl(table, { class: 'table parliament-actor-table' }, [
  180. thead(tr(
  181. th({ class: 'parliament-actor-col' }, String(actorLabel).toUpperCase()),
  182. th({ class: 'parliament-description-col' }, i18n.description.toUpperCase())
  183. )),
  184. tbody(
  185. tr(
  186. td({ class: 'parliament-actor-col' }, div({ class: 'leader-cell' }, actorLink)),
  187. td({ class: 'parliament-description-col' }, p(actorBio || '-'))
  188. ),
  189. membersRow
  190. )
  191. ])
  192. )
  193. : null
  194. );
  195. };
  196. const NoGovernment = () => div({ class: 'empty' }, p(i18n.parliamentNoStableGov));
  197. const NoProposals = () => div({ class: 'empty' }, p(i18n.parliamentNoProposals));
  198. const NoLaws = () => div({ class: 'empty' }, p(i18n.parliamentNoLaws));
  199. const NoGovernments = () => div({ class: 'empty' }, p(i18n.parliamentNoGovernments));
  200. const NoRevocations = () => null;
  201. const CandidatureForm = () =>
  202. div(
  203. { class: 'div-center' },
  204. h2(i18n.parliamentCandidatureFormTitle),
  205. form(
  206. { method: 'POST', action: '/parliament/candidatures/propose' },
  207. label(i18n.parliamentCandidatureId), br(),
  208. input({ type: 'text', name: 'candidateId', placeholder: i18n.parliamentCandidatureIdPh, required: true }), br(), br(),
  209. label(i18n.parliamentCandidatureMethod), br(),
  210. select({ name: 'method' },
  211. ['DEMOCRACY','MAJORITY','MINORITY','DICTATORSHIP','KARMATOCRACY'].map(m => option({ value: m }, i18n[`parliamentMethod${m}`] || m))
  212. ), br(), br(),
  213. button({ type: 'submit', class: 'create-button' }, i18n.parliamentCandidatureProposeBtn)
  214. )
  215. );
  216. const pickLeader = (arr) => {
  217. if (!arr || !arr.length) return null;
  218. const sorted = [...arr].sort((a, b) => {
  219. const va = Number(a.votes || 0), vb = Number(b.votes || 0);
  220. if (vb !== va) return vb - va;
  221. const ka = Number(a.karma || 0), kb = Number(b.karma || 0);
  222. if (kb !== ka) return kb - ka;
  223. const sa = Number(a.profileSince || 0), sb = Number(b.profileSince || 0);
  224. if (sa !== sb) return sa - sb;
  225. const ca = new Date(a.createdAt).getTime(), cb = new Date(b.createdAt).getTime();
  226. if (ca !== cb) return ca - cb;
  227. return String(a.targetId).localeCompare(String(b.targetId));
  228. });
  229. return sorted[0];
  230. };
  231. const CandidatureStats = (cands, govCard, leaderMeta) => {
  232. if (!cands || !cands.length) return null;
  233. const leader = pickLeader(cands || []);
  234. if (!leader) return null;
  235. const methodKey = String(leader.method || '').toUpperCase();
  236. const methodLabel = String(i18n[`parliamentMethod${methodKey}`] || methodKey).toUpperCase();
  237. const votes = String(leader.votes || 0);
  238. const avatarSrc = (leaderMeta && leaderMeta.avatarUrl) ? leaderMeta.avatarUrl : '/assets/images/default-avatar.png';
  239. const winLbl = (i18n.parliamentWinningCandidature || i18n.parliamentCurrentLeader || 'WINNING CANDIDATURE').toUpperCase();
  240. const idLink = leader
  241. ? (leader.targetType === 'inhabitant'
  242. ? userLink(leader.targetId)
  243. : a({ class: 'tag-link', href: `/tribe/${encodeURIComponent(leader.targetId)}?` }, leader.targetTitle || leader.targetId))
  244. : null;
  245. return div(
  246. { class: 'card' },
  247. h2(i18n.parliamentElectionsStatusTitle),
  248. div({ class: 'card-field card-field--spaced' },
  249. span({ class: 'card-label' }, winLbl + ': '),
  250. span({ class: 'card-value' }, idLink)
  251. ),
  252. div({ class: 'card-field card-field--spaced' },
  253. span({ class: 'card-label' }, (i18n.parliamentGovMethod + ': ').toUpperCase()),
  254. span({ class: 'card-value' }, methodLabel)
  255. ),
  256. div(
  257. { class: 'table-wrap mt-2' },
  258. applyEl(table, [
  259. thead(tr(
  260. th(i18n.parliamentThLeader),
  261. th({ class: 'parliament-method-col' }, i18n.parliamentGovMethod),
  262. th({ class: 'parliament-votes-col' }, i18n.parliamentVotesReceived)
  263. )),
  264. tbody(tr(
  265. td(
  266. img({ src: avatarSrc })
  267. ),
  268. td({ class: 'parliament-method-col' },
  269. img({ src: methodImageSrc(methodKey), alt: methodLabel, class: 'method-hero__icon' })
  270. ),
  271. td({ class: 'parliament-votes-col' }, span({ class: 'votes-value' }, votes))
  272. ))
  273. ])
  274. )
  275. );
  276. };
  277. const CandidaturesTable = (candidatures) => {
  278. const rows = (candidatures || []).map(c => {
  279. const idLink =
  280. c.targetType === 'inhabitant'
  281. ? p(userLink(c.targetId))
  282. : p(a({ class: 'tag-link', href: `/tribe/${encodeURIComponent(c.targetId)}?` }, c.targetTitle || c.targetId));
  283. return tr(
  284. td(idLink),
  285. td(fmt(c.createdAt)),
  286. td({ class: 'nowrap' }, c.method),
  287. td(c.targetType === 'inhabitant' ? String(c.karma || 0) : '-'),
  288. td(String(c.votes || 0)),
  289. td(form({ method: 'POST', action: `/parliament/candidatures/${encodeURIComponent(c.id)}/vote` }, button({ class: 'vote-btn' }, i18n.parliamentVoteBtn)))
  290. );
  291. });
  292. return div(
  293. { class: 'table-wrap' },
  294. h2(i18n.parliamentCandidaturesListTitle),
  295. applyEl(table, { class: 'table table--centered' }, [
  296. thead(tr(
  297. th(i18n.parliamentThId),
  298. th(i18n.parliamentThProposalDate),
  299. th(i18n.parliamentThMethod),
  300. th(i18n.parliamentThKarma),
  301. th(i18n.parliamentThSupports),
  302. th(i18n.parliamentThVote)
  303. )),
  304. applyEl(tbody, null, rows)
  305. ])
  306. );
  307. };
  308. const ProposalForm = () =>
  309. div(
  310. { class: 'div-center' },
  311. h2(i18n.parliamentProposalFormTitle),
  312. form(
  313. { method: 'POST', action: '/parliament/proposals/create' },
  314. label(i18n.parliamentProposalTitle), br(),
  315. input({ type: 'text', name: 'title', required: true }), br(), br(),
  316. label(i18n.parliamentProposalDescription), br(),
  317. textarea({ name: 'description', rows: 5, maxlength: 1000 }), br(), br(),
  318. button({ type: 'submit', class: 'create-button' }, i18n.parliamentProposalPublish)
  319. )
  320. );
  321. const ProposalsList = (proposals) => {
  322. if (!proposals || !proposals.length) return null;
  323. const cards = proposals.map(pItem => {
  324. const titleNode = pItem && pItem.voteId
  325. ? a({ class: 'proposal-title-link', href: `/votes/${encodeURIComponent(pItem.voteId)}` }, pItem.title || '')
  326. : (pItem.title || '');
  327. const onTrackLabel = pItem && pItem.onTrack
  328. ? (i18n.parliamentProposalOnTrackYes || 'THRESHOLD REACHED')
  329. : (i18n.parliamentProposalOnTrackNo || 'BELOW THRESHOLD');
  330. return div(
  331. { class: 'card' },
  332. br(),
  333. div(
  334. { class: 'card-field' },
  335. span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '),
  336. span({ class: 'card-value' }, fmt(pItem.createdAt))
  337. ),
  338. div(
  339. { class: 'card-field' },
  340. span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '),
  341. span({ class: 'card-value' }, userLink(pItem.proposer))
  342. ),
  343. div(
  344. { class: 'card-field' },
  345. span({ class: 'card-label' }, i18n.parliamentGovMethod.toUpperCase() + ': '),
  346. span({ class: 'card-value' }, pItem.method)
  347. ),
  348. br(),
  349. div(
  350. h2(titleNode),
  351. p(pItem.description || '')
  352. ),
  353. pItem.deadline
  354. ? div(
  355. { class: 'card-field' },
  356. span({ class: 'card-label' }, i18n.parliamentProposalDeadlineLabel.toUpperCase() + ': '),
  357. span({ class: 'card-value' }, fmt(pItem.deadline))
  358. )
  359. : null,
  360. pItem.deadline
  361. ? div(
  362. { class: 'card-field' },
  363. span({ class: 'card-label' }, i18n.parliamentProposalTimeLeft.toUpperCase() + ': '),
  364. span({ class: 'card-value' }, timeLeft(pItem.deadline))
  365. )
  366. : null,
  367. showVoteMetrics(pItem.method)
  368. ? div(
  369. { class: 'card-field' },
  370. span({ class: 'card-label' }, i18n.parliamentVotesNeeded.toUpperCase() + ': '),
  371. span({ class: 'card-value' }, String(pItem.needed || reqVotes(pItem.method, pItem.total)))
  372. )
  373. : null,
  374. showVoteMetrics(pItem.method)
  375. ? div(
  376. { class: 'card-field' },
  377. span({ class: 'card-label' }, i18n.parliamentVotesSlashTotal.toUpperCase() + ': '),
  378. span({ class: 'card-value' }, `${Number(pItem.yes || 0)}/${Number(pItem.total || 0)}`)
  379. )
  380. : null,
  381. showVoteMetrics(pItem.method)
  382. ? div(
  383. { class: 'card-field' },
  384. span({ class: 'card-label' }, i18n.parliamentProposalVoteStatusLabel.toUpperCase() + ': '),
  385. span({ class: 'card-value' }, onTrackLabel)
  386. )
  387. : null,
  388. pItem && pItem.voteId
  389. ? form(
  390. { method: 'GET', action: `/votes/${encodeURIComponent(pItem.voteId)}` },
  391. button({ type: 'submit', class: 'vote-btn' }, i18n.parliamentVoteAction)
  392. )
  393. : null
  394. );
  395. });
  396. return div(
  397. { class: 'cards' },
  398. h2(i18n.parliamentCurrentProposalsTitle),
  399. applyEl(div, null, cards)
  400. );
  401. };
  402. const FutureLawsList = (rows) => {
  403. if (!rows || !rows.length) return null;
  404. const cards = rows.map(pItem =>
  405. div(
  406. { class: 'card' },
  407. br(),
  408. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentGovMethod.toUpperCase() + ': '), span({ class: 'card-value' }, pItem.method)),
  409. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '), span({ class: 'card-value' }, fmt(pItem.createdAt))),
  410. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), span({ class: 'card-value' }, userLink(pItem.proposer))),
  411. h2(pItem.title || ''),
  412. p(pItem.description || '')
  413. )
  414. );
  415. return div(
  416. { class: 'cards' },
  417. h2(i18n.parliamentFutureLawsTitle),
  418. applyEl(div, null, cards)
  419. );
  420. };
  421. const RevocationForm = (laws = []) =>
  422. div(
  423. { class: 'div-center' },
  424. h2(i18n.parliamentRevocationFormTitle),
  425. form(
  426. {
  427. method: 'POST',
  428. action: '/parliament/revocations/create'
  429. },
  430. label(i18n.parliamentRevocationLaw), br(),
  431. select(
  432. { name: 'lawId', required: true },
  433. ...(laws || []).map(l =>
  434. option(
  435. { value: l.id },
  436. `${l.question || l.title || l.id}`
  437. )
  438. )
  439. ),
  440. br(), br(),
  441. label(i18n.parliamentRevocationReasons), br(),
  442. textarea({ name: 'reasons', rows: 4, maxlength: 1000 }),
  443. br(), br(),
  444. button({ type: 'submit', class: 'create-button' }, i18n.parliamentRevocationPublish || 'Publish Revocation')
  445. )
  446. );
  447. const RevocationsList = (revocations) => {
  448. if (!revocations || !revocations.length) return null;
  449. const cards = revocations.map(pItem => {
  450. const titleNode = pItem && pItem.voteId
  451. ? a({ class: 'revocation-title-link', href: `/votes/${encodeURIComponent(pItem.voteId)}` }, pItem.title || pItem.lawTitle || '')
  452. : (pItem.title || pItem.lawTitle || '');
  453. const onTrackLabel = pItem && pItem.onTrack
  454. ? (i18n.parliamentProposalOnTrackYes || 'THRESHOLD REACHED')
  455. : (i18n.parliamentProposalOnTrackNo || 'BELOW THRESHOLD');
  456. return div(
  457. { class: 'card' },
  458. br(),
  459. div(
  460. { class: 'card-field' },
  461. span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '),
  462. span({ class: 'card-value' }, fmt(pItem.createdAt))
  463. ),
  464. div(
  465. { class: 'card-field' },
  466. span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '),
  467. span(
  468. { class: 'card-value' },
  469. userLink(pItem.proposer)
  470. )
  471. ),
  472. div(
  473. { class: 'card-field' },
  474. span({ class: 'card-label' }, i18n.parliamentGovMethod + ': '),
  475. span({ class: 'card-value' }, pItem.method)
  476. ),
  477. br(),
  478. div(
  479. h2(titleNode),
  480. p(pItem.reasons || '')
  481. ),
  482. pItem.deadline
  483. ? div(
  484. { class: 'card-field' },
  485. span({ class: 'card-label' }, i18n.parliamentProposalDeadlineLabel.toUpperCase() + ': '),
  486. span({ class: 'card-value' }, fmt(pItem.deadline))
  487. )
  488. : null,
  489. pItem.deadline
  490. ? div(
  491. { class: 'card-field' },
  492. span({ class: 'card-label' }, i18n.parliamentProposalTimeLeft.toUpperCase() + ': '),
  493. span({ class: 'card-value' }, timeLeft(pItem.deadline))
  494. )
  495. : null,
  496. showVoteMetrics(pItem.method)
  497. ? div(
  498. { class: 'card-field' },
  499. span({ class: 'card-label' }, i18n.parliamentVotesNeeded.toUpperCase() + ': '),
  500. span({ class: 'card-value' }, String(pItem.needed || reqVotes(pItem.method, pItem.total)))
  501. )
  502. : null,
  503. showVoteMetrics(pItem.method)
  504. ? div(
  505. { class: 'card-field' },
  506. span({ class: 'card-label' }, i18n.parliamentVotesSlashTotal.toUpperCase() + ': '),
  507. span({ class: 'card-value' }, `${Number(pItem.yes || 0)}/${Number(pItem.total || 0)}`)
  508. )
  509. : null,
  510. showVoteMetrics(pItem.method)
  511. ? div(
  512. { class: 'card-field' },
  513. span({ class: 'card-label' }, i18n.parliamentProposalVoteStatusLabel.toUpperCase() + ': '),
  514. span({ class: 'card-value' }, onTrackLabel)
  515. )
  516. : null,
  517. pItem && pItem.voteId
  518. ? form(
  519. { method: 'GET', action: `/votes/${encodeURIComponent(pItem.voteId)}` },
  520. button({ type: 'submit', class: 'vote-btn' }, i18n.parliamentVoteAction)
  521. )
  522. : null
  523. );
  524. });
  525. return div(
  526. { class: 'cards' },
  527. h2(i18n.parliamentCurrentRevocationsTitle),
  528. applyEl(div, null, cards)
  529. );
  530. };
  531. const FutureRevocationsList = (rows) => {
  532. if (!rows || !rows.length) return null;
  533. const cards = rows.map(pItem =>
  534. div(
  535. { class: 'card' },
  536. br(),
  537. pItem.method ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentGovMethod.toUpperCase() + ': '), span({ class: 'card-value' }, pItem.method)) : null,
  538. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '), span({ class: 'card-value' }, fmt(pItem.createdAt))),
  539. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), span({ class: 'card-value' }, userLink(pItem.proposer))),
  540. h2(pItem.title || pItem.lawTitle || ''),
  541. p(pItem.reasons || '')
  542. )
  543. );
  544. return div(
  545. { class: 'cards' },
  546. h2(i18n.parliamentFutureRevocationsTitle),
  547. applyEl(div, null, cards)
  548. );
  549. };
  550. const LawsStats = (laws = [], revocatedCount = 0) => {
  551. const proposed = laws.length;
  552. const approved = laws.length;
  553. const declined = 0;
  554. const discarded = 0;
  555. const revocated = Number(revocatedCount || 0);
  556. return div(
  557. { class: 'table-wrap' },
  558. h2(i18n.parliamentPoliciesTitle || 'POLICIES'),
  559. applyEl(table, { class: 'table table--centered' }, [
  560. thead(tr(
  561. th(i18n.parliamentThProposed),
  562. th(i18n.parliamentThApproved),
  563. th(i18n.parliamentThDeclined),
  564. th(i18n.parliamentThDiscarded),
  565. th(i18n.parliamentPoliciesRevocated)
  566. )),
  567. tbody(
  568. tr(
  569. td(String(proposed)),
  570. td(String(approved)),
  571. td(String(declined)),
  572. td(String(discarded)),
  573. td(String(revocated))
  574. )
  575. )
  576. ])
  577. );
  578. };
  579. const LawsList = (laws) => {
  580. if (!laws || !laws.length) return NoLaws();
  581. const cards = laws.map(l => {
  582. const total = Number((l.votes && (l.votes.total || l.votes.TOTAL)) || 0);
  583. const yes = Number((l.votes && (l.votes.YES || l.votes.Yes || l.votes.yes)) || 0);
  584. const needed = reqVotes(l.method, total);
  585. const showMetricsFlag = showVoteMetrics(l.method);
  586. return div(
  587. { class: 'card' },
  588. br(),
  589. div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod + ': ').toUpperCase()), span({ class: 'card-value' }, l.method)),
  590. div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawEnacted + ': ').toUpperCase()), span({ class: 'card-value' }, fmt(l.enactedAt))),
  591. div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), span({ class: 'card-value' }, userLink(l.proposer))),
  592. h2(l.question || ''),
  593. p(l.description || ''),
  594. showMetricsFlag ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesNeeded + ': ').toUpperCase()), span({ class: 'card-value' }, String(needed))) : null,
  595. showMetricsFlag ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesSlashTotal + ': ').toUpperCase()), span({ class: 'card-value' }, `${yes}/${total}`)) : null
  596. );
  597. });
  598. return div(
  599. { class: 'cards' },
  600. h2(i18n.parliamentLawsTitle || 'LAWS'),
  601. applyEl(div, null, cards)
  602. );
  603. };
  604. const HistoricalGovsSummary = (rows = []) => {
  605. const byMethod = new Map();
  606. for (const g of rows) {
  607. const k = String(g.method || 'ANARCHY').toUpperCase();
  608. byMethod.set(k, (byMethod.get(k) || 0) + 1);
  609. }
  610. const entries = Array.from(byMethod.entries()).sort((a,b) => String(a[0]).localeCompare(String(b[0])));
  611. const lines = entries.map(([method, count]) =>
  612. tr(td(method), td(String(count)))
  613. );
  614. return div(
  615. { class: 'table-wrap' },
  616. h2(i18n.parliamentHistoricalGovernmentsTitle || 'Governments'),
  617. applyEl(table, { class: 'table table--centered' }, [
  618. thead(tr(th(i18n.parliamentGovMethod), th(i18n.parliamentThCycles))),
  619. applyEl(tbody, null, lines)
  620. ])
  621. );
  622. };
  623. const HistoricalList = (rows, metasByKey = {}) => {
  624. if (!rows || !rows.length) return NoGovernments();
  625. const cards = rows.map(g => {
  626. const key = `${g.powerType}:${g.powerId}`;
  627. const meta = metasByKey[key];
  628. const showActor = g.powerType === 'tribe' || g.powerType === 'inhabitant';
  629. const showMembers = g.powerType === 'tribe';
  630. const actorLabel =
  631. g.powerType === 'tribe'
  632. ? (i18n.parliamentActorInPowerTribe || 'TRIBE RULING')
  633. : (i18n.parliamentActorInPowerInhabitant || 'INHABITANT RULING');
  634. return div(
  635. { class: 'card' },
  636. h2(g.method),
  637. div({ class: 'card-field' },
  638. span({ class: 'card-label' }, (i18n.parliamentLegSince + ': ').toUpperCase()),
  639. span({ class: 'card-value' }, fmt(g.since))
  640. ),
  641. div({ class: 'card-field' },
  642. span({ class: 'card-label' }, (i18n.parliamentLegEnd + ': ').toUpperCase()),
  643. span({ class: 'card-value' }, fmt(g.end))
  644. ),
  645. showActor ? div({ class: 'card-field' },
  646. span({ class: 'card-label' }, String(actorLabel).toUpperCase() + ': '),
  647. span({ class: 'card-value' },
  648. g.powerType === 'tribe'
  649. ? a({ class: 'user-link', href: `/tribe/${encodeURIComponent(g.powerId)}` }, g.powerTitle || g.powerId)
  650. : userLink(g.powerId, g.powerTitle)
  651. )
  652. ) : null,
  653. (g.method !== 'ANARCHY')
  654. ? div({ class: 'card-field' },
  655. span({ class: 'card-label' }, i18n.parliamentVotesReceived + ': '),
  656. span({ class: 'card-value' }, `${g.votesReceived} (${g.totalVotes})`)
  657. )
  658. : null,
  659. br(),
  660. showActor && meta && (meta.avatarUrl || meta.bio)
  661. ? div(
  662. { class: 'actor-meta' },
  663. meta.avatarUrl ? img({ src: meta.avatarUrl, alt: '', class: 'avatar--lg' }) : null,
  664. meta.bio ? p({ class: 'bio' }, meta.bio) : null
  665. )
  666. : null,
  667. showMembers
  668. ? div({ class: 'card-field' },
  669. span({ class: 'card-label' }, i18n.parliamentMembers + ': '),
  670. span({ class: 'card-value' }, String(g.members || 0))
  671. )
  672. : null,
  673. div({ class: 'card-field' },
  674. span({ class: 'card-label' }, i18n.parliamentPoliciesProposal + ': '),
  675. span({ class: 'card-value' }, String(g.proposed || 0))
  676. ),
  677. div({ class: 'card-field' },
  678. span({ class: 'card-label' }, i18n.parliamentPoliciesApproved + ': '),
  679. span({ class: 'card-value' }, String(g.approved || 0))
  680. ),
  681. div({ class: 'card-field' },
  682. span({ class: 'card-label' }, i18n.parliamentPoliciesDeclined + ': '),
  683. span({ class: 'card-value' }, String(g.declined || 0))
  684. ),
  685. div({ class: 'card-field' },
  686. span({ class: 'card-label' }, i18n.parliamentPoliciesDiscarded + ': '),
  687. span({ class: 'card-value' }, String(g.discarded || 0))
  688. ),
  689. div({ class: 'card-field' },
  690. span({ class: 'card-label' }, i18n.parliamentPoliciesRevocated + ': '),
  691. span({ class: 'card-value' }, String(g.revocated || 0))
  692. ),
  693. div({ class: 'card-field' },
  694. span({ class: 'card-label' }, i18n.parliamentEfficiency + ': '),
  695. span({ class: 'card-value' }, `${g.efficiency || 0} %`)
  696. )
  697. );
  698. });
  699. return div(
  700. { class: 'cards' },
  701. h2(i18n.parliamentHistoricalElectionsTitle || 'ELECTION CYCLES'),
  702. applyEl(div, null, cards)
  703. );
  704. };
  705. const countCandidaturesByActor = (cands = []) => {
  706. const m = new Map();
  707. for (const c of cands) {
  708. const key = `${c.targetType}:${c.targetId}`;
  709. m.set(key, (m.get(key) || 0) + 1);
  710. }
  711. return m;
  712. };
  713. const LeadersSummary = (leaders = [], candidatures = []) => {
  714. const candCounts = countCandidaturesByActor(candidatures);
  715. const totals = leaders.reduce((acc, l) => {
  716. const key = `${l.powerType}:${l.powerId}`;
  717. const candsFromMap = candCounts.get(key) || 0;
  718. const presentedNorm = Math.max(Number(l.presented || 0), Number(l.inPower || 0), candsFromMap);
  719. acc.presented += presentedNorm;
  720. acc.inPower += Number(l.inPower || 0);
  721. acc.proposed += Number(l.proposed || 0);
  722. acc.approved += Number(l.approved || 0);
  723. acc.declined += Number(l.declined || 0);
  724. acc.discarded += Number(l.discarded || 0);
  725. acc.revocated += Number(l.revocated || 0);
  726. return acc;
  727. }, { presented:0, inPower:0, proposed:0, approved:0, declined:0, discarded:0, revocated:0 });
  728. const efficiencyPct = totals.proposed > 0 ? Math.round((totals.approved / totals.proposed) * 100) : 0;
  729. return div(
  730. { class: 'table-wrap' },
  731. h2(i18n.parliamentHistoricalLawsTitle || 'Actions'),
  732. applyEl(table, { class: 'table table--centered' }, [
  733. thead(tr(
  734. th(i18n.parliamentThTotalCandidatures),
  735. th(i18n.parliamentThTimesInPower),
  736. th(i18n.parliamentThProposed),
  737. th(i18n.parliamentThApproved),
  738. th(i18n.parliamentThDeclined),
  739. th(i18n.parliamentThDiscarded),
  740. th(i18n.parliamentPoliciesRevocated),
  741. th(i18n.parliamentEfficiency)
  742. )),
  743. tbody(
  744. tr(
  745. td(String(totals.presented)),
  746. td(String(totals.inPower)),
  747. td(String(totals.proposed)),
  748. td(String(totals.approved)),
  749. td(String(totals.declined)),
  750. td(String(totals.discarded)),
  751. td(String(totals.revocated)),
  752. td(`${efficiencyPct} %`)
  753. )
  754. )
  755. ])
  756. );
  757. };
  758. const LeadersList = (leaders, metas = {}, candidatures = []) => {
  759. if (!leaders || !leaders.length) return div({ class: 'empty' }, p(i18n.parliamentNoLeaders));
  760. const rows = leaders.map(l => {
  761. const key = `${l.powerType}:${l.powerId}`;
  762. const meta = metas[key] || {};
  763. const avatar = meta.avatarUrl ? img({ src: meta.avatarUrl, alt: '', class: 'leader-table__avatar' }) : null;
  764. const link = l.powerType === 'tribe'
  765. ? a({ class: 'user-link', href: `/tribe/${encodeURIComponent(l.powerId)}` }, l.powerTitle || l.powerId)
  766. : userLink(l.powerId, l.powerTitle);
  767. const leaderCell = div({ class: 'leader-cell' }, avatar, link);
  768. return tr(
  769. td(leaderCell),
  770. td(String(l.proposed || 0)),
  771. td(String(l.approved || 0)),
  772. td(String(l.declined || 0)),
  773. td(String(l.discarded || 0)),
  774. td(String(l.revocated || 0)),
  775. td(`${(l.efficiency != null ? Math.round(l.efficiency * 100) : (l.proposed > 0 ? Math.round((l.approved / l.proposed) * 100) : 0))} %`)
  776. );
  777. });
  778. return div(
  779. { class: 'table-wrap' },
  780. h2(i18n.parliamentHistoricalLeadersTitle),
  781. applyEl(table, { class: 'table table--centered gov-overview' }, [
  782. thead(tr(
  783. th(i18n.parliamentActorInPowerInhabitant),
  784. th(i18n.parliamentPoliciesProposal),
  785. th(i18n.parliamentPoliciesApproved),
  786. th(i18n.parliamentPoliciesDeclined),
  787. th(i18n.parliamentPoliciesDiscarded),
  788. th(i18n.parliamentPoliciesRevocated),
  789. th(i18n.parliamentEfficiency)
  790. )),
  791. applyEl(tbody, null, rows)
  792. ])
  793. );
  794. };
  795. const RulesContent = () =>
  796. div(
  797. { class: 'card' },
  798. h2(i18n.parliamentRulesTitle),
  799. ul(
  800. li(i18n.parliamentRulesIntro),
  801. li(i18n.parliamentRulesTerm),
  802. li(i18n.parliamentRulesMethods),
  803. li(i18n.parliamentRulesAnarchy),
  804. li(i18n.parliamentRulesCandidates),
  805. li(i18n.parliamentRulesElection),
  806. li(i18n.parliamentRulesTies),
  807. li(i18n.parliamentRulesProposals),
  808. li(i18n.parliamentRulesLimit),
  809. li(i18n.parliamentRulesLaws),
  810. li(i18n.parliamentRulesRevocations),
  811. li(i18n.parliamentRulesHistorical),
  812. li(i18n.parliamentRulesLeaders)
  813. )
  814. );
  815. const CandidaturesSection = (governmentCard, candidatures, leaderMeta) => {
  816. return div(
  817. h2(i18n.parliamentGovernmentCard),
  818. GovHeader(governmentCard || {}),
  819. CandidatureStats(candidatures || [], governmentCard || null, leaderMeta || null),
  820. CandidatureForm(),
  821. candidatures && candidatures.length ? CandidaturesTable(candidatures) : null
  822. );
  823. };
  824. const ProposalsSection = (governmentCard, proposals, futureLaws, canPropose) => {
  825. const has = proposals && proposals.length > 0;
  826. const fl = FutureLawsList(futureLaws || []);
  827. if (!has && canPropose) return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), ProposalForm(), fl);
  828. if (!has && !canPropose) return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), NoProposals(), fl);
  829. return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), ProposalForm(), ProposalsList(proposals), fl);
  830. };
  831. const RevocationsSection = (governmentCard, laws, revocations, futureRevocations) =>
  832. div(
  833. h2(i18n.parliamentGovernmentCard),
  834. GovHeader(governmentCard || {}),
  835. RevocationForm(laws || []),
  836. RevocationsList(revocations || []) || '',
  837. FutureRevocationsList(futureRevocations || []) || ''
  838. );
  839. const normalizeGovCard = (governmentCard, inhabitantsTotal) => {
  840. const pop = Number(inhabitantsTotal ?? governmentCard?.inhabitantsTotal ?? 0) || 0;
  841. if (governmentCard && (governmentCard.method || governmentCard.since || governmentCard.end || governmentCard.powerType)) {
  842. return { ...governmentCard, inhabitantsTotal: pop };
  843. }
  844. return null;
  845. };
  846. const parliamentView = async (state) => {
  847. const {
  848. filter,
  849. governmentCard,
  850. candidatures,
  851. proposals,
  852. futureLaws,
  853. canPropose,
  854. laws,
  855. historical,
  856. leaders,
  857. leaderMeta,
  858. powerMeta,
  859. revocations,
  860. futureRevocations,
  861. revocationsEnactedCount,
  862. historicalMetas = {},
  863. leadersMetas = {},
  864. inhabitantsTotal
  865. } = state;
  866. const fallbackGov = {
  867. method: 'ANARCHY',
  868. votesReceived: 0,
  869. totalVotes: 0,
  870. proposed: 0,
  871. approved: 0,
  872. declined: 0,
  873. discarded: 0,
  874. revocated: 0,
  875. efficiency: 0,
  876. powerType: 'none',
  877. powerId: null,
  878. powerTitle: 'ANARCHY',
  879. since: moment().toISOString(),
  880. end: moment().add(TERM_DAYS, 'days').toISOString(),
  881. inhabitantsTotal: Number(inhabitantsTotal ?? 0) || 0
  882. };
  883. const gov = normalizeGovCard(governmentCard, inhabitantsTotal) || fallbackGov;
  884. const LawsSectionWrap = () =>
  885. div(
  886. LawsStats(laws || [], revocationsEnactedCount || 0),
  887. LawsList(laws || [])
  888. );
  889. return template(
  890. i18n.parliamentTitle,
  891. section(div({ class: 'tags-header' }, h2(i18n.parliamentTitle), p(i18n.parliamentDescription)), Tabs(filter)),
  892. section(
  893. filter === 'government' ? GovernmentCard(gov, powerMeta) : null,
  894. filter === 'candidatures' ? CandidaturesSection(gov, candidatures, leaderMeta) : null,
  895. filter === 'proposals' ? ProposalsSection(gov, proposals, futureLaws, canPropose) : null,
  896. filter === 'laws' ? LawsSectionWrap() : null,
  897. filter === 'revocations' ? RevocationsSection(gov, laws, revocations, futureRevocations) : null,
  898. filter === 'historical' ? div(HistoricalGovsSummary(historical || []), HistoricalList(historical || [], historicalMetas)) : null,
  899. filter === 'leaders' ? div(LeadersSummary(leaders || [], candidatures || []), LeadersList(leaders || [], leadersMetas, candidatures || [])) : null,
  900. filter === 'rules' ? RulesContent() : null
  901. )
  902. );
  903. };
  904. module.exports = { parliamentView, pickLeader };