parliament_view.js 36 KB

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