upstream: integrar 0.9.5 en style.css y las vistas grandes
style.css pasa a 0.9.5: lo unico que la rama le habia hecho era borrar reglas de housing, y su estilo propio vive en OasisMobile.css, que carga despues y gana igual. Se adopta 0.9.5 en tags_view (anade busqueda y filtros mine/recent), vote_view, market_view y activity_view, donde la rama solo arrastraba la version anterior: renderContentActions de dos parametros, comentarios de mercado propios y un import muerto de renderHiveNav que no se llegaba a llamar. Quedan a proposito en la version de la rama forum_view y chats_view: llevan funcionalidad movil propia (la tira de temas del foro y la interfaz de responder mensajes, que acompana al replyTo del modelo) entrelazada con estructuras que 0.9.5 ha reescrito. Integrarlas pide rehacer esa funcionalidad sobre la forma nueva, y no es un cambio que convenga colar entre otros veinte. Verificado con la app en marcha: 21 rutas OK, 0 claves i18n rotas, 0 requires rotos, interfaz intacta y orden de hojas correcto (style.css, mobile.css, OasisMobile.css).
This commit is contained in:
parent
373fba5d96
commit
7eeac8b004
5 changed files with 1722 additions and 643 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
|||
const { div, h2, p, section, button, form, a, input, label, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink, userLinkLabel, renderSpreadButton, renderContentActions, renderHiveNav } = require('./main_views');
|
||||
const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink, userLinkLabel, renderSpreadButton, renderContentActions } = require('./main_views');
|
||||
const opinionCategories = require('../backend/opinion_categories');
|
||||
|
||||
const OPINION_TYPES = new Set(['bookmark','votes','feed','image','audio','video','document','torrent','transfer']);
|
||||
const OPINION_TYPES = new Set(['bookmark','votes','feed','image','audio','video','document','torrent']);
|
||||
const OPINION_ROUTES = {
|
||||
feed: (id) => `/feed/opinions/${encodeURIComponent(id)}`,
|
||||
bookmark: (id) => `/bookmarks/opinions/${encodeURIComponent(id)}`,
|
||||
|
|
@ -11,11 +11,11 @@ const OPINION_ROUTES = {
|
|||
torrent: (id) => `/torrents/opinions/${encodeURIComponent(id)}`,
|
||||
video: (id) => `/videos/opinions/${encodeURIComponent(id)}`,
|
||||
document: (id) => `/documents/opinions/${encodeURIComponent(id)}`,
|
||||
votes: (id) => `/votes/opinions/${encodeURIComponent(id)}`,
|
||||
transfer: (id) => `/transfers/opinions/${encodeURIComponent(id)}`
|
||||
votes: (id) => `/votes/opinions/${encodeURIComponent(id)}`
|
||||
};
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { renderUrl } = require('../backend/renderUrl');
|
||||
const { letterOf } = require('./polls_view');
|
||||
const { getConfig } = require("../configs/config-manager.js");
|
||||
const { sanitizeHtml } = require('../backend/sanitizeHtml');
|
||||
|
||||
|
|
@ -228,7 +228,14 @@ function buildActivityItemsWithPostThreads(deduped, allActions) {
|
|||
}
|
||||
|
||||
exports.renderActionCards = renderActionCards;
|
||||
function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
||||
const SPREADABLE_TYPES = new Set([
|
||||
'post', 'audio', 'video', 'image', 'document', 'torrent', 'bookmark',
|
||||
'event', 'calendar', 'task', 'votes', 'vote', 'market', 'shop', 'shopProduct',
|
||||
'project', 'transfer', 'housing', 'job', 'report', 'industry', 'industryBuild', 'industryBlueprint',
|
||||
'chat', 'chatMessage', 'pad', 'padEntry', 'forum', 'map', 'poll', 'blog'
|
||||
]);
|
||||
|
||||
function renderActionCards(actions, userId, allActions, spreadMap = new Map(), extras = {}) {
|
||||
const all = Array.isArray(allActions) ? allActions : actions;
|
||||
const byIdAll = new Map();
|
||||
for (const a0 of all) {
|
||||
|
|
@ -343,7 +350,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
} else if (type === 'shopProduct') {
|
||||
headerText = `[SHOP · PRODUCT]`;
|
||||
} else if (type === 'chat') {
|
||||
headerText = `[CHAT \u00b7 NEW]`;
|
||||
headerText = `[CHAT \u00b7 ${String(i18n.chatThreadsLabel).toUpperCase()}]`;
|
||||
} else if (type === 'pad') {
|
||||
headerText = `[PAD · ${String(i18n.padNew || 'NEW').toUpperCase()}]`;
|
||||
} else if (type === 'ubiClaim') {
|
||||
|
|
@ -551,29 +558,19 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
}
|
||||
|
||||
if (type === 'tribe') {
|
||||
const { title, image, description, location, tags, inviteMode, isAnonymous } = content;
|
||||
const { title, image, description, isAnonymous } = content;
|
||||
if (isAnonymous === true) { skip = true; }
|
||||
const validTags = Array.isArray(tags) ? tags : [];
|
||||
cardBody.push(
|
||||
div({ class: 'card-section tribe' },
|
||||
h2({ class: 'tribe-title' },
|
||||
a({ href: `/tribe/${encodeURIComponent(action.id)}`, class: "user-link" }, title)
|
||||
),
|
||||
div({ style: 'display:flex; gap:.6em; flex-wrap:wrap;' },
|
||||
location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeLocationLabel.toUpperCase()) + ':'), span({ class: 'card-value' }, ...renderUrl(location))) : "",
|
||||
typeof isAnonymous === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel+ ':'), span({ class: 'card-value' }, isAnonymous ? i18n.tribePrivate : i18n.tribePublic)) : "",
|
||||
inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeModeLabel) + ':'), span({ class: 'card-value' }, inviteMode.toUpperCase())) : ""
|
||||
),
|
||||
image
|
||||
? (/^(\/|https?:)/.test(String(image))
|
||||
? img({ src: image, class: 'feed-image tribe-image' })
|
||||
: renderMediaBlob(image, null))
|
||||
: null,
|
||||
p({ class: 'tribe-description' }, ...renderUrl(description || '')),
|
||||
validTags.length
|
||||
? div({ class: 'card-tags' }, validTags.map(tag =>
|
||||
a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)))
|
||||
: ""
|
||||
p({ class: 'tribe-description' }, ...renderUrl(description || ''))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -745,7 +742,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
}
|
||||
|
||||
if (type === 'event') {
|
||||
const { title, description, date, location, price, attendees, organizer, isPublic } = content;
|
||||
const { title, description, date, location, price, attendees, isPublic } = content;
|
||||
cardBody.push(
|
||||
div({ class: 'card-section event' },
|
||||
div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)),
|
||||
|
|
@ -754,7 +751,6 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
typeof isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.isPublic || 'Public') + ':'), span({ class: 'card-value' }, isPublic ? 'Yes' : 'No')) : "",
|
||||
price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.price || 'Price') + ':'), span({ class: 'card-value' }, price + " ECO")) : "",
|
||||
br(),
|
||||
organizer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.organizer || 'Organizer') + ': '), userLink(organizer)) : "",
|
||||
Array.isArray(attendees) ? h2({ class: 'card-label' }, (i18n.attendees || 'Attendees') + ': ' + attendees.length) : ""
|
||||
)
|
||||
);
|
||||
|
|
@ -838,9 +834,9 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
(url) =>
|
||||
`<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`
|
||||
);
|
||||
bodyNode = div({ class: 'post-text', style: 'max-height:180px;overflow:hidden;', innerHTML: sanitizeHtml(linkified) });
|
||||
bodyNode = div({ class: 'post-text post-text-clamped', innerHTML: sanitizeHtml(linkified) });
|
||||
} else {
|
||||
bodyNode = p({ class: 'post-text post-text-pre', style: 'max-height:180px;overflow:hidden;' }, ...renderUrlPreserveNewlines(displayText));
|
||||
bodyNode = p({ class: 'post-text post-text-pre post-text-clamped' }, ...renderUrlPreserveNewlines(displayText));
|
||||
}
|
||||
const threadId = getThreadIdFromPost(action);
|
||||
const replyToId = getReplyToIdFromPost(action, byIdAll);
|
||||
|
|
@ -855,18 +851,18 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
div({ class: 'card-section post' },
|
||||
isReply
|
||||
? div(
|
||||
{ class: 'reply-context', style: 'border-left:3px solid #666;padding-left:10px;margin-bottom:8px;opacity:0.85;' },
|
||||
span({ style: 'font-size:0.85em;' },
|
||||
{ class: 'reply-context' },
|
||||
span({ class: 'reply-context-label' },
|
||||
a({ href: ctxHref, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'),
|
||||
parentAuthor ? span(' ', userLink(parentAuthor, parentName)) : ''
|
||||
),
|
||||
parentText ? p({ class: 'post-text reply-context-text post-text-pre', style: 'font-size:0.85em;max-height:80px;overflow:hidden;margin-top:4px;' }, ...renderUrlPreserveNewlines(parentText)) : ''
|
||||
parentText ? p({ class: 'post-text reply-context-text post-text-pre' }, ...renderUrlPreserveNewlines(parentText)) : ''
|
||||
)
|
||||
: '',
|
||||
contentWarning ? h2({ class: 'content-warning' }, contentWarning) : '',
|
||||
bodyNode,
|
||||
isTruncated && threadId
|
||||
? div({ style: 'margin-top:6px;' },
|
||||
? div({ class: 'card-section-action' },
|
||||
a({ href: `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(action.id || threadId)}`, class: 'filter-btn' }, i18n.keepReading || 'Keep reading...')
|
||||
)
|
||||
: ''
|
||||
|
|
@ -893,12 +889,16 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
span({ class: 'pm-exposition-text' }, `${String(i18n.typePost || 'POST').toUpperCase()} · THREAD`)
|
||||
)
|
||||
),
|
||||
renderContentActions(threadId, href)
|
||||
renderContentActions(threadId, href, {
|
||||
author: action.author,
|
||||
spread: spreadMap.get(threadId) || null,
|
||||
...favOptsFor('post', threadId, extras)
|
||||
})
|
||||
),
|
||||
div({ class: 'card-body' },
|
||||
root && root.text
|
||||
? div({ class: 'card-section' },
|
||||
p({ class: 'post-text', style: 'white-space:pre-wrap;' }, ...renderUrlPreserveNewlines(root.text))
|
||||
p({ class: 'post-text post-text-pre' }, ...renderUrlPreserveNewlines(root.text))
|
||||
)
|
||||
: '',
|
||||
div({ class: 'card-section' },
|
||||
|
|
@ -906,7 +906,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
const rDate = r.ts ? new Date(r.ts).toLocaleString() : '';
|
||||
return div({ class: 'thread-reply-item' },
|
||||
div({ class: 'thread-reply' },
|
||||
r.text ? p({ class: 'post-text', style: 'white-space:pre-wrap;' }, ...renderUrlPreserveNewlines(r.text)) : ''
|
||||
r.text ? p({ class: 'post-text post-text-pre' }, ...renderUrlPreserveNewlines(r.text)) : ''
|
||||
),
|
||||
div({ class: 'card-footer thread-reply-footer' },
|
||||
span({ class: 'date-link' }, rDate),
|
||||
|
|
@ -937,30 +937,46 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
div({ class: 'card-header activity-card-header' },
|
||||
div({ class: 'card-chips-row' },
|
||||
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||
span({ class: 'pm-exposition-text' }, `${String(i18n.typeChat || 'CHAT').toUpperCase()} · THREAD`)
|
||||
span({ class: 'pm-exposition-text' }, `${String(i18n.typeChat || 'CHAT').toUpperCase()} \u00b7 ${String(i18n.chatThreadsLabel).toUpperCase()}`)
|
||||
)
|
||||
),
|
||||
renderContentActions(chatRoot, href)
|
||||
renderContentActions(chatRoot, href, {
|
||||
author: action.author,
|
||||
reportTitle: chatTitle,
|
||||
spread: spreadMap.get(chatRoot) || null,
|
||||
...favOptsFor('chat', chatRoot, extras)
|
||||
})
|
||||
),
|
||||
div({ class: 'card-body' },
|
||||
div({ class: 'card-section chat' },
|
||||
div({ class: 'card-field' }, span({ class: 'card-value' }, a({ href, class: 'user-link' }, chatTitle))),
|
||||
chatDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, chatDesc)) : ''
|
||||
div({ class: 'card-field' }, a({ href, class: 'card-value user-link' }, chatTitle)),
|
||||
chatDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, chatDesc)) : '',
|
||||
div({ class: 'card-field chat-thread-meta' },
|
||||
span({ class: 'card-label' }, `${i18n.chatParticipants}: `),
|
||||
span({ class: 'card-value' }, String(c.members || 0)),
|
||||
span({ class: 'card-label' }, ` · ${i18n.chatMessagesLabel}: `),
|
||||
span({ class: 'card-value' }, String(c.messageCount || show.length))
|
||||
)
|
||||
),
|
||||
div({ class: 'card-section' },
|
||||
show.map(r => {
|
||||
const rDate = r.ts ? new Date(r.ts).toLocaleString() : '';
|
||||
return div({ class: 'thread-reply-item' },
|
||||
div({ class: 'thread-reply' },
|
||||
r.text ? p({ class: 'post-text', style: 'white-space:pre-wrap;' }, ...renderUrlPreserveNewlines(r.text)) : ''
|
||||
),
|
||||
div({ class: 'card-footer thread-reply-footer' },
|
||||
span({ class: 'date-link' }, rDate),
|
||||
userLink(r.author, action.authorNames && action.authorNames[r.author])
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
show.length
|
||||
? details({ class: 'chat-thread-details' },
|
||||
summary({ class: 'comments-summary chat-thread-summary' }, i18n.keepReading || 'Keep reading...'),
|
||||
div({ class: 'card-section' },
|
||||
show.map(r => {
|
||||
const rDate = r.ts ? new Date(r.ts).toLocaleString() : '';
|
||||
return div({ class: 'thread-reply-item' },
|
||||
div({ class: 'thread-reply' },
|
||||
r.text ? p({ class: 'post-text thread-reply-text' }, ...renderUrlPreserveNewlines(r.text)) : ''
|
||||
),
|
||||
div({ class: 'card-footer thread-reply-footer' },
|
||||
span({ class: 'date-link' }, rDate),
|
||||
userLink(r.author, action.authorNames && action.authorNames[r.author])
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
)
|
||||
: null
|
||||
),
|
||||
p({ class: 'card-footer' },
|
||||
span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `),
|
||||
|
|
@ -976,9 +992,10 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
const linkText = (title && String(title).trim()) ? title : '';
|
||||
cardBody.push(
|
||||
div({ class: 'card-section forum' },
|
||||
div({ class: 'card-field', style: "font-size:1.12em; margin-bottom:5px;" },
|
||||
span({ class: 'card-label', style: "font-weight:800;color:#ff9800;" }, i18n.title + ': '),
|
||||
a({ href: `/forum/${encodeURIComponent(linkKey)}`, style: "font-weight:800;color:#4fc3f7;" }, linkText)
|
||||
div({ class: 'card-field' },
|
||||
linkKey
|
||||
? a({ href: `/forum/${encodeURIComponent(linkKey)}`, class: 'card-value user-link' }, linkText || linkKey)
|
||||
: span({ class: 'card-value' }, linkText)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
@ -993,15 +1010,15 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
cardBody.push(
|
||||
div({ class: 'card-section forum' },
|
||||
div(
|
||||
{ class: 'reply-context', style: 'border-left:3px solid #666;padding-left:10px;margin-bottom:8px;opacity:0.85;' },
|
||||
span({ style: 'font-size:0.85em;' },
|
||||
{ class: 'reply-context' },
|
||||
span({ class: 'reply-context-meta' },
|
||||
a({ href: `/forum/${encodeURIComponent(hrefKey)}`, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'),
|
||||
parentAuthor ? span(' ', userLink(parentAuthor, parentName)) : ''
|
||||
),
|
||||
parentTitle ? p({ class: 'post-text reply-context-text', style: 'font-size:0.85em;max-height:60px;overflow:hidden;margin-top:4px;font-weight:bold;color:#4fc3f7;' }, parentTitle) : ''
|
||||
parentTitle ? p({ class: 'post-text reply-context-text' }, parentTitle) : ''
|
||||
),
|
||||
div({ class: 'card-field', style: 'margin-bottom:12px;' },
|
||||
p({ style: "margin:0 0 8px 0; word-break:break-all;" }, ...renderUrl(text))
|
||||
div({ class: 'card-field forum-reply-body' },
|
||||
p({ class: 'forum-reply-text' }, ...renderUrl(text))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
@ -1044,8 +1061,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
spreadTitle ? h2({ class: 'post-title activity-spread-title' }, spreadTitle) : '',
|
||||
spreadContentWarning ? h2({ class: 'content-warning' }, spreadContentWarning) : '',
|
||||
spreadExcerpt
|
||||
? div({ class: 'post-text activity-spread-text post-text-pre', style: 'max-height:200px;overflow:hidden;' }, ...renderUrlPreserveNewlines(spreadExcerpt))
|
||||
: div({ class: 'post-text activity-spread-text', style: 'opacity:0.6;font-style:italic;' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'),
|
||||
? div({ class: 'post-text activity-spread-text post-text-pre activity-spread-clamped' }, ...renderUrlPreserveNewlines(spreadExcerpt))
|
||||
: div({ class: 'post-text activity-spread-text activity-spread-missing' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'),
|
||||
spreadOriginalAuthor
|
||||
? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, (i18n.spreadBy || 'By') + ': '),
|
||||
|
|
@ -1059,7 +1076,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
)
|
||||
: '',
|
||||
link
|
||||
? div({ style: 'margin-top:6px;' },
|
||||
? div({ class: 'card-section-action' },
|
||||
a({ href: `/thread/${encodeURIComponent(link)}#${encodeURIComponent(link)}`, class: 'filter-btn' }, i18n.viewDetails || 'View details')
|
||||
)
|
||||
: ''
|
||||
|
|
@ -1205,7 +1222,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
h2({ class: 'tribe-title' },
|
||||
a({ href: `/industry/${encodeURIComponent(facKey)}`, class: "user-link" }, name || (i18n.industryTitle || 'Industry'))
|
||||
),
|
||||
div({ style: 'display:flex; gap:.6em; flex-wrap:wrap;' },
|
||||
div({ class: 'card-field-row' },
|
||||
sector ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industrySector || 'Sector').toUpperCase() + ':'), span({ class: 'card-value' }, String(sector).toUpperCase())) : "",
|
||||
status ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryStatusLabel || 'Status').toUpperCase() + ':'), span({ class: 'card-value' }, statusValue)) : "",
|
||||
membershipPolicy ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryMembershipPolicy || 'Membership policy').toUpperCase() + ':'), span({ class: 'card-value' }, policyValue)) : ""
|
||||
|
|
@ -1233,7 +1250,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
h2({ class: 'tribe-title' },
|
||||
a({ href: `/industry/build/${encodeURIComponent(buildKey)}`, class: "user-link" }, title || (i18n.industryBuild || 'Build'))
|
||||
),
|
||||
(buildStatus || startDate || endDate) ? div({ style: 'display:flex; gap:.6em; flex-wrap:wrap;' },
|
||||
(buildStatus || startDate || endDate) ? div({ class: 'card-field-row' },
|
||||
buildStatus ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryStatusLabel || 'Status').toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['industryBuildStatus_' + buildStatus] || buildStatus).toUpperCase())) : "",
|
||||
startDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryBuildStart || 'Start date').toUpperCase() + ':'), span({ class: 'card-value' }, moment(startDate).format('YYYY-MM-DD'))) : "",
|
||||
endDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryBuildEnd || 'End date').toUpperCase() + ':'), span({ class: 'card-value' }, moment(endDate).format('YYYY-MM-DD'))) : "",
|
||||
|
|
@ -1258,7 +1275,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
? a({ href: `/industry/${encodeURIComponent(facility)}`, class: "user-link" }, name || (i18n.industryBlueprint || 'Blueprint'))
|
||||
: (name || (i18n.industryBlueprint || 'Blueprint'))
|
||||
),
|
||||
div({ style: 'display:flex; gap:.6em; flex-wrap:wrap;' },
|
||||
div({ class: 'card-field-row' },
|
||||
outKind ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryOutputKind || 'Product type').toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['industryKind_' + outKind] || outKind).toUpperCase())) : "",
|
||||
laborHours ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryLaborHours || 'Labor hours').toUpperCase() + ':'), span({ class: 'card-value' }, String(laborHours))) : "",
|
||||
estTotal != null ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryEstTotal || 'Estimated price').toUpperCase() + ':'), span({ class: 'card-value' }, `${Number(estTotal).toFixed(2)} ECO`)) : ""
|
||||
|
|
@ -1290,10 +1307,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
const chatImageNode = renderMediaBlob(image);
|
||||
cardBody.push(
|
||||
div({ class: 'card-section chat' },
|
||||
div({ class: 'card-field' }, span({ class: 'card-label' }, 'Chat:'), span({ class: 'card-value' }, chatKey ? a({ href: `/chats/${encodeURIComponent(chatKey)}`, class: 'user-link' }, title || chatKey) : (title || ''))),
|
||||
displayDesc ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatDescription || 'Description') + ':'), span({ class: 'card-value' }, displayDesc)) : '',
|
||||
category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, category)) : '',
|
||||
status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatStatus || 'Status') + ':'), span({ class: 'card-value' }, status)) : ''
|
||||
div({ class: 'card-field' },
|
||||
chatKey
|
||||
? a({ href: `/chats/${encodeURIComponent(chatKey)}`, class: 'card-value user-link' }, title || chatKey)
|
||||
: span({ class: 'card-value' }, title || '')),
|
||||
displayDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, displayDesc)) : '',
|
||||
category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, category)) : ''
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -1303,7 +1322,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
const padTitle = content.title || action.title || '';
|
||||
cardBody.push(
|
||||
div({ class: 'card-section' },
|
||||
div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.padTitle || 'Pad') + ':'), span({ class: 'card-value' }, padKey ? a({ href: `/pads/${encodeURIComponent(padKey)}`, class: 'user-link' }, padTitle || padKey) : '')),
|
||||
div({ class: 'card-field' }, padKey ? a({ href: `/pads/${encodeURIComponent(padKey)}`, class: 'card-value user-link' }, padTitle || padKey) : span({ class: 'card-value' }, padTitle || '')),
|
||||
content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.padDeadlineLabel || 'Deadline') + ':'), span({ class: 'card-value' }, content.deadline)) : ''
|
||||
)
|
||||
);
|
||||
|
|
@ -1314,8 +1333,41 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
const calTitle = content.title || action.title || '';
|
||||
cardBody.push(
|
||||
div({ class: 'card-section' },
|
||||
div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.calendarTitle || 'Calendar') + ':'), span({ class: 'card-value' }, calKey ? a({ href: `/calendars/${encodeURIComponent(calKey)}`, class: 'user-link' }, calTitle || calKey) : '')),
|
||||
content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.calendarStatusLabel || 'Status') + ':'), span({ class: 'card-value' }, content.status)) : ''
|
||||
div({ class: 'card-field' }, calKey ? a({ href: `/calendars/${encodeURIComponent(calKey)}`, class: 'card-value user-link' }, calTitle || calKey) : span({ class: 'card-value' }, calTitle || ''))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'poll') {
|
||||
const options = Array.isArray(content.options) ? content.options : [];
|
||||
const counts = (action.pollCounts && typeof action.pollCounts === 'object') ? action.pollCounts : {};
|
||||
const voters = Number(action.pollVoters) || 0;
|
||||
const pctOf = (opt) => (voters > 0 ? Math.round(((counts[opt] || 0) / voters) * 100) : 0);
|
||||
const step = (opt) => Math.round(pctOf(opt) / 5) * 5;
|
||||
cardBody.push(
|
||||
div({ class: 'card-section poll' },
|
||||
div({ class: 'card-field' },
|
||||
span({ class: 'card-value' }, content.question || '')
|
||||
),
|
||||
options.length
|
||||
? table({ class: 'poll-results' },
|
||||
...options.map((opt, i) =>
|
||||
tr(
|
||||
td({ class: 'poll-result-option' }, `${letterOf(i)})`),
|
||||
td({ class: 'poll-result-bar' },
|
||||
div({ class: 'poll-bar' },
|
||||
div({ class: `poll-bar-fill poll-bar-fill-${step(opt)} poll-hue-${i % 8}` })
|
||||
)
|
||||
),
|
||||
td({ class: 'poll-result-count' }, `${counts[opt] || 0} (${pctOf(opt)}%)`)
|
||||
)
|
||||
)
|
||||
)
|
||||
: null,
|
||||
div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, `${i18n.pollVoters}: `),
|
||||
span({ class: 'card-value' }, String(voters))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -1483,6 +1535,51 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
);
|
||||
}
|
||||
|
||||
if (type === 'housing') {
|
||||
const { title, housing_type, property_type, price, place, status, rooms, size, capacity } = content;
|
||||
const isFree = String(housing_type || '').toLowerCase() === 'couchsurfing';
|
||||
cardBody.push(
|
||||
div({ class: 'card-section report' },
|
||||
div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.title + ':'),
|
||||
span({ class: 'card-value' }, title)
|
||||
),
|
||||
housing_type ? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.housingType + ':'),
|
||||
span({ class: 'card-value' }, String(housing_type).toUpperCase())
|
||||
) : null,
|
||||
property_type ? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.housingProperty + ':'),
|
||||
span({ class: 'card-value' }, String(property_type).toUpperCase())
|
||||
) : null,
|
||||
div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.housingPrice + ':'),
|
||||
span({ class: 'card-value' }, isFree ? (i18n.housingFree || 'FREE') : `${Number(price || 0).toFixed(2)} ECO`)
|
||||
),
|
||||
place ? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.housingPlace + ':'),
|
||||
span({ class: 'card-value' }, place)
|
||||
) : null,
|
||||
Number(rooms) > 0 ? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.housingRooms + ':'),
|
||||
span({ class: 'card-value' }, String(rooms))
|
||||
) : null,
|
||||
Number(size) > 0 ? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.housingSize + ':'),
|
||||
span({ class: 'card-value' }, `${size} m²`)
|
||||
) : null,
|
||||
Number(capacity) > 0 ? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.housingCapacity + ':'),
|
||||
span({ class: 'card-value' }, String(capacity))
|
||||
) : null,
|
||||
status ? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.housingStatus + ':'),
|
||||
span({ class: 'card-value' }, String(status).toUpperCase())
|
||||
) : null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'job') {
|
||||
const { title, job_type, tasks, location, vacants, salary, status, subscribers } = content;
|
||||
cardBody.push(
|
||||
|
|
@ -1491,26 +1588,26 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
span({ class: 'card-label' }, i18n.title + ':'),
|
||||
span({ class: 'card-value' }, title)
|
||||
),
|
||||
salary && div({ class: 'card-field' },
|
||||
salary ? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.jobSalary + ':'),
|
||||
span({ class: 'card-value' }, salary + ' ECO')
|
||||
),
|
||||
status && div({ class: 'card-field' },
|
||||
) : null,
|
||||
status ? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.jobStatus + ':'),
|
||||
span({ class: 'card-value' }, status.toUpperCase())
|
||||
),
|
||||
job_type && div({ class: 'card-field' },
|
||||
span({ class: 'card-value' }, String(status).toUpperCase())
|
||||
) : null,
|
||||
job_type ? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.jobType + ':'),
|
||||
span({ class: 'card-value' }, job_type.toUpperCase())
|
||||
),
|
||||
location && div({ class: 'card-field' },
|
||||
span({ class: 'card-value' }, String(job_type).toUpperCase())
|
||||
) : null,
|
||||
location ? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.jobLocation + ':'),
|
||||
span({ class: 'card-value' }, location.toUpperCase())
|
||||
),
|
||||
vacants && div({ class: 'card-field' },
|
||||
span({ class: 'card-value' }, String(location).toUpperCase())
|
||||
) : null,
|
||||
Number(vacants) > 0 ? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.jobVacants + ':'),
|
||||
span({ class: 'card-value' }, vacants)
|
||||
),
|
||||
span({ class: 'card-value' }, String(vacants))
|
||||
) : null,
|
||||
div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.jobSubscribers + ':'),
|
||||
span({ class: 'card-value' },
|
||||
|
|
@ -1542,7 +1639,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
}
|
||||
|
||||
if (type === 'parliamentTerm') {
|
||||
const { method, powerType, powerId, powerTitle, winnerVotes, totalVotes, startAt, endAt } = content;
|
||||
const { method, powerType, powerId, powerTitle, startAt, endAt } = content;
|
||||
const powerTypeNorm = String(powerType || '').toLowerCase();
|
||||
const winnerLink =
|
||||
powerTypeNorm === 'tribe'
|
||||
|
|
@ -1551,17 +1648,11 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
? a({ href: `/parliament?filter=government`, class: 'user-link' }, (i18n.parliamentAnarchy || 'ANARCHY'))
|
||||
: userLink(powerId, powerTitle);
|
||||
|
||||
const methodUpper = String(
|
||||
i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
|
||||
).toUpperCase();
|
||||
|
||||
cardBody.push(
|
||||
div({ class: 'card-section parliament' },
|
||||
startAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsStart.toUpperCase() || 'Elections start') + ':'), span({ class: 'card-value' }, new Date(startAt).toLocaleString())) : '',
|
||||
endAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsEnd.toUpperCase() || 'Elections end') + ':'), span({ class: 'card-value' }, new Date(endAt).toLocaleString())) : '',
|
||||
div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentCurrentLeader.toUpperCase() || 'Winning candidature') + ':'), span({ class: 'card-value' }, winnerLink)),
|
||||
div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod.toUpperCase() || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
|
||||
div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesReceived.toUpperCase() || 'Votes received') + ':'), span({ class: 'card-value' }, `${Number(winnerVotes || 0)} (${Number(totalVotes || 0)})`))
|
||||
div({ class: 'card-field' }, span({ class: 'card-value' }, winnerLink))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -1576,7 +1667,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
cardBody.push(
|
||||
div({ class: 'card-section parliament' },
|
||||
title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '',
|
||||
description ? p({ style: 'margin:.4rem 0' }, description) : '',
|
||||
description ? p({ class: 'card-section-text' }, description) : '',
|
||||
div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
|
||||
createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '',
|
||||
voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '',
|
||||
|
|
@ -1594,7 +1685,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
cardBody.push(
|
||||
div({ class: 'card-section parliament' },
|
||||
title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '',
|
||||
reasons ? p({ style: 'margin:.4rem 0' }, reasons) : '',
|
||||
reasons ? p({ class: 'card-section-text' }, reasons) : '',
|
||||
div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
|
||||
createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '',
|
||||
voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '',
|
||||
|
|
@ -1615,7 +1706,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
cardBody.push(
|
||||
div({ class: 'card-section parliament' },
|
||||
question ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawQuestion || 'Question') + ':'), span({ class: 'card-value' }, question)) : '',
|
||||
description ? p({ style: 'margin:.4rem 0' }, description) : '',
|
||||
description ? p({ class: 'card-section-text' }, description) : '',
|
||||
div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
|
||||
proposer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawProposer || 'Proposer') + ':'), span({ class: 'card-value' }, userLink(proposer))) : '',
|
||||
enactedAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawEnacted || 'Enacted at') + ':'), span({ class: 'card-value' }, new Date(enactedAt).toLocaleString())) : '',
|
||||
|
|
@ -1677,7 +1768,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const detailHref = (type !== 'feed' && type !== 'aiExchange' && type !== 'bankWallet')
|
||||
const detailHref = (type !== 'aiExchange' && type !== 'bankWallet')
|
||||
? (isParliamentTarget
|
||||
? `/parliament?filter=${encodeURIComponent(parliamentFilter)}`
|
||||
: isCourtsTarget
|
||||
|
|
@ -1693,7 +1784,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
span({ class: 'pm-exposition-text' }, String(type || '').toUpperCase())
|
||||
)
|
||||
),
|
||||
renderContentActions(msgId, detailHref)
|
||||
renderContentActions(msgId, detailHref, {
|
||||
author: action.author,
|
||||
reportTitle: (content && (content.title || content.question || content.concept || content.name)) || '',
|
||||
spread: SPREADABLE_TYPES.has(type) ? (spreadMap.get(action.id) || null) : undefined,
|
||||
...favOptsFor(type, msgId, extras)
|
||||
})
|
||||
),
|
||||
...cardBody,
|
||||
(() => {
|
||||
|
|
@ -1703,27 +1799,18 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
const ops = (action.value?.content?.opinions) || (action.content?.opinions) || {};
|
||||
const opsTotal = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0);
|
||||
return details({ class: 'opinions-voting-collapse' },
|
||||
summary({ class: 'opinions-summary' }, `${i18n.opinionsTitle || 'Opinions'} (${opsTotal})`),
|
||||
summary({ class: 'opinions-summary' },
|
||||
span({ class: 'opinions-summary-icon' }, 'ꔍ'),
|
||||
span({ class: 'opinions-summary-count' }, `(${opsTotal})`)),
|
||||
div({ class: 'voting-buttons' },
|
||||
opinionCategories.map(cat =>
|
||||
form({ method: 'POST', action: `${routeFn(action.id)}/${cat}` },
|
||||
button({ class: 'vote-btn' }, `${i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat} [${ops[cat] || 0}]`)
|
||||
button({ class: 'vote-btn' }, `${String(i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat).toUpperCase()} [${ops[cat] || 0}]`)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
})(),
|
||||
(() => {
|
||||
const SPREADABLE = new Set([
|
||||
'post','audio','video','image','document','torrent','bookmark',
|
||||
'event','calendar','task','votes','vote','market','shop','shopProduct',
|
||||
'project','transfer','job','report','industry','industryBuild','industryBlueprint',
|
||||
'chat','chatMessage','pad','padEntry','forum','map'
|
||||
]);
|
||||
if (!SPREADABLE.has(type)) return null;
|
||||
const btn = renderSpreadButton(action.id, spreadMap.get(action.id));
|
||||
return btn ? div({ class: 'card-spread-left' }, btn) : null;
|
||||
})(),
|
||||
(() => {
|
||||
const footerAuthorId = action.author || (content && content.proposer) || '';
|
||||
return p({ class: 'card-footer' },
|
||||
|
|
@ -1741,6 +1828,26 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) {
|
|||
return filteredCards;
|
||||
}
|
||||
|
||||
const FAV_KIND_BY_TYPE = {
|
||||
image: 'images', audio: 'audios', video: 'videos', document: 'documents',
|
||||
bookmark: 'bookmarks', torrent: 'torrents', event: 'events', task: 'tasks',
|
||||
report: 'reports', votes: 'votes', poll: 'polls', market: 'market',
|
||||
housing: 'housing', job: 'jobs', project: 'projects', shop: 'shops',
|
||||
chat: 'chats', chatThread: 'chats', pad: 'pads', calendar: 'calendars',
|
||||
map: 'maps', forum: 'forum', transfer: 'transfers', post: 'blogs'
|
||||
};
|
||||
|
||||
const favOptsFor = (type, id, extras = {}) => {
|
||||
const favKind = FAV_KIND_BY_TYPE[type];
|
||||
if (!favKind) return {};
|
||||
const index = extras.favIndex instanceof Map ? extras.favIndex : null;
|
||||
return {
|
||||
favKind,
|
||||
isFavorite: index ? index.get(String(id)) === favKind : false,
|
||||
returnTo: extras.returnTo || '/activity'
|
||||
};
|
||||
};
|
||||
|
||||
function getViewDetailsAction(type, action) {
|
||||
const id = encodeURIComponent(safeMsgId(action.tipId || action.id || action.key || action));
|
||||
switch (type) {
|
||||
|
|
@ -1762,11 +1869,18 @@ function getViewDetailsAction(type, action) {
|
|||
const link = normalizeSpreadLink(action.content?.spreadTargetId || action.content?.vote?.link || '');
|
||||
return link ? `/thread/${encodeURIComponent(link)}#${encodeURIComponent(link)}` : `/activity`;
|
||||
}
|
||||
case 'post': return `/thread/${id}#${id}`;
|
||||
case 'post': {
|
||||
const root = action.content && action.content.root;
|
||||
return root
|
||||
? `/blogs/${encodeURIComponent(root)}#${id}`
|
||||
: `/blogs/${id}`;
|
||||
}
|
||||
case 'feed': return `/feed/${id}`;
|
||||
case 'vote': return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`;
|
||||
case 'votes': return `/votes/${id}`;
|
||||
case 'transfer': return `/transfers/${id}`;
|
||||
case 'pixelia': return `/pixelia`;
|
||||
case 'poll': return `/polls/${id}`;
|
||||
case 'larpHousePost': return action.content?.house ? `/larp/${encodeURIComponent(action.content.house)}` : `/activity`;
|
||||
case 'tribe': return `/tribe/${id}`;
|
||||
case 'curriculum': return `/inhabitant/${encodeURIComponent(action.author)}`;
|
||||
|
|
@ -1791,6 +1905,7 @@ function getViewDetailsAction(type, action) {
|
|||
case 'chat': return `/chats/${id}`;
|
||||
case 'pad': return `/pads/${id}`;
|
||||
case 'calendar': return `/calendars/${id}`;
|
||||
case 'housing': return `/housing/${id}`;
|
||||
case 'job': return `/jobs/${id}`;
|
||||
case 'project': return `/projects/${id}`;
|
||||
case 'industry': return `/industry/${id}`;
|
||||
|
|
@ -1805,15 +1920,18 @@ function getViewDetailsAction(type, action) {
|
|||
}
|
||||
}
|
||||
|
||||
exports.getViewDetailsAction = getViewDetailsAction;
|
||||
|
||||
exports.activityView = (actions, filter, userId, q = '', extras = {}) => {
|
||||
const spreadMap = (extras && extras.spreadMap) || new Map();
|
||||
const title = filter === 'mine' ? i18n.yourActivity : i18n.globalActivity;
|
||||
const desc = i18n.activityDesc;
|
||||
|
||||
const activityTypes = [
|
||||
{ type: 'recent', label: i18n.typeRecent },
|
||||
{ type: 'all', label: i18n.allButton },
|
||||
{ type: 'mine', label: i18n.mineButton },
|
||||
{ type: 'recent', label: i18n.typeRecent },
|
||||
{ type: 'top', label: i18n.typeTop },
|
||||
{ type: 'inhabitants', label: i18n.typeInhabitants },
|
||||
{ type: 'tribe', label: i18n.typeTribe },
|
||||
{ type: 'larp', label: i18n.typeLarp },
|
||||
|
|
@ -1834,10 +1952,10 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => {
|
|||
{ type: 'market', label: i18n.typeMarket },
|
||||
{ type: 'project', label: i18n.typeProject },
|
||||
{ type: 'industry', label: i18n.typeIndustry },
|
||||
{ type: 'housing', label: i18n.typeHousing },
|
||||
{ type: 'job', label: i18n.typeJob },
|
||||
{ type: 'shop', label: i18n.typeShop },
|
||||
{ type: 'transfer', label: i18n.typeTransfer },
|
||||
{ type: 'curriculum',label: i18n.typeCurriculum },
|
||||
{ type: 'audio', label: i18n.typeAudio },
|
||||
{ type: 'bookmark', label: i18n.typeBookmark },
|
||||
{ type: 'document', label: i18n.typeDocument },
|
||||
|
|
@ -1846,12 +1964,13 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => {
|
|||
{ type: 'video', label: i18n.typeVideo }
|
||||
];
|
||||
|
||||
const FILTER_META = new Set(['recent', 'all', 'mine']);
|
||||
const FILTER_META = new Set(['all', 'mine', 'recent']);
|
||||
const GROUP_SUBTYPES = {
|
||||
parliament: ['parliamentCandidature', 'parliamentTerm', 'parliamentProposal', 'parliamentRevocation', 'parliamentLaw'],
|
||||
courts: ['courtsCase', 'courtsNomination', 'courtsNominationVote'],
|
||||
banking: ['bankWallet', 'bankClaim', 'ubiClaim'],
|
||||
task: ['task', 'taskAssignment'],
|
||||
votes: ['votes', 'poll'],
|
||||
shop: ['shop', 'shopProduct'],
|
||||
larp: ['larpHousePost'],
|
||||
inhabitants:['about'],
|
||||
|
|
@ -1896,7 +2015,12 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => {
|
|||
} else if (filter === 'industry') {
|
||||
filteredActions = actions.filter(action => ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'].includes(action.type) && action.type !== 'tombstone');
|
||||
} else {
|
||||
filteredActions = actions.filter(action => (action.type === filter || filter === 'all' || (filter === 'shop' && action.type === 'shopProduct')) && action.type !== 'tombstone');
|
||||
filteredActions = actions.filter(action =>
|
||||
(action.type === filter
|
||||
|| filter === 'all'
|
||||
|| (filter === 'shop' && action.type === 'shopProduct')
|
||||
|| (filter === 'votes' && action.type === 'poll'))
|
||||
&& action.type !== 'tombstone');
|
||||
}
|
||||
|
||||
const larpEarliest = new Map();
|
||||
|
|
@ -1993,36 +2117,49 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => {
|
|||
h2(i18n.activityList),
|
||||
p(desc)
|
||||
),
|
||||
input({ type: 'checkbox', id: 'activity-filter-toggle', class: 'activity-filter-toggle' }),
|
||||
label({ for: 'activity-filter-toggle', class: 'activity-filter-btn' },
|
||||
span(i18n.filter || 'Filter'), span({ class: 'activity-filter-caret' }, '▾')),
|
||||
div({ class: 'activity-filter-panel' },
|
||||
div({ class: 'activity-filter-grid' },
|
||||
...[
|
||||
activityTypes.slice(0, 3),
|
||||
activityTypes.slice(3, 8),
|
||||
activityTypes.slice(8, 13),
|
||||
activityTypes.slice(13, 19),
|
||||
activityTypes.slice(19, 26),
|
||||
activityTypes.slice(26)
|
||||
].map(col =>
|
||||
div({ class: 'activity-filter-col' },
|
||||
col.map(({ type, label }) =>
|
||||
form({ method: 'GET', action: '/activity' },
|
||||
input({ type: 'hidden', name: 'filter', value: type }),
|
||||
button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, label)
|
||||
)
|
||||
div({ class: 'activity-filter-grid' },
|
||||
...(() => {
|
||||
const COLUMNS = [
|
||||
['all', 'mine', 'recent', 'top'],
|
||||
['inhabitants', 'tribe', 'larp', 'parliament', 'courts'],
|
||||
['votes', 'event', 'calendar', 'task', 'report'],
|
||||
['banking', 'market', 'housing', 'project', 'industry', 'job', 'shop', 'transfer'],
|
||||
['post', 'feed', 'chat', 'pad', 'forum', 'map'],
|
||||
['audio', 'bookmark', 'document', 'image', 'torrent', 'video']
|
||||
];
|
||||
const byType = new Map(activityTypes.map(t => [t.type, t]));
|
||||
const placed = new Set(COLUMNS.flat());
|
||||
const leftovers = activityTypes.filter(t => !placed.has(t.type));
|
||||
return COLUMNS
|
||||
.map(col => col.map(type => byType.get(type)).filter(Boolean))
|
||||
.concat(leftovers.length ? [leftovers] : [])
|
||||
.filter(col => col.length);
|
||||
})().map(col =>
|
||||
div({ class: 'activity-filter-col' },
|
||||
col.map(({ type, label }) =>
|
||||
form({ method: 'GET', action: '/activity' },
|
||||
input({ type: 'hidden', name: 'filter', value: type }),
|
||||
button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, label)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
sub
|
||||
? div({ class: 'activity-sub-filter' },
|
||||
sub.filters.map(f => a({ href: `${sub.url}?filter=${encodeURIComponent(f)}`, class: 'filter-btn' }, String(f).toUpperCase()))
|
||||
)
|
||||
: null
|
||||
)
|
||||
),
|
||||
section({ class: 'feed-container' }, renderActionCards(filteredActions, userId, actions, spreadMap))
|
||||
sub
|
||||
? div({ class: 'activity-sub-filter' },
|
||||
sub.filters.map(f => a({ href: `${sub.url}?filter=${encodeURIComponent(f)}`, class: 'filter-btn' }, String(f).toUpperCase()))
|
||||
)
|
||||
: null,
|
||||
div({ class: 'filters' },
|
||||
form({ method: 'GET', action: '/activity', class: 'filter-box' },
|
||||
input({ type: 'hidden', name: 'filter', value: filter }),
|
||||
input({ type: 'text', name: 'q', value: qs, placeholder: i18n.activitySearchPlaceholder, class: 'filter-box__input' }),
|
||||
div({ class: 'filter-box__controls' },
|
||||
button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
section({ class: 'feed-container' }, renderActionCards(filteredActions, userId, actions, spreadMap, extras))
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, th, td, progress, video, audio } = require("../server/node_modules/hyperaxe")
|
||||
const { template, i18n, userLink, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton } = require("./main_views")
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||
const { template, i18n, userLink, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderSpreadEditWarning, renderOpinionsVoting, renderEngagement , renderContentActions } = require("./main_views")
|
||||
const opinionCategories = require("../backend/opinion_categories")
|
||||
const moment = require("../server/node_modules/moment")
|
||||
const { config } = require("../server/SSB_server.js")
|
||||
const { renderUrl } = require("../backend/renderUrl")
|
||||
|
|
@ -95,6 +97,17 @@ const buildReturnTo = (filter, q, minPrice, maxPrice, sort) => {
|
|||
return `/market${params.length ? `?${params.join("&")}` : ""}`
|
||||
}
|
||||
|
||||
|
||||
const sumCats = (opinions = {}, cats = []) => (cats || []).reduce((acc, c) => acc + (Number((opinions || {})[c]) || 0), 0)
|
||||
|
||||
const renderStarRating = (opinions, voterCount) => {
|
||||
const pos = sumCats(opinions, opinionCategories.positive)
|
||||
const neg = sumCats(opinions, opinionCategories.constructive) + sumCats(opinions, opinionCategories.moderation)
|
||||
const total = pos + neg
|
||||
const full = total > 0 ? Math.round((pos / total) * 5) : 0
|
||||
return span({ class: "shop-product-stars" }, "★".repeat(full) + "☆".repeat(5 - full) + ` (${voterCount})`)
|
||||
}
|
||||
|
||||
const renderCardField = (labelText, value = "") =>
|
||||
div({ class: "card-field" }, span({ class: "card-label" }, labelText), span({ class: "card-value" }, ...renderUrl(String(value))))
|
||||
|
||||
|
|
@ -121,59 +134,12 @@ const renderStockBar = (stockValue, maxValue) => {
|
|||
}
|
||||
|
||||
const renderMarketCommentsSection = (itemId, returnTo, comments = []) => {
|
||||
const commentsCount = Array.isArray(comments) ? comments.length : 0
|
||||
|
||||
return div(
|
||||
{ class: "vote-comments-section market-comments" },
|
||||
div({ class: "comments-count" }, span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), span({ class: "card-value" }, String(commentsCount))),
|
||||
div(
|
||||
{ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form(
|
||||
{ method: "POST", action: `/market/${encodeURIComponent(itemId)}/comments`, class: "comment-form", enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
textarea({ id: "comment-text", name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton)
|
||||
)
|
||||
),
|
||||
(() => {
|
||||
const visibleComments = (comments || []).filter(c => {
|
||||
const t = c && c.value && c.value.content && c.value.content.text
|
||||
return t && String(t).trim()
|
||||
})
|
||||
return visibleComments.length
|
||||
? div(
|
||||
{ class: "comments-list" },
|
||||
visibleComments.map((c) => {
|
||||
const author = c.value && c.value.author ? c.value.author : ""
|
||||
const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""
|
||||
const relDate = ts ? moment(ts).fromNow() : ""
|
||||
const userName = author && author.includes("@") ? author.split("@")[1] : author
|
||||
const rootId = c.value && c.value.content ? c.value.content.fork || c.value.content.root : null
|
||||
const text = c.value && c.value.content && c.value.content.text ? c.value.content.text : ""
|
||||
|
||||
return div(
|
||||
{ class: "votations-comment-card" },
|
||||
span(
|
||||
{ class: "created-at" },
|
||||
span(i18n.createdBy),
|
||||
author ? userLink(author) : span("(unknown)"),
|
||||
absDate ? span(" | ") : "",
|
||||
absDate ? span({ class: "votations-comment-date" }, absDate) : "",
|
||||
relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "",
|
||||
relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : ""
|
||||
),
|
||||
p({ class: "votations-comment-text" }, ...renderUrl(text))
|
||||
)
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)
|
||||
})()
|
||||
)
|
||||
}
|
||||
return renderSharedCommentsSection({
|
||||
action: `/market/${encodeURIComponent(itemId)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: returnTo
|
||||
});
|
||||
};
|
||||
|
||||
const isMyBidItem = (item) => {
|
||||
const polls = Array.isArray(item.auctions_poll) ? item.auctions_poll : []
|
||||
|
|
@ -231,9 +197,9 @@ const renderMarketOwnerActions = (item, returnTo) => {
|
|||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
select(
|
||||
{ name: "status", class: "project-control-select" },
|
||||
option({ value: "FOR SALE", selected: cur === "FOR SALE" }, i18n.marketFilterForSale),
|
||||
option({ value: "SOLD", selected: cur === "SOLD" }, i18n.marketFilterSold),
|
||||
option({ value: "DISCARDED", selected: cur === "DISCARDED" }, i18n.marketFilterDiscarded)
|
||||
option({ value: "FOR SALE", ...(cur === "FOR SALE" ? { selected: true } : {})}, i18n.marketFilterForSale),
|
||||
option({ value: "SOLD", ...(cur === "SOLD" ? { selected: true } : {})}, i18n.marketFilterSold),
|
||||
option({ value: "DISCARDED", ...(cur === "DISCARDED" ? { selected: true } : {})}, i18n.marketFilterDiscarded)
|
||||
),
|
||||
button({ class: "status-btn project-control-btn", type: "submit" }, i18n.marketActionsChangeStatus)
|
||||
)
|
||||
|
|
@ -260,21 +226,7 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => {
|
|||
const maxPrice = params.maxPrice
|
||||
const sort = params.sort || "recent"
|
||||
|
||||
let title = i18n.marketAllSectionTitle
|
||||
switch (filter) {
|
||||
case "mine":
|
||||
title = i18n.marketMineSectionTitle
|
||||
break
|
||||
case "create":
|
||||
title = i18n.marketCreateSectionTitle
|
||||
break
|
||||
case "edit":
|
||||
title = i18n.marketUpdateSectionTitle
|
||||
break
|
||||
case "mybids":
|
||||
title = i18n.marketFilterMyBids
|
||||
break
|
||||
}
|
||||
const title = i18n.marketTitle
|
||||
|
||||
let filtered = []
|
||||
switch (filter) {
|
||||
|
|
@ -337,6 +289,7 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => {
|
|||
]
|
||||
|
||||
const isFormMode = filter === "create" || filter === "edit"
|
||||
const spreadWarning = filter === "edit" ? await renderSpreadEditWarning(itemEdit && (itemEdit.id || itemEdit.key)) : null
|
||||
|
||||
return template(
|
||||
title,
|
||||
|
|
@ -347,18 +300,18 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => {
|
|||
form(
|
||||
{ method: "GET", action: "/market", class: "ui-toolbar ui-toolbar--filters" },
|
||||
...hiddenCtx,
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterItems),
|
||||
button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAuctions),
|
||||
button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMyBids),
|
||||
button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterNew),
|
||||
button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterUsed),
|
||||
button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterBroken),
|
||||
button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterForSale),
|
||||
button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterSold),
|
||||
button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterDiscarded),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterRecent),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterItems).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAuctions).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMyBids).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterNew).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterUsed).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterBroken).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterForSale).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterSold).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterDiscarded).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterRecent).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.marketCreateButton)
|
||||
)
|
||||
),
|
||||
|
|
@ -394,9 +347,9 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => {
|
|||
),
|
||||
select(
|
||||
{ name: "sort", class: "filter-box__select" },
|
||||
option({ value: "recent", selected: sort === "recent" }, i18n.marketSortRecent),
|
||||
option({ value: "price", selected: sort === "price" }, i18n.marketSortPrice),
|
||||
option({ value: "deadline", selected: sort === "deadline" }, i18n.marketSortDeadline)
|
||||
option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.marketSortRecent),
|
||||
option({ value: "price", ...(sort === "price" ? { selected: true } : {})}, i18n.marketSortPrice),
|
||||
option({ value: "deadline", ...(sort === "deadline" ? { selected: true } : {})}, i18n.marketSortDeadline)
|
||||
),
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.marketSearchButton)
|
||||
)
|
||||
|
|
@ -408,6 +361,7 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => {
|
|||
isFormMode
|
||||
? div(
|
||||
{ class: "market-form" },
|
||||
spreadWarning,
|
||||
form(
|
||||
{ action: filter === "edit" ? `/market/update/${encodeURIComponent(itemEdit.id)}` : "/market/create", method: "POST", enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "returnTo", value: "/market?filter=mine" }),
|
||||
|
|
@ -416,14 +370,14 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => {
|
|||
br(),
|
||||
select(
|
||||
{ name: "item_type", id: "item_type", required: true },
|
||||
option({ value: "auction", selected: itemEdit && itemEdit.item_type === "auction" }, "Auction"),
|
||||
option({ value: "exchange", selected: itemEdit && itemEdit.item_type === "exchange" }, "Exchange")
|
||||
option({ value: "auction", ...(itemEdit && itemEdit.item_type === "auction" ? { selected: true } : {})}, "Auction"),
|
||||
option({ value: "exchange", ...(itemEdit && itemEdit.item_type === "exchange" ? { selected: true } : {})}, "Exchange")
|
||||
),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.marketItemTitle),
|
||||
br(),
|
||||
input({ type: "text", name: "title", id: "title", value: (itemEdit && itemEdit.title) || params.title || "", required: true }),
|
||||
input({ type: "text", name: "title", maxlength: "100", id: "title", value: (itemEdit && itemEdit.title) || params.title || "", required: true }),
|
||||
br(),
|
||||
br(),
|
||||
label(i18n.marketItemDescription),
|
||||
|
|
@ -440,9 +394,9 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => {
|
|||
br(),
|
||||
select(
|
||||
{ name: "item_status", id: "item_status" },
|
||||
option({ value: "BROKEN", selected: itemEdit && itemEdit.item_status === "BROKEN" }, "BROKEN"),
|
||||
option({ value: "USED", selected: itemEdit && itemEdit.item_status === "USED" }, "USED"),
|
||||
option({ value: "NEW", selected: itemEdit && itemEdit.item_status === "NEW" }, "NEW")
|
||||
option({ value: "BROKEN", ...(itemEdit && itemEdit.item_status === "BROKEN" ? { selected: true } : {})}, "BROKEN"),
|
||||
option({ value: "USED", ...(itemEdit && itemEdit.item_status === "USED" ? { selected: true } : {})}, "USED"),
|
||||
option({ value: "NEW", ...(itemEdit && itemEdit.item_status === "NEW" ? { selected: true } : {})}, "NEW")
|
||||
),
|
||||
br(),
|
||||
br(),
|
||||
|
|
@ -460,8 +414,8 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => {
|
|||
br(),
|
||||
select(
|
||||
{ name: "visibility" },
|
||||
option({ value: "PUBLIC", selected: (itemEdit?.visibility || "PUBLIC") === "PUBLIC" }, i18n.visibilityPublic || "Public"),
|
||||
option({ value: "HIDDEN", selected: itemEdit?.visibility === "HIDDEN" }, i18n.visibilityHidden || "Hidden")
|
||||
option({ value: "PUBLIC", ...((itemEdit?.visibility || "PUBLIC") === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"),
|
||||
option({ value: "HIDDEN", ...(itemEdit?.visibility === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden")
|
||||
),
|
||||
br(),
|
||||
br(),
|
||||
|
|
@ -541,10 +495,15 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => {
|
|||
renderMediaBlob(item.image, '/assets/images/default-market.png', { class: 'tribe-card-hero-image' })
|
||||
)
|
||||
),
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(item.id, `/market/${encodeURIComponent(item.id)}`, { spread: params.spreads || null, author: item.seller, favKind: 'market', isFavorite: item.isFavorite, reportTitle: item.title })
|
||||
),
|
||||
div({ class: "tribe-card-body" },
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, a({ href: `/market/${encodeURIComponent(item.id)}` }, item.title))
|
||||
),
|
||||
renderStarRating(item.opinions, Array.isArray(item.opinions_inhabitants) ? item.opinions_inhabitants.length : 0),
|
||||
div({ class: "card-chips-row" },
|
||||
renderStateChip("encrypted", "", String(item.item_type || "").toUpperCase()),
|
||||
item.item_status ? renderStateChip("whole", "", String(item.item_status).toUpperCase()) : null,
|
||||
|
|
@ -553,18 +512,7 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => {
|
|||
item.industry ? a({ href: `/industry/${encodeURIComponent(item.industry)}` }, renderStateChip("whole", "🏭", String(i18n.industryTitle || "Industry").toUpperCase())) : null,
|
||||
renderLifespanChip(item.lifetime, i18n)
|
||||
),
|
||||
div({ class: "market-card-price card-date-highlight" }, `${item.price} ECO`),
|
||||
div({ class: "card-visit-btn-centered" },
|
||||
form({ method: 'GET', action: `/market/${encodeURIComponent(item.id)}` },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
input({ type: "hidden", name: "filter", value: filter || "all" }),
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "minPrice", value: String(minPrice ?? "") }),
|
||||
input({ type: "hidden", name: "maxPrice", value: String(maxPrice ?? "") }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: 'submit', class: 'filter-btn' }, i18n.viewItem || i18n.marketVisitItem || 'View Item')
|
||||
)
|
||||
)
|
||||
div({ class: "market-card-price card-date-highlight" }, `${item.price} ECO`)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
|
@ -589,6 +537,7 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => {
|
|||
|
||||
return template(
|
||||
item.title,
|
||||
section(div({ class: "tags-header" }, h2(i18n.marketTitle), p(i18n.marketDescription))),
|
||||
section(
|
||||
div(
|
||||
{ class: "filters" },
|
||||
|
|
@ -598,18 +547,18 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => {
|
|||
input({ type: "hidden", name: "minPrice", value: minPrice ?? "" }),
|
||||
input({ type: "hidden", name: "maxPrice", value: maxPrice ?? "" }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterItems),
|
||||
button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAuctions),
|
||||
button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMyBids),
|
||||
button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterNew),
|
||||
button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterUsed),
|
||||
button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterBroken),
|
||||
button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterForSale),
|
||||
button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterSold),
|
||||
button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterDiscarded),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterRecent),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterItems).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAuctions).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMyBids).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterNew).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterUsed).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterBroken).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterForSale).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterSold).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterDiscarded).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterRecent).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.marketCreateButton)
|
||||
)
|
||||
),
|
||||
|
|
@ -637,26 +586,13 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => {
|
|||
))
|
||||
pushRow(i18n.marketItemSeller, userLink(item.seller))
|
||||
if (item.shopId && item.shopTitle) pushRow(i18n.marketShopLabel || "Shop", a({ href: `/shops/${encodeURIComponent(item.shopId)}`, class: "user-link" }, item.shopTitle))
|
||||
if (item.deadline) pushRow(i18n.marketItemAvailable, moment(item.deadline).format("YYYY/MM/DD HH:mm:ss"))
|
||||
|
||||
const itemSide = div({ class: "tribe-side" },
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, item.title)
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(item.id, params.spreads)),
|
||||
renderMediaBlob(item.image, "/assets/images/default-market.png"),
|
||||
div({ class: "card-date-highlight" }, `${item.price} ECO`),
|
||||
renderStockBar(item.stock, maxStock),
|
||||
table({ class: "tribe-info-table jobs-info-table" }, ...infoRows),
|
||||
tagsNode
|
||||
)
|
||||
if (item.deadline) pushRow(i18n.marketItemAvailable, moment(item.deadline).format("YYYY/MM/DD HH:mm"))
|
||||
|
||||
const visibilityRow = String(item.seller) === String(userId)
|
||||
? (() => {
|
||||
const vis = isHidden ? 'HIDDEN' : 'PUBLIC'
|
||||
const next = vis === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC'
|
||||
return div({ class: "tribe-side-actions" },
|
||||
return div({ class: "tribe-side-actions housing-status-row" },
|
||||
span({ class: "card-label" }, `${i18n.visibilityLabel || 'Visibility'}: `),
|
||||
renderVisibilityChip(vis, i18n),
|
||||
form({ method: "POST", action: `/market/visibility/${encodeURIComponent(item.id)}`, class: "inline-form" },
|
||||
|
|
@ -670,12 +606,6 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => {
|
|||
: null
|
||||
|
||||
const marketActions = []
|
||||
if (item.seller && String(item.seller) !== String(userId)) {
|
||||
marketActions.push(form({ method: "GET", action: "/pm" },
|
||||
input({ type: "hidden", name: "recipients", value: item.seller }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)
|
||||
))
|
||||
}
|
||||
if (String(item.seller) === String(userId)) {
|
||||
marketActions.push(form({ method: "GET", action: `/market/edit/${encodeURIComponent(item.id)}` },
|
||||
button({ type: "submit", class: "update-btn" }, i18n.marketUpdateButton || "Update")
|
||||
|
|
@ -684,9 +614,26 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => {
|
|||
button({ type: "submit", class: "delete-btn" }, i18n.marketDeleteButton || "Delete")
|
||||
))
|
||||
}
|
||||
const itemMain = div({ class: "tribe-main" },
|
||||
marketActions.length ? div({ class: "tribe-side-actions" }, ...marketActions) : null,
|
||||
|
||||
const itemSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(item.id, null, { spread: params.spreads || null, author: item.seller, favKind: 'market', isFavorite: item.isFavorite, reportTitle: item.title })
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, item.title)
|
||||
),
|
||||
renderStarRating(item.opinions, Array.isArray(item.opinions_inhabitants) ? item.opinions_inhabitants.length : 0),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
renderMediaBlob(item.image, "/assets/images/default-market.png"),
|
||||
div({ class: "card-date-highlight" }, `${item.price} ECO`),
|
||||
renderStockBar(item.stock, maxStock),
|
||||
table({ class: "tribe-info-table jobs-info-table" }, ...infoRows),
|
||||
tagsNode,
|
||||
visibilityRow,
|
||||
marketActions.length ? div({ class: "tribe-side-actions" }, ...marketActions) : null
|
||||
)
|
||||
|
||||
const itemMain = div({ class: "tribe-main" },
|
||||
item.description
|
||||
? div({ class: "job-section" },
|
||||
h2({ class: "job-section-title" }, i18n.marketItemDescription),
|
||||
|
|
@ -704,7 +651,7 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => {
|
|||
{ class: "auction-bid-table" },
|
||||
tr(th(i18n.marketAuctionBidTime), th(i18n.marketAuctionUser), th(i18n.marketAuctionBidAmount)),
|
||||
parsedBids.map((bid) =>
|
||||
tr(td(moment(bid.time).format("YYYY-MM-DD HH:mm:ss")), td(a({ href: `/author/${encodeURIComponent(bid.bidder)}` }, bid.bidder)), td(`${parseFloat(bid.amount).toFixed(6)} ECO`))
|
||||
tr(td(moment(bid.time).format("YYYY/MM/DD HH:mm")), td(a({ href: `/author/${encodeURIComponent(bid.bidder)}` }, bid.bidder)), td(`${parseFloat(bid.amount).toFixed(6)} ECO`))
|
||||
)
|
||||
)
|
||||
: p({ class: "tribe-side-description" }, i18n.marketNoBids || "No bids yet"),
|
||||
|
|
@ -729,7 +676,12 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => {
|
|||
)
|
||||
)
|
||||
: null,
|
||||
renderMarketCommentsSection(item.id, returnTo, comments)
|
||||
renderEngagement(item.id,
|
||||
String(item.seller) !== String(userId) && item.purchasedByViewer
|
||||
? renderOpinionsVoting("/market/opinions", item.id, item.opinions, returnTo, item.opinions_inhabitants)
|
||||
: null,
|
||||
renderMarketCommentsSection(item.id, returnTo, comments)
|
||||
)
|
||||
)
|
||||
|
||||
return div({ class: "tribe-details" }, itemSide, itemMain)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
const { form, button, div, h2, p, section, table, thead, tr, th, td, a, tbody } = require("../server/node_modules/hyperaxe");
|
||||
const { form, button, div, h2, p, section, table, thead, tr, th, td, a, tbody, input } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n } = require('./main_views');
|
||||
|
||||
const getFilteredTags = (filter, tags) => {
|
||||
let filteredTags = Array.isArray(tags) ? tags : [];
|
||||
if (filter === 'top') {
|
||||
filteredTags = filteredTags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
||||
} else {
|
||||
filteredTags = filteredTags.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
return filteredTags;
|
||||
const filteredTags = Array.isArray(tags) ? [...tags] : [];
|
||||
if (filter === 'top') return filteredTags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
||||
if (filter === 'mine') return filteredTags.sort((a, b) => (b.mine || 0) - (a.mine || 0) || a.name.localeCompare(b.name));
|
||||
if (filter === 'recent') return filteredTags.sort((a, b) => (b.lastTs || 0) - (a.lastTs || 0));
|
||||
return filteredTags.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
const renderTagsTable = (filteredTags) => {
|
||||
|
|
@ -56,13 +54,11 @@ const renderTagsCloud = (mergedTags) => {
|
|||
);
|
||||
};
|
||||
|
||||
exports.tagsView = async (tags, filter) => {
|
||||
exports.tagsView = async (tags, filter, search = '') => {
|
||||
const filteredTags = getFilteredTags(filter, tags);
|
||||
const query = String(search || '').trim();
|
||||
|
||||
const title =
|
||||
filter === 'top' ? i18n.tagsTopSectionTitle :
|
||||
filter === 'cloud' ? i18n.tagsCloudSectionTitle :
|
||||
i18n.tagsAllSectionTitle;
|
||||
const title = i18n.tagsTitle;
|
||||
|
||||
return template(
|
||||
title,
|
||||
|
|
@ -72,15 +68,25 @@ exports.tagsView = async (tags, filter) => {
|
|||
p(i18n.tagsDescription)
|
||||
),
|
||||
div({ class: 'filters' },
|
||||
form({ method: 'GET', action: '/tags' },
|
||||
button({ type: 'submit', name: 'filter', value: 'all', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, i18n.tagsFilterAll),
|
||||
button({ type: 'submit', name: 'filter', value: 'top', class: filter === 'top' ? 'filter-btn active' : 'filter-btn' }, i18n.tagsFilterTop),
|
||||
button({ type: 'submit', name: 'filter', value: 'cloud', class: filter === 'cloud' ? 'filter-btn active' : 'filter-btn' }, i18n.tagsFilterCloud)
|
||||
form({ method: 'GET', action: '/tags', class: 'ui-toolbar ui-toolbar--filters' },
|
||||
input({ type: 'hidden', name: 'search', value: query }),
|
||||
button({ type: 'submit', name: 'filter', value: 'all', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterAll).toUpperCase()),
|
||||
button({ type: 'submit', name: 'filter', value: 'mine', class: filter === 'mine' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterMine).toUpperCase()),
|
||||
button({ type: 'submit', name: 'filter', value: 'recent', class: filter === 'recent' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterRecent).toUpperCase()),
|
||||
button({ type: 'submit', name: 'filter', value: 'top', class: filter === 'top' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterTop).toUpperCase()),
|
||||
button({ type: 'submit', name: 'filter', value: 'cloud', class: filter === 'cloud' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterCloud).toUpperCase())
|
||||
)
|
||||
),
|
||||
div({ class: 'tags-search' },
|
||||
form({ method: 'GET', action: '/tags', class: 'filter-box' },
|
||||
input({ type: 'hidden', name: 'filter', value: filter || 'all' }),
|
||||
input({ type: 'text', name: 'search', value: query, placeholder: i18n.tagsSearchPlaceholder, class: 'filter-box__input' }),
|
||||
div({ class: 'filter-box__controls' }, button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton))
|
||||
)
|
||||
),
|
||||
div({ class: 'tags-list' },
|
||||
filteredTags.length === 0
|
||||
? p(i18n.tagsNoItems)
|
||||
? p(query ? i18n.noResultsFound : i18n.tagsNoItems)
|
||||
: filter !== 'cloud'
|
||||
? renderTagsTable(filteredTags)
|
||||
: renderTagsCloud(filteredTags)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, label, span } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views");
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||
const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions } = require("./main_views");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderUrl } = require("../backend/renderUrl");
|
||||
|
|
@ -13,15 +14,15 @@ const voteLabel = (opt) =>
|
|||
i18n["vote" + opt.split("_").map(w => w.charAt(0) + w.slice(1).toLowerCase()).join("")] || opt;
|
||||
|
||||
const computeVoteOutcome = (baseCounts, voteOptions, totalVotesNum) => {
|
||||
const noResult = { text: i18n.voteNoQuorum || "NO QUORUM", color: "#ffcc00" };
|
||||
const noResult = { text: i18n.voteNoQuorum || "NO QUORUM", variant: "pending" };
|
||||
if (totalVotesNum < VOTE_QUORUM) return noResult;
|
||||
const opts = voteOptions.filter((o) => o !== "FOLLOW_MAJORITY");
|
||||
const maxCount = opts.reduce((m, o) => Math.max(m, baseCounts[o] || 0), 0);
|
||||
const topOpts = opts.filter((o) => (baseCounts[o] || 0) === maxCount);
|
||||
if (maxCount === 0 || topOpts.length > 1) return noResult;
|
||||
const winner = topOpts[0];
|
||||
const color = winner === "YES" ? "#4caf50" : winner === "NO" ? "#e53935" : "#ffcc00";
|
||||
return { text: voteLabel(winner), color };
|
||||
const variant = winner === "YES" ? "yes" : winner === "NO" ? "no" : "pending";
|
||||
return { text: voteLabel(winner), variant };
|
||||
};
|
||||
|
||||
const normalizeStatus = (v) => {
|
||||
|
|
@ -110,7 +111,7 @@ const renderVoteListItem = (v, voteOptionsDefault, activeFilter, spreadInfo) =>
|
|||
return div({ class: "trending-card vote-card" + (isOwn ? " own-content" : "") },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(v.id, `/votes/${encodeURIComponent(v.id)}`)
|
||||
renderContentActions(v.id, `/votes/${encodeURIComponent(v.id)}`, { spread: spreadInfo || null, author: v.createdBy, favKind: 'votes', isFavorite: v.isFavorite, reportTitle: v.question })
|
||||
),
|
||||
div({ class: "card-section vote-card-body" },
|
||||
div({ class: "shop-title-row" },
|
||||
|
|
@ -123,8 +124,7 @@ const renderVoteListItem = (v, voteOptionsDefault, activeFilter, spreadInfo) =>
|
|||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${totalVotesNum}`)
|
||||
),
|
||||
div({ class: "job-meta-line", style: `color:${outcome.color};font-weight:bold;` }, `${i18n.voteResults || "Results"}: ${outcome.text}`),
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(v.id, spreadInfo))
|
||||
div({ class: `job-meta-line vote-outcome vote-outcome-${outcome.variant}` }, `${i18n.voteResults || "Results"}: ${outcome.text}`)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
@ -147,12 +147,6 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti
|
|||
].filter(Boolean);
|
||||
|
||||
const sideActions = [];
|
||||
if (v.createdBy && v.createdBy !== userId) {
|
||||
sideActions.push(form({ method: "GET", action: "/pm" },
|
||||
input({ type: "hidden", name: "recipients", value: v.createdBy }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)
|
||||
));
|
||||
}
|
||||
for (const a of renderVoteOwnerActions(v, returnTo, mode || "")) sideActions.push(a);
|
||||
|
||||
const cleanTags = (Array.isArray(v.tags) ? v.tags : []).filter((t) => t && !String(t).includes(":"));
|
||||
|
|
@ -172,17 +166,20 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti
|
|||
pushRow(i18n.voteStatus, statusLabel(v.status));
|
||||
|
||||
const voteSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(v.id, null, { spread: params.spreads || null, author: v.createdBy, favKind: 'votes', isFavorite: v.isFavorite, reportTitle: v.question })
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, v.question)
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(v.id, params.spreads)),
|
||||
table({ class: "tribe-info-table jobs-info-table" }, ...infoRows),
|
||||
tagsNode,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${totalVotesNum}`)
|
||||
),
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null,
|
||||
renderDocumentActions('votes', v.id)
|
||||
);
|
||||
|
||||
const voteButtonsNode = renderVoteButtons(v, voteOptions, firstRow, secondRow, returnTo);
|
||||
|
|
@ -197,7 +194,7 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti
|
|||
div({ class: "job-section" },
|
||||
h2({ class: "job-section-title" },
|
||||
`${i18n.voteResults || "Results"}: `,
|
||||
span({ style: `color:${outcome.color} !important;font-weight:bold;` }, outcome.text)
|
||||
span({ class: `vote-outcome vote-outcome-${outcome.variant}` }, outcome.text)
|
||||
),
|
||||
div({ class: "vote-table" },
|
||||
table(
|
||||
|
|
@ -210,71 +207,20 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti
|
|||
span({ class: "date-link" }, `${moment(v.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(v.createdBy)
|
||||
),
|
||||
renderOpinionsBar(v, returnTo)
|
||||
renderEngagement(v.id, renderOpinionsBar(v, returnTo),
|
||||
mode === "detail" ? renderCommentsSection(v.id, params.comments || [], activeFilter) : null)
|
||||
);
|
||||
|
||||
return div({ class: "tribe-details" }, voteSide, voteMain);
|
||||
};
|
||||
|
||||
const renderCommentsSection = (voteId, comments, activeFilter) => {
|
||||
const commentsCount = Array.isArray(comments) ? comments.length : 0;
|
||||
const returnTo = `/votes/${encodeURIComponent(voteId)}?filter=${encodeURIComponent(activeFilter || "all")}`;
|
||||
|
||||
return div(
|
||||
{ class: "vote-comments-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, i18n.voteCommentsLabel + ": "),
|
||||
span({ class: "card-value" }, String(commentsCount))
|
||||
),
|
||||
div(
|
||||
{ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form(
|
||||
{ method: "POST", action: `/votes/${encodeURIComponent(voteId)}/comments`, class: "comment-form", enctype: "multipart/form-data" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
textarea({
|
||||
id: "comment-text",
|
||||
name: "text",
|
||||
rows: 4,
|
||||
class: "comment-textarea",
|
||||
placeholder: i18n.voteNewCommentPlaceholder
|
||||
}),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton)
|
||||
)
|
||||
),
|
||||
comments && comments.length
|
||||
? div(
|
||||
{ class: "comments-list" },
|
||||
comments.map((c) => {
|
||||
const author = c.value && c.value.author ? c.value.author : "";
|
||||
const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp;
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "";
|
||||
const relDate = ts ? moment(ts).fromNow() : "";
|
||||
|
||||
const content = c.value && c.value.content ? c.value.content : {};
|
||||
const root = content.fork || content.root || "";
|
||||
const text = content.text || "";
|
||||
|
||||
return div(
|
||||
{ class: "votations-comment-card" },
|
||||
span(
|
||||
{ class: "created-at" },
|
||||
span(i18n.createdBy),
|
||||
author ? userLink(author) : span("(unknown)"),
|
||||
absDate ? span(" | ") : "",
|
||||
absDate ? span({ class: "votations-comment-date" }, absDate) : "",
|
||||
relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "",
|
||||
relDate && root ? a({ href: `/thread/${encodeURIComponent(root)}#${encodeURIComponent(c.key)}` }, relDate) : ""
|
||||
),
|
||||
p({ class: "votations-comment-text" }, ...renderUrl(String(text)))
|
||||
);
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)
|
||||
);
|
||||
return renderSharedCommentsSection({
|
||||
action: `/votes/${encodeURIComponent(voteId)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: returnTo
|
||||
});
|
||||
};
|
||||
|
||||
exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, params = {}) => {
|
||||
|
|
@ -285,14 +231,7 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam,
|
|||
? activeFilterParam
|
||||
: (standardFilters.includes(mode) ? mode : "all");
|
||||
|
||||
const title =
|
||||
mode === "mine" ? i18n.voteMineSectionTitle :
|
||||
mode === "create" ? i18n.voteCreateSectionTitle :
|
||||
mode === "edit" ? i18n.voteUpdateSectionTitle :
|
||||
mode === "open" ? i18n.voteOpenTitle :
|
||||
mode === "closed" ? i18n.voteClosedTitle :
|
||||
mode === "detail" ? (i18n.voteDetailSectionTitle || i18n.voteAllSectionTitle) :
|
||||
i18n.voteAllSectionTitle;
|
||||
const title = i18n.votationsTitle;
|
||||
|
||||
const voteToEdit = list.find((v) => v.id === voteId) || {};
|
||||
const editTags = Array.isArray(voteToEdit.tags) ? voteToEdit.tags.filter(Boolean) : [];
|
||||
|
|
@ -317,8 +256,19 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam,
|
|||
|
||||
const listReturnTo = standardFilters.includes(activeFilter) ? `/votes?filter=${encodeURIComponent(activeFilter)}` : "/votes";
|
||||
|
||||
const deadlineMin = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm");
|
||||
const deadlineValue = voteToEdit.deadline ? moment(voteToEdit.deadline).format("YYYY-MM-DDTHH:mm") : "";
|
||||
const minVoteDays = Number(params.minVoteDays) > 0 ? Number(params.minVoteDays) : 7;
|
||||
const deadlineMin = moment().add(minVoteDays, "days").format("YYYY-MM-DDTHH:mm");
|
||||
const draft = params.draft || {};
|
||||
const deadlineValue = draft.deadline
|
||||
? String(draft.deadline)
|
||||
: (voteToEdit.deadline ? moment(voteToEdit.deadline).format("YYYY-MM-DDTHH:mm") : "");
|
||||
const questionValue = draft.question !== undefined && draft.question !== "" ? String(draft.question) : (voteToEdit.question || "");
|
||||
const tagsValue = draft.tags ? String(draft.tags) : editTags.join(", ");
|
||||
const formError = params.error === "deadline"
|
||||
? String(i18n.voteErrorDeadlineMin || "The deadline must be at least {days} days from now.").replace("{days}", String(minVoteDays))
|
||||
: params.error
|
||||
? (i18n.voteErrorGeneric || "The vote could not be saved.")
|
||||
: "";
|
||||
|
||||
return template(
|
||||
title,
|
||||
|
|
@ -328,23 +278,41 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam,
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/votes" },
|
||||
button({ type: "submit", name: "filter", value: "all", class: mode === "all" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: mode === "mine" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "open", class: mode === "open" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterOpen),
|
||||
button({ type: "submit", name: "filter", value: "closed", class: mode === "closed" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterClosed),
|
||||
button({ type: "submit", name: "filter", value: "all", class: mode === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: mode === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "open", class: mode === "open" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterOpen).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "closed", class: mode === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterClosed).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: mode === "create" ? "create-button active" : "create-button" }, i18n.voteCreateButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
mode !== "create" && mode !== "edit"
|
||||
? div({ class: "filters" },
|
||||
form({ method: "GET", action: "/votes", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: mode }),
|
||||
input({ type: "text", name: "q", value: (params && params.q) || "", placeholder: i18n.votesSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
)
|
||||
: null
|
||||
),
|
||||
section(
|
||||
(mode === "edit" || mode === "create")
|
||||
? div(
|
||||
{ class: "vote-form" },
|
||||
mode === "edit" ? await renderSpreadEditWarning(voteId) : null,
|
||||
formError
|
||||
? div({ class: "error-box vote-form-error" },
|
||||
p({ class: "error-title" }, i18n.voteErrorTitle || "Check the form"),
|
||||
p(formError)
|
||||
)
|
||||
: null,
|
||||
form(
|
||||
{ action: mode === "edit" ? `/votes/update/${encodeURIComponent(voteId)}` : "/votes/create", method: "POST" },
|
||||
input({ type: "hidden", name: "returnTo", value: listReturnTo }),
|
||||
h2(i18n.voteQuestionLabel),
|
||||
input({ type: "text", name: "question", id: "question", required: true, value: voteToEdit.question || "" }), br(), br(),
|
||||
input({ type: "text", name: "question", id: "question", required: true, value: questionValue }), br(), br(),
|
||||
label(i18n.voteDeadlineLabel), br(),
|
||||
input({
|
||||
type: "datetime-local",
|
||||
|
|
@ -355,16 +323,15 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam,
|
|||
value: deadlineValue
|
||||
}), br(), br(),
|
||||
label(i18n.voteTagsLabel), br(),
|
||||
input({ type: "text", name: "tags", id: "tags", value: editTags.join(", ") }), br(), br(),
|
||||
input({ type: "text", name: "tags", id: "tags", value: tagsValue }), br(), br(),
|
||||
button({ type: "submit" }, mode === "edit" ? i18n.voteUpdateButton : i18n.voteCreateButton)
|
||||
)
|
||||
)
|
||||
: mode === "detail" && voteId
|
||||
? renderVoteDetail(filtered[0] || list.find(v => v.id === voteId) || {}, voteOptions, firstRow, secondRow, mode, activeFilter, params)
|
||||
? renderVoteDetail(filtered[0] || list.find(v => v.id === voteId) || {}, voteOptions, firstRow, secondRow, mode, activeFilter, { ...params, comments })
|
||||
: filtered.length > 0
|
||||
? div({ class: "jobs-grid" }, filtered.map((v) => renderVoteListItem(v, voteOptions, activeFilter, params.spreadMap && params.spreadMap.get(v.id))))
|
||||
: p(i18n.novotes),
|
||||
(mode === "detail" && voteId) ? renderCommentsSection(voteId, comments, activeFilter) : null
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue