Primera tanda de la integracion 0.9.1 -> 0.9.5. Se actualizan los 57 ficheros que upstream cambio y la rama no tocaba, entre ellos los 11 de traduccion: eso ya solo es posible gracias al overlay de i18n, antes eran 11 conflictos garantizados. Se portan ademas las dependencias que 0.9.5 introduce y de las que dependen los ficheros actualizados: pdfDocument, content_favorites, recurrence, media_gallery, comments_view, gallery_view y polls (modelo, limites y vista). Quedan 48 ficheros con divergencia real por resolver a mano (backend.js, main_views.js, OasisMobile.css y 45 vistas y modelos). Verificado: 0 errores de sintaxis en src/, 0 requires relativos rotos, la app arranca declarando 0.9.5 y las 47 rutas principales responden 200/302.
468 lines
21 KiB
JavaScript
468 lines
21 KiB
JavaScript
const { div, h1, h2, h3, p, section, button, form, input, span, img, a, br, table, tr, td, textarea, label, strong, details, summary } = require("../server/node_modules/hyperaxe");
|
|
const { template, i18n, userLink } = require("./main_views");
|
|
const { config } = require("../server/SSB_server.js");
|
|
|
|
const userId = config.keys.id;
|
|
const moment = require("../server/node_modules/moment");
|
|
|
|
const fmtCycle = (c) => c && c.formatted ? c.formatted : '';
|
|
|
|
const houseImageSrc = (house) => (house && house.image) ? house.image : '/assets/larp/images/default.jpg';
|
|
|
|
const renderCycleBanner = (cycle) => div({ class: 'larp-cycle-banner' },
|
|
span({ class: 'larp-cycle-label' }, i18n.larpCycleLabel || 'Cycle'),
|
|
span({ class: 'larp-cycle-value' }, fmtCycle(cycle))
|
|
);
|
|
|
|
const renderHouseBadges = ({ myHouse, governingHouse, houses }) => {
|
|
const academia = Array.isArray(houses) ? houses.find(h => h.key === 'academia') : null;
|
|
const isNewcomer = !myHouse;
|
|
return div({ class: 'larp-house-badges' },
|
|
myHouse ? div({ class: 'larp-my-house' },
|
|
span({ class: 'larp-my-house-label' }, i18n.larpMyHouse || 'My House'),
|
|
a({ href: `/larp/${myHouse.key}`, class: 'larp-my-house-link' }, myHouse.name)
|
|
) : null,
|
|
governingHouse ? div({ class: 'larp-my-house' },
|
|
span({ class: 'larp-my-house-label' }, i18n.larpGoverning || 'Ruling:'),
|
|
a({ href: `/larp/${governingHouse.key}`, class: 'larp-my-house-link' }, governingHouse.name)
|
|
) : null,
|
|
isNewcomer && academia ? div({ class: 'larp-my-house larp-academia-hint' },
|
|
span({ class: 'larp-my-house-label' }, i18n.larpStartHere),
|
|
a({ href: `/larp/${academia.key}`, class: 'larp-my-house-link' }, academia.name)
|
|
) : null
|
|
);
|
|
};
|
|
|
|
const renderHouseNav = (houses, currentKey) => div({ class: 'larp-house-nav' },
|
|
span({ class: 'larp-house-nav-label' }, i18n.larpAllHouses || 'Houses:'),
|
|
houses.map(h => a({
|
|
href: `/larp/${h.key}`,
|
|
class: h.key === currentKey ? 'larp-house-nav-chip active' : 'larp-house-nav-chip',
|
|
title: h.name
|
|
}, h.name))
|
|
);
|
|
|
|
const renderHousePanel = (house, members, posts, { canPost, viewerHouseKey, cyclesUntilRuling, canTakeTest = false }) => {
|
|
const { renderEncryptedChip } = require('./clearnet_view');
|
|
const isInHouse = viewerHouseKey === house.key;
|
|
const isInLarp = viewerHouseKey !== null && viewerHouseKey !== undefined;
|
|
const isMemberE2E = isInHouse && house.key !== 'academia';
|
|
const isAcademia = house.key === 'academia';
|
|
const canInvite = isInHouse && !isAcademia;
|
|
const canLeaveToAcademia = isInHouse && !isAcademia;
|
|
const canLeaveLarp = isInHouse && isAcademia;
|
|
const canJoinAcademia = !isInLarp && isAcademia;
|
|
const showInviteForm = !isAcademia && !isInHouse && (viewerHouseKey === 'academia' || !isInLarp);
|
|
return div({ class: 'larp-detail' },
|
|
div({ class: 'larp-detail-image-col' },
|
|
img({ src: houseImageSrc(house), alt: house.name, class: 'larp-detail-image' })
|
|
),
|
|
div({ class: 'larp-detail-body' },
|
|
div({ class: 'title-with-chip' },
|
|
h2({ class: 'larp-detail-name' }, house.name),
|
|
(cyclesUntilRuling === 0 && !isAcademia)
|
|
? span({ class: 'larp-badge larp-badge-ruling' }, i18n.larpBadgeRuling || 'Ruling')
|
|
: null,
|
|
isMemberE2E ? renderEncryptedChip(i18n) : null
|
|
),
|
|
p({ class: 'larp-detail-motto' }, '“' + house.motto + '”'),
|
|
table({ class: 'larp-info-table' },
|
|
tr(td({ class: 'card-label' }, i18n.larpRolesLabel || 'Roles'), td({ class: 'card-value' }, house.roles)),
|
|
tr(td({ class: 'card-label' }, i18n.larpFunctionLabel || 'Function'), td({ class: 'card-value' }, house.function)),
|
|
tr(td({ class: 'card-label' }, i18n.larpMonthLabel || 'Governance cycle'), td({ class: 'card-value' }, String(house.month || ''))),
|
|
(typeof cyclesUntilRuling === 'number' && cyclesUntilRuling > 0)
|
|
? tr(td({ class: 'card-label' }, i18n.larpCyclesUntilRuling || 'Next governance cycle'), td({ class: 'card-value' }, String(cyclesUntilRuling)))
|
|
: null,
|
|
tr(td({ class: 'card-label' }, i18n.larpMembersCount || 'Members'), td({ class: 'card-value' }, String(Array.isArray(members) ? members.length : (house.memberCount || 0))))
|
|
),
|
|
p({ class: 'larp-detail-description' }, house.description),
|
|
div({ class: 'larp-actions' },
|
|
isAcademia && isInHouse && canTakeTest
|
|
? a({ href: '/larp/test', class: 'filter-btn larp-start-journey' }, i18n.larpStartJourney)
|
|
: null,
|
|
isInHouse
|
|
? a({ href: `/larp/tribe/${encodeURIComponent(house.key)}`, class: 'filter-btn' }, i18n.larpVisitTribe || 'Visit Tribe')
|
|
: null,
|
|
canInvite
|
|
? form({ method: 'POST', action: '/larp/invite/create', class: 'larp-invite-create-form' },
|
|
input({ type: 'hidden', name: 'house', value: house.key }),
|
|
button({ type: 'submit', class: 'filter-btn' }, i18n.larpInviteCreate || 'Generate invitation code')
|
|
)
|
|
: null,
|
|
canLeaveToAcademia
|
|
? form({ method: 'POST', action: '/larp/join' },
|
|
input({ type: 'hidden', name: 'house', value: 'academia' }),
|
|
button({ type: 'submit', class: 'filter-btn danger-btn' }, i18n.larpLeaveToAcademia || 'Leave House')
|
|
)
|
|
: null,
|
|
canLeaveLarp
|
|
? form({ method: 'POST', action: '/larp/leave' },
|
|
button({ type: 'submit', class: 'filter-btn danger-btn' }, i18n.larpLeaveToAcademia || 'Leave House')
|
|
)
|
|
: null,
|
|
canJoinAcademia
|
|
? form({ method: 'POST', action: '/larp/join' },
|
|
input({ type: 'hidden', name: 'house', value: 'academia' }),
|
|
button({ type: 'submit', class: 'filter-btn' }, i18n.larpInviteRedeem || 'Join House')
|
|
)
|
|
: null,
|
|
showInviteForm
|
|
? form({ method: 'POST', action: '/larp/invite/redeem', class: 'larp-card-invite-form' },
|
|
input({ type: 'hidden', name: 'returnTo', value: `/larp/${house.key}` }),
|
|
input({ type: 'text', name: 'code', placeholder: i18n.larpInvitePlaceholder || 'Invitation code', maxlength: '32', required: 'required' }),
|
|
button({ type: 'submit', class: 'filter-btn' }, i18n.larpInviteRedeem || 'Join House')
|
|
)
|
|
: null
|
|
)
|
|
)
|
|
);
|
|
};
|
|
|
|
const formatCooldown = (ms) => {
|
|
if (ms <= 0) return '';
|
|
const days = Math.ceil(ms / (24 * 60 * 60 * 1000));
|
|
return String(days);
|
|
};
|
|
|
|
const renderAcademiaJoinPanel = (allHouses, testStatus, housesById, questions, myHouseKey) => {
|
|
const myKey = myHouseKey || 'academia';
|
|
const ordered = [
|
|
...allHouses.filter(h => h.key === myKey),
|
|
...allHouses.filter(h => h.key !== myKey)
|
|
];
|
|
const last = testStatus && testStatus.last ? testStatus.last : null;
|
|
const lastHouse = last && housesById ? housesById[last.house] : null;
|
|
const lastHouseName = lastHouse ? lastHouse.name : (last ? last.house : null);
|
|
const remainingMs = testStatus && !testStatus.allowed ? (testStatus.nextAt - Date.now()) : 0;
|
|
const canTake = testStatus && testStatus.allowed;
|
|
const qs = Array.isArray(questions) ? questions : [];
|
|
return div({ class: 'larp-academia-join' },
|
|
h2(i18n.larpAcademiaJoinTitle || 'Houses Current Status'),
|
|
last
|
|
? div({ class: 'larp-last-attempt' },
|
|
h2({ class: 'larp-last-attempt-title' }, i18n.larpLastAttemptTitle || 'Your last attempt'),
|
|
table({ class: 'larp-info-table' },
|
|
tr(
|
|
td({ class: 'card-label' }, i18n.larpLastAttemptHouse || 'House'),
|
|
td({ class: 'card-value' }, lastHouse
|
|
? a({ href: `/larp/${lastHouse.key}` }, lastHouseName)
|
|
: lastHouseName || '—')
|
|
),
|
|
Number.isFinite(last.score)
|
|
? tr(
|
|
td({ class: 'card-label' }, i18n.larpTestScore || 'Score'),
|
|
td({ class: 'card-value' }, String(last.score))
|
|
)
|
|
: null,
|
|
last.ts
|
|
? tr(
|
|
td({ class: 'card-label' }, i18n.larpLastAttemptWhen || 'When'),
|
|
td({ class: 'card-value' }, moment(last.ts).format('YYYY/MM/DD HH:mm:ss'))
|
|
)
|
|
: null,
|
|
!testStatus.allowed
|
|
? tr(
|
|
td({ class: 'card-label' }, i18n.larpLastAttemptCooldown || 'Next attempt in'),
|
|
td({ class: 'card-value' }, formatCooldown(remainingMs) + ' ' + (i18n.larpTestCooldownDays || 'cycles'))
|
|
)
|
|
: null
|
|
)
|
|
)
|
|
: (testStatus && !testStatus.allowed
|
|
? p({ class: 'larp-test-cooldown-banner' },
|
|
(i18n.larpTestCooldownActive || 'Next test available in') + ' ' + formatCooldown(remainingMs) + ' ' + (i18n.larpTestCooldownDays || 'cycles')
|
|
)
|
|
: null),
|
|
div({ class: 'larp-academia-grid' },
|
|
ordered.map(h => a({ href: `/larp/${h.key}`, class: h.key === myKey ? 'larp-academia-thumb larp-academia-thumb-mine' : 'larp-academia-thumb' },
|
|
img({ src: houseImageSrc(h), alt: h.name, class: 'larp-academia-thumb-image' }),
|
|
span({ class: 'larp-academia-thumb-name' }, h.name),
|
|
table({ class: 'larp-info-table larp-academia-thumb-table' },
|
|
tr(td({ class: 'card-label' }, i18n.larpRolesLabel || 'Roles'), td({ class: 'card-value' }, h.roles)),
|
|
tr(td({ class: 'card-label' }, i18n.larpFunctionLabel || 'Function'), td({ class: 'card-value' }, h.function)),
|
|
tr(td({ class: 'card-label' }, i18n.larpMembersCount || 'Members'), td({ class: 'card-value' }, String(h.memberCount || 0)))
|
|
)
|
|
))
|
|
),
|
|
);
|
|
};
|
|
|
|
|
|
const renderPostsBlock = (posts, house, canPost) => {
|
|
const stream = (Array.isArray(posts) ? posts : [])
|
|
.map(post => ({ ts: Date.parse(post.createdAt) || post.ts || 0, post }))
|
|
.sort((a, b) => b.ts - a.ts);
|
|
|
|
return div({ class: 'larp-posts-block' },
|
|
h2(i18n.larpPostsTitle || 'Wall'),
|
|
canPost
|
|
? form({ method: 'POST', action: '/larp/post', class: 'larp-post-form' },
|
|
input({ type: 'hidden', name: 'house', value: house.key }),
|
|
textarea({ id: 'larp_post_text', name: 'text', rows: '3', maxlength: '4000', placeholder: i18n.larpPostPlaceholder || 'What does this house need to say?' }),
|
|
button({ type: 'submit', class: 'filter-btn larp-post-submit' }, i18n.larpPostSubmit || 'Publish')
|
|
)
|
|
: null,
|
|
stream.length === 0
|
|
? p({ class: 'empty' }, i18n.larpPostsEmpty || 'No posts yet.')
|
|
: div({ class: 'larp-posts-list' },
|
|
stream.map(entry => div({ class: 'larp-post' },
|
|
div({ class: 'larp-post-head' },
|
|
userLink(entry.post.author),
|
|
span({ class: 'larp-post-time' }, moment(entry.post.createdAt).format('YYYY/MM/DD HH:mm'))
|
|
),
|
|
p({ class: 'larp-post-text' }, entry.post.text)
|
|
))
|
|
)
|
|
);
|
|
};
|
|
|
|
const renderHouseSearch = (q) => div({ class: 'filters' },
|
|
form({ method: 'GET', action: '/larp', class: 'filter-box' },
|
|
input({ type: 'hidden', name: 'filter', value: 'houses' }),
|
|
input({ type: 'text', name: 'q', value: q || '', placeholder: i18n.larpSearchPlaceholder, class: 'filter-box__input' }),
|
|
div({ class: 'filter-box__controls' },
|
|
button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)
|
|
)
|
|
)
|
|
);
|
|
|
|
const matchesHouse = (house, q) => {
|
|
const needle = String(q || '').trim().toLowerCase();
|
|
if (!needle) return true;
|
|
return ['name', 'motto', 'roles', 'function', 'description']
|
|
.some(f => String(house[f] || '').toLowerCase().includes(needle));
|
|
};
|
|
|
|
const renderModeButtons = (filter) => div({ class: 'mode-buttons stats-mode-row' },
|
|
['ruling', 'houses', 'rules'].map(m =>
|
|
form({ method: 'GET', action: '/larp' },
|
|
input({ type: 'hidden', name: 'filter', value: m }),
|
|
button({
|
|
type: 'submit',
|
|
class: filter === m ? 'filter-btn active' : 'filter-btn'
|
|
},
|
|
m === 'ruling' ? (i18n.larpFilterRuling || 'RULING')
|
|
: m === 'houses' ? (i18n.larpFilterHouses || 'HOUSES')
|
|
: (i18n.larpFilterRules || 'FAQ')
|
|
)
|
|
)
|
|
)
|
|
);
|
|
|
|
const renderRules = () => {
|
|
const points = [
|
|
i18n.larpRulesEntryText,
|
|
i18n.larpRulesOneHouseText,
|
|
i18n.larpRulesTestEntryText,
|
|
i18n.larpRulesInviteEntryText,
|
|
i18n.larpRulesGovernanceText,
|
|
i18n.larpRulesWallText,
|
|
i18n.larpRulesSignText
|
|
];
|
|
return div(
|
|
{ class: 'card' },
|
|
h2(i18n.larpRulesTitle || 'F.A.Q.'),
|
|
div({ class: 'rules-points' },
|
|
points.filter(Boolean).map((text, i) =>
|
|
div({ class: 'rules-point' },
|
|
span({ class: 'rules-point-num' }, String(i + 1)),
|
|
span({ class: 'rules-point-text' }, text)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
};
|
|
|
|
const renderHousesGrid = (houses, myHouseKey, governingKey) => {
|
|
const inLarp = !!myHouseKey;
|
|
const mine = inLarp ? houses.filter(h => h.key === myHouseKey) : [];
|
|
const ruling = houses.filter(h => h.key === governingKey && h.key !== myHouseKey);
|
|
const rest = houses.filter(h => h.key !== myHouseKey && h.key !== governingKey);
|
|
const ordered = [...mine, ...ruling, ...rest];
|
|
return div({ class: 'larp-grid' },
|
|
ordered.map(h => {
|
|
const isMine = h.key === myHouseKey;
|
|
const isGoverning = h.key === governingKey;
|
|
const highlight = isGoverning ? 'larp-card-ruling' : (isMine ? 'larp-card-mine' : '');
|
|
return div({ class: `larp-card larp-card-${h.key}${highlight ? ' ' + highlight : ''}` },
|
|
a({ href: `/larp/${h.key}`, class: 'larp-card-image-link' },
|
|
img({ class: 'larp-card-image', src: houseImageSrc(h), alt: h.name })
|
|
),
|
|
div({ class: 'larp-card-body' },
|
|
div({ class: 'larp-card-title-row' },
|
|
a({ href: `/larp/${h.key}`, class: 'larp-card-title' }, h.name),
|
|
isMine ? span({ class: 'larp-badge larp-badge-mine' }, i18n.larpBadgeYou || 'You') : null,
|
|
isGoverning ? span({ class: 'larp-badge larp-badge-ruling' }, i18n.larpBadgeRuling || 'Ruling') : null
|
|
),
|
|
p({ class: 'larp-card-motto' }, '“' + h.motto + '”'),
|
|
p({ class: 'larp-card-roles' }, h.roles),
|
|
div({ class: 'tribe-card-members' },
|
|
span({ class: 'tribe-members-count' }, `${i18n.larpMembersCount || 'Members'}: ${h.memberCount || 0}`)
|
|
),
|
|
h.openInviteCode ? p({ class: 'larp-card-count' }, i18n.tribeInviteCodeText, span({ class: 'tribe-open-invite-code' }, h.openInviteCode)) : null
|
|
)
|
|
);
|
|
})
|
|
);
|
|
};
|
|
|
|
exports.larpListView = ({ filter, houses, myHouseKey, cycle, governingKey, governingHouse, governingMembers, governingPosts, canPost, q }) => {
|
|
const title = i18n.larpTitle || 'L.A.R.P.';
|
|
const description = i18n.larpDescription || 'A live action role-playing layer for collaborative experimentation.';
|
|
const myHouse = houses.find(h => h.key === myHouseKey) || null;
|
|
const search = String(q || '').trim();
|
|
const mode = search ? 'houses' : filter === 'houses' ? 'houses' : filter === 'rules' ? 'rules' : 'ruling';
|
|
const matched = search ? houses.filter(h => matchesHouse(h, search)) : houses;
|
|
|
|
return template(
|
|
title,
|
|
section(
|
|
div({ class: 'tags-header' },
|
|
h1(title),
|
|
p(description)
|
|
),
|
|
renderCycleBanner(cycle),
|
|
renderHouseBadges({ myHouse, governingHouse, houses }),
|
|
renderModeButtons(mode),
|
|
renderHouseSearch(search),
|
|
mode === 'houses'
|
|
? (matched.length
|
|
? renderHousesGrid(matched, myHouseKey, governingKey)
|
|
: p({ class: 'no-content' }, i18n.larpNoHousesMatch))
|
|
: mode === 'rules'
|
|
? renderRules()
|
|
: [
|
|
renderHousePanel(governingHouse, governingMembers, governingPosts, { canPost, viewerHouseKey: myHouseKey, cyclesUntilRuling: 0 }),
|
|
renderPostsBlock(governingPosts, governingHouse, canPost)
|
|
]
|
|
)
|
|
);
|
|
};
|
|
|
|
const computeCyclesUntilRuling = (house, governingKey, now = new Date()) => {
|
|
if (!house || house.key === governingKey) return 0;
|
|
const houseIdx = (Number(house.month) || 1) - 1;
|
|
const currentMonth = now.getMonth();
|
|
for (let i = 1; i <= 12; i += 1) {
|
|
const futureMonth = (currentMonth + i) % 12;
|
|
if (futureMonth % 9 === houseIdx) return i;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, houses, posts, canPost, testStatus, inviteCode, questions }) => {
|
|
const myHouse = houses.find(h => h.key === myHouseKey) || null;
|
|
const governingHouse = houses.find(h => h.key === governingKey) || null;
|
|
const isAcademia = house.key === 'academia';
|
|
const viewerInAcademia = myHouseKey === 'academia';
|
|
const viewerIsMember = myHouseKey === house.key && house.key !== 'academia';
|
|
const canWriteWall = myHouseKey === house.key;
|
|
const showWall = viewerIsMember || house.key === governingKey || house.key === 'academia';
|
|
const cycles = computeCyclesUntilRuling(house, governingKey);
|
|
const housesById = Object.fromEntries(houses.map(h => [h.key, h]));
|
|
|
|
return template(
|
|
house.name,
|
|
section(
|
|
div({ class: 'tags-header' },
|
|
h1(house.name),
|
|
p({ class: 'larp-detail-motto' }, '“' + house.motto + '”')
|
|
),
|
|
renderCycleBanner(cycle),
|
|
renderHouseBadges({ myHouse, governingHouse, houses }),
|
|
renderHouseNav(houses, house.key),
|
|
renderHousePanel(house, members, posts, {
|
|
canPost,
|
|
viewerHouseKey: myHouseKey,
|
|
cyclesUntilRuling: cycles,
|
|
canTakeTest: isAcademia && viewerInAcademia && !!(testStatus && testStatus.allowed)
|
|
}),
|
|
inviteCode
|
|
? div({ class: 'larp-invite-banner' },
|
|
p({ class: 'larp-invite-banner-title' }, strong(i18n.larpInviteBannerTitle || 'New invitation code')),
|
|
p({ class: 'larp-invite-banner-hint' }, i18n.larpInviteBannerHint || 'Share this code with someone in ACADEMIA. It expires in 30 cycles or after one use.'),
|
|
p({ class: 'larp-invite-banner-code' }, inviteCode)
|
|
)
|
|
: null,
|
|
isAcademia && viewerInAcademia ? renderAcademiaJoinPanel(houses, testStatus, housesById, questions, myHouseKey) : null,
|
|
showWall
|
|
? renderPostsBlock(posts, house, canWriteWall)
|
|
: null
|
|
)
|
|
);
|
|
};
|
|
|
|
exports.larpTestView = ({ questions, cycle, houses, myHouseKey, governingKey, testStatus }) => {
|
|
const myHouse = houses.find(h => h.key === myHouseKey) || null;
|
|
const governingHouse = houses.find(h => h.key === governingKey) || null;
|
|
const title = i18n.larpTitle || 'L.A.R.P.';
|
|
const description = i18n.larpDescription || 'A live action role-playing layer for collaborative experimentation.';
|
|
|
|
return template(
|
|
title,
|
|
section(
|
|
div({ class: 'tags-header' },
|
|
h1(title),
|
|
p(description)
|
|
),
|
|
renderCycleBanner(cycle),
|
|
renderHouseBadges({ myHouse, governingHouse, houses }),
|
|
renderModeButtons(''),
|
|
testStatus && !testStatus.allowed
|
|
? div({ class: 'larp-test-cooldown' },
|
|
p((i18n.larpTestCooldownActive || 'Next test available in') + ' ' + formatCooldown(testStatus.nextAt - Date.now()) + ' ' + (i18n.larpTestCooldownDays || 'cycles')),
|
|
p(a({ href: '/larp/academia', class: 'filter-btn' }, i18n.larpBackToAcademia || 'Back to ACADEMIA'))
|
|
)
|
|
: form({ method: 'POST', action: '/larp/test', class: 'larp-test-form' },
|
|
questions.map((q, idx) => div({ class: 'larp-test-question' },
|
|
p({ class: 'larp-test-q-text' }, strong(`${idx + 1}. `), i18n[q.key] || q.question),
|
|
div({ class: 'larp-test-options' },
|
|
q.options.map((opt, oi) => label({ class: 'larp-test-option' },
|
|
input({ type: 'radio', name: `q${idx}`, value: String(oi), required: 'required' }),
|
|
' ', i18n[opt.key] || opt.text
|
|
))
|
|
)
|
|
)),
|
|
div({ class: 'larp-actions' },
|
|
button({ type: 'submit', class: 'filter-btn' }, i18n.larpTestSubmit || 'Join House')
|
|
)
|
|
)
|
|
)
|
|
);
|
|
};
|
|
|
|
exports.larpTestResultView = ({ house, result, cycle, houses, myHouseKey, governingKey }) => {
|
|
const myHouse = houses.find(h => h.key === myHouseKey) || null;
|
|
const governingHouse = houses.find(h => h.key === governingKey) || null;
|
|
const titleText = (i18n.larpTestResultTitle || 'Test result') + (house && house.name ? ' — ' + house.name : '');
|
|
const ranking = Array.isArray(result && result.ranking) ? result.ranking : [];
|
|
const housesById = Object.fromEntries(houses.map(h => [h.key, h]));
|
|
|
|
return template(
|
|
titleText,
|
|
section(
|
|
div({ class: 'tags-header' }, h1(titleText)),
|
|
renderCycleBanner(cycle),
|
|
renderHouseBadges({ myHouse, governingHouse, houses }),
|
|
div({ class: 'larp-test-result' },
|
|
ranking.length
|
|
? div({ class: 'larp-test-ranking' },
|
|
h2({ class: 'larp-test-ranking-title' }, i18n.larpTestRankingTitle || 'Score by house'),
|
|
table({ class: 'larp-info-table' },
|
|
ranking.map(([key, score]) => tr(
|
|
td({ class: 'card-label' }, housesById[key] ? a({ href: `/larp/${key}` }, housesById[key].name) : key),
|
|
td({ class: 'card-value' }, String(score))
|
|
))
|
|
)
|
|
)
|
|
: null,
|
|
p({ class: 'larp-test-next' }, i18n.larpTestNextAttempt || 'You can attempt another test in 30 cycles.'),
|
|
div({ class: 'larp-actions' },
|
|
a({ href: house ? `/larp/${house.key}` : '/larp/academia', class: 'filter-btn' },
|
|
i18n.larpGoToHouse || 'Go to your house'
|
|
)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
};
|