upstream: integrar 0.9.5 en lo que no diverge (57 ficheros) + dependencias nuevas
Primera tanda de la integracion 0.9.1 -> 0.9.5. Se actualizan los 57 ficheros que upstream cambio y la rama no tocaba, entre ellos los 11 de traduccion: eso ya solo es posible gracias al overlay de i18n, antes eran 11 conflictos garantizados. Se portan ademas las dependencias que 0.9.5 introduce y de las que dependen los ficheros actualizados: pdfDocument, content_favorites, recurrence, media_gallery, comments_view, gallery_view y polls (modelo, limites y vista). Quedan 48 ficheros con divergencia real por resolver a mano (backend.js, main_views.js, OasisMobile.css y 45 vistas y modelos). Verificado: 0 errores de sintaxis en src/, 0 requires relativos rotos, la app arranca declarando 0.9.5 y las 47 rutas principales responden 200/302.
This commit is contained in:
parent
b3986b12a0
commit
f964e61a1c
67 changed files with 5740 additions and 10907 deletions
|
|
@ -9,74 +9,34 @@ exports.aiView = (history = [], userPrompt = '') => {
|
|||
div({ class: "tags-header" },
|
||||
h2(i18n.aiTitle),
|
||||
p(i18n.aiDescription),
|
||||
userPrompt ? div({ class: 'user-prompt', style: 'margin-bottom: 2em; font-size: 0.95em; color: #888;' },
|
||||
userPrompt ? div({ class: 'user-prompt' },
|
||||
`${i18n.aiPromptUsed || 'System Prompt'}: `,
|
||||
span({ style: 'font-style: italic;' }, `"${userPrompt}"`)
|
||||
span({ class: 'user-prompt-text' }, `"${userPrompt}"`)
|
||||
) : null,
|
||||
form({ method: 'POST', action: '/ai', style: "margin-bottom: 0;" },
|
||||
form({ method: 'POST', action: '/ai', class: 'ai-prompt-form' },
|
||||
textarea({ name: 'input', rows: 4, placeholder: i18n.aiInputPlaceholder, required: true }),
|
||||
br(),
|
||||
div({ style: "display: flex; gap: 1.5em; justify-content: flex-end; align-items: center; margin-top: 0.7em;" },
|
||||
div({ class: 'ai-submit-row' },
|
||||
button({ type: 'submit' }, i18n.aiSubmitButton)
|
||||
)
|
||||
),
|
||||
div({ style: "display: flex; justify-content: flex-end; margin-bottom: 2em;" },
|
||||
form({ method: 'POST', action: '/ai/clear', style: "display: inline;" },
|
||||
button({
|
||||
type: 'submit',
|
||||
style: `
|
||||
background: #b80c09;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.4em 1.2em;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
margin-left: 1em;
|
||||
`
|
||||
}, i18n.aiClearHistory)
|
||||
div({ class: 'ai-clear-row' },
|
||||
form({ method: 'POST', action: '/ai/clear', class: 'ai-clear-form' },
|
||||
button({ type: 'submit', class: 'ai-clear-btn' }, i18n.aiClearHistory)
|
||||
)
|
||||
),
|
||||
br(),
|
||||
...history.map(entry =>
|
||||
div({
|
||||
class: 'chat-entry',
|
||||
style: `
|
||||
margin-bottom: 2em;
|
||||
position: relative;
|
||||
background: #191919;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 8px #0004;
|
||||
padding-top: 1.8em;
|
||||
`
|
||||
},
|
||||
entry.timestamp ? span({
|
||||
style: `
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
right: 1.3em;
|
||||
font-size: 0.92em;
|
||||
color: #888;
|
||||
`
|
||||
}, new Date(entry.timestamp).toLocaleString()) : null,
|
||||
div({ class: 'chat-entry' },
|
||||
entry.timestamp
|
||||
? span({ class: 'chat-entry-timestamp' }, new Date(entry.timestamp).toLocaleString())
|
||||
: null,
|
||||
br(), br(),
|
||||
div({ class: 'user-question', style: 'margin-bottom: 0.75em;' },
|
||||
div({ class: 'user-question' },
|
||||
h2(`${i18n.aiUserQuestion}:`),
|
||||
p(...renderUrl(entry.question))
|
||||
),
|
||||
div({
|
||||
class: 'ai-response',
|
||||
style: `
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
background: #111;
|
||||
padding: 1.25em;
|
||||
border-radius: 6px;
|
||||
font-family: sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #ffcc00;
|
||||
`
|
||||
},
|
||||
div({ class: 'ai-response' },
|
||||
h2(`${i18n.aiResponseTitle}:`),
|
||||
...String(entry.answer || '')
|
||||
.split('\n\n')
|
||||
|
|
@ -84,31 +44,19 @@ exports.aiView = (history = [], userPrompt = '') => {
|
|||
paragraph
|
||||
.split('\n')
|
||||
.map(line =>
|
||||
p({ style: "margin-bottom: 1.2em;" }, ...renderUrl(line.trim()))
|
||||
p(...renderUrl(line.trim()))
|
||||
)
|
||||
)
|
||||
),
|
||||
div({
|
||||
class: 'ai-train-bar',
|
||||
style: `
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:12px;
|
||||
margin: 12px auto 8px auto;
|
||||
max-width: 800px;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
},
|
||||
div({ class: 'ai-train-bar' },
|
||||
Array.isArray(entry.snippets) && entry.snippets.length
|
||||
? span({ style: 'color:#9aa; font-size:0.95em;' }, `${i18n.aiSnippetsUsed}: ${entry.snippets.length}`)
|
||||
? span({ class: 'ai-snippets-used' }, `${i18n.aiSnippetsUsed}: ${entry.snippets.length}`)
|
||||
: null,
|
||||
h2(`${i18n.statsAITraining}:`),
|
||||
entry.trainStatus === 'approved'
|
||||
? span({ style: 'color:#5ad25a; font-weight:600;' }, i18n.aiTrainApproved)
|
||||
? span({ class: 'ai-train-approved' }, i18n.aiTrainApproved)
|
||||
: entry.trainStatus === 'rejected'
|
||||
? span({ style: 'color:#ff6b6b; font-weight:600;' }, i18n.aiTrainRejected)
|
||||
? span({ class: 'ai-train-rejected' }, i18n.aiTrainRejected)
|
||||
: null,
|
||||
entry.trainStatus === 'approved' || entry.trainStatus === 'rejected'
|
||||
? null
|
||||
|
|
|
|||
|
|
@ -297,7 +297,8 @@ const renderTaxes = (data, lookup) => {
|
|||
);
|
||||
};
|
||||
|
||||
const renderOverviewSummaryTable = (s, rules, userEcoinTax) => {
|
||||
const renderOverviewSummaryTable = (s, rules, userEcoinTax, isPub = false) => {
|
||||
const ubiWired = !!s.pubId || isPub;
|
||||
const score = Number(s.userEngagementScore || 0);
|
||||
const pool = Number(s.pool || 0);
|
||||
const W = Math.max(1, Number(s.weightsSum || 1));
|
||||
|
|
@ -315,16 +316,16 @@ const renderOverviewSummaryTable = (s, rules, userEcoinTax) => {
|
|||
tbody(
|
||||
kvRow(i18n.bankUserBalance, `${Number(s.userBalance || 0).toFixed(6)} ECO`),
|
||||
kvRow(i18n.bankIndustryBalance || 'Industry Production (estimated)', a({ href: '/industry' }, `${Number(s.industryNetworkTotal || 0).toFixed(6)} ECO`)),
|
||||
kvRow(i18n.bankUbiAvailability, span({ class: availClass }, availLabel)),
|
||||
ubiWired ? kvRow(i18n.bankUbiAvailability, span({ class: availClass }, availLabel)) : null,
|
||||
s.pubId ? kvRow(i18n.pubIdLabel, userLink(s.pubId)) : null,
|
||||
kvRow(i18n.bankEpoch, String(s.epochId || "-")),
|
||||
kvRow(i18n.bankPool, `${pool.toFixed(6)} ECO`),
|
||||
kvRow(i18n.bankWeightsSum, String(W.toFixed(6))),
|
||||
kvRow(i18n.bankingUserEngagementScore, String(score)),
|
||||
kvRow(i18n.bankYourUbiMonth || 'Your UBI (this month)', `${future.toFixed(6)} ECO`),
|
||||
ubiWired ? kvRow(i18n.bankYourUbiMonth || 'Your UBI (this month)', `${future.toFixed(6)} ECO`) : null,
|
||||
kvRow(i18n.bankYourIndustryBalance || 'Your Industry Share', a({ href: '/industry?filter=MEMBER' }, `${Number(s.industryBalance || 0).toFixed(6)} ECO`)),
|
||||
kvRow(i18n.bankOverviewYourTaxes || 'Your taxes', a({ href: '/banking?filter=taxes' }, `${tax.toFixed(6)} ECO`)),
|
||||
kvRow(i18n.bankYourFundsMonth || 'Your Funds (this month)', `${(future + Number(s.industryBalance || 0) - tax).toFixed(6)} ECO`)
|
||||
kvRow(i18n.bankYourFundsMonth || 'Your Funds (this month)', `${((ubiWired ? future : 0) + Number(s.industryBalance || 0) - tax).toFixed(6)} ECO`)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
@ -621,7 +622,7 @@ const renderBankingView = (data, filter, userId, isPub) =>
|
|||
generateFilterButtons(["overview","exchange","taxes","mine","pending","closed","claimed","expired","epochs","rules","addresses"], filter, "/banking"),
|
||||
filter === "overview"
|
||||
? div(
|
||||
renderOverviewSummaryTable(data.summary || {}, data.rules, data.userTotalTax || data.userEcoinTax),
|
||||
renderOverviewSummaryTable(data.summary || {}, data.rules, data.userTotalTax || data.userEcoinTax, isPub),
|
||||
renderClaimUBIBlock(data.pendingUBI || null, isPub, data.alreadyClaimed, (data.summary || {}).pubId, (data.summary || {}).hasValidWallet, (data.summary || {}).ubiAvailability),
|
||||
allocationsTable((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), userId)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ const renderFediverseReach = (prefs, i18nObj = {}) => {
|
|||
a({ href: url, target: '_blank', rel: 'noopener noreferrer', class: 'pm-exposition-chip-link' },
|
||||
span({ class: 'pm-exposition-chip pm-exposition-fediverse' },
|
||||
span({ class: 'pm-exposition-icon' }, '🐘'),
|
||||
span({ class: 'pm-exposition-text' }, i18nObj.fediverse || 'Fediverse')
|
||||
span({ class: 'pm-exposition-text' }, i18nObj.fediverse || 'Multiverse')
|
||||
)
|
||||
),
|
||||
a({ href: url, target: '_blank', rel: 'noopener noreferrer', class: 'fediverse-reach-url' }, handle)
|
||||
|
|
|
|||
77
src/views/comments_view.js
Normal file
77
src/views/comments_view.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
const { div, p, h2, span, a, form, input, textarea, button, label, br, details, summary } = require("../server/node_modules/hyperaxe");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { i18n, userLink } = require("./main_views");
|
||||
const { renderUrl } = require("../backend/renderUrl");
|
||||
|
||||
const COMMENT_ICON = "✑";
|
||||
|
||||
const visibleComments = (comments) => (Array.isArray(comments) ? comments : []).filter(c => {
|
||||
const t = c && c.value && c.value.content && c.value.content.text;
|
||||
return t && String(t).trim();
|
||||
});
|
||||
|
||||
const renderCommentCard = (c) => {
|
||||
const author = (c && c.value && c.value.author) || "";
|
||||
const ts = (c && c.value && c.value.timestamp) || (c && c.timestamp);
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm") : "";
|
||||
const relDate = ts ? moment(ts).fromNow() : "";
|
||||
const content = (c && c.value && c.value.content) || {};
|
||||
const rootId = content.fork || content.root || null;
|
||||
const text = content.text || "";
|
||||
|
||||
return div({ class: "votations-comment-card", id: c && c.key ? c.key : undefined },
|
||||
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(String(text)))
|
||||
);
|
||||
};
|
||||
|
||||
const renderCommentsSection = ({ action, comments = [], returnTo = null, closedNote = null, extraClass = "" } = {}) => {
|
||||
const list = visibleComments(comments);
|
||||
return details({ class: "comments-collapse" + (extraClass ? ` ${extraClass}` : "") },
|
||||
summary({ class: list.length > 0 ? "comments-summary engage-on" : "comments-summary" },
|
||||
span({ class: "comments-summary-icon" }, COMMENT_ICON),
|
||||
span({ class: "comments-summary-count" }, `(${list.length})`)
|
||||
),
|
||||
div({ class: "vote-comments-section" },
|
||||
closedNote
|
||||
? p({ class: "muted" }, closedNote)
|
||||
: div({ class: "comment-form-wrapper" },
|
||||
h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel),
|
||||
form({ method: "POST", action, class: "comment-form", enctype: "multipart/form-data" },
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
textarea({ 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)
|
||||
)
|
||||
),
|
||||
list.length
|
||||
? div({ class: "comments-list" }, ...list.map(renderCommentCard))
|
||||
: (closedNote ? null : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet))
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const renderCommentsLink = ({ href, count = 0 } = {}) => {
|
||||
if (!href) return null;
|
||||
const total = Number(count) || 0;
|
||||
return a({ href, class: total > 0 ? "comments-summary engage-on comments-link" : "comments-summary comments-link" },
|
||||
span({ class: "comments-summary-icon" }, COMMENT_ICON),
|
||||
span({ class: "comments-summary-count" }, `(${total})`)
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
COMMENT_ICON,
|
||||
renderCommentsLink,
|
||||
visibleComments,
|
||||
renderCommentCard,
|
||||
renderCommentsSection
|
||||
};
|
||||
|
|
@ -188,13 +188,13 @@ exports.devFileView = async (file) => {
|
|||
form({ method: "GET", action: "/reports" },
|
||||
input({ type: "hidden", name: "filter", value: "create" }),
|
||||
input({ type: "hidden", name: "category", value: "BUGS" }),
|
||||
input({ type: "hidden", name: "title", value: reportTitle }),
|
||||
input({ type: "hidden", name: "title", maxlength: "100", value: reportTitle }),
|
||||
input({ type: "hidden", name: "description", value: reportDescription }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.devReportBug || "Report Bug")
|
||||
),
|
||||
form({ method: "GET", action: "/tasks" },
|
||||
input({ type: "hidden", name: "filter", value: "create" }),
|
||||
input({ type: "hidden", name: "title", value: taskTitle }),
|
||||
input({ type: "hidden", name: "title", maxlength: "100", value: taskTitle }),
|
||||
input({ type: "hidden", name: "description", value: reportDescription }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.devCreateTask || "Create Task")
|
||||
),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,30 @@ const userId = config.keys.id;
|
|||
const safeArr = (v) => (Array.isArray(v) ? v : []);
|
||||
const safeText = (v) => String(v || "").trim();
|
||||
|
||||
const FILTER_KINDS = [
|
||||
{ value: "audios", label: () => i18n.favoritesFilterAudios },
|
||||
{ value: "bookmarks", label: () => i18n.favoritesFilterBookmarks },
|
||||
{ value: "calendars", label: () => i18n.favoritesFilterCalendars },
|
||||
{ value: "chats", label: () => i18n.favoritesFilterChats },
|
||||
{ value: "documents", label: () => i18n.favoritesFilterDocuments },
|
||||
{ value: "events", label: () => i18n.favoritesFilterEvents },
|
||||
{ value: "forum", label: () => i18n.favoritesFilterForum },
|
||||
{ value: "housing", label: () => i18n.favoritesFilterHousing },
|
||||
{ value: "images", label: () => i18n.favoritesFilterImages },
|
||||
{ value: "jobs", label: () => i18n.favoritesFilterJobs },
|
||||
{ value: "maps", label: () => i18n.favoritesFilterMaps },
|
||||
{ value: "market", label: () => i18n.favoritesFilterMarket },
|
||||
{ value: "pads", label: () => i18n.favoritesFilterPads },
|
||||
{ value: "projects", label: () => i18n.favoritesFilterProjects },
|
||||
{ value: "reports", label: () => i18n.favoritesFilterReports },
|
||||
{ value: "shopProducts", label: () => i18n.favoritesFilterShopProducts },
|
||||
{ value: "tasks", label: () => i18n.favoritesFilterTasks },
|
||||
{ value: "torrents", label: () => i18n.favoritesFilterTorrents },
|
||||
{ value: "transfers", label: () => i18n.favoritesFilterTransfers },
|
||||
{ value: "videos", label: () => i18n.favoritesFilterVideos },
|
||||
{ value: "votes", label: () => i18n.favoritesFilterVotes }
|
||||
];
|
||||
|
||||
const buildReturnTo = (filter) => {
|
||||
const f = safeText(filter || "all");
|
||||
return `/favorites?filter=${encodeURIComponent(f)}`;
|
||||
|
|
@ -25,6 +49,47 @@ const renderTags = (tags) => {
|
|||
: null;
|
||||
};
|
||||
|
||||
const DETAIL_KEYS = [
|
||||
"status", "category", "severity", "priority", "location", "place",
|
||||
"date", "deadline", "startTime", "endTime", "price", "salary", "amount",
|
||||
"concept", "question", "housing_type", "property_type", "item_type", "sector"
|
||||
];
|
||||
|
||||
const fieldLabel = (key) => {
|
||||
const camel = key.replace(/_(.)/g, (_, c) => c.toUpperCase());
|
||||
return i18n[camel + "Label"] || i18n["search" + camel.charAt(0).toUpperCase() + camel.slice(1)] || camel.toUpperCase();
|
||||
};
|
||||
|
||||
const fieldValue = (v) => {
|
||||
if (v === null || v === undefined) return "";
|
||||
if (Array.isArray(v)) return v.filter(Boolean).join(", ");
|
||||
if (typeof v === "object") return "";
|
||||
return String(v).trim();
|
||||
};
|
||||
|
||||
const renderDetailFields = (item) => {
|
||||
const content = item && item.content;
|
||||
if (!content || typeof content !== "object") return null;
|
||||
const rows = DETAIL_KEYS
|
||||
.map((k) => [k, fieldValue(content[k])])
|
||||
.filter(([, v]) => v)
|
||||
.map(([k, v]) =>
|
||||
div({ class: "card-field" },
|
||||
span({ class: "card-label" }, `${fieldLabel(k)}: `),
|
||||
span({ class: "card-value" }, v)
|
||||
)
|
||||
);
|
||||
const members = ["members", "participants", "attendees", "assignees", "confirmedBy"]
|
||||
.map((k) => [k, Array.isArray(content[k]) ? content[k].length : null])
|
||||
.find(([, n]) => typeof n === "number" && n > 0);
|
||||
if (members) {
|
||||
rows.push(div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${fieldLabel(members[0])}: ${members[1]}`)
|
||||
));
|
||||
}
|
||||
return rows.length ? div({ class: "cv-card-fields" }, ...rows) : null;
|
||||
};
|
||||
|
||||
const renderBookmarkUrl = (item) => {
|
||||
if (item.kind !== "bookmarks") return null;
|
||||
if (!item.url) return null;
|
||||
|
|
@ -60,7 +125,7 @@ const renderFavoriteCard = (item, filter) => {
|
|||
const title = safeText(item.title) || safeText(item.name) || safeText(item.category) || safeText(item.url) || "";
|
||||
|
||||
const ts = item.updatedAt || item.createdAt;
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "";
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm") : "";
|
||||
|
||||
const isOwn = item.author && String(item.author) === String(userId);
|
||||
return div(
|
||||
|
|
@ -95,6 +160,7 @@ const renderFavoriteCard = (item, filter) => {
|
|||
renderImagePreview(item),
|
||||
renderBookmarkUrl(item),
|
||||
safeText(item.description) ? p(...renderUrl(item.description)) : null,
|
||||
renderDetailFields(item),
|
||||
renderTags(item.tags),
|
||||
p(
|
||||
{ class: "card-footer" },
|
||||
|
|
@ -105,7 +171,7 @@ const renderFavoriteCard = (item, filter) => {
|
|||
);
|
||||
};
|
||||
|
||||
exports.favoritesView = async (items, filter = "all", counts = {}) => {
|
||||
exports.favoritesView = async (items, filter = "all", counts = {}, q = "") => {
|
||||
const c = counts || {};
|
||||
const total = typeof c.all === "number" ? c.all : safeArr(items).length;
|
||||
|
||||
|
|
@ -125,53 +191,22 @@ exports.favoritesView = async (items, filter = "all", counts = {}) => {
|
|||
{ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" },
|
||||
`${i18n.favoritesFilterRecent} (${total})`
|
||||
),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "audios", class: filter === "audios" ? "filter-btn active" : "filter-btn" },
|
||||
`${i18n.favoritesFilterAudios} (${c.audios || 0})`
|
||||
),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "bookmarks", class: filter === "bookmarks" ? "filter-btn active" : "filter-btn" },
|
||||
`${i18n.favoritesFilterBookmarks} (${c.bookmarks || 0})`
|
||||
),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "documents", class: filter === "documents" ? "filter-btn active" : "filter-btn" },
|
||||
`${i18n.favoritesFilterDocuments} (${c.documents || 0})`
|
||||
),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "images", class: filter === "images" ? "filter-btn active" : "filter-btn" },
|
||||
`${i18n.favoritesFilterImages} (${c.images || 0})`
|
||||
),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "maps", class: filter === "maps" ? "filter-btn active" : "filter-btn" },
|
||||
`${i18n.favoritesFilterMaps} (${c.maps || 0})`
|
||||
),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "pads", class: filter === "pads" ? "filter-btn active" : "filter-btn" },
|
||||
`${i18n.favoritesFilterPads || "PADS"} (${c.pads || 0})`
|
||||
),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "chats", class: filter === "chats" ? "filter-btn active" : "filter-btn" },
|
||||
`${i18n.favoritesFilterChats || "CHATS"} (${c.chats || 0})`
|
||||
),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "calendars", class: filter === "calendars" ? "filter-btn active" : "filter-btn" },
|
||||
`${i18n.favoritesFilterCalendars || "CALENDARS"} (${c.calendars || 0})`
|
||||
),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "videos", class: filter === "videos" ? "filter-btn active" : "filter-btn" },
|
||||
`${i18n.favoritesFilterVideos} (${c.videos || 0})`
|
||||
),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "torrents", class: filter === "torrents" ? "filter-btn active" : "filter-btn" },
|
||||
`${i18n.favoritesFilterTorrents || "TORRENTS"} (${c.torrents || 0})`
|
||||
),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "market", class: filter === "market" ? "filter-btn active" : "filter-btn" },
|
||||
`${i18n.favoritesFilterMarket || "MARKET"} (${c.market || 0})`
|
||||
),
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: "shopProducts", class: filter === "shopProducts" ? "filter-btn active" : "filter-btn" },
|
||||
`${i18n.favoritesFilterShopProducts || "SHOP ITEMS"} (${c.shopProducts || 0})`
|
||||
...FILTER_KINDS.map((k) =>
|
||||
button(
|
||||
{ type: "submit", name: "filter", value: k.value, class: filter === k.value ? "filter-btn active" : "filter-btn" },
|
||||
`${k.label()} (${c[k.value] || 0})`
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
div(
|
||||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/favorites", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: filter }),
|
||||
input({ type: "text", name: "q", value: q, placeholder: i18n.favoritesSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const moment = require("../server/node_modules/moment");
|
|||
const fmtDate = (ts) => {
|
||||
if (!ts) return "";
|
||||
const m = moment(ts);
|
||||
return m.isValid() ? m.format("YYYY-MM-DD HH:mm") : "";
|
||||
return m.isValid() ? m.format("YYYY/MM/DD HH:mm") : "";
|
||||
};
|
||||
|
||||
const renderMedia = (m) => {
|
||||
|
|
@ -111,7 +111,7 @@ const mastodonBox = (account, stats, actions, showLabel) => {
|
|||
|
||||
const disconnectedBlock = () => {
|
||||
const text = String(i18n.fediverseDisconnected || "");
|
||||
const settingsLink = a({ href: "/settings#fediverse" }, i18n.fediverseEmptyLink || "settings");
|
||||
const settingsLink = a({ href: "/settings#multiverse" }, i18n.fediverseEmptyLink || "settings");
|
||||
const parts = text.split("%LINK%");
|
||||
const para = parts.length > 1
|
||||
? p(parts[0], settingsLink, parts.slice(1).join("%LINK%"))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const { div, h2, p, section, button, form, a, span, textarea, br, input, h1, label } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderContentActions } = require("./main_views");
|
||||
const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view");
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderContentActions, renderEngagement } = require("./main_views");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderTextWithStyles } = require("../backend/renderTextWithStyles");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
|
|
@ -56,7 +57,7 @@ const generateFilterButtons = (filters, currentFilter, action, extra = {}) => {
|
|||
{ method: "GET", action },
|
||||
input({ type: "hidden", name: "filter", value: mode }),
|
||||
...hiddenInputs(extra),
|
||||
button({ type: "submit", class: cur === mode ? "filter-btn active" : "filter-btn" }, i18n[mode + "Button"] || mode)
|
||||
button({ type: "submit", class: cur === mode ? "filter-btn active" : "filter-btn" }, String(i18n[mode + "Button"] || mode).toUpperCase())
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
@ -79,65 +80,11 @@ const renderCardField = (labelText, value) =>
|
|||
);
|
||||
|
||||
const renderFeedCommentsSection = (feedKey, comments = []) => {
|
||||
const list = Array.isArray(comments) ? comments : [];
|
||||
const commentsCount = list.length;
|
||||
|
||||
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 || i18n.feedPostComment || "Post a comment"),
|
||||
form(
|
||||
{ method: "POST", action: `/feed/${encodeURIComponent(feedKey)}/comments`, class: "comment-form", enctype: "multipart/form-data" },
|
||||
textarea({
|
||||
id: "comment-text",
|
||||
name: "text",
|
||||
rows: 4,
|
||||
class: "comment-textarea",
|
||||
placeholder: i18n.voteNewCommentPlaceholder || ""
|
||||
}),
|
||||
div({ class: "comment-file-upload" }, label(i18n.uploadMedia || "Upload media"), input({ type: "file", name: "blob" })),
|
||||
br(),
|
||||
button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton || i18n.feedPostComment || "Send")
|
||||
)
|
||||
),
|
||||
list.length
|
||||
? div(
|
||||
{ class: "comments-list" },
|
||||
list.map((c) => {
|
||||
const author = c?.value?.author || "";
|
||||
const ts = 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?.content || {};
|
||||
const text = content.text || c?.value?.text || "";
|
||||
const threadRoot = content.fork || content.root || null;
|
||||
|
||||
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 && threadRoot
|
||||
? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate)
|
||||
: ""
|
||||
),
|
||||
p({ class: "votations-comment-text" }, ...renderUrl(text))
|
||||
);
|
||||
})
|
||||
)
|
||||
: p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet || i18n.noComments || "")
|
||||
);
|
||||
return renderSharedCommentsSection({
|
||||
action: `/feed/${encodeURIComponent(feedKey)}/comments`,
|
||||
comments: comments,
|
||||
returnTo: null
|
||||
});
|
||||
};
|
||||
|
||||
const renderFeedCard = (feed) => {
|
||||
|
|
@ -163,7 +110,7 @@ const renderFeedCard = (feed) => {
|
|||
div(
|
||||
{ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(feed.key, `/feed/${encodeURIComponent(feed.key)}`)
|
||||
renderContentActions(feed.key, `/feed/${encodeURIComponent(feed.key)}`, { author: authorId, reportTitle: safeText })
|
||||
),
|
||||
div(
|
||||
{ class: "card-section feed-card-body" },
|
||||
|
|
@ -189,8 +136,8 @@ const renderFeedCard = (feed) => {
|
|||
const maxVal = Math.max(...entries.map(([, v]) => Number(v)));
|
||||
const dominant = entries.filter(([, v]) => Number(v) === maxVal).map(([k]) => i18n['vote' + k.charAt(0).toUpperCase() + k.slice(1)] || k);
|
||||
return [
|
||||
span({ style: 'margin:0 8px;opacity:0.5;' }, '|'),
|
||||
span({ style: 'font-weight:700;' }, `${i18n.moreVoted || 'More Voted'}: ${dominant.join(' + ')}`)
|
||||
span({ class: 'feed-vote-sep' }, '|'),
|
||||
span({ class: 'feed-vote-dominant' }, `${i18n.moreVoted || 'More Voted'}: ${dominant.join(' + ')}`)
|
||||
];
|
||||
})()
|
||||
),
|
||||
|
|
@ -202,25 +149,9 @@ const renderFeedCard = (feed) => {
|
|||
)
|
||||
)
|
||||
),
|
||||
div(
|
||||
{ class: "card-comments-summary" },
|
||||
span({ class: "card-label" }, `${i18n.voteCommentsLabel || "Comments"}:`),
|
||||
span({ class: "card-value" }, String(commentCount)),
|
||||
br(),
|
||||
br(),
|
||||
form(
|
||||
{ method: "GET", action: `/feed/${encodeURIComponent(feed.key)}` },
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton || i18n.feedOpenDiscussion || "Open Discussion")
|
||||
)
|
||||
),
|
||||
div(
|
||||
{ class: "card-comments-summary feed-opinions-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "),
|
||||
span({ class: "card-value" }, String(totalCount))
|
||||
),
|
||||
renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants)
|
||||
renderEngagement(feed.key,
|
||||
renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants),
|
||||
renderCommentsLink({ href: `/feed/${encodeURIComponent(feed.key)}`, count: commentCount })
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
@ -264,11 +195,13 @@ exports.feedView = (feeds, opts = "ALL") => {
|
|||
div(
|
||||
{ class: "feed-tools-row" },
|
||||
form(
|
||||
{ method: "GET", action: "/feed", class: "feed-search-form" },
|
||||
{ method: "GET", action: "/feed", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: filter }),
|
||||
tag ? input({ type: "hidden", name: "tag", value: tag }) : null,
|
||||
input({ type: "text", name: "q", value: q, placeholder: i18n.searchPlaceholder || "Search", class: "feed-search-input" }),
|
||||
button({ type: "submit", class: "filter-btn feed-search-btn" }, i18n.searchButton || "Search")
|
||||
input({ type: "text", name: "q", value: q, placeholder: i18n.searchPlaceholder || "Search", class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton || "Search")
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
|
|
@ -321,7 +254,7 @@ exports.feedCreateView = (opts = {}) => {
|
|||
);
|
||||
};
|
||||
|
||||
exports.singleFeedView = (feed, comments = []) => {
|
||||
exports.singleFeedView = (feed, comments = [], params = {}) => {
|
||||
const content = feed.value?.content || {};
|
||||
const rawText = typeof content.text === "string" ? content.text : "";
|
||||
const safeText = rawText.trim();
|
||||
|
|
@ -335,6 +268,7 @@ exports.singleFeedView = (feed, comments = []) => {
|
|||
|
||||
return template(
|
||||
i18n.feedDetailTitle || "Feed",
|
||||
section(div({ class: "tags-header" }, h2(i18n.feedTitle), p(i18n.FeedshareYourOpinions))),
|
||||
section(
|
||||
div(
|
||||
{ class: "filters" },
|
||||
|
|
@ -349,6 +283,10 @@ exports.singleFeedView = (feed, comments = []) => {
|
|||
),
|
||||
div(
|
||||
{ class: "bookmark-item card feed-detail-card" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(feed.key, null, { spread: params.spreads || null, author: authorId, reportTitle: safeText })
|
||||
),
|
||||
br,
|
||||
div(
|
||||
{ class: "feed-row" },
|
||||
|
|
@ -380,15 +318,9 @@ exports.singleFeedView = (feed, comments = []) => {
|
|||
)
|
||||
)
|
||||
),
|
||||
renderFeedCommentsSection(feed.key, comments),
|
||||
div(
|
||||
{ class: "card-comments-summary feed-opinions-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "),
|
||||
span({ class: "card-value" }, String(Object.values(content.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)))
|
||||
),
|
||||
renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants)
|
||||
renderEngagement(feed.key,
|
||||
renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants),
|
||||
renderFeedCommentsSection(feed.key, comments)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
|||
118
src/views/gallery_view.js
Normal file
118
src/views/gallery_view.js
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
const { div, a, span, img, video, input, button, label, br } = require("../server/node_modules/hyperaxe")
|
||||
const { i18n } = require("./main_views")
|
||||
const { MAX_IMAGES } = require("../models/media_gallery")
|
||||
|
||||
const safeArr = (v) => (Array.isArray(v) ? v : [])
|
||||
const safeText = (v) => String(v == null ? "" : v).trim()
|
||||
|
||||
const blobIdOf = (entry) => {
|
||||
const value = safeText(entry)
|
||||
const m = value.match(/\(([^)\s]+)\)/)
|
||||
return m ? m[1] : value
|
||||
}
|
||||
|
||||
const blobUrl = (id, size) => size
|
||||
? `/image/${size}/${encodeURIComponent(id)}`
|
||||
: `/blob/${encodeURIComponent(id)}`
|
||||
|
||||
const isVideoEntry = (entry) => /\[video:[^\]]*\]\(/.test(String(entry || ""))
|
||||
|
||||
const imagesOf = (item) => {
|
||||
const list = safeArr(item && item.images).filter(Boolean)
|
||||
if (list.length) return list
|
||||
return item && item.image ? [item.image] : []
|
||||
}
|
||||
|
||||
const videoOf = (item) => safeText(item && item.video)
|
||||
|
||||
const renderMediaThumb = (entry, alt = "") => isVideoEntry(entry)
|
||||
? video({ controls: true, class: "gallery-image", src: blobUrl(blobIdOf(entry)) })
|
||||
: img({ src: blobUrl(blobIdOf(entry), 256), class: "gallery-image", alt })
|
||||
|
||||
const lightboxId = (scope, itemId, index) => `${scope}-photo-${encodeURIComponent(itemId)}-${index}`
|
||||
|
||||
let zoomSeq = 0
|
||||
const renderZoomableImage = (src, { alt = "", imgClass = "" } = {}) => {
|
||||
if (!src) return null
|
||||
zoomSeq = (zoomSeq + 1) % 1000000
|
||||
const id = `zoom-${zoomSeq}-${String(src).replace(/[^a-zA-Z0-9]/g, "").slice(-10)}`
|
||||
return [
|
||||
a({ href: `#${id}`, class: "zoom-link" }, img({ src, class: imgClass, alt })),
|
||||
div({ id, class: "lightbox" },
|
||||
a({ href: "#", class: "lightbox-close" }, "×"),
|
||||
img({ src, class: "lightbox-image", alt })
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
const renderPhotoGallery = (item, scope = "media") => {
|
||||
const list = imagesOf(item)
|
||||
const clip = videoOf(item)
|
||||
if (!list.length && !clip) return null
|
||||
const id = (item && (item.id || item.key || item.rootId)) || ""
|
||||
return div({ class: "media-gallery" },
|
||||
list.length
|
||||
? div({ class: "gallery" },
|
||||
list.map((entry, i) =>
|
||||
isVideoEntry(entry)
|
||||
? span({ class: "gallery-item" }, renderMediaThumb(entry))
|
||||
: a({ href: `#${lightboxId(scope, id, i)}`, class: "gallery-item" }, renderMediaThumb(entry))
|
||||
)
|
||||
)
|
||||
: null,
|
||||
list.filter(entry => !isVideoEntry(entry)).map((entry) =>
|
||||
div({ id: lightboxId(scope, id, list.indexOf(entry)), class: "lightbox" },
|
||||
a({ href: "#", class: "lightbox-close" }, "×"),
|
||||
img({ src: blobUrl(blobIdOf(entry)), class: "lightbox-image", alt: "" })
|
||||
)
|
||||
),
|
||||
clip
|
||||
? div({ class: "media-video media-video-centered" },
|
||||
video({ controls: true, class: "media-video-player", src: blobUrl(blobIdOf(clip)) })
|
||||
)
|
||||
: null
|
||||
)
|
||||
}
|
||||
|
||||
const renderGalleryFields = (item = {}, isEdit = false, maxImages = MAX_IMAGES) => {
|
||||
const list = imagesOf(item)
|
||||
return [
|
||||
label(i18n.galleryImages),
|
||||
br(),
|
||||
...list.map(entry => input({ type: "hidden", name: "keepImages", value: entry })),
|
||||
list.length < maxImages
|
||||
? div({ class: "media-file-row" },
|
||||
input({ type: "file", name: "images", accept: "image/*", multiple: true }),
|
||||
button({ type: "submit", name: "action", value: "addPhoto", attrs: { formnovalidate: "formnovalidate" }, class: "filter-btn media-add-btn" }, i18n.galleryAddPhoto)
|
||||
)
|
||||
: null,
|
||||
br(),
|
||||
list.length
|
||||
? [
|
||||
div({ class: "gallery media-form-gallery" },
|
||||
list.map((entry, i) =>
|
||||
span({ class: "gallery-item" },
|
||||
renderMediaThumb(entry),
|
||||
button({ type: "submit", name: "removePhoto", value: String(i), attrs: { formnovalidate: "formnovalidate" }, class: "media-remove-btn" }, i18n.galleryRemovePhoto)
|
||||
)
|
||||
)
|
||||
),
|
||||
br()
|
||||
]
|
||||
: null,
|
||||
br()
|
||||
]
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MAX_IMAGES,
|
||||
blobIdOf,
|
||||
blobUrl,
|
||||
isVideoEntry,
|
||||
imagesOf,
|
||||
videoOf,
|
||||
renderMediaThumb,
|
||||
renderPhotoGallery,
|
||||
renderGalleryFields,
|
||||
renderZoomableImage
|
||||
}
|
||||
|
|
@ -99,13 +99,13 @@ exports.gamesView = (filter = 'all', hall = null) => {
|
|||
form({ method: 'GET', action: '/games' },
|
||||
input({ type: 'hidden', name: 'filter', value: 'all' }),
|
||||
button({ type: 'submit', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' },
|
||||
i18n.gamesFilterAll
|
||||
String(i18n.gamesFilterAll).toUpperCase()
|
||||
)
|
||||
),
|
||||
form({ method: 'GET', action: '/games' },
|
||||
input({ type: 'hidden', name: 'filter', value: 'scoring' }),
|
||||
button({ type: 'submit', class: filter === 'scoring' ? 'filter-btn active' : 'filter-btn' },
|
||||
i18n.gamesFilterScoring
|
||||
String(i18n.gamesFilterScoring).toUpperCase()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -88,9 +88,9 @@ const buildGraphSvg = (me, peers) => {
|
|||
+ `</svg>`;
|
||||
};
|
||||
|
||||
const kpi = (label, value) => div({ class: 'stats-kpi' },
|
||||
div({ class: 'stats-kpi-label' }, label),
|
||||
div({ class: 'stats-kpi-value' }, String(value))
|
||||
const kpi = (label, value) => div({ class: 'stats-kpi stats-kpi-inline' },
|
||||
span({ class: 'stats-kpi-label' }, `${String(label).toUpperCase()}: `),
|
||||
span({ class: 'stats-kpi-value' }, String(value))
|
||||
);
|
||||
|
||||
const legendItem = (kind, label) =>
|
||||
|
|
@ -126,27 +126,27 @@ exports.graphosView = ({ filter, me, peers, kpis, focus = null, focusId = null,
|
|||
modes.map(m =>
|
||||
form({ method: 'GET', action: '/graphos' },
|
||||
input({ type: 'hidden', name: 'filter', value: m }),
|
||||
button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, i18n[m + 'Button'])
|
||||
button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, String(i18n[m + 'Button']).toUpperCase())
|
||||
)
|
||||
)
|
||||
),
|
||||
capped
|
||||
? p({ class: 'graphos-cap-note' }, `${i18n.graphosShowing || 'Showing'} ${shown} / ${total}`)
|
||||
: null,
|
||||
div({ class: 'graphos-legend' },
|
||||
legendItem('me', i18n.graphosYou || 'You'),
|
||||
legendItem('online', i18n.online || 'Online'),
|
||||
filter !== 'MINE' ? legendItem('discovered', i18n.discovered || 'Discovered') : null,
|
||||
filter !== 'MINE' ? legendItem('unknown', i18n.unknown || 'Unknown') : null
|
||||
),
|
||||
div({ class: 'graphos-canvas', innerHTML: buildGraphSvg(me, peers) }),
|
||||
div({ class: 'stats-block' },
|
||||
div({ class: 'stats-block graphos-stats' },
|
||||
div({ class: 'stats-grid' },
|
||||
kpi(i18n.graphosTotalNodes || 'Total nodes', kpis.total),
|
||||
kpi(i18n.online || 'Online', kpis.online),
|
||||
filter !== 'MINE' ? kpi(i18n.discovered || 'Discovered', kpis.discovered) : null,
|
||||
filter !== 'MINE' ? kpi(i18n.unknown || 'Unknown', kpis.unknown) : null
|
||||
)
|
||||
),
|
||||
div({ class: 'graphos-canvas', innerHTML: buildGraphSvg(me, peers) }),
|
||||
div({ class: 'graphos-legend' },
|
||||
legendItem('me', i18n.graphosYou || 'You'),
|
||||
legendItem('online', i18n.online || 'Online'),
|
||||
filter !== 'MINE' ? legendItem('discovered', i18n.discovered || 'Discovered') : null,
|
||||
filter !== 'MINE' ? legendItem('unknown', i18n.unknown || 'Unknown') : null
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const { div, h2, p, section, button, form, img, a, textarea, input, br, span, strong } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink, renderUserSensors, renderContentActions } = require('./main_views');
|
||||
const { div, h2, p, section, button, form, img, a, textarea, input, span, strong } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink, renderUserSensors, renderContentActions, renderRelationshipBlock } = require('./main_views');
|
||||
const { renderZoomableImage } = require('./gallery_view');
|
||||
const { renderContentStats } = require('./clearnet_view');
|
||||
const { renderUrl } = require('../backend/renderUrl');
|
||||
const { getConfig } = require('../configs/config-manager');
|
||||
|
|
@ -48,20 +49,40 @@ function resolvePhoto(photoField, size = 256) {
|
|||
if (photoField.startsWith('/blob/')) return photoField;
|
||||
if (photoField.startsWith('/image/')) {
|
||||
if (isDefaultImageId(photoField)) return '/assets/images/default-avatar.png';
|
||||
return photoField.replace('/image/256/','/image/'+size+'/').replace('/image/512/','/image/'+size+'/');
|
||||
return photoField.replace('/image/256/','/image/'+size+'/').replace('/image/512/','/image/'+size+'/') + '?fallback=avatar';
|
||||
}
|
||||
}
|
||||
return toImageUrl(photoField, size);
|
||||
return toImageUrl(photoField, size) + '?fallback=avatar';
|
||||
}
|
||||
|
||||
const generateFilterButtons = (filters, currentFilter) =>
|
||||
const filterLabel = (mode) =>
|
||||
String(i18n[mode + 'Button'] || i18n[mode + 'SectionTitle'] || mode).toUpperCase();
|
||||
|
||||
const generateFilterButtons = (filters, currentFilter, labelOf = filterLabel) =>
|
||||
filters.map(mode =>
|
||||
form({ method: 'GET', action: '/inhabitants' },
|
||||
input({ type: 'hidden', name: 'filter', value: mode }),
|
||||
button({
|
||||
type: 'submit',
|
||||
class: currentFilter === mode ? 'filter-btn active' : 'filter-btn'
|
||||
}, i18n[mode + 'Button'] || i18n[mode + 'SectionTitle'] || mode)
|
||||
}, labelOf(mode))
|
||||
)
|
||||
);
|
||||
|
||||
const MAIN_FILTERS = ['all', 'TOP', 'contacts', 'blocked', 'CVs', 'SUGGESTED', 'GALLERY'];
|
||||
|
||||
const renderMainFilters = (filter, isTop) =>
|
||||
div({ class: 'inhabitant-action' },
|
||||
...MAIN_FILTERS.map(mode =>
|
||||
form({ method: 'GET', action: '/inhabitants' },
|
||||
input({ type: 'hidden', name: 'filter', value: mode === 'TOP' ? 'TOP ACTIVITY' : mode }),
|
||||
button({
|
||||
type: 'submit',
|
||||
class: (mode === 'TOP' ? isTop : filter === mode) ? 'filter-btn active' : 'filter-btn'
|
||||
}, mode === 'TOP'
|
||||
? String(i18n.topSectionTitle || 'TOP').toUpperCase()
|
||||
: filterLabel(mode))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
|
@ -89,6 +110,27 @@ function lastActivityBadge(user, isMe) {
|
|||
|
||||
const lightboxId = (id) => 'inhabitant_' + String(id || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
|
||||
const cvField = (labelText, value) => value
|
||||
? div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, `${labelText}: `),
|
||||
span({ class: 'card-value' }, value)
|
||||
)
|
||||
: null;
|
||||
|
||||
const renderCvFields = (user) => {
|
||||
const languages = Array.isArray(user.languages) ? user.languages.filter(Boolean) : [];
|
||||
const skills = Array.isArray(user.skills) ? user.skills.filter(Boolean) : [];
|
||||
const fields = [
|
||||
cvField(i18n.locationLabel, user.location),
|
||||
cvField(i18n.languagesLabel, languages.length ? languages.join(', ').toUpperCase() : ''),
|
||||
cvField(i18n.skillsLabel, skills.length ? skills.join(', ') : ''),
|
||||
cvField(i18n.statusLabel || 'Status', user.status),
|
||||
cvField(i18n.preferencesLabel || 'Preferences', user.preferences),
|
||||
cvField(i18n.createdAtLabel || 'Created at', user.createdAt ? new Date(user.createdAt).toLocaleString() : '')
|
||||
].filter(Boolean);
|
||||
return fields.length ? div({ class: 'cv-card-fields' }, ...fields) : null;
|
||||
};
|
||||
|
||||
const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured) => {
|
||||
const isMe = user.id === currentUserId;
|
||||
const raw = user.visibilityPrefs || {};
|
||||
|
|
@ -102,7 +144,7 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured)
|
|||
wallet: raw.wallet === true,
|
||||
ecoTax: raw.ecoTax !== false,
|
||||
larpSign: raw.larpSign === true,
|
||||
gpg: raw.gpg !== false,
|
||||
gpg: raw.gpg === true,
|
||||
clearnet: hasClearnet,
|
||||
fediverse: raw.fediverse === true,
|
||||
fediverseHandle: typeof raw.fediverseHandle === 'string' ? raw.fediverseHandle : ''
|
||||
|
|
@ -111,7 +153,7 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured)
|
|||
div(
|
||||
{ class: 'card-header activity-card-header' },
|
||||
span(),
|
||||
renderContentActions(user.id, `/inhabitant/${encodeURIComponent(user.id)}`)
|
||||
renderContentActions(user.id, `/inhabitant/${encodeURIComponent(user.id)}`, { author: user.id })
|
||||
),
|
||||
div({ class: 'card-section inhabitants-card-body' },
|
||||
div({ class: 'inhabitant-card' },
|
||||
|
|
@ -128,26 +170,30 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured)
|
|||
estimatedUBI: user.estimatedUBI, lastClaimedDate: user.lastClaimedDate, totalClaimed: user.totalClaimed,
|
||||
larpHouse: user.larpHouse, stats: user.stats
|
||||
}, { relationshipNode: isMe ? span({ class: 'status you' }, i18n.relationshipYou) : null, excludeContent: true }),
|
||||
filter === 'CVs'
|
||||
? div(
|
||||
{ class: 'cv-actions doc-export-actions' },
|
||||
form(
|
||||
{ method: 'GET', action: `/cv/pdf/${encodeURIComponent(user.id)}` },
|
||||
button({ type: 'submit', class: 'filter-btn' }, i18n.generatePdf)
|
||||
),
|
||||
form(
|
||||
{ method: 'POST', action: `/cv/share/${encodeURIComponent(user.id)}` },
|
||||
button({ type: 'submit', class: 'filter-btn' }, i18n.sharePm)
|
||||
)
|
||||
)
|
||||
: null,
|
||||
!isMe
|
||||
? div(
|
||||
{ class: 'cv-actions' },
|
||||
form(
|
||||
{ method: 'GET', action: '/pm' },
|
||||
input({ type: 'hidden', name: 'recipients', value: user.id }),
|
||||
button({ type: 'submit', class: 'btn' }, i18n.pmCreateButton)
|
||||
)
|
||||
{ class: 'inhabitant-user-actions' },
|
||||
div({ class: 'inhabitant-relationship' }, renderRelationshipBlock(user.relationship || {}, user.id))
|
||||
)
|
||||
: null
|
||||
),
|
||||
div({ class: 'inhabitant-details' },
|
||||
h2(user.name || 'Anonymous'),
|
||||
user.description ? p(...renderUrl(user.description)) : null,
|
||||
filter === 'MATCHSKILLS' && user.commonSkills?.length
|
||||
? div({ class: 'matchskills' },
|
||||
p(`${i18n.commonSkills}: ${user.commonSkills.join(', ')}`),
|
||||
p(`${i18n.matchScore}: ${Math.round((user.matchScore || 0) * 100)}%`)
|
||||
)
|
||||
: null,
|
||||
filter === 'CVs' ? renderCvFields(user) : null,
|
||||
filter === 'SUGGESTED' && (user.followsYou || user.commonSkills?.length || user.mutualCount)
|
||||
? div({ class: 'suggested-meta' },
|
||||
user.followsYou ? span({ class: 'suggested-badge' }, i18n.suggestedFollowsYou || 'Follows you') : null,
|
||||
|
|
@ -160,31 +206,6 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured)
|
|||
filter === 'blocked' && user.isBlocked
|
||||
? p(i18n.blockedLabel) : null,
|
||||
p(userLink(user.id)),
|
||||
!isMe ? (() => {
|
||||
const rel = user.relationship || {}
|
||||
const blockedBoth = rel.blocking && rel.blockedBy
|
||||
const mutual = rel.following && rel.followsMe
|
||||
const supportAction = rel.following ? 'unfollow' : (rel.blocking ? 'unblock' : 'follow')
|
||||
return div({ class: 'relationship-status inhabitant-relationship' },
|
||||
blockedBoth
|
||||
? span({ class: 'status blocked' }, i18n.relationshipMutualBlock)
|
||||
: [
|
||||
rel.blocking ? span({ class: 'status blocked' }, i18n.relationshipBlocking) : null,
|
||||
rel.blockedBy ? span({ class: 'status blocked-by' }, i18n.relationshipBlockedBy) : null,
|
||||
mutual
|
||||
? span({ class: 'status mutual' }, i18n.relationshipMutuals)
|
||||
: [
|
||||
span({ class: 'status supporting' }, rel.following ? i18n.relationshipFollowing : i18n.relationshipNone),
|
||||
span({ class: 'status supported-by' }, rel.followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing)
|
||||
]
|
||||
],
|
||||
div({ class: 'relationship-actions' },
|
||||
form({ method: 'POST', action: `/${supportAction}/${encodeURIComponent(user.id)}` },
|
||||
button({ type: 'submit', class: 'btn' }, i18n[supportAction])
|
||||
)
|
||||
)
|
||||
)
|
||||
})() : null,
|
||||
renderContentStats(user.stats, i18n)
|
||||
)
|
||||
)
|
||||
|
|
@ -229,19 +250,11 @@ function msgIdOf(m) {
|
|||
}
|
||||
|
||||
exports.inhabitantsView = (inhabitants, filter, query, currentUserId, fediverseConfigured) => {
|
||||
const title = filter === 'contacts' ? i18n.yourContacts
|
||||
: filter === 'CVs' ? i18n.allCVs
|
||||
: filter === 'MATCHSKILLS' ? i18n.matchSkills
|
||||
: filter === 'SUGGESTED' ? i18n.suggestedSectionTitle
|
||||
: filter === 'blocked' ? i18n.blockedSectionTitle
|
||||
: filter === 'GALLERY' ? i18n.gallerySectionTitle
|
||||
: filter === 'TOP KARMA' ? i18n.topkarmaSectionTitle
|
||||
: filter === 'TOP ECO' ? (i18n.topecoSectionTitle || 'Top Eco')
|
||||
: filter === 'TOP ACTIVITY' ? i18n.topactivitySectionTitle
|
||||
: i18n.allInhabitants;
|
||||
const title = i18n.allInhabitants;
|
||||
|
||||
const showCVFilters = filter === 'CVs' || filter === 'MATCHSKILLS';
|
||||
const filters = ['all', 'TOP ACTIVITY', 'TOP KARMA', 'TOP ECO', 'contacts', 'SUGGESTED', 'blocked', 'CVs', 'MATCHSKILLS', 'GALLERY'];
|
||||
const showCVFilters = filter === 'CVs';
|
||||
const TOP_FILTERS = ['TOP ACTIVITY', 'TOP INACTIVITY', 'TOP KARMA', 'TOP ECO'];
|
||||
const isTop = TOP_FILTERS.includes(filter);
|
||||
|
||||
return template(
|
||||
title,
|
||||
|
|
@ -251,28 +264,33 @@ exports.inhabitantsView = (inhabitants, filter, query, currentUserId, fediverseC
|
|||
p(i18n.discoverPeople)
|
||||
),
|
||||
div({ class: 'filters' },
|
||||
form({ method: 'GET', action: '/inhabitants' },
|
||||
form({ method: 'GET', action: '/inhabitants', class: 'filter-box' },
|
||||
input({ type: 'hidden', name: 'filter', value: filter }),
|
||||
input({
|
||||
type: 'text',
|
||||
name: 'search',
|
||||
placeholder: i18n.searchInhabitantsPlaceholder,
|
||||
value: (query && query.search) || ''
|
||||
value: (query && query.search) || '',
|
||||
class: 'filter-box__input'
|
||||
}),
|
||||
showCVFilters
|
||||
? [
|
||||
input({ type: 'text', name: 'location', placeholder: i18n.filterLocation, value: (query && query.location) || '' }),
|
||||
input({ type: 'text', name: 'language', placeholder: i18n.filterLanguage, value: (query && query.language) || '' }),
|
||||
input({ type: 'text', name: 'skills', placeholder: i18n.filterSkills, value: (query && query.skills) || '' })
|
||||
]
|
||||
: null,
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.applyFilters)
|
||||
div({ class: 'filter-box__controls' },
|
||||
showCVFilters
|
||||
? [
|
||||
input({ type: 'text', name: 'location', placeholder: i18n.filterLocation, value: (query && query.location) || '', class: 'filter-box__number' }),
|
||||
input({ type: 'text', name: 'language', placeholder: i18n.filterLanguage, value: (query && query.language) || '', class: 'filter-box__number' }),
|
||||
input({ type: 'text', name: 'skills', placeholder: i18n.filterSkills, value: (query && query.skills) || '', class: 'filter-box__number' })
|
||||
]
|
||||
: null,
|
||||
button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
div({ class: 'inhabitant-action' },
|
||||
...generateFilterButtons(filters, filter)
|
||||
),
|
||||
renderMainFilters(filter, isTop),
|
||||
isTop
|
||||
? div({ class: 'inhabitant-action inhabitant-subfilters' },
|
||||
...generateFilterButtons(TOP_FILTERS, filter, (mode) => filterLabel(mode).replace(/^TOP\s+/, ''))
|
||||
)
|
||||
: null,
|
||||
filter === 'GALLERY'
|
||||
? renderGalleryInhabitants(inhabitants)
|
||||
: div({ class: 'inhabitants-list' },
|
||||
|
|
@ -338,7 +356,7 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) =
|
|||
wallet: rawPrefs.wallet === true,
|
||||
ecoTax: rawPrefs.ecoTax !== false,
|
||||
larpSign: rawPrefs.larpSign === true,
|
||||
gpg: rawPrefs.gpg !== false,
|
||||
gpg: rawPrefs.gpg === true,
|
||||
clearnet: rawPrefs.clearnet === true || clearnetSubKeys.some(k => rawPrefs[k] === true),
|
||||
fediverse: rawPrefs.fediverse === true,
|
||||
fediverseHandle: typeof rawPrefs.fediverseHandle === 'string' ? rawPrefs.fediverseHandle : ''
|
||||
|
|
@ -350,14 +368,18 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) =
|
|||
const providedBucket = typeof safe.lastActivityBucket === 'string' ? safe.lastActivityBucket : null;
|
||||
const dotClass = providedBucket === 'green' ? 'green' : providedBucket === 'orange' ? 'orange' : 'red';
|
||||
|
||||
const fieldNodes = [
|
||||
cvField(i18n.locationLabel, location),
|
||||
cvField(i18n.languagesLabel, languages.length ? languages.join(', ').toUpperCase() : ''),
|
||||
cvField(i18n.skillsLabel, skills.length ? skills.join(', ') : ''),
|
||||
cvField(i18n.statusLabel || 'Status', status),
|
||||
cvField(i18n.preferencesLabel || 'Preferences', preferences),
|
||||
cvField(i18n.createdAtLabel || 'Created at', createdAt)
|
||||
].filter(Boolean);
|
||||
|
||||
const detailNodes = [
|
||||
description ? p(...renderUrl(description)) : null,
|
||||
location ? p(`${i18n.locationLabel}: ${location}`) : null,
|
||||
languages.length ? p(`${i18n.languagesLabel}: ${languages.join(', ').toUpperCase()}`) : null,
|
||||
skills.length ? p(`${i18n.skillsLabel}: ${skills.join(', ')}`) : null,
|
||||
status ? p(`${i18n.statusLabel || 'Status'}: ${status}`) : null,
|
||||
preferences ? p(`${i18n.preferencesLabel || 'Preferences'}: ${preferences}`) : null,
|
||||
createdAt ? p(`${i18n.createdAtLabel || 'Created at'}: ${createdAt}`) : null
|
||||
fieldNodes.length ? div({ class: 'cv-card-fields' }, ...fieldNodes) : null
|
||||
].filter(Boolean);
|
||||
|
||||
return template(
|
||||
|
|
@ -367,9 +389,7 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) =
|
|||
h2(title),
|
||||
p(i18n.discoverPeople)
|
||||
),
|
||||
div({ class: 'mode-buttons' },
|
||||
...generateFilterButtons(['all', 'TOP ACTIVITY', 'TOP KARMA', 'TOP ECO', 'contacts', 'SUGGESTED', 'blocked', 'CVs', 'MATCHSKILLS', 'GALLERY'], 'all')
|
||||
),
|
||||
renderMainFilters('all', false),
|
||||
div({ class: 'inhabitant-card' },
|
||||
div({ class: 'inhabitant-left' },
|
||||
img({ class: 'inhabitant-photo-details', src: image, alt: name || 'Anonymous' }),
|
||||
|
|
@ -382,11 +402,16 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) =
|
|||
larpHouse, stats: safe.stats
|
||||
}, { excludeContent: true }),
|
||||
(!isMe && (id || viewedId))
|
||||
? form(
|
||||
{ method: 'GET', action: '/pm' },
|
||||
input({ type: 'hidden', name: 'recipients', value: id || viewedId }),
|
||||
button({ type: 'submit', class: 'btn' }, i18n.pmCreateButton)
|
||||
? div({ class: 'cv-actions doc-export-actions' },
|
||||
form(
|
||||
{ method: 'GET', action: '/pm' },
|
||||
input({ type: 'hidden', name: 'recipients', value: id || viewedId }),
|
||||
button({ type: 'submit', class: 'filter-btn' }, i18n.pmCreateButton)
|
||||
)
|
||||
)
|
||||
: null,
|
||||
(!isMe && (id || viewedId) && safe.relationship)
|
||||
? div({ class: 'inhabitant-relationship' }, renderRelationshipBlock(safe.relationship, id || viewedId))
|
||||
: null
|
||||
),
|
||||
(() => {
|
||||
|
|
@ -405,7 +430,7 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) =
|
|||
return div({ class: 'post' },
|
||||
visitBtn,
|
||||
parts.clean && parts.clean.trim() ? p(...renderUrl(parts.clean)) : null,
|
||||
...(parts.imgs || []).map(src => img({ src, class: 'post-image', alt: 'image' }))
|
||||
...(parts.imgs || []).map(src => renderZoomableImage(src, { imgClass: 'post-image', alt: 'image' }))
|
||||
);
|
||||
})
|
||||
) ]
|
||||
|
|
@ -420,3 +445,4 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) =
|
|||
};
|
||||
|
||||
exports.lastActivityBadge = lastActivityBadge;
|
||||
exports.resolvePhoto = resolvePhoto;
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
section(
|
||||
div({ class: 'pubs-section' },
|
||||
h2(i18n.invitesPubsTitle),
|
||||
p(i18n.invitesPubsHint),
|
||||
form(
|
||||
{ action: '/settings/invite/accept', method: 'post' },
|
||||
input({ name: 'invite', type: 'text', placeholder: i18n.invitesPubInviteCodePlaceholder, autofocus: true, required: true }),
|
||||
|
|
@ -129,6 +130,7 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
section(
|
||||
div({ class: 'federations-section' },
|
||||
h2(i18n.invitesFederationsTitle || 'Federations'),
|
||||
p(i18n.invitesFederationsHint),
|
||||
div({ class: 'conn-actions invites-pubs-actions' },
|
||||
form({ action: '/invites/refresh-pubs', method: 'post' }, button({ type: 'submit' }, i18n.invitesPubsRefresh || 'Refresh')),
|
||||
form({ action: '/invites/clear-unreachable', method: 'post' }, button({ type: 'submit' }, i18n.invitesPubsClearUnreachable || 'Remove unreachable')),
|
||||
|
|
@ -180,6 +182,7 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
section(
|
||||
div({ class: 'invites-houses', id: 'invites-houses' },
|
||||
h2(i18n.invitesHousesTitle || 'Houses'),
|
||||
p(i18n.invitesHousesHint),
|
||||
form(
|
||||
{ action: '/larp/invite/redeem', method: 'post' },
|
||||
input({ type: 'hidden', name: 'returnTo', value: '/invites' }),
|
||||
|
|
@ -192,6 +195,7 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
section(
|
||||
div({ class: 'invites-tribes', id: 'invites-tribes' },
|
||||
h2(i18n.invitesTribesTitle),
|
||||
p(i18n.invitesTribesHint),
|
||||
form(
|
||||
{ action: '/tribes/join-code', method: 'post' },
|
||||
input({ name: 'inviteCode', type: 'text', placeholder: i18n.invitesTribeInviteCodePlaceholder, required: true }),
|
||||
|
|
@ -203,6 +207,7 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
section(
|
||||
div({ class: 'invites-chats', id: 'invites-chats' },
|
||||
h2(i18n.invitesChatsTitle || 'Chats'),
|
||||
p(i18n.invitesChatsHint),
|
||||
form(
|
||||
{ action: '/chats/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesChatInviteCodePlaceholder || 'Enter chat invite code', required: true }),
|
||||
|
|
@ -214,6 +219,7 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
section(
|
||||
div({ class: 'invites-pads', id: 'invites-pads' },
|
||||
h2(i18n.invitesPadsTitle || 'Pads'),
|
||||
p(i18n.invitesPadsHint),
|
||||
form(
|
||||
{ action: '/pads/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesPadInviteCodePlaceholder || 'Enter pad invite code', required: true }),
|
||||
|
|
@ -225,6 +231,7 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
section(
|
||||
div({ class: 'invites-calendars', id: 'invites-calendars' },
|
||||
h2(i18n.invitesCalendarsTitle || 'Calendars'),
|
||||
p(i18n.invitesCalendarsHint),
|
||||
form(
|
||||
{ action: '/calendars/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesCalendarInviteCodePlaceholder || 'Enter calendar invite code', required: true }),
|
||||
|
|
@ -236,6 +243,7 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
section(
|
||||
div({ class: 'invites-events', id: 'invites-events' },
|
||||
h2(i18n.invitesEventsTitle || 'Events'),
|
||||
p(i18n.invitesEventsHint),
|
||||
form(
|
||||
{ action: '/events/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesEventInviteCodePlaceholder || 'Enter event invite code', required: true }),
|
||||
|
|
@ -247,6 +255,7 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
section(
|
||||
div({ class: 'invites-forums', id: 'invites-forums' },
|
||||
h2(i18n.invitesForumsTitle || 'Forums'),
|
||||
p(i18n.invitesForumsHint),
|
||||
form(
|
||||
{ action: '/forum/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesForumInviteCodePlaceholder || 'Enter forum invite code', required: true }),
|
||||
|
|
@ -258,6 +267,7 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
section(
|
||||
div({ class: 'invites-maps', id: 'invites-maps' },
|
||||
h2(i18n.invitesMapsTitle || 'Maps'),
|
||||
p(i18n.invitesMapsHint),
|
||||
form(
|
||||
{ action: '/maps/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesMapInviteCodePlaceholder || 'Enter map invite code', required: true }),
|
||||
|
|
@ -269,6 +279,7 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
section(
|
||||
div({ class: 'invites-shops', id: 'invites-shops' },
|
||||
h2(i18n.invitesShopsTitle || 'Shops'),
|
||||
p(i18n.invitesShopsHint),
|
||||
form(
|
||||
{ action: '/shops/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesShopInviteCodePlaceholder || 'Enter shop invite code', required: true }),
|
||||
|
|
@ -280,6 +291,7 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
section(
|
||||
div({ class: 'invites-inhabitants', id: 'invites-inhabitants' },
|
||||
h2(i18n.invitesInhabitantsTitle || 'Inhabitants'),
|
||||
p(i18n.invitesInhabitantsHint),
|
||||
form(
|
||||
{ action: '/invites/inhabitant/follow', method: 'post' },
|
||||
input({ name: 'feedId', id: 'inh_oasis_id', type: 'text', placeholder: '@...=.ed25519', pattern: '@[A-Za-z0-9+/_\\-]{43}=\\.ed25519', required: true, maxlength: 56 }),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
const { div, h1, h2, h3, p, section, button, form, input, span, img, a, br, table, tr, td, textarea, label, strong } = require("../server/node_modules/hyperaxe");
|
||||
const { div, h1, h2, h3, p, section, button, form, input, span, img, a, br, table, tr, td, textarea, label, strong, details, summary } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink } = require("./main_views");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
|
||||
const userId = config.keys.id;
|
||||
const moment = require("../server/node_modules/moment");
|
||||
|
||||
const fmtCycle = (c) => c && c.formatted ? c.formatted : '';
|
||||
|
|
@ -11,16 +14,24 @@ const renderCycleBanner = (cycle) => div({ class: 'larp-cycle-banner' },
|
|||
span({ class: 'larp-cycle-value' }, fmtCycle(cycle))
|
||||
);
|
||||
|
||||
const renderHouseBadges = ({ myHouse, governingHouse }) => div({ class: 'larp-house-badges' },
|
||||
myHouse ? div({ class: 'larp-my-house' },
|
||||
span({ class: 'larp-my-house-label' }, i18n.larpMyHouse || 'My House'),
|
||||
a({ href: `/larp/${myHouse.key}`, class: 'larp-my-house-link' }, myHouse.name)
|
||||
) : null,
|
||||
governingHouse ? div({ class: 'larp-my-house' },
|
||||
span({ class: 'larp-my-house-label' }, i18n.larpGoverning || 'Governing'),
|
||||
a({ href: `/larp/${governingHouse.key}`, class: 'larp-my-house-link' }, governingHouse.name)
|
||||
) : null
|
||||
);
|
||||
const renderHouseBadges = ({ myHouse, governingHouse, houses }) => {
|
||||
const academia = Array.isArray(houses) ? houses.find(h => h.key === 'academia') : null;
|
||||
const isNewcomer = !myHouse;
|
||||
return div({ class: 'larp-house-badges' },
|
||||
myHouse ? div({ class: 'larp-my-house' },
|
||||
span({ class: 'larp-my-house-label' }, i18n.larpMyHouse || 'My House'),
|
||||
a({ href: `/larp/${myHouse.key}`, class: 'larp-my-house-link' }, myHouse.name)
|
||||
) : null,
|
||||
governingHouse ? div({ class: 'larp-my-house' },
|
||||
span({ class: 'larp-my-house-label' }, i18n.larpGoverning || 'Ruling:'),
|
||||
a({ href: `/larp/${governingHouse.key}`, class: 'larp-my-house-link' }, governingHouse.name)
|
||||
) : null,
|
||||
isNewcomer && academia ? div({ class: 'larp-my-house larp-academia-hint' },
|
||||
span({ class: 'larp-my-house-label' }, i18n.larpStartHere),
|
||||
a({ href: `/larp/${academia.key}`, class: 'larp-my-house-link' }, academia.name)
|
||||
) : null
|
||||
);
|
||||
};
|
||||
|
||||
const renderHouseNav = (houses, currentKey) => div({ class: 'larp-house-nav' },
|
||||
span({ class: 'larp-house-nav-label' }, i18n.larpAllHouses || 'Houses:'),
|
||||
|
|
@ -31,7 +42,7 @@ const renderHouseNav = (houses, currentKey) => div({ class: 'larp-house-nav' },
|
|||
}, h.name))
|
||||
);
|
||||
|
||||
const renderHousePanel = (house, members, posts, { canPost, viewerHouseKey, cyclesUntilRuling }) => {
|
||||
const renderHousePanel = (house, members, posts, { canPost, viewerHouseKey, cyclesUntilRuling, canTakeTest = false }) => {
|
||||
const { renderEncryptedChip } = require('./clearnet_view');
|
||||
const isInHouse = viewerHouseKey === house.key;
|
||||
const isInLarp = viewerHouseKey !== null && viewerHouseKey !== undefined;
|
||||
|
|
@ -66,6 +77,9 @@ const renderHousePanel = (house, members, posts, { canPost, viewerHouseKey, cycl
|
|||
),
|
||||
p({ class: 'larp-detail-description' }, house.description),
|
||||
div({ class: 'larp-actions' },
|
||||
isAcademia && isInHouse && canTakeTest
|
||||
? a({ href: '/larp/test', class: 'filter-btn larp-start-journey' }, i18n.larpStartJourney)
|
||||
: null,
|
||||
isInHouse
|
||||
? a({ href: `/larp/tribe/${encodeURIComponent(house.key)}`, class: 'filter-btn' }, i18n.larpVisitTribe || 'Visit Tribe')
|
||||
: null,
|
||||
|
|
@ -162,54 +176,63 @@ const renderAcademiaJoinPanel = (allHouses, testStatus, housesById, questions, m
|
|||
div({ class: 'larp-academia-grid' },
|
||||
ordered.map(h => a({ href: `/larp/${h.key}`, class: h.key === myKey ? 'larp-academia-thumb larp-academia-thumb-mine' : 'larp-academia-thumb' },
|
||||
img({ src: houseImageSrc(h), alt: h.name, class: 'larp-academia-thumb-image' }),
|
||||
span({ class: 'larp-academia-thumb-name' }, h.name)
|
||||
span({ class: 'larp-academia-thumb-name' }, h.name),
|
||||
table({ class: 'larp-info-table larp-academia-thumb-table' },
|
||||
tr(td({ class: 'card-label' }, i18n.larpRolesLabel || 'Roles'), td({ class: 'card-value' }, h.roles)),
|
||||
tr(td({ class: 'card-label' }, i18n.larpFunctionLabel || 'Function'), td({ class: 'card-value' }, h.function)),
|
||||
tr(td({ class: 'card-label' }, i18n.larpMembersCount || 'Members'), td({ class: 'card-value' }, String(h.memberCount || 0)))
|
||||
)
|
||||
))
|
||||
),
|
||||
canTake && qs.length
|
||||
? [
|
||||
h2(i18n.larpWillTestTitle || 'WILL test'),
|
||||
p({ class: 'larp-will-test-hint' }, i18n.larpWillTestHint || 'Once completed, you will be assigned the house that best matches your answers. Your individual answers are processed locally and never stored — only the resulting house assignment is published to your feed.'),
|
||||
form({ method: 'POST', action: '/larp/test', class: 'larp-test-form larp-test-form-compact' },
|
||||
qs.map((q, idx) => div({ class: 'larp-test-question' },
|
||||
p({ class: 'larp-test-q-text' }, strong(`${idx + 1}. `), i18n[q.key] || q.question),
|
||||
div({ class: 'larp-test-options' },
|
||||
q.options.map((opt, oi) => label({ class: 'larp-test-option' },
|
||||
input({ type: 'radio', name: `q${idx}`, value: String(oi), required: 'required' }),
|
||||
' ', i18n[opt.key] || opt.text
|
||||
))
|
||||
)
|
||||
)),
|
||||
div({ class: 'larp-actions' },
|
||||
button({ type: 'submit', class: 'filter-btn' }, i18n.larpTestSubmit || 'Join House')
|
||||
)
|
||||
)
|
||||
]
|
||||
: null
|
||||
);
|
||||
};
|
||||
|
||||
const renderPostsBlock = (posts, house, canPost) => div({ class: 'larp-posts-block' },
|
||||
h2(i18n.larpPostsTitle || 'Wall'),
|
||||
canPost
|
||||
? form({ method: 'POST', action: '/larp/post', class: 'larp-post-form' },
|
||||
input({ type: 'hidden', name: 'house', value: house.key }),
|
||||
textarea({ id: 'larp_post_text', name: 'text', rows: '3', maxlength: '4000', placeholder: i18n.larpPostPlaceholder || 'What does this house need to say?' }),
|
||||
button({ type: 'submit', class: 'filter-btn larp-post-submit' }, i18n.larpPostSubmit || 'Publish')
|
||||
)
|
||||
: null,
|
||||
posts.length === 0
|
||||
? p({ class: 'empty' }, i18n.larpPostsEmpty || 'No posts yet.')
|
||||
: div({ class: 'larp-posts-list' },
|
||||
posts.map(post => div({ class: 'larp-post' },
|
||||
div({ class: 'larp-post-head' },
|
||||
userLink(post.author),
|
||||
span({ class: 'larp-post-time' }, moment(post.createdAt).format('YYYY-MM-DD HH:mm'))
|
||||
),
|
||||
p({ class: 'larp-post-text' }, post.text)
|
||||
))
|
||||
)
|
||||
|
||||
const renderPostsBlock = (posts, house, canPost) => {
|
||||
const stream = (Array.isArray(posts) ? posts : [])
|
||||
.map(post => ({ ts: Date.parse(post.createdAt) || post.ts || 0, post }))
|
||||
.sort((a, b) => b.ts - a.ts);
|
||||
|
||||
return div({ class: 'larp-posts-block' },
|
||||
h2(i18n.larpPostsTitle || 'Wall'),
|
||||
canPost
|
||||
? form({ method: 'POST', action: '/larp/post', class: 'larp-post-form' },
|
||||
input({ type: 'hidden', name: 'house', value: house.key }),
|
||||
textarea({ id: 'larp_post_text', name: 'text', rows: '3', maxlength: '4000', placeholder: i18n.larpPostPlaceholder || 'What does this house need to say?' }),
|
||||
button({ type: 'submit', class: 'filter-btn larp-post-submit' }, i18n.larpPostSubmit || 'Publish')
|
||||
)
|
||||
: null,
|
||||
stream.length === 0
|
||||
? p({ class: 'empty' }, i18n.larpPostsEmpty || 'No posts yet.')
|
||||
: div({ class: 'larp-posts-list' },
|
||||
stream.map(entry => div({ class: 'larp-post' },
|
||||
div({ class: 'larp-post-head' },
|
||||
userLink(entry.post.author),
|
||||
span({ class: 'larp-post-time' }, moment(entry.post.createdAt).format('YYYY/MM/DD HH:mm'))
|
||||
),
|
||||
p({ class: 'larp-post-text' }, entry.post.text)
|
||||
))
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const renderHouseSearch = (q) => div({ class: 'filters' },
|
||||
form({ method: 'GET', action: '/larp', class: 'filter-box' },
|
||||
input({ type: 'hidden', name: 'filter', value: 'houses' }),
|
||||
input({ type: 'text', name: 'q', value: q || '', placeholder: i18n.larpSearchPlaceholder, class: 'filter-box__input' }),
|
||||
div({ class: 'filter-box__controls' },
|
||||
button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const matchesHouse = (house, q) => {
|
||||
const needle = String(q || '').trim().toLowerCase();
|
||||
if (!needle) return true;
|
||||
return ['name', 'motto', 'roles', 'function', 'description']
|
||||
.some(f => String(house[f] || '').toLowerCase().includes(needle));
|
||||
};
|
||||
|
||||
const renderModeButtons = (filter) => div({ class: 'mode-buttons stats-mode-row' },
|
||||
['ruling', 'houses', 'rules'].map(m =>
|
||||
form({ method: 'GET', action: '/larp' },
|
||||
|
|
@ -226,33 +249,29 @@ const renderModeButtons = (filter) => div({ class: 'mode-buttons stats-mode-row'
|
|||
)
|
||||
);
|
||||
|
||||
const renderRules = () => div({ class: 'larp-rules' },
|
||||
h2(i18n.larpRulesTitle || 'F.A.Q.'),
|
||||
div({ class: 'larp-rules-section' },
|
||||
h3(i18n.larpRulesEntryTitle || 'Starting house'),
|
||||
p(i18n.larpRulesEntryText || 'Every inhabitant begins in ACADEMIA by default.')
|
||||
),
|
||||
div({ class: 'larp-rules-section' },
|
||||
h3(i18n.larpRulesOneHouseTitle || 'One house at a time'),
|
||||
p(i18n.larpRulesOneHouseText || 'You can belong to at most one house at any given moment. If you belong to none, you are in ACADEMIA. Every non-ACADEMIA house page shows a "Leave House" button to its members that resets membership back to ACADEMIA. Leaving does NOT waive the test cooldown.')
|
||||
),
|
||||
div({ class: 'larp-rules-section' },
|
||||
h3(i18n.larpRulesTestEntry || 'WILL test'),
|
||||
p(i18n.larpRulesTestEntryText || 'Each option weights one or more houses; when you submit, the system tallies the scores and auto-admits you into the highest-scoring house.')
|
||||
),
|
||||
div({ class: 'larp-rules-section' },
|
||||
h3(i18n.larpRulesInviteEntry || 'Invitation code'),
|
||||
p(i18n.larpRulesInviteEntryText || 'House members can generate invitation codes that grant direct access to the house.')
|
||||
),
|
||||
div({ class: 'larp-rules-section' },
|
||||
h3(i18n.larpRulesGovernanceTitle || 'Governance Wall'),
|
||||
p(i18n.larpRulesGovernanceText || 'During its cycle, the governing house wears the "Ruling" badge and is the only one whose Wall is surfaced publicly under the RULING tab.')
|
||||
),
|
||||
div({ class: 'larp-rules-section' },
|
||||
h3(i18n.larpRulesSignTitle || 'L.A.R.P. Emblem'),
|
||||
p(i18n.larpRulesSignText || 'Inhabitants can choose whether to add a sensor on their avatar with the emblem of their current house.')
|
||||
)
|
||||
);
|
||||
const renderRules = () => {
|
||||
const points = [
|
||||
i18n.larpRulesEntryText,
|
||||
i18n.larpRulesOneHouseText,
|
||||
i18n.larpRulesTestEntryText,
|
||||
i18n.larpRulesInviteEntryText,
|
||||
i18n.larpRulesGovernanceText,
|
||||
i18n.larpRulesWallText,
|
||||
i18n.larpRulesSignText
|
||||
];
|
||||
return div(
|
||||
{ class: 'card' },
|
||||
h2(i18n.larpRulesTitle || 'F.A.Q.'),
|
||||
div({ class: 'rules-points' },
|
||||
points.filter(Boolean).map((text, i) =>
|
||||
div({ class: 'rules-point' },
|
||||
span({ class: 'rules-point-num' }, String(i + 1)),
|
||||
span({ class: 'rules-point-text' }, text)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const renderHousesGrid = (houses, myHouseKey, governingKey) => {
|
||||
const inLarp = !!myHouseKey;
|
||||
|
|
@ -277,7 +296,9 @@ const renderHousesGrid = (houses, myHouseKey, governingKey) => {
|
|||
),
|
||||
p({ class: 'larp-card-motto' }, '“' + h.motto + '”'),
|
||||
p({ class: 'larp-card-roles' }, h.roles),
|
||||
p({ class: 'larp-card-count' }, `${i18n.larpMembersCount || 'Members'}: ${h.memberCount || 0}`),
|
||||
div({ class: 'tribe-card-members' },
|
||||
span({ class: 'tribe-members-count' }, `${i18n.larpMembersCount || 'Members'}: ${h.memberCount || 0}`)
|
||||
),
|
||||
h.openInviteCode ? p({ class: 'larp-card-count' }, i18n.tribeInviteCodeText, span({ class: 'tribe-open-invite-code' }, h.openInviteCode)) : null
|
||||
)
|
||||
);
|
||||
|
|
@ -285,11 +306,13 @@ const renderHousesGrid = (houses, myHouseKey, governingKey) => {
|
|||
);
|
||||
};
|
||||
|
||||
exports.larpListView = ({ filter, houses, myHouseKey, cycle, governingKey, governingHouse, governingMembers, governingPosts, canPost }) => {
|
||||
exports.larpListView = ({ filter, houses, myHouseKey, cycle, governingKey, governingHouse, governingMembers, governingPosts, canPost, q }) => {
|
||||
const title = i18n.larpTitle || 'L.A.R.P.';
|
||||
const description = i18n.larpDescription || 'A live action role-playing layer for collaborative experimentation.';
|
||||
const myHouse = houses.find(h => h.key === myHouseKey) || null;
|
||||
const mode = filter === 'houses' ? 'houses' : filter === 'rules' ? 'rules' : 'ruling';
|
||||
const search = String(q || '').trim();
|
||||
const mode = search ? 'houses' : filter === 'houses' ? 'houses' : filter === 'rules' ? 'rules' : 'ruling';
|
||||
const matched = search ? houses.filter(h => matchesHouse(h, search)) : houses;
|
||||
|
||||
return template(
|
||||
title,
|
||||
|
|
@ -299,10 +322,13 @@ exports.larpListView = ({ filter, houses, myHouseKey, cycle, governingKey, gover
|
|||
p(description)
|
||||
),
|
||||
renderCycleBanner(cycle),
|
||||
renderHouseBadges({ myHouse, governingHouse }),
|
||||
renderHouseBadges({ myHouse, governingHouse, houses }),
|
||||
renderModeButtons(mode),
|
||||
renderHouseSearch(search),
|
||||
mode === 'houses'
|
||||
? renderHousesGrid(houses, myHouseKey, governingKey)
|
||||
? (matched.length
|
||||
? renderHousesGrid(matched, myHouseKey, governingKey)
|
||||
: p({ class: 'no-content' }, i18n.larpNoHousesMatch))
|
||||
: mode === 'rules'
|
||||
? renderRules()
|
||||
: [
|
||||
|
|
@ -330,7 +356,8 @@ exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, hous
|
|||
const isAcademia = house.key === 'academia';
|
||||
const viewerInAcademia = myHouseKey === 'academia';
|
||||
const viewerIsMember = myHouseKey === house.key && house.key !== 'academia';
|
||||
const showWall = viewerIsMember || house.key === governingKey;
|
||||
const canWriteWall = myHouseKey === house.key;
|
||||
const showWall = viewerIsMember || house.key === governingKey || house.key === 'academia';
|
||||
const cycles = computeCyclesUntilRuling(house, governingKey);
|
||||
const housesById = Object.fromEntries(houses.map(h => [h.key, h]));
|
||||
|
||||
|
|
@ -342,9 +369,14 @@ exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, hous
|
|||
p({ class: 'larp-detail-motto' }, '“' + house.motto + '”')
|
||||
),
|
||||
renderCycleBanner(cycle),
|
||||
renderHouseBadges({ myHouse, governingHouse }),
|
||||
renderHouseBadges({ myHouse, governingHouse, houses }),
|
||||
renderHouseNav(houses, house.key),
|
||||
renderHousePanel(house, members, posts, { canPost, viewerHouseKey: myHouseKey, cyclesUntilRuling: cycles }),
|
||||
renderHousePanel(house, members, posts, {
|
||||
canPost,
|
||||
viewerHouseKey: myHouseKey,
|
||||
cyclesUntilRuling: cycles,
|
||||
canTakeTest: isAcademia && viewerInAcademia && !!(testStatus && testStatus.allowed)
|
||||
}),
|
||||
inviteCode
|
||||
? div({ class: 'larp-invite-banner' },
|
||||
p({ class: 'larp-invite-banner-title' }, strong(i18n.larpInviteBannerTitle || 'New invitation code')),
|
||||
|
|
@ -352,10 +384,10 @@ exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, hous
|
|||
p({ class: 'larp-invite-banner-code' }, inviteCode)
|
||||
)
|
||||
: null,
|
||||
isAcademia && viewerInAcademia ? renderAcademiaJoinPanel(houses, testStatus, housesById, questions, myHouseKey) : null,
|
||||
showWall
|
||||
? renderPostsBlock(posts, house, viewerIsMember)
|
||||
: null,
|
||||
isAcademia && viewerInAcademia ? renderAcademiaJoinPanel(houses, testStatus, housesById, questions, myHouseKey) : null
|
||||
? renderPostsBlock(posts, house, canWriteWall)
|
||||
: null
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
@ -363,17 +395,19 @@ exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, hous
|
|||
exports.larpTestView = ({ questions, cycle, houses, myHouseKey, governingKey, testStatus }) => {
|
||||
const myHouse = houses.find(h => h.key === myHouseKey) || null;
|
||||
const governingHouse = houses.find(h => h.key === governingKey) || null;
|
||||
const titleText = i18n.larpTestTitle || 'Profile test';
|
||||
const title = i18n.larpTitle || 'L.A.R.P.';
|
||||
const description = i18n.larpDescription || 'A live action role-playing layer for collaborative experimentation.';
|
||||
|
||||
return template(
|
||||
titleText,
|
||||
title,
|
||||
section(
|
||||
div({ class: 'tags-header' },
|
||||
h1(titleText),
|
||||
p(i18n.larpTestIntro || 'Answer the psychological questions. The system will pick the house that best fits your answers. You can attempt one test every 30 cycles.')
|
||||
h1(title),
|
||||
p(description)
|
||||
),
|
||||
renderCycleBanner(cycle),
|
||||
renderHouseBadges({ myHouse, governingHouse }),
|
||||
renderHouseBadges({ myHouse, governingHouse, houses }),
|
||||
renderModeButtons(''),
|
||||
testStatus && !testStatus.allowed
|
||||
? div({ class: 'larp-test-cooldown' },
|
||||
p((i18n.larpTestCooldownActive || 'Next test available in') + ' ' + formatCooldown(testStatus.nextAt - Date.now()) + ' ' + (i18n.larpTestCooldownDays || 'cycles')),
|
||||
|
|
@ -409,7 +443,7 @@ exports.larpTestResultView = ({ house, result, cycle, houses, myHouseKey, govern
|
|||
section(
|
||||
div({ class: 'tags-header' }, h1(titleText)),
|
||||
renderCycleBanner(cycle),
|
||||
renderHouseBadges({ myHouse, governingHouse }),
|
||||
renderHouseBadges({ myHouse, governingHouse, houses }),
|
||||
div({ class: 'larp-test-result' },
|
||||
ranking.length
|
||||
? div({ class: 'larp-test-ranking' },
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
const { div, h2, p, section, button, form, span, table, thead, tbody, tr, th, td, input, textarea, br, option, select } = require("../server/node_modules/hyperaxe");
|
||||
const { div, h2, p, section, button, form, span, table, thead, tbody, tr, th, td, input, textarea, br, option, select, a, label } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n } = require("./main_views");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderUrl } = require("../backend/renderUrl");
|
||||
|
||||
const userId = config.keys.id;
|
||||
|
||||
const safeArr = v => Array.isArray(v) ? v : [];
|
||||
|
||||
const FILTERS = ["today", "week", "month", "year", "always"];
|
||||
|
|
@ -18,22 +21,22 @@ const filterLabel = (f) => {
|
|||
return map[f] || f.toUpperCase();
|
||||
};
|
||||
|
||||
const renderFilterBar = (current, hasItems = false) =>
|
||||
div({ class: "logs-toolbar" },
|
||||
form({ method: "GET", action: "/logs", class: "logs-toolbar-inline" },
|
||||
const renderFilterBar = (current, hasItems = true) =>
|
||||
div({ class: "activity-sub-filter" },
|
||||
form({ method: "GET", action: "/logs", class: "sub-filter-form" },
|
||||
FILTERS.map(f =>
|
||||
button({
|
||||
type: "submit", name: "filter", value: f,
|
||||
class: current === f ? "filter-btn active" : "filter-btn"
|
||||
}, filterLabel(f))
|
||||
}, String(filterLabel(f)).toUpperCase())
|
||||
)
|
||||
),
|
||||
form({ method: "GET", action: "/logs", class: "logs-toolbar-inline" },
|
||||
form({ method: "GET", action: "/logs", class: "sub-filter-form" },
|
||||
input({ type: "hidden", name: "view", value: "create" }),
|
||||
button({ type: "submit", class: "create-button" }, i18n.logsCreate || 'Create Log')
|
||||
),
|
||||
hasItems
|
||||
? form({ method: "GET", action: "/logs/export", class: "logs-toolbar-inline" },
|
||||
? form({ method: "GET", action: "/logs/export", class: "sub-filter-form" },
|
||||
button({ type: "submit", class: "create-button" }, i18n.logsExport || 'Export Logs')
|
||||
)
|
||||
: null
|
||||
|
|
@ -71,12 +74,9 @@ const renderToolbar = (current, search, hasItems) =>
|
|||
renderSearchBox(current, search)
|
||||
);
|
||||
|
||||
const MAX_PREVIEW = 140;
|
||||
|
||||
const truncate = (str) => {
|
||||
const s = String(str || '');
|
||||
if (s.length <= MAX_PREVIEW) return s;
|
||||
return s.slice(0, MAX_PREVIEW).replace(/\s+\S*$/, '') + '…';
|
||||
const truncate = (value, max = 160) => {
|
||||
const text = String(value == null ? '' : value).trim();
|
||||
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
||||
};
|
||||
|
||||
const renderLogPreview = (item) => {
|
||||
|
|
@ -91,9 +91,8 @@ const renderTable = (items) => {
|
|||
tr(
|
||||
th(i18n.logsColumnDate || 'Date'),
|
||||
th(i18n.logsColumnType || 'Type'),
|
||||
th(i18n.logsColumnLog || 'Log'),
|
||||
th(''),
|
||||
th('')
|
||||
th({ class: "logs-col-log" }, i18n.logsColumnLog || 'Log'),
|
||||
th({ class: "logs-col-actions" }, '')
|
||||
)
|
||||
),
|
||||
tbody(
|
||||
|
|
@ -114,13 +113,13 @@ const renderTable = (items) => {
|
|||
renderLogPreview(item)
|
||||
),
|
||||
td({ class: "logs-col-actions" },
|
||||
form({ method: "GET", action: `/logs/view/${encodeURIComponent(item.key)}` },
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.logsViewDetails || 'View Details')
|
||||
)
|
||||
),
|
||||
td({ class: "logs-col-actions" },
|
||||
form({ method: "GET", action: `/logs/export/${encodeURIComponent(item.key)}` },
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.logsExportOne || 'Export')
|
||||
div({ class: "logs-row-actions" },
|
||||
form({ method: "GET", action: `/logs/view/${encodeURIComponent(item.key)}` },
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.logsViewDetails || 'View Details')
|
||||
),
|
||||
form({ method: "GET", action: `/logs/export/${encodeURIComponent(item.key)}` },
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.logsExportOne || 'Export')
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -161,7 +160,6 @@ const renderCreateForm = (mode, aiModOn) => {
|
|||
: div({ class: "div-center audio-form" },
|
||||
form({ method: "POST", action: "/logs/create" },
|
||||
input({ type: "hidden", name: "mode", value: "manual" }),
|
||||
span(i18n.logsManualPrompt || 'Write your log'), br(),
|
||||
textarea({ name: "text", rows: "8", required: true, placeholder: i18n.logsTextPlaceholder || 'Describe your experiences...' }),
|
||||
br(), br(),
|
||||
button({ type: "submit", class: "create-button" }, i18n.logsWriteButton || 'Write')
|
||||
|
|
@ -172,9 +170,8 @@ const renderCreateForm = (mode, aiModOn) => {
|
|||
|
||||
const renderEditForm = (entry) => {
|
||||
return div({ class: "div-center audio-form" },
|
||||
h2(i18n.logsEditTitle || 'Edit Log'),
|
||||
form({ method: "POST", action: `/logs/edit/${encodeURIComponent(entry.key)}` },
|
||||
span(i18n.logsManualPrompt || 'Write your log...'), br(),
|
||||
h2(i18n.logsEditTitle || 'Update Log'),
|
||||
form({ method: "POST", action: `/logs/update/${encodeURIComponent(entry.key)}` },
|
||||
textarea({ name: "text", rows: "8", required: true }, entry.text || ''),
|
||||
input({ type: "hidden", name: "label", value: entry.label || '' }),
|
||||
br(), br(),
|
||||
|
|
@ -189,25 +186,24 @@ const renderDetail = (entry) => {
|
|||
h2(headerLine),
|
||||
entry.label ? div({ class: "logs-entry-label" }, entry.label) : null,
|
||||
div({ class: "logs-detail-text" }, ...renderUrl(String(entry.text || ''))),
|
||||
div({ class: "logs-detail-actions" },
|
||||
div({ class: "tribe-side-actions logs-detail-actions" },
|
||||
form({ method: "GET", action: "/logs" },
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.walletBack || 'Back')
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.walletBack || 'Back')
|
||||
),
|
||||
form({ method: "GET", action: `/logs/export/${encodeURIComponent(entry.key)}` },
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.logsExportOne || 'Export')
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.generatePdf)
|
||||
),
|
||||
form({ method: "GET", action: "/logs" },
|
||||
input({ type: "hidden", name: "view", value: "edit" }),
|
||||
input({ type: "hidden", name: "id", value: entry.key }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.logsEdit || 'Edit')
|
||||
form({ method: "GET", action: `/logs/edit/${encodeURIComponent(entry.key)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.logsEdit || 'Update')
|
||||
),
|
||||
form({ method: "POST", action: `/logs/delete/${encodeURIComponent(entry.key)}` },
|
||||
button({ type: "submit", class: "filter-btn danger-btn" }, i18n.logsDelete || 'Delete')
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.logsDelete || 'Delete')
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
exports.logsView = (items, filter, mode, opts = {}) => {
|
||||
const listTitle = i18n.logsTitle || 'Logs';
|
||||
const description = i18n.logsDescription || 'Record your experience in the network.';
|
||||
|
|
@ -215,30 +211,23 @@ exports.logsView = (items, filter, mode, opts = {}) => {
|
|||
const aiModOn = !!opts.aiModOn;
|
||||
const hasItems = Array.isArray(items) && items.length > 0;
|
||||
|
||||
const screen = (...blocks) => template(
|
||||
listTitle,
|
||||
section(
|
||||
div({ class: "tags-header" }, h2(listTitle), p(description)),
|
||||
renderFilterBar(filter),
|
||||
...blocks
|
||||
)
|
||||
);
|
||||
|
||||
if (view === 'create') {
|
||||
const h = i18n.logsCreateTitle || 'Create Log';
|
||||
const body = section(
|
||||
div({ class: "tags-header" }, h2(h), p(description)),
|
||||
renderFilterBar(filter, hasItems),
|
||||
renderCreateForm(mode, aiModOn)
|
||||
);
|
||||
return template(h, body);
|
||||
return screen(renderCreateForm(mode, aiModOn));
|
||||
}
|
||||
if (view === 'edit' && opts.entry) {
|
||||
const h = i18n.logsEditTitle || 'Edit Log';
|
||||
const body = section(
|
||||
div({ class: "tags-header" }, h2(h), p(description)),
|
||||
renderEditForm(opts.entry)
|
||||
);
|
||||
return template(h, body);
|
||||
return screen(renderEditForm(opts.entry));
|
||||
}
|
||||
if (view === 'detail' && opts.entry) {
|
||||
const h = i18n.logsViewTitle || 'Log';
|
||||
const body = section(
|
||||
div({ class: "tags-header" }, h2(h), p(description)),
|
||||
renderDetail(opts.entry)
|
||||
);
|
||||
return template(h, body);
|
||||
return screen(renderDetail(opts.entry));
|
||||
}
|
||||
const body = section(
|
||||
div({ class: "tags-header" }, h2(listTitle), p(description)),
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ const renderBcsList = (bcsAudios) => {
|
|||
span({ class: "card-label" }, `${i18n.melodyByLabel || "By"}: `),
|
||||
userLink(a.author),
|
||||
span({ class: "melody-meta-sep" }, " · "),
|
||||
span({ class: "card-value" }, moment(a.createdAt).format("YYYY/MM/DD HH:mm:ss"))
|
||||
span({ class: "card-value" }, moment(a.createdAt).format("YYYY/MM/DD HH:mm"))
|
||||
)
|
||||
),
|
||||
a.url
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { div, h2, h3, h4, p, section, button, form, a, span, br, textarea, input, label, select, option, table, tr, td } = require("../server/node_modules/hyperaxe")
|
||||
const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton } = require("./main_views")
|
||||
const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton , renderContentActions } = require("./main_views")
|
||||
const { renderEncryptedChip } = require("./clearnet_view")
|
||||
const moment = require("../server/node_modules/moment")
|
||||
const { config } = require("../server/SSB_server.js")
|
||||
|
|
@ -105,6 +105,10 @@ const renderPadCard = (pad, filter, spreadInfo) => {
|
|||
renderLifespanChip(pad.lifetime, i18n)
|
||||
].filter(Boolean)
|
||||
return div({ class: "tribe-card" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(pad.rootId || pad.key, `/pads/${encodeURIComponent(pad.rootId)}`, { spread: spreadInfo || null, author: pad.author, favKind: 'pads', isFavorite: pad.isFavorite, reportTitle: pad.title })
|
||||
),
|
||||
div({ class: "tribe-card-body" },
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" },
|
||||
|
|
@ -113,17 +117,10 @@ const renderPadCard = (pad, filter, spreadInfo) => {
|
|||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
pad.deadline
|
||||
? p({ class: "job-meta-line" }, `${i18n.padDeadlineLabel || "Deadline"}: ${moment(pad.deadline).format("YYYY-MM-DD HH:mm")}`)
|
||||
? p({ class: "job-meta-line" }, `${i18n.padDeadlineLabel || "Deadline"}: ${moment(pad.deadline).format("YYYY/MM/DD HH:mm")}`)
|
||||
: null,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.padMembersLabel || "Members"}: ${pad.members.length}`)
|
||||
),
|
||||
(() => {
|
||||
const btn = renderSpreadButton(pad.rootId, spreadInfo);
|
||||
return btn ? div({ class: "card-spread-left" }, btn) : null;
|
||||
})(),
|
||||
div({ class: "visit-btn-centered" },
|
||||
a({ href: `/pads/${encodeURIComponent(pad.rootId)}`, class: "filter-btn" }, i18n.padVisitPad || "Visit Pad")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -139,7 +136,7 @@ const renderCreateForm = (padToEdit, params) => {
|
|||
},
|
||||
tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null,
|
||||
span(i18n.padTitleLabel || "Title"), require("../server/node_modules/hyperaxe").br(),
|
||||
input({ type: "text", name: "title", value: padToEdit ? padToEdit.title : "", placeholder: i18n.padTitlePlaceholder || "Enter pad title...", required: true }),
|
||||
input({ type: "text", name: "title", maxlength: "100", value: padToEdit ? padToEdit.title : "", placeholder: i18n.padTitlePlaceholder || "Enter pad title...", required: true }),
|
||||
require("../server/node_modules/hyperaxe").br(), require("../server/node_modules/hyperaxe").br(),
|
||||
span(i18n.padStatusLabel || "Status"), require("../server/node_modules/hyperaxe").br(),
|
||||
select({ name: "status" },
|
||||
|
|
@ -178,14 +175,7 @@ exports.renderPadInvitePage = (code) => {
|
|||
exports.padsView = async (pads, filter, padToEdit, params) => {
|
||||
const q = String((params && params.q) || "").trim()
|
||||
const isForm = filter === "create" || filter === "edit"
|
||||
const headerMap = {
|
||||
all: i18n.padAllSectionTitle || "Pads",
|
||||
mine: i18n.padMineSectionTitle || "Your Pads",
|
||||
recent: i18n.padRecentSectionTitle || "Recent Pads",
|
||||
open: i18n.padOpenSectionTitle || "Open Pads",
|
||||
closed: i18n.padClosedSectionTitle || "Closed Pads"
|
||||
}
|
||||
const headerText = headerMap[filter] || headerMap.all
|
||||
const headerText = i18n.padsTitle
|
||||
|
||||
const filteredPads = q
|
||||
? pads.filter(pd => String(pd.title || "").toLowerCase().includes(q.toLowerCase()))
|
||||
|
|
@ -199,12 +189,12 @@ exports.padsView = async (pads, filter, padToEdit, params) => {
|
|||
renderModeButtons(filter),
|
||||
!isForm
|
||||
? div({ class: "filters" },
|
||||
form({ method: "GET", action: "/pads" },
|
||||
form({ method: "GET", action: "/pads", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: filter }),
|
||||
input({ type: "text", name: "q", placeholder: i18n.padSearchPlaceholder || "Search pads...", value: q }),
|
||||
br(),
|
||||
button({ type: "submit" }, i18n.search),
|
||||
br()
|
||||
input({ type: "text", name: "q", placeholder: i18n.padSearchPlaceholder || "Search pads...", value: q, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
)
|
||||
: null,
|
||||
|
|
@ -213,7 +203,7 @@ exports.padsView = async (pads, filter, padToEdit, params) => {
|
|||
: div(
|
||||
filteredPads.length === 0
|
||||
? p({ class: "no-content" }, i18n.padsNoItems || "No pads found.")
|
||||
: div({ class: "tribe-grid" }, ...filteredPads.map(pd => renderPadCard(pd, filter, params && params.spreadMap && params.spreadMap.get(pd.rootId))))
|
||||
: div({ class: "tribe-grid pads-grid" }, ...filteredPads.map(pd => renderPadCard(pd, filter, params && params.spreadMap && params.spreadMap.get(pd.rootId))))
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -225,7 +215,6 @@ exports.singlePadView = async (pad, entries, params) => {
|
|||
const isMember = pad.members.includes(userId) || (!!pad.tribeId && !!pad.isTribeMember)
|
||||
const padClosed = pad.isClosed
|
||||
const returnTo = `/pads/${encodeURIComponent(pad.rootId)}`
|
||||
const shareUrl = `/pads/${encodeURIComponent(pad.rootId)}`
|
||||
const isRestrictedInviteOnly = !isMember && !isAuthor && pad.status === "INVITE-ONLY"
|
||||
|
||||
const tags = !isRestrictedInviteOnly && Array.isArray(pad.tags) && pad.tags.length > 0
|
||||
|
|
@ -237,76 +226,65 @@ exports.singlePadView = async (pad, entries, params) => {
|
|||
renderEncryptedChip(i18n),
|
||||
renderLifespanChip(pad.lifetime, i18n)
|
||||
].filter(Boolean)
|
||||
const inviteActions = [
|
||||
isAuthor && pad.status === "INVITE-ONLY"
|
||||
? form({ method: "POST", action: `/pads/generate-invite/${encodeURIComponent(pad.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite)
|
||||
)
|
||||
: null,
|
||||
(() => {
|
||||
if (!(isAuthor && pad.status === "INVITE-ONLY")) return null
|
||||
const invs = Array.isArray(pad.invites) ? pad.invites : []
|
||||
const openInvite = invs.find(inv => typeof inv === "object" && inv && inv.public === true && inv.code)
|
||||
if (openInvite) return [
|
||||
div({ class: "tribe-open-invite" },
|
||||
span({ class: "card-label" }, i18n.tribeInviteCodeText),
|
||||
span({ class: "tribe-open-invite-code" }, openInvite.code)
|
||||
),
|
||||
form({ method: "POST", action: `/pads/open-invite/remove/${encodeURIComponent(pad.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation)
|
||||
)
|
||||
]
|
||||
return form({ method: "POST", action: `/pads/open-invite/create/${encodeURIComponent(pad.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation)
|
||||
)
|
||||
})()
|
||||
].filter(Boolean)
|
||||
|
||||
const padSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(pad.rootId, null, {
|
||||
author: pad.author,
|
||||
favKind: 'pads',
|
||||
isFavorite: pad.isFavorite,
|
||||
spread: (params && params.spreads) || null,
|
||||
returnTo,
|
||||
reportTitle: pad.title
|
||||
})
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, pad.title || "\u2014")
|
||||
),
|
||||
detailChips.length ? div({ class: "card-chips-row" }, ...detailChips) : null,
|
||||
div({ class: "shop-share" },
|
||||
span({ class: "tribe-info-label" }, i18n.padShareUrl || "Share URL"),
|
||||
input({ type: "text", readonly: true, value: shareUrl, class: "shop-share-input" })
|
||||
table({ class: "tribe-info-table jobs-info-table" },
|
||||
tr(td({ class: "tribe-info-label" }, i18n.padCreated || "Created"), td({ class: "tribe-info-value", colspan: "3" }, moment(pad.createdAt).format("YYYY/MM/DD HH:mm"))),
|
||||
(isRestrictedInviteOnly || !pad.deadline) ? null : tr(td({ class: "tribe-info-label" }, i18n.padDeadlineLabel || "Deadline"), td({ class: "tribe-info-value", colspan: "3" }, moment(pad.deadline).format("YYYY/MM/DD HH:mm"))),
|
||||
isRestrictedInviteOnly ? null : tr(td({ class: "tribe-info-value pad-author-cell", colspan: "4" }, userLink(pad.author)))
|
||||
),
|
||||
(() => {
|
||||
const btn = renderSpreadButton(pad.rootId);
|
||||
return btn ? div({ class: "card-spread-centered" }, btn) : null;
|
||||
})(),
|
||||
tags,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.padMembersLabel || "Members"}: ${pad.members.length}`)
|
||||
),
|
||||
table({ class: "tribe-info-table" },
|
||||
tr(td({ class: "tribe-info-label" }, i18n.padCreated || "Created"), td({ class: "tribe-info-value", colspan: "3" }, moment(pad.createdAt).format("YYYY-MM-DD"))),
|
||||
isRestrictedInviteOnly ? null : tr(td({ class: "tribe-info-value pad-author-cell", colspan: "4" }, userLink(pad.author))),
|
||||
isRestrictedInviteOnly ? null : tr(td({ class: "tribe-info-label" }, i18n.padDeadlineLabel || "Deadline"), td({ class: "tribe-info-value", colspan: "3" }, pad.deadline ? moment(pad.deadline).format("YYYY-MM-DD HH:mm") : "\u2014"))
|
||||
),
|
||||
isRestrictedInviteOnly ? null : div({ class: "tribe-side-actions" },
|
||||
isAuthor && pad.status === "INVITE-ONLY"
|
||||
? form({ method: "POST", action: `/pads/generate-invite/${encodeURIComponent(pad.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite)
|
||||
)
|
||||
: null,
|
||||
(() => {
|
||||
if (!(isAuthor && pad.status === "INVITE-ONLY")) return null
|
||||
const invs = Array.isArray(pad.invites) ? pad.invites : []
|
||||
const openInvite = invs.find(inv => typeof inv === "object" && inv && inv.public === true && inv.code)
|
||||
if (openInvite) return [
|
||||
div({ class: "tribe-open-invite" },
|
||||
span({ class: "card-label" }, i18n.tribeInviteCodeText),
|
||||
span({ class: "tribe-open-invite-code" }, openInvite.code)
|
||||
),
|
||||
form({ method: "POST", action: `/pads/open-invite/remove/${encodeURIComponent(pad.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation)
|
||||
)
|
||||
]
|
||||
return form({ method: "POST", action: `/pads/open-invite/create/${encodeURIComponent(pad.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation)
|
||||
)
|
||||
})(),
|
||||
form(
|
||||
{ method: "POST", action: pad.isFavorite ? `/pads/favorites/remove/${encodeURIComponent(pad.key)}` : `/pads/favorites/add/${encodeURIComponent(pad.key)}` },
|
||||
returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null,
|
||||
button({ type: "submit", class: "tribe-action-btn" }, pad.isFavorite ? (i18n.padRemoveFavorite || "Remove Favorite") : (i18n.padAddFavorite || "Add Favorite"))
|
||||
),
|
||||
!isAuthor
|
||||
? a({ href: `/pm?to=${encodeURIComponent(pad.author)}`, class: "tribe-action-btn" }, "PM")
|
||||
: null,
|
||||
isAuthor
|
||||
? form({ method: "GET", action: "/pads" },
|
||||
input({ type: "hidden", name: "filter", value: "edit" }),
|
||||
input({ type: "hidden", name: "id", value: pad.rootId }),
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.padUpdate || "Update")
|
||||
)
|
||||
: null,
|
||||
isRestrictedInviteOnly ? null : div({ class: "tribe-side-actions housing-status-row" },
|
||||
span({ class: "card-label" }, `${i18n.padStatusLabel || "Status"}: `),
|
||||
renderPadStatusChip(pad.status, padClosed),
|
||||
isAuthor && pad.status !== "CLOSED" && !padClosed
|
||||
? form({ method: "POST", action: `/pads/close/${encodeURIComponent(pad.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.padClose || "Close Pad")
|
||||
)
|
||||
: null,
|
||||
isAuthor
|
||||
? form({ method: "POST", action: `/pads/delete/${encodeURIComponent(pad.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.padDelete || "Delete")
|
||||
button({ type: "submit", class: "status-btn project-control-btn" }, i18n.padClose || "Close")
|
||||
)
|
||||
: null
|
||||
),
|
||||
(isRestrictedInviteOnly || !inviteActions.length) ? null : div({ class: "tribe-side-actions" }, ...inviteActions),
|
||||
!isAuthor && pad.status === "INVITE-ONLY" && !isMember
|
||||
? div({ class: "pad-invite-section" },
|
||||
a({ class: "tribe-action-btn", href: "/invites#invites-pads" }, i18n.tribeEnterInvite)
|
||||
|
|
@ -317,7 +295,16 @@ exports.singlePadView = async (pad, entries, params) => {
|
|||
button({ type: "submit", class: "create-button" }, i18n.padStartEditing || "START EDITING!")
|
||||
)
|
||||
: null,
|
||||
tags
|
||||
isRestrictedInviteOnly || !isAuthor ? null : div({ class: "tribe-side-actions" },
|
||||
form({ method: "GET", action: "/pads" },
|
||||
input({ type: "hidden", name: "filter", value: "edit" }),
|
||||
input({ type: "hidden", name: "id", value: pad.rootId }),
|
||||
button({ type: "submit", class: "tribe-action-btn" }, i18n.padUpdate || "Update")
|
||||
),
|
||||
form({ method: "POST", action: `/pads/delete/${encodeURIComponent(pad.rootId)}` },
|
||||
button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.padDelete || "Delete")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
let canonicalEntries = entries
|
||||
|
|
@ -343,7 +330,7 @@ exports.singlePadView = async (pad, entries, params) => {
|
|||
h4(i18n.padVersionHistory || "Version History"),
|
||||
...visibleEntries.slice().reverse().map((e, idx) =>
|
||||
div({ class: "pad-version-item" },
|
||||
span({ class: "pad-version-date" }, moment(e.createdAt).format("YYYY-MM-DD HH:mm")),
|
||||
span({ class: "pad-version-date" }, moment(e.createdAt).format("YYYY/MM/DD HH:mm")),
|
||||
span({ class: "pad-version-author" },
|
||||
span({ class: "pad-author-swatch " + memberColorClass(pad.members, e.author) }),
|
||||
userLink(e.author)
|
||||
|
|
|
|||
|
|
@ -47,22 +47,22 @@ exports.pixeliaView = (pixelArt, errorMessage) => {
|
|||
input({ type: "number", id: "y", name: "y", min: 1, max: gridHeight, required: true }),
|
||||
label({ for: "color" }, i18n.colorLabel),
|
||||
select({ id: "color", name: "color", required: true },
|
||||
option({ value: "#000000", style: "background-color:#000000;" }, "Black"),
|
||||
option({ value: "#ffffff", style: "background-color:#ffffff;" }, "White"),
|
||||
option({ value: "#17f018", style: "background-color:#17f018;" }, "Green"),
|
||||
option({ value: "#ffbb00", style: "background-color:#ffbb00;" }, "Yellow"),
|
||||
option({ value: "#ff0000", style: "background-color:#ff0000;" }, "Red"),
|
||||
option({ value: "#0000ff", style: "background-color:#0000ff;" }, "Blue"),
|
||||
option({ value: "#ffff00", style: "background-color:#ffff00;" }, "Lime"),
|
||||
option({ value: "#00ff00", style: "background-color:#00ff00;" }, "Spring Green"),
|
||||
option({ value: "#00ffff", style: "background-color:#00ffff;" }, "Aqua"),
|
||||
option({ value: "#ff00ff", style: "background-color:#ff00ff;" }, "Fuchsia"),
|
||||
option({ value: "#a52a2a", style: "background-color:#a52a2a;" }, "Brown"),
|
||||
option({ value: "#800080", style: "background-color:#800080;" }, "Purple"),
|
||||
option({ value: "#808000", style: "background-color:#808000;" }, "Olive"),
|
||||
option({ value: "#00bfff", style: "background-color:#00bfff;" }, "Deep Sky Blue"),
|
||||
option({ value: "#d3d3d3", style: "background-color:#d3d3d3;" }, "Light Grey"),
|
||||
option({ value: "#ff6347", style: "background-color:#ff6347;" }, "Tomato")
|
||||
option({ value: "#000000", class: "pixelia-swatch pixelia-swatch-000000" }, "Black"),
|
||||
option({ value: "#ffffff", class: "pixelia-swatch pixelia-swatch-ffffff" }, "White"),
|
||||
option({ value: "#17f018", class: "pixelia-swatch pixelia-swatch-17f018" }, "Green"),
|
||||
option({ value: "#ffbb00", class: "pixelia-swatch pixelia-swatch-ffbb00" }, "Yellow"),
|
||||
option({ value: "#ff0000", class: "pixelia-swatch pixelia-swatch-ff0000" }, "Red"),
|
||||
option({ value: "#0000ff", class: "pixelia-swatch pixelia-swatch-0000ff" }, "Blue"),
|
||||
option({ value: "#ffff00", class: "pixelia-swatch pixelia-swatch-ffff00" }, "Lime"),
|
||||
option({ value: "#00ff00", class: "pixelia-swatch pixelia-swatch-00ff00" }, "Spring Green"),
|
||||
option({ value: "#00ffff", class: "pixelia-swatch pixelia-swatch-00ffff" }, "Aqua"),
|
||||
option({ value: "#ff00ff", class: "pixelia-swatch pixelia-swatch-ff00ff" }, "Fuchsia"),
|
||||
option({ value: "#a52a2a", class: "pixelia-swatch pixelia-swatch-a52a2a" }, "Brown"),
|
||||
option({ value: "#800080", class: "pixelia-swatch pixelia-swatch-800080" }, "Purple"),
|
||||
option({ value: "#808000", class: "pixelia-swatch pixelia-swatch-808000" }, "Olive"),
|
||||
option({ value: "#00bfff", class: "pixelia-swatch pixelia-swatch-00bfff" }, "Deep Sky Blue"),
|
||||
option({ value: "#d3d3d3", class: "pixelia-swatch pixelia-swatch-d3d3d3" }, "Light Grey"),
|
||||
option({ value: "#ff6347", class: "pixelia-swatch pixelia-swatch-ff6347" }, "Tomato")
|
||||
),
|
||||
button({ type: "submit" }, i18n.paintButton)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -156,7 +156,13 @@ exports.pmView = async (initialRecipients = '', initialSubject = '', initialText
|
|||
span({ class: "pm-fileshare-meta" }, `${fileSharePreview.sizeLabel || ''} · ${String(fileSharePreview.mime || 'application/octet-stream')}`)
|
||||
),
|
||||
form({ method: "POST", action: "/pm/file", class: "pm-fileshare-send-form" },
|
||||
input({ type: "hidden", name: "recipient", value: fileSharePreview.recipient }),
|
||||
fileSharePreview.recipient
|
||||
? input({ type: "hidden", name: "recipient", value: fileSharePreview.recipient })
|
||||
: div({ class: "pm-fileshare-recipient" },
|
||||
label({ for: "fs-preview-recipient" }, i18n.pmRecipients),
|
||||
br(),
|
||||
input({ id: "fs-preview-recipient", type: "text", name: "recipient", placeholder: i18n.fileShareRecipientPlaceholder || 'Enter Oasis ID (@....ed25519)', required: true, maxlength: "120" })
|
||||
),
|
||||
input({ type: "hidden", name: "subject", value: fileSharePreview.subject || '' }),
|
||||
input({ type: "hidden", name: "manifestBlobId", value: fileSharePreview.manifestBlobId }),
|
||||
input({ type: "hidden", name: "keyHex", value: fileSharePreview.keyHex }),
|
||||
|
|
|
|||
323
src/views/polls_view.js
Normal file
323
src/views/polls_view.js
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
const { div, h2, p, section, button, form, a, input, label, span, textarea, br, table, tr, td } = require("../server/node_modules/hyperaxe");
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
|
||||
const { template, i18n, userLink, renderOpinionsVoting, renderEngagement, renderSpreadButton, renderContentActions, renderStateChip, renderLifespanChip, renderSpreadEditWarning } = require("./main_views");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderUrl } = require("../backend/renderUrl");
|
||||
const { MAX_OPTIONS, MIN_OPTIONS, MAX_OPTION_LENGTH } = require("../models/polls_model_limits");
|
||||
|
||||
const userId = config.keys.id;
|
||||
|
||||
const FILTERS = [
|
||||
{ key: "ALL", i18n: "pollFilterAll" },
|
||||
{ key: "MINE", i18n: "pollFilterMine" },
|
||||
{ key: "RECENT", i18n: "pollFilterRecent" },
|
||||
{ key: "TOP", i18n: "pollFilterTop" },
|
||||
{ key: "VOTED", i18n: "pollFilterVoted" },
|
||||
{ key: "OPEN", i18n: "pollFilterOpen" },
|
||||
{ key: "CLOSED", i18n: "pollFilterClosed" }
|
||||
];
|
||||
|
||||
const safeArr = (v) => (Array.isArray(v) ? v : []);
|
||||
|
||||
const pct = (n, total) => (total > 0 ? Math.round((Number(n) || 0) / total * 100) : 0);
|
||||
const letterOf = (i) => {
|
||||
let n = Number(i) || 0;
|
||||
let out = "";
|
||||
do {
|
||||
out = String.fromCharCode(65 + (n % 26)) + out;
|
||||
n = Math.floor(n / 26) - 1;
|
||||
} while (n >= 0);
|
||||
return out;
|
||||
};
|
||||
const pctStep = (n, total) => Math.round(pct(n, total) / 5) * 5;
|
||||
|
||||
const outcomeOf = (poll) => {
|
||||
if (!poll.totalVoters) return i18n.pollNoVotesYet;
|
||||
const max = Math.max(...poll.options.map(o => poll.counts[o] || 0));
|
||||
if (!max) return i18n.pollNoVotesYet;
|
||||
const winners = poll.options
|
||||
.map((o, i) => ({ o, letter: letterOf(i) }))
|
||||
.filter(({ o }) => (poll.counts[o] || 0) === max);
|
||||
if (winners.length > 1) return `${i18n.pollTie}: ${winners.map(w => `${w.letter})`).join(", ")}`;
|
||||
return `${winners[0].letter}) ${pct(max, poll.totalVoters)}%`;
|
||||
};
|
||||
|
||||
const renderStatusChip = (poll) =>
|
||||
renderStateChip(poll.status === "OPEN" ? "whole" : "mutuals", "◆",
|
||||
poll.status === "OPEN" ? i18n.pollStatusOpen : i18n.pollStatusClosed);
|
||||
|
||||
const renderModeChips = (poll) => [
|
||||
poll.anonymous ? renderStateChip("mutuals", "◈", i18n.pollAnonymous) : null,
|
||||
poll.multiple ? renderStateChip("whole", "☰", i18n.pollMultiple) : null
|
||||
].filter(Boolean);
|
||||
|
||||
const renderResults = (poll) =>
|
||||
table({ class: "poll-results" },
|
||||
...poll.options.map((opt, i) =>
|
||||
tr({ class: poll.myChoices.includes(opt) ? "poll-result-row poll-result-mine" : "poll-result-row" },
|
||||
td({ class: "poll-result-option" }, `${letterOf(i)})`),
|
||||
td({ class: "poll-result-bar" },
|
||||
div({ class: "poll-bar" },
|
||||
div({ class: `poll-bar-fill poll-bar-fill-${pctStep(poll.counts[opt], poll.totalVoters)} poll-hue-${i % 8}` })
|
||||
)
|
||||
),
|
||||
td({ class: "poll-result-count" },
|
||||
`${poll.counts[opt] || 0} (${pct(poll.counts[opt], poll.totalVoters)}%)`)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const renderBallot = (poll, returnTo, basePath) => {
|
||||
if (poll.status === "CLOSED" || poll.hasVoted) return null;
|
||||
const inputType = poll.multiple ? "checkbox" : "radio";
|
||||
return form({ method: "POST", action: `${basePath}/vote/${encodeURIComponent(poll.id)}`, class: "poll-ballot" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
...poll.options.map((opt, i) =>
|
||||
div({ class: "poll-option-row" },
|
||||
label({ for: `opt-${i}-${poll.id}` },
|
||||
input({
|
||||
type: inputType, name: "choices", value: opt, id: `opt-${i}-${poll.id}`,
|
||||
...(poll.myChoices.includes(opt) ? { checked: true } : {})
|
||||
}),
|
||||
" ",
|
||||
`${letterOf(i)}) ${opt}`
|
||||
)
|
||||
)
|
||||
),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.pollVoteButton)
|
||||
);
|
||||
};
|
||||
|
||||
const renderPollCard = (poll, filter, spreadInfo, basePath = "/polls") => {
|
||||
const href = `${basePath}/${encodeURIComponent(poll.id)}`;
|
||||
const isOwn = String(poll.author) === String(userId);
|
||||
const chips = [
|
||||
renderStatusChip(poll),
|
||||
...renderModeChips(poll),
|
||||
poll.deadline
|
||||
? renderStateChip(poll.status === "OPEN" ? "whole" : "mutuals", "◷", `${i18n.pollDeadline}: ${moment(poll.deadline).format("YYYY/MM/DD HH:mm")}`)
|
||||
: null,
|
||||
renderLifespanChip(poll.lifetime, i18n)
|
||||
].filter(Boolean);
|
||||
return div({ class: "tribe-card poll-card" + (isOwn ? " own-content" : "") },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(poll.id, href, { spread: spreadInfo || null, author: poll.author, favKind: 'polls', isFavorite: poll.isFavorite, reportTitle: poll.question })
|
||||
),
|
||||
div({ class: "tribe-card-body" },
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, a({ href }, poll.question))
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.pollVoters}: ${poll.totalVoters}`)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const renderCreateForm = (poll = null, params = {}) => {
|
||||
const isEdit = !!poll;
|
||||
const action = isEdit ? `/polls/update/${encodeURIComponent(poll.id)}` : "/polls/create";
|
||||
return section(
|
||||
params.spreadWarning || null,
|
||||
div({ class: "div-center audio-form" },
|
||||
h2(isEdit ? i18n.pollEditTitle : i18n.pollCreateTitle),
|
||||
form({ method: "POST", action },
|
||||
label(i18n.pollQuestion),
|
||||
br(),
|
||||
input({ type: "text", name: "question", required: true, maxlength: "300", value: isEdit ? poll.question : "", placeholder: i18n.pollQuestionPlaceholder }),
|
||||
br(), br(),
|
||||
label(i18n.pollOptions),
|
||||
br(),
|
||||
textarea({
|
||||
name: "options",
|
||||
rows: String(MAX_OPTIONS),
|
||||
required: true,
|
||||
maxlength: String(MAX_OPTIONS * (MAX_OPTION_LENGTH + 1))
|
||||
}, isEdit ? poll.options.join("\n") : ""),
|
||||
br(),
|
||||
div({ class: "poll-switch" },
|
||||
input({ type: "hidden", name: "anonymous", value: "0" }),
|
||||
label(
|
||||
input({ type: "checkbox", name: "anonymous", value: "1", ...(isEdit && poll.anonymous ? { checked: true } : {}) }),
|
||||
" ", i18n.pollAnonymousLabel
|
||||
)
|
||||
),
|
||||
div({ class: "poll-switch" },
|
||||
input({ type: "hidden", name: "multiple", value: "0" }),
|
||||
label(
|
||||
input({ type: "checkbox", name: "multiple", value: "1", ...(isEdit && poll.multiple ? { checked: true } : {}) }),
|
||||
" ", i18n.pollMultipleLabel
|
||||
)
|
||||
),
|
||||
br(),
|
||||
label(i18n.pollDeadline),
|
||||
br(),
|
||||
input({
|
||||
type: "datetime-local", name: "deadline",
|
||||
min: moment().format("YYYY-MM-DDTHH:mm"),
|
||||
value: isEdit && poll.deadline ? moment(poll.deadline).format("YYYY-MM-DDTHH:mm") : ""
|
||||
}),
|
||||
br(), br(),
|
||||
label(i18n.pollTags),
|
||||
br(),
|
||||
input({ type: "text", name: "tags", value: isEdit ? poll.tags.join(", ") : "" }),
|
||||
br(), br(),
|
||||
button({ type: "submit", class: "create-button" }, isEdit ? i18n.pollUpdateButton : i18n.pollPublishButton)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const renderComments = (poll, comments, basePath) => {
|
||||
const href = `${basePath}/${encodeURIComponent(poll.id)}`;
|
||||
return renderSharedCommentsSection({
|
||||
action: `${href}/comments`,
|
||||
comments: comments,
|
||||
returnTo: href
|
||||
});
|
||||
};
|
||||
|
||||
const renderFilterBar = (filter, q, showSearch = true) =>
|
||||
section(
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/polls", class: "ui-toolbar ui-toolbar--filters" },
|
||||
...FILTERS.map(f =>
|
||||
button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase())
|
||||
),
|
||||
button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.pollCreateButton)
|
||||
)
|
||||
),
|
||||
showSearch ? div({ class: "filters" },
|
||||
form({ method: "GET", action: "/polls", class: "filter-box" },
|
||||
input({ type: "hidden", name: "filter", value: filter }),
|
||||
input({ type: "text", name: "q", value: q || "", placeholder: i18n.pollSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
) : null
|
||||
);
|
||||
|
||||
exports.pollsView = async (polls = [], filter = "ALL", params = {}) => {
|
||||
const mode = String(filter).toUpperCase();
|
||||
const spreadMap = params.spreadMap instanceof Map ? params.spreadMap : new Map();
|
||||
if (mode === "CREATE" || mode === "EDIT") {
|
||||
const editing = params.poll || null;
|
||||
if (editing) params = { ...params, spreadWarning: await renderSpreadEditWarning(editing.id) };
|
||||
return template(
|
||||
i18n.pollsTitle,
|
||||
section(div({ class: "tags-header" }, h2(i18n.pollsTitle), p(i18n.pollsDescription))),
|
||||
renderFilterBar(mode, params.q, false),
|
||||
renderCreateForm(editing, params)
|
||||
);
|
||||
}
|
||||
return template(
|
||||
i18n.pollsTitle,
|
||||
section(div({ class: "tags-header" }, h2(i18n.pollsTitle), p(i18n.pollsDescription))),
|
||||
renderFilterBar(mode, params.q),
|
||||
section(
|
||||
polls.length
|
||||
? div({ class: "jobs-grid" }, ...polls.map(pl => renderPollCard(pl, mode, spreadMap.get(pl.id))))
|
||||
: p({ class: "no-content" }, i18n.pollsNoItems)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
exports.singlePollView = async (poll, comments = [], params = {}) => {
|
||||
const basePath = "/polls";
|
||||
const href = `${basePath}/${encodeURIComponent(poll.id)}`;
|
||||
const isOwn = String(poll.author) === String(userId);
|
||||
const chips = [renderStatusChip(poll), ...renderModeChips(poll), renderLifespanChip(poll.lifetime, i18n)].filter(Boolean);
|
||||
|
||||
const infoRows = [];
|
||||
const pushRow = (labelText, valueNode) =>
|
||||
infoRows.push(tr(
|
||||
td({ class: "tribe-info-label" }, labelText),
|
||||
td({ class: "tribe-info-value" }, valueNode)
|
||||
));
|
||||
if (poll.deadline) pushRow(i18n.pollDeadline, moment(poll.deadline).format("YYYY/MM/DD HH:mm"));
|
||||
pushRow(i18n.pollOutcome, outcomeOf(poll));
|
||||
|
||||
const tagsNode = poll.tags.length
|
||||
? div({ class: "card-tags" }, poll.tags.map(tag =>
|
||||
a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)))
|
||||
: null;
|
||||
|
||||
const ownerActions = isOwn
|
||||
? div({ class: "tribe-side-actions" },
|
||||
poll.totalVoters === 0 && poll.status === "OPEN"
|
||||
? form({ method: "GET", action: "/polls" },
|
||||
input({ type: "hidden", name: "filter", value: "EDIT" }),
|
||||
input({ type: "hidden", name: "id", value: poll.id }),
|
||||
button({ type: "submit", class: "update-btn" }, i18n.pollUpdateButton)
|
||||
)
|
||||
: null,
|
||||
form({ method: "POST", action: `/polls/delete/${encodeURIComponent(poll.id)}` },
|
||||
button({ type: "submit", class: "delete-btn" }, i18n.pollDeleteButton)
|
||||
)
|
||||
)
|
||||
: null;
|
||||
|
||||
const pollSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(poll.id, null, { spread: params.spreads || null, author: poll.author, favKind: 'polls', isFavorite: poll.isFavorite, returnTo: href, reportTitle: poll.question })
|
||||
),
|
||||
div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, poll.question)),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
table({ class: "tribe-info-table jobs-info-table" }, ...infoRows),
|
||||
tagsNode,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.pollVoters}: ${poll.totalVoters}`)
|
||||
),
|
||||
isOwn
|
||||
? div({ class: "tribe-side-actions housing-status-row" },
|
||||
span({ class: "card-label" }, `${i18n.pollStatusLabel}: `),
|
||||
renderStatusChip(poll),
|
||||
poll.status === "OPEN"
|
||||
? form({ method: "POST", action: `/polls/close/${encodeURIComponent(poll.id)}` },
|
||||
button({ type: "submit", class: "status-btn project-control-btn" }, i18n.pollCloseButton)
|
||||
)
|
||||
: null
|
||||
)
|
||||
: null,
|
||||
ownerActions
|
||||
);
|
||||
|
||||
const pollMain = div({ class: "tribe-main" },
|
||||
div({ class: "job-section" },
|
||||
h2({ class: "job-section-title" }, i18n.pollOutcome),
|
||||
renderBallot(poll, href, basePath),
|
||||
renderResults(poll)
|
||||
),
|
||||
poll.votersHidden
|
||||
? null
|
||||
: (poll.voters.length
|
||||
? div({ class: "card-assigned-list" }, ...poll.voters.map(v => userLink(v)))
|
||||
: null),
|
||||
p({ class: "card-footer" },
|
||||
span({ class: "date-link" }, moment(poll.createdAt).format("YYYY/MM/DD HH:mm")),
|
||||
span(" · "),
|
||||
userLink(poll.author)
|
||||
),
|
||||
renderEngagement(poll.id,
|
||||
renderOpinionsVoting('/polls/opinions', poll.id, poll.opinions, href, poll.opinions_inhabitants),
|
||||
renderComments(poll, comments, basePath)
|
||||
)
|
||||
);
|
||||
|
||||
return template(
|
||||
poll.question,
|
||||
section(div({ class: "tags-header" }, h2(i18n.pollsTitle), p(i18n.pollsDescription))),
|
||||
renderFilterBar(String(params.filter || "ALL").toUpperCase(), params.q, false),
|
||||
section(div({ class: "tribe-details" }, pollSide, pollMain))
|
||||
);
|
||||
};
|
||||
|
||||
exports.renderPollCard = renderPollCard;
|
||||
exports.renderResults = renderResults;
|
||||
exports.renderBallot = renderBallot;
|
||||
exports.outcomeOf = outcomeOf;
|
||||
exports.letterOf = letterOf;
|
||||
|
|
@ -177,19 +177,19 @@ exports.statsView = (stats, filter) => {
|
|||
const pct = networkCO2 > 0 ? Math.min(100, (userCO2 / networkCO2) * 100) : 0;
|
||||
return div({ class: 'carbon-chart' },
|
||||
div({ class: 'carbon-bar-label' },
|
||||
span(i18n.statsCarbonUser || 'Your footprint'),
|
||||
span(`${userCO2} g CO₂`)
|
||||
),
|
||||
div({ class: 'carbon-bar-track' },
|
||||
div({ class: `carbon-bar-fill carbon-bar-mine ${wClass(pct)}` })
|
||||
),
|
||||
div({ class: 'carbon-bar-label' },
|
||||
span(i18n.statsCarbonNetwork || 'Network total'),
|
||||
span(`HcT — ${i18n.statsCarbonNetwork || 'Network total'}`),
|
||||
span(`${networkCO2} g CO₂`)
|
||||
),
|
||||
div({ class: 'carbon-bar-track' },
|
||||
div({ class: 'carbon-bar-fill carbon-bar-network stats-w-100' })
|
||||
),
|
||||
div({ class: 'carbon-bar-label' },
|
||||
span(`HcH — ${i18n.statsCarbonUser || 'Your footprint'}`),
|
||||
span(`${userCO2} g CO₂`)
|
||||
),
|
||||
div({ class: 'carbon-bar-track' },
|
||||
div({ class: `carbon-bar-fill carbon-bar-mine ${wClass(pct)}` })
|
||||
),
|
||||
p({ class: 'carbon-bar-note' }, strong(`${pct.toFixed(1)}%`), ` ${i18n.statsCarbonOfNetwork || 'of network total'}`),
|
||||
p({ class: 'carbon-bar-formula' }, 'Based on local data storage weight ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)'))
|
||||
);
|
||||
|
|
@ -201,13 +201,6 @@ exports.statsView = (stats, filter) => {
|
|||
const tombCO2 = parseFloat((tombMB * kWhPerMB * gCO2PerKWh).toFixed(4));
|
||||
const tombPct = networkCO2 > 0 ? Math.min(100, (tombCO2 / networkCO2) * 100) : 0;
|
||||
return div({ class: 'carbon-chart' },
|
||||
div({ class: 'carbon-bar-label' },
|
||||
span(i18n.statsCarbonTombstone || 'Tombstoning footprint'),
|
||||
span(`${tombCO2} g CO₂`)
|
||||
),
|
||||
div({ class: 'carbon-bar-track' },
|
||||
div({ class: `carbon-bar-fill carbon-bar-mine ${wClass(tombPct)}` })
|
||||
),
|
||||
div({ class: 'carbon-bar-label' },
|
||||
span(i18n.statsCarbonNetwork || 'Network total'),
|
||||
span(`${networkCO2} g CO₂`)
|
||||
|
|
@ -215,62 +208,59 @@ exports.statsView = (stats, filter) => {
|
|||
div({ class: 'carbon-bar-track' },
|
||||
div({ class: 'carbon-bar-fill carbon-bar-network stats-w-100' })
|
||||
),
|
||||
div({ class: 'carbon-bar-label' },
|
||||
span(i18n.statsCarbonTombstone || 'Tombstoning footprint'),
|
||||
span(`${tombCO2} g CO₂`)
|
||||
),
|
||||
div({ class: 'carbon-bar-track' },
|
||||
div({ class: `carbon-bar-fill carbon-bar-mine ${wClass(tombPct)}` })
|
||||
),
|
||||
p({ class: 'carbon-bar-note' }, strong(`${tombPct.toFixed(1)}%`), ` ${i18n.statsCarbonOfNetwork || 'of network total'} (${tombCount} tombstones × ~${avgTombBytes} bytes)`),
|
||||
p({ class: 'carbon-bar-formula' }, 'Based on estimated tombstone message size ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)'))
|
||||
);
|
||||
}
|
||||
const pct = Math.min(100, (networkCO2 / maxAnnualCO2) * 100);
|
||||
const now = new Date();
|
||||
const yearStart = new Date(now.getFullYear(), 0, 1).getTime();
|
||||
const yearEnd = new Date(now.getFullYear() + 1, 0, 1).getTime();
|
||||
const yearPct = Math.min(100, ((now.getTime() - yearStart) / (yearEnd - yearStart)) * 100);
|
||||
return div({ class: 'carbon-chart' },
|
||||
div({ class: 'carbon-bar-label' },
|
||||
span(i18n.statsCarbonNetwork || 'Network footprint'),
|
||||
span(`${networkCO2} g CO₂`)
|
||||
),
|
||||
div({ class: 'carbon-bar-track' },
|
||||
div({ class: `carbon-bar-fill carbon-bar-network ${wClass(pct)}` })
|
||||
),
|
||||
div({ class: 'carbon-bar-label' },
|
||||
span(i18n.statsCarbonMaxAnnual || 'Annual max estimate'),
|
||||
span(`${maxAnnualCO2} g CO₂`)
|
||||
),
|
||||
div({ class: 'carbon-bar-track' },
|
||||
div({ class: 'carbon-bar-fill carbon-bar-max stats-w-100' })
|
||||
div({ class: `carbon-bar-fill carbon-bar-max ${wClass(yearPct)}` })
|
||||
),
|
||||
p({ class: 'carbon-bar-note' }, strong(`${pct.toFixed(1)}%`), ` ${i18n.statsCarbonOfEstMax || 'of estimated max capacity'}`),
|
||||
div({ class: 'carbon-bar-label' },
|
||||
span(`HcT — ${i18n.statsCarbonNetwork || 'Network footprint'}`),
|
||||
span(`${networkCO2} g CO₂`)
|
||||
),
|
||||
div({ class: 'carbon-bar-track' },
|
||||
div({ class: `carbon-bar-fill carbon-bar-network ${wClass(pct)}` })
|
||||
),
|
||||
p({ class: 'carbon-bar-note' }, strong(`${pct.toFixed(1)}%`), ` ${i18n.statsCarbonOfEstMax || 'of estimated max capacity'} · `, strong(`${yearPct.toFixed(1)}%`), ` ${i18n.statsCarbonYearProgress || 'of the year elapsed'}`),
|
||||
p({ class: 'carbon-bar-formula' }, 'Based on local data storage weight ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)'))
|
||||
);
|
||||
})();
|
||||
|
||||
const headerCard = div({ class: 'stats-card' },
|
||||
const storageCard = div({ class: 'stats-card' },
|
||||
h3({ class: 'stats-section-h' }, i18n.statsStorageTitle || 'Storage'),
|
||||
table({ class: 'block-info-table' },
|
||||
tr(td({ class: 'card-label' }, i18n.statsCreatedAt), td({ class: 'card-value' }, stats.createdAt)),
|
||||
tr(td({ class: 'card-label' }, 'ID'), td({ class: 'card-value' }, userLink(stats.id))),
|
||||
tr(td({ class: 'card-label' }, i18n.statsBlobsSize), td({ class: 'card-value' }, stats.statsBlobsSize)),
|
||||
tr(td({ class: 'card-label' }, i18n.statsBlockchainSize), td({ class: 'card-value' }, stats.statsBlockchainSize)),
|
||||
tr(td({ class: 'card-label' }, i18n.statsSize), td({ class: 'card-value' }, stats.folderSize))
|
||||
)
|
||||
);
|
||||
|
||||
const totalInhabitants = stats.usersKPIs?.totalInhabitants || stats.inhabitants || 0;
|
||||
const networkKPIs = stats.networkKPIs || {};
|
||||
|
||||
const topStrip = div({ class: 'stats-block' },
|
||||
kpiGrid(
|
||||
kpi(i18n.bankingUserEngagementScore, C(stats, 'karmaScore')),
|
||||
kpi(i18n.statsUsersTitle, totalInhabitants),
|
||||
kpi(i18n.statsTotalMsgs || 'Total messages', networkKPIs.totalMsgs || 0),
|
||||
kpi(i18n.statsLogsTitle || 'Logs', stats?.logsCount || 0),
|
||||
kpi(i18n.statsAITraining, C(stats, 'aiExchange') || 0),
|
||||
kpi(i18n.statsPUBs, stats.pubsCount || 0)
|
||||
)
|
||||
);
|
||||
|
||||
const carbonCard = div({ class: 'stats-card' },
|
||||
h3({ class: 'stats-section-h' }, i18n.statsCarbonFootprintTitle || 'Carbon Footprint'),
|
||||
carbonChart
|
||||
);
|
||||
|
||||
const bankingCard = div({ class: 'stats-card' },
|
||||
const accountCard = div({ class: 'stats-card' },
|
||||
h3({ class: 'stats-section-h' }, i18n.statsAccountTitle || 'Account'),
|
||||
table({ class: 'block-info-table' },
|
||||
tr(td({ class: 'card-label' }, 'ID'), td({ class: 'card-value' }, userLink(stats.id))),
|
||||
tr(td({ class: 'card-label' }, i18n.statsCreatedAt), td({ class: 'card-value' }, stats.createdAt)),
|
||||
C(stats, 'karmaScore') > 0
|
||||
? tr(td({ class: 'card-label' }, i18n.bankingUserEngagementScore), td({ class: 'card-value' }, String(C(stats, 'karmaScore'))))
|
||||
: null,
|
||||
tr(td({ class: 'card-label' }, i18n.statsEcoWalletLabel), td({ class: 'card-value' }, a({ href: '/wallet', class: 'stats-link-break' }, stats?.banking?.myAddress || i18n.statsEcoWalletNotConfigured))),
|
||||
tr(td({ class: 'card-label' }, i18n.profileGpgChip || 'GPG'), td({ class: 'card-value' },
|
||||
stats?.gpgFingerprint
|
||||
|
|
@ -280,7 +270,27 @@ exports.statsView = (stats, filter) => {
|
|||
)
|
||||
);
|
||||
|
||||
const totalInhabitants = stats.usersKPIs?.totalInhabitants || stats.inhabitants || 0;
|
||||
const networkKPIs = stats.networkKPIs || {};
|
||||
|
||||
const networkStrip = div({ class: 'stats-block' },
|
||||
h3({ class: 'stats-section-h' }, i18n.statsNetworkTitle || 'Network'),
|
||||
kpiGrid(
|
||||
kpi(i18n.statsUsersTitle, totalInhabitants),
|
||||
kpi(i18n.statsTotalMsgs || 'Total messages', networkKPIs.totalMsgs || 0),
|
||||
kpi(i18n.statsLogsTitle || 'Logs', stats?.logsCount || 0),
|
||||
kpi(i18n.statsAITraining, C(stats, 'aiExchange') || 0),
|
||||
kpi(i18n.statsPUBs, stats.pubsCount || 0)
|
||||
)
|
||||
);
|
||||
|
||||
const carbonCard = div({ id: 'carbon', class: 'stats-card' },
|
||||
h3({ class: 'stats-section-h' }, i18n.statsCarbonFootprintTitle || 'Carbon Footprint'),
|
||||
carbonChart
|
||||
);
|
||||
|
||||
const networkBlock = div({ class: 'stats-block' },
|
||||
h3({ class: 'stats-section-h' }, i18n.statsAveragesTitle || 'Averages'),
|
||||
kpiGrid(
|
||||
filter === 'MINE'
|
||||
? kpi(i18n.statsMyShare || 'Your share of the network', `${fmtNum(networkKPIs.myShare || 0)}%`)
|
||||
|
|
@ -454,6 +464,7 @@ exports.statsView = (stats, filter) => {
|
|||
}
|
||||
return div({ class: 'stats-container' }, [
|
||||
div({ class: 'stats-block' },
|
||||
h3({ class: 'stats-section-h' }, i18n.TOMBSTONEButton),
|
||||
kpiGrid(
|
||||
kpi(i18n.TOMBSTONEButton, userTomb),
|
||||
kpi(i18n.statsTombstoneRatio, `${(stats.tombstoneKPIs?.ratio || 0).toFixed(2)}%`)
|
||||
|
|
@ -466,6 +477,7 @@ exports.statsView = (stats, filter) => {
|
|||
const ets = stats.ecoTaxStats || null;
|
||||
const userTax = Number(stats.userEcoinTax || 0);
|
||||
const ecoTaxBlock = ets ? div({ class: 'stats-card' },
|
||||
h3({ class: 'stats-section-h' }, i18n.statsEcoTaxTitle || 'ECO Tax'),
|
||||
table({ class: 'block-info-table' },
|
||||
filter === 'MINE'
|
||||
? tr(td({ class: 'card-label' }, i18n.bankTaxesUserAmount || 'Your ECO tax'), td({ class: 'card-value' }, `${userTax.toFixed(6)} ECO`))
|
||||
|
|
@ -489,18 +501,19 @@ exports.statsView = (stats, filter) => {
|
|||
modes.map(m =>
|
||||
form({ method: 'GET', action: '/stats' },
|
||||
input({ type: 'hidden', name: 'filter', value: m }),
|
||||
button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, i18n[m + 'Button'])
|
||||
button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, String(i18n[m + 'Button']).toUpperCase())
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
topStrip,
|
||||
headerCard,
|
||||
filter === 'ALL' ? networkStrip : null,
|
||||
filter === 'MINE' ? accountCard : null,
|
||||
filter === 'ALL' ? storageCard : null,
|
||||
tombMode,
|
||||
carbonCard,
|
||||
filter !== 'TOMBSTONE' ? ecoTaxBlock : null,
|
||||
bankingCard,
|
||||
allMode,
|
||||
mineMode,
|
||||
tombMode
|
||||
mineMode
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const { div, h2, p, section, button, form, a, input, br, span, label, select, option, progress, table, tr, td } = require("../server/node_modules/hyperaxe")
|
||||
const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views")
|
||||
const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views")
|
||||
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view")
|
||||
const moment = require("../server/node_modules/moment")
|
||||
const { config } = require("../server/SSB_server.js")
|
||||
|
||||
|
|
@ -130,7 +131,7 @@ const renderUpdatedLabel = (createdAt, updatedAt) => {
|
|||
const updatedTs = updatedAt ? new Date(updatedAt).getTime() : NaN
|
||||
const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs)
|
||||
return showUpdated
|
||||
? span({ class: "votations-comment-date" }, ` | ${i18n.transfersUpdatedAt}: ${moment(updatedAt).format("YYYY-MM-DD HH:mm")}`)
|
||||
? span({ class: "votations-comment-date" }, ` | ${i18n.transfersUpdatedAt}: ${moment(updatedAt).format("YYYY/MM/DD HH:mm")}`)
|
||||
: null
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +175,7 @@ const generateTransferCard = (transfer, filter, params = {}) => {
|
|||
div(
|
||||
{ class: "card-header activity-card-header" },
|
||||
span(),
|
||||
renderContentActions(transfer.id, `/transfers/${encodeURIComponent(transfer.id)}`)
|
||||
renderContentActions(transfer.id, `/transfers/${encodeURIComponent(transfer.id)}`, { spread: params.spreadMap && params.spreadMap.get(transfer.id) || null, author: transfer.from, favKind: 'transfers', isFavorite: transfer.isFavorite, reportTitle: transfer.concept })
|
||||
),
|
||||
div({ class: "card-section transfer-card-body" },
|
||||
div({ class: "shop-title-row" },
|
||||
|
|
@ -184,24 +185,14 @@ const generateTransferCard = (transfer, filter, params = {}) => {
|
|||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
!isUbi && dl && dl.isValid()
|
||||
? p({ class: "card-date-highlight" }, dl.format("YYYY-MM-DD HH:mm"))
|
||||
? p({ class: "card-date-highlight" }, dl.format("YYYY/MM/DD HH:mm"))
|
||||
: null,
|
||||
cat !== "TRUST"
|
||||
? div({ class: "job-price-line card-salary" }, fmtAmountWithUnit(transfer))
|
||||
? div({ class: "job-price-line card-salary transfer-amount-centered" }, fmtAmountWithUnit(transfer))
|
||||
: null,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.transfersConfirmations}: ${confirmedCount}/${required}`)
|
||||
),
|
||||
div(
|
||||
{ class: "card-comments-summary feed-opinions-section" },
|
||||
div(
|
||||
{ class: "comments-count" },
|
||||
span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "),
|
||||
span({ class: "card-value" }, String(Object.values(transfer.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)))
|
||||
),
|
||||
renderOpinionsVoting('/transfers/opinions', transfer.id, transfer.opinions, returnTo, transfer.opinions_inhabitants)
|
||||
),
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(transfer.id, params.spreadMap && params.spreadMap.get(transfer.id)))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -209,20 +200,7 @@ const generateTransferCard = (transfer, filter, params = {}) => {
|
|||
exports.transferView = async (transfers, filter, transferId, params = {}) => {
|
||||
const normalizedFilter = filter === "favs" ? "all" : (filter || "all")
|
||||
|
||||
const title =
|
||||
normalizedFilter === "mine" ? i18n.transfersMineSectionTitle :
|
||||
normalizedFilter === "ubi" ? i18n.transfersUBISectionTitle :
|
||||
normalizedFilter === "pending" ? i18n.transfersPendingSectionTitle :
|
||||
normalizedFilter === "top" ? i18n.transfersTopSectionTitle :
|
||||
normalizedFilter === "unconfirmed" ? i18n.transfersUnconfirmedSectionTitle :
|
||||
normalizedFilter === "closed" ? i18n.transfersClosedSectionTitle :
|
||||
normalizedFilter === "discarded" ? i18n.transfersDiscardedSectionTitle :
|
||||
normalizedFilter === "create" ? i18n.transfersCreateSectionTitle :
|
||||
normalizedFilter === "edit" ? i18n.transfersUpdateSectionTitle :
|
||||
normalizedFilter === "economic" ? (i18n.transfersEconomicSectionTitle || (i18n.transfersCategoryEconomic + " " + i18n.transfersTitle)) :
|
||||
normalizedFilter === "time" ? (i18n.transfersTimeSectionTitle || (i18n.transfersCategoryTime + " " + i18n.transfersTitle)) :
|
||||
normalizedFilter === "trust" ? (i18n.transfersTrustSectionTitle || (i18n.transfersCategoryTrust + " " + i18n.transfersTitle)) :
|
||||
i18n.transfersAllSectionTitle
|
||||
const title = i18n.transfersTitle
|
||||
|
||||
const q = safeText(params.q || "")
|
||||
const minAmountRaw = params.minAmount ?? ""
|
||||
|
|
@ -295,18 +273,18 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => {
|
|||
input({ type: "hidden", name: "minAmount", value: String(minAmountRaw ?? "") }),
|
||||
input({ type: "hidden", name: "maxAmount", value: String(maxAmountRaw ?? "") }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: normalizedFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: normalizedFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "ubi", class: normalizedFilter === "ubi" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterUBI),
|
||||
button({ type: "submit", name: "filter", value: "all", class: normalizedFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: normalizedFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "ubi", class: normalizedFilter === "ubi" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterUBI).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "economic", class: normalizedFilter === "economic" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterEconomic || (i18n.transfersCategoryEconomic || "ECONOMIC")),
|
||||
button({ type: "submit", name: "filter", value: "time", class: normalizedFilter === "time" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTime || (i18n.transfersCategoryTime || "TIME")),
|
||||
button({ type: "submit", name: "filter", value: "trust", class: normalizedFilter === "trust" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTrust || (i18n.transfersCategoryTrust || "TRUST")),
|
||||
button({ type: "submit", name: "filter", value: "market", class: normalizedFilter === "market" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterMarket),
|
||||
button({ type: "submit", name: "filter", value: "pending", class: normalizedFilter === "pending" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterPending),
|
||||
button({ type: "submit", name: "filter", value: "unconfirmed", class: normalizedFilter === "unconfirmed" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterUnconfirmed),
|
||||
button({ type: "submit", name: "filter", value: "closed", class: normalizedFilter === "closed" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterClosed),
|
||||
button({ type: "submit", name: "filter", value: "discarded", class: normalizedFilter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterDiscarded),
|
||||
button({ type: "submit", name: "filter", value: "top", class: normalizedFilter === "top" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTop),
|
||||
button({ type: "submit", name: "filter", value: "market", class: normalizedFilter === "market" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterMarket).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "pending", class: normalizedFilter === "pending" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterPending).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "unconfirmed", class: normalizedFilter === "unconfirmed" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterUnconfirmed).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "closed", class: normalizedFilter === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterClosed).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "discarded", class: normalizedFilter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterDiscarded).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "top", class: normalizedFilter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterTop).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.transfersCreateButton)
|
||||
)
|
||||
)
|
||||
|
|
@ -463,9 +441,14 @@ exports.singleTransferView = async (transfer, filter, params = {}) => {
|
|||
button({ type: "submit", class: "delete-btn" }, i18n.transfersDeleteButton)
|
||||
))
|
||||
}
|
||||
sideActions.push(form({ method: "GET", action: `/transfers/contract/${encodeURIComponent(transfer.id)}`, class: "transfer-contract-form" },
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.transfersExportContract || 'Create Contract')
|
||||
))
|
||||
const exportActions = div({ class: "doc-export-actions" },
|
||||
form({ method: "GET", action: `/transfers/contract/${encodeURIComponent(transfer.id)}`, class: "transfer-contract-form" },
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.transfersExportContract)
|
||||
),
|
||||
form({ method: "POST", action: `/transfers/${encodeURIComponent(transfer.id)}/share` },
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.sharePm)
|
||||
)
|
||||
)
|
||||
|
||||
const infoRows = []
|
||||
const pushRow = (labelText, valueNode) =>
|
||||
|
|
@ -477,15 +460,24 @@ exports.singleTransferView = async (transfer, filter, params = {}) => {
|
|||
pushRow(i18n.transfersTo, userLink(transfer.to))
|
||||
if (cat !== "TRUST") pushRow(i18n.transfersAmount, fmtAmountWithUnit(transfer))
|
||||
pushRow(i18n.transfersCategory || "Category", categoryLabel(cat))
|
||||
if (!isUbi) pushRow(i18n.transfersDeadline, dl && dl.isValid() ? dl.format("YYYY-MM-DD HH:mm") : "")
|
||||
if (!isUbi) pushRow(i18n.transfersDeadline, dl && dl.isValid() ? dl.format("YYYY/MM/DD HH:mm") : "")
|
||||
pushRow(i18n.transfersStatus, i18n[statusKey(transfer.status)] || String(transfer.status || ""))
|
||||
|
||||
const transferSide = div({ class: "tribe-side" },
|
||||
div({ class: "card-header activity-card-header" },
|
||||
renderContentActions(transfer.id, null, {
|
||||
spread: params.spreads || null,
|
||||
author: transfer.from,
|
||||
favKind: 'transfers',
|
||||
isFavorite: transfer.isFavorite,
|
||||
returnTo,
|
||||
reportTitle: transfer.concept
|
||||
})
|
||||
),
|
||||
div({ class: "shop-title-row" },
|
||||
h2({ class: "tribe-card-title" }, transfer.concept || i18n.transfersTitle)
|
||||
),
|
||||
chips.length ? div({ class: "card-chips-row" }, ...chips) : null,
|
||||
div({ class: "card-spread-centered" }, renderSpreadButton(transfer.id, params.spreads)),
|
||||
tagsNode,
|
||||
div({ class: "tribe-card-members" },
|
||||
span({ class: "tribe-members-count" }, `${i18n.transfersConfirmations}: ${confirmedCount}/${required}`)
|
||||
|
|
@ -496,50 +488,42 @@ exports.singleTransferView = async (transfer, filter, params = {}) => {
|
|||
span({ class: "card-value" }, a({ class: "user-link", href: `/blockexplorer/block/${encodeURIComponent(params.block.id)}` }, params.block.id))
|
||||
)
|
||||
: null,
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null
|
||||
sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null,
|
||||
exportActions
|
||||
)
|
||||
|
||||
const confirmedList = Array.isArray(transfer.confirmedBy) ? transfer.confirmedBy.filter(Boolean) : []
|
||||
|
||||
const transferMain = div({ class: "tribe-main" },
|
||||
div({ class: "job-section" },
|
||||
div({ class: "card-field" },
|
||||
span({ class: "card-label" }, `${i18n.transfersFrom}: `),
|
||||
span({ class: "card-value" }, userLink(transfer.from))
|
||||
),
|
||||
div({ class: "card-field" },
|
||||
span({ class: "card-label" }, `${i18n.transfersTo}: `),
|
||||
span({ class: "card-value" }, userLink(transfer.to))
|
||||
),
|
||||
cat !== "TRUST" ? div({ class: "card-field" },
|
||||
span({ class: "card-label" }, `${i18n.transfersAmount}: `),
|
||||
span({ class: "card-value card-salary" }, fmtAmountWithUnit(transfer))
|
||||
) : null,
|
||||
div({ class: "card-field" },
|
||||
span({ class: "card-label" }, `${i18n.transfersCategory || "Category"}: `),
|
||||
span({ class: "card-value" }, categoryLabel(cat))
|
||||
),
|
||||
div({ class: "card-field" },
|
||||
span({ class: "card-label" }, `${i18n.transfersConcept}: `),
|
||||
span({ class: "card-value" }, transfer.concept || "")
|
||||
),
|
||||
!isUbi && dl && dl.isValid() ? div({ class: "card-field" },
|
||||
span({ class: "card-label" }, `${i18n.transfersDeadline}: `),
|
||||
span({ class: "card-value" }, dl.format("YYYY-MM-DD HH:mm"))
|
||||
) : null,
|
||||
div({ class: "card-field" },
|
||||
span({ class: "card-label" }, `${i18n.transfersStatus}: `),
|
||||
span({ class: "card-value" }, i18n[statusKey(transfer.status)] || String(transfer.status || ""))
|
||||
)
|
||||
h2({ class: "job-section-title" }, i18n.transfersConcept),
|
||||
p({ class: "tribe-side-description" }, transfer.concept || "")
|
||||
),
|
||||
div({ class: "job-section" },
|
||||
h2({ class: "job-section-title" }, i18n.transfersConfirmations),
|
||||
p({ class: "tribe-side-description" }, `${confirmedCount}/${required}`),
|
||||
confirmedList.length
|
||||
? div({ class: "card-assigned-list" }, ...confirmedList.map(id => userLink(id)))
|
||||
: null
|
||||
),
|
||||
p({ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(transfer.createdAt).format("YYYY-MM-DD HH:mm")} ${i18n.performed} `),
|
||||
span({ class: "date-link" }, `${moment(transfer.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `),
|
||||
userLink(transfer.from),
|
||||
renderUpdatedLabel(transfer.createdAt, transfer.updatedAt)
|
||||
),
|
||||
renderOpinionsVoting('/transfers/opinions', transfer.id, transfer.opinions, returnTo, transfer.opinions_inhabitants)
|
||||
renderEngagement(transfer.id,
|
||||
renderOpinionsVoting('/transfers/opinions', transfer.id, transfer.opinions, returnTo, transfer.opinions_inhabitants),
|
||||
renderSharedCommentsSection({
|
||||
action: `/transfers/${encodeURIComponent(transfer.id)}/comments`,
|
||||
comments: params.comments || [],
|
||||
returnTo
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
return template(
|
||||
transfer.concept,
|
||||
section(div({ class: "tags-header" }, h2(i18n.transfersTitle), p(i18n.transfersDescription))),
|
||||
section(
|
||||
div(
|
||||
{ class: "filters" },
|
||||
|
|
@ -549,18 +533,18 @@ exports.singleTransferView = async (transfer, filter, params = {}) => {
|
|||
input({ type: "hidden", name: "minAmount", value: String(params.minAmount ?? "") }),
|
||||
input({ type: "hidden", name: "maxAmount", value: String(params.maxAmount ?? "") }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: normalizedFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterAll),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: normalizedFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterMine),
|
||||
button({ type: "submit", name: "filter", value: "ubi", class: normalizedFilter === "ubi" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterUBI),
|
||||
button({ type: "submit", name: "filter", value: "all", class: normalizedFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterAll).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "mine", class: normalizedFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterMine).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "ubi", class: normalizedFilter === "ubi" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterUBI).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "economic", class: normalizedFilter === "economic" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterEconomic || (i18n.transfersCategoryEconomic || "ECONOMIC")),
|
||||
button({ type: "submit", name: "filter", value: "time", class: normalizedFilter === "time" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTime || (i18n.transfersCategoryTime || "TIME")),
|
||||
button({ type: "submit", name: "filter", value: "trust", class: normalizedFilter === "trust" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTrust || (i18n.transfersCategoryTrust || "TRUST")),
|
||||
button({ type: "submit", name: "filter", value: "market", class: normalizedFilter === "market" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterMarket),
|
||||
button({ type: "submit", name: "filter", value: "pending", class: normalizedFilter === "pending" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterPending),
|
||||
button({ type: "submit", name: "filter", value: "unconfirmed", class: normalizedFilter === "unconfirmed" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterUnconfirmed),
|
||||
button({ type: "submit", name: "filter", value: "closed", class: normalizedFilter === "closed" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterClosed),
|
||||
button({ type: "submit", name: "filter", value: "discarded", class: normalizedFilter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterDiscarded),
|
||||
button({ type: "submit", name: "filter", value: "top", class: normalizedFilter === "top" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTop),
|
||||
button({ type: "submit", name: "filter", value: "market", class: normalizedFilter === "market" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterMarket).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "pending", class: normalizedFilter === "pending" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterPending).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "unconfirmed", class: normalizedFilter === "unconfirmed" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterUnconfirmed).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "closed", class: normalizedFilter === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterClosed).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "discarded", class: normalizedFilter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterDiscarded).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "top", class: normalizedFilter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterTop).toUpperCase()),
|
||||
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.transfersCreateButton)
|
||||
)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const { div, h2, h3, p, section, button, form, a, input, img, label, select, opt
|
|||
const moment = require("../server/node_modules/moment");
|
||||
const { template, i18n, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderModeChip, renderInviteQrCard, renderContentActions } = require('./main_views');
|
||||
const { renderEncryptedChip: renderTribeEncryptedChip } = require('./clearnet_view');
|
||||
const { renderResults: renderPollResults, renderBallot: renderPollBallot } = require('./polls_view');
|
||||
const { config } = require('../server/SSB_server.js');
|
||||
const { renderUrl } = require('../backend/renderUrl');
|
||||
const { renderMapLocationUrl, renderMapLocationGrid, renderMapLocationVisitLabel } = require("./maps_view");
|
||||
|
|
@ -166,25 +167,17 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul
|
|||
return cb - ca;
|
||||
});
|
||||
|
||||
const title =
|
||||
filter === 'recent' ? i18n.tribeRecentSectionTitle :
|
||||
filter === 'mine' ? i18n.tribeMineSectionTitle :
|
||||
filter === 'create' ? i18n.tribeCreateSectionTitle :
|
||||
filter === 'edit' ? i18n.tribeUpdateSectionTitle :
|
||||
filter === 'gallery' ? i18n.tribeGallerySectionTitle :
|
||||
filter === 'top' ? i18n.tribeTopSectionTitle :
|
||||
filter === 'subtribes' ? (i18n.tribeSubTribes || 'SUB-TRIBES') :
|
||||
i18n.tribeAllSectionTitle;
|
||||
const title = i18n.tribesTitle;
|
||||
|
||||
const header = div({ class: 'tags-header' }, h2(title), p(i18n.tribeDescription));
|
||||
|
||||
const filters = div({ class: 'filters' },
|
||||
form({ method: 'GET', action: '/tribes' },
|
||||
form({ method: 'GET', action: '/tribes', class: 'filter-box' },
|
||||
input({ type: 'hidden', name: 'filter', value: filter }),
|
||||
input({ type: 'text', name: 'search', placeholder: i18n.searchTribesPlaceholder, value: query.search || '' }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.applyFilters),
|
||||
br()
|
||||
input({ type: 'text', name: 'search', placeholder: i18n.searchTribesPlaceholder, value: query.search || '', class: 'filter-box__input' }),
|
||||
div({ class: 'filter-box__controls' },
|
||||
button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
|
@ -214,7 +207,7 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul
|
|||
},
|
||||
label({ for: 'title' }, i18n.tribeTitleLabel),
|
||||
br,
|
||||
input({ type: 'text', name: 'title', id: 'title', required: true, placeholder: i18n.tribeTitlePlaceholder, value: tribeToEdit.title || '' }),
|
||||
input({ type: 'text', name: 'title', maxlength: '100', id: 'title', required: true, placeholder: i18n.tribeTitlePlaceholder, value: tribeToEdit.title || '' }),
|
||||
br(),
|
||||
label({ for: 'description' }, i18n.tribeDescriptionLabel),
|
||||
br,
|
||||
|
|
@ -265,7 +258,7 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul
|
|||
return div({ class: 'trending-card tribes-card' + (isOwn ? ' own-content' : '') },
|
||||
div({ class: 'card-header activity-card-header' },
|
||||
span(),
|
||||
renderContentActions(t.id, `/tribe/${encodeURIComponent(t.id)}`)
|
||||
renderContentActions(t.id, `/tribe/${encodeURIComponent(t.id)}`, { author: t.author, reportTitle: t.title })
|
||||
),
|
||||
div({ class: 'card-section tribes-card-body' },
|
||||
div({ class: 'tribe-card-image-wrapper' },
|
||||
|
|
@ -358,7 +351,7 @@ const renderFeedTribeView = async (feedItems, tribe, query = {}, filter) => {
|
|||
form({ method: 'GET', action: `/tribe/${encodeURIComponent(tribe.id)}` },
|
||||
input({ type: 'hidden', name: 'section', value: 'feed' }),
|
||||
input({ type: 'hidden', name: 'feedFilter', value: f }),
|
||||
button({ type: 'submit', class: feedFilter === f ? 'filter-btn active' : 'filter-btn' }, i18n[`tribeFeedFilter${f}`])
|
||||
button({ type: 'submit', class: feedFilter === f ? 'filter-btn active' : 'filter-btn' }, String(i18n[`tribeFeedFilter${f}`]).toUpperCase())
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
@ -399,7 +392,7 @@ const renderSectionNav = (tribe, section) => {
|
|||
if (!tribe.parentTribeId) firstGroup.push({ key: 'governance', label: i18n.tribeSectionGovernance || 'GOVERNANCE' });
|
||||
const sections = [
|
||||
{ items: firstGroup },
|
||||
{ items: [{ key: 'votations', label: i18n.tribeSectionVotations }, { key: 'events', label: i18n.tribeSectionEvents }, { key: 'tasks', label: i18n.tribeSectionTasks }] },
|
||||
{ items: [{ key: 'votations', label: i18n.tribeSectionVotations }, { key: 'polls', label: i18n.pollsTitle }, { key: 'events', label: i18n.tribeSectionEvents }, { key: 'tasks', label: i18n.tribeSectionTasks }] },
|
||||
{ items: [{ key: 'feed', label: i18n.tribeSectionFeed }, { key: 'forum', label: i18n.tribeSectionForum }, { key: 'maps', label: i18n.tribeSectionMaps || 'MAPS' }, { key: 'torrents', label: i18n.tribeSectionTorrents || 'TORRENTS' }, { key: 'pads', label: i18n.tribeSectionPads || 'PADS' }, { key: 'chats', label: i18n.tribeSectionChats || 'CHATS' }, { key: 'calendars', label: i18n.tribeSectionCalendars || 'CALENDARS' }] },
|
||||
{ items: [{ key: 'images', label: i18n.tribeSectionImages || 'IMAGES' }, { key: 'audios', label: i18n.tribeSectionAudios || 'AUDIOS' }, { key: 'videos', label: i18n.tribeSectionVideos || 'VIDEOS' }, { key: 'documents', label: i18n.tribeSectionDocuments || 'DOCUMENTS' }, { key: 'bookmarks', label: i18n.tribeSectionBookmarks || 'BOOKMARKS' }] },
|
||||
{ items: [{ key: 'tags', label: i18n.tribeSectionTags || 'TAGS' }, { key: 'search', label: i18n.tribeSectionSearch }] },
|
||||
|
|
@ -476,12 +469,12 @@ const contentTypeVerb = (ct) => {
|
|||
};
|
||||
|
||||
const contentTypeName = (ct) => {
|
||||
const map = { event: i18n.tribeSectionEvents, task: i18n.tribeSectionTasks, votation: i18n.tribeSectionVotations, forum: i18n.tribeSectionForum, 'forum-reply': i18n.tribeSectionForum, media: i18n.tribeSectionMedia, feed: i18n.tribeSectionFeed, pad: i18n.tribeSectionPads || 'PADS', chat: i18n.tribeSectionChats || 'CHATS', calendar: i18n.tribeSectionCalendars || 'CALENDARS', map: i18n.tribeSectionMaps || 'MAPS' };
|
||||
const map = { event: i18n.tribeSectionEvents, task: i18n.tribeSectionTasks, votation: i18n.tribeSectionVotations, poll: i18n.pollsTitle, forum: i18n.tribeSectionForum, 'forum-reply': i18n.tribeSectionForum, media: i18n.tribeSectionMedia, feed: i18n.tribeSectionFeed, pad: i18n.tribeSectionPads || 'PADS', chat: i18n.tribeSectionChats || 'CHATS', calendar: i18n.tribeSectionCalendars || 'CALENDARS', map: i18n.tribeSectionMaps || 'MAPS' };
|
||||
return map[ct] || ct;
|
||||
};
|
||||
|
||||
const activitySectionMap = {
|
||||
event: 'events', task: 'tasks', votation: 'votations',
|
||||
event: 'events', task: 'tasks', votation: 'votations', poll: 'polls',
|
||||
forum: 'forum', 'forum-reply': 'forum',
|
||||
feed: 'feed',
|
||||
pad: 'pads', chat: 'chats', calendar: 'calendars', map: 'maps', torrent: 'torrents'
|
||||
|
|
@ -522,7 +515,7 @@ const renderTribeActivitySection = (tribe, sectionData) => {
|
|||
item.title ? div({ class: 'card-field' }, span({ class: 'card-value' }, item.title)) : null,
|
||||
item.description ? p(String(item.description).substring(0, 200)) : null,
|
||||
replies.map(r => div({ class: 'thread-reply-item' },
|
||||
r.text ? p({ class: 'post-text', style: 'white-space:pre-wrap;' }, String(r.text)) : null,
|
||||
r.text ? p({ class: 'post-text post-text-pre' }, String(r.text)) : null,
|
||||
div({ class: 'card-footer thread-reply-footer' },
|
||||
span({ class: 'date-link' }, r.createdAt ? new Date(r.createdAt).toLocaleString() : ''),
|
||||
userLink(r.author, '')
|
||||
|
|
@ -910,6 +903,72 @@ const renderTasksSection = (tribe, items, query) => {
|
|||
);
|
||||
};
|
||||
|
||||
const renderTribePollsSection = (tribe, items, query) => {
|
||||
const polls = Array.isArray(items) ? items : [];
|
||||
const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`;
|
||||
if (query.action === 'create') {
|
||||
return div({ class: 'create-tribe-form' },
|
||||
form({ method: 'POST', action: `${tribeUrl}/polls/create` },
|
||||
label(i18n.pollQuestion), br,
|
||||
input({ type: 'text', name: 'question', required: true, maxlength: '300', placeholder: i18n.pollQuestionPlaceholder }), br(),
|
||||
label(i18n.pollOptions), br,
|
||||
textarea({ name: 'options', rows: 5, required: true, placeholder: i18n.pollOptionsPlaceholder }, ''), br(),
|
||||
div({ class: 'poll-switch' },
|
||||
input({ type: 'hidden', name: 'anonymous', value: '0' }),
|
||||
label(input({ type: 'checkbox', name: 'anonymous', value: '1' }), ' ', i18n.pollAnonymousLabel)
|
||||
),
|
||||
div({ class: 'poll-switch' },
|
||||
input({ type: 'hidden', name: 'multiple', value: '0' }),
|
||||
label(input({ type: 'checkbox', name: 'multiple', value: '1' }), ' ', i18n.pollMultipleLabel)
|
||||
),
|
||||
button({ type: 'submit', class: 'create-button' }, i18n.pollPublishButton)
|
||||
)
|
||||
);
|
||||
}
|
||||
const returnTo = `${tribeUrl}?section=polls`;
|
||||
return div({ class: 'tribe-content-list' },
|
||||
div({ class: 'tribe-content-header' },
|
||||
h2(i18n.pollsTitle),
|
||||
form({ method: 'GET', action: tribeUrl },
|
||||
input({ type: 'hidden', name: 'section', value: 'polls' }),
|
||||
input({ type: 'hidden', name: 'action', value: 'create' }),
|
||||
button({ type: 'submit', class: 'create-button' }, i18n.pollCreateButton)
|
||||
)
|
||||
),
|
||||
polls.length === 0 ? p(i18n.pollsNoItems) :
|
||||
polls.map(poll => div({ class: 'tribe-content-card poll-card' },
|
||||
h2(poll.undecryptable ? i18n.contentAccessDenied : poll.question),
|
||||
poll.undecryptable ? null : div({ class: 'card-chips-row' },
|
||||
statusBadge(poll.status),
|
||||
poll.anonymous ? span({ class: 'chat-poll-tag' }, i18n.pollAnonymous) : null,
|
||||
poll.multiple ? span({ class: 'chat-poll-tag' }, i18n.pollMultiple) : null
|
||||
),
|
||||
poll.undecryptable ? null : (poll.hasVoted || poll.status === 'CLOSED' ? renderPollResults(poll) : null),
|
||||
poll.undecryptable ? null : renderPollBallot(poll, returnTo, '/polls'),
|
||||
poll.undecryptable ? null : div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, `${i18n.pollVoters}: `),
|
||||
span({ class: 'card-value' }, String(poll.totalVoters))
|
||||
),
|
||||
div({ class: 'card-field' },
|
||||
span({ class: 'card-label' }, i18n.createdBy + ': '),
|
||||
userLink(poll.author)
|
||||
),
|
||||
poll.author === userId && poll.status === 'OPEN'
|
||||
? form({ method: 'POST', action: `/polls/close/${encodeURIComponent(poll.id)}` },
|
||||
input({ type: 'hidden', name: 'returnTo', value: returnTo }),
|
||||
button({ type: 'submit', class: 'filter-btn' }, i18n.pollCloseButton)
|
||||
)
|
||||
: null,
|
||||
poll.author === userId
|
||||
? form({ method: 'POST', action: `/polls/delete/${encodeURIComponent(poll.id)}` },
|
||||
input({ type: 'hidden', name: 'returnTo', value: returnTo }),
|
||||
button({ type: 'submit', class: 'filter-btn danger-btn' }, i18n.pollDeleteButton)
|
||||
)
|
||||
: null
|
||||
))
|
||||
);
|
||||
};
|
||||
|
||||
const renderVotationsSection = (tribe, items, query) => {
|
||||
const votations = Array.isArray(items) ? items : [];
|
||||
const action = query.action;
|
||||
|
|
@ -918,7 +977,7 @@ const renderVotationsSection = (tribe, items, query) => {
|
|||
return div({ class: 'create-tribe-form' },
|
||||
form({ method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/votations/create` },
|
||||
label({ for: 'title' }, i18n.tribeVotationTitle), br,
|
||||
input({ type: 'text', name: 'title', id: 'title', required: true, placeholder: i18n.tribeVotationTitle }), br(),
|
||||
input({ type: 'text', name: 'title', maxlength: '100', id: 'title', required: true, placeholder: i18n.tribeVotationTitle }), br(),
|
||||
label({ for: 'description' }, i18n.tribeVotationDescription), br,
|
||||
textarea({ name: 'description', id: 'description', rows: 3, placeholder: i18n.tribeVotationDescription }, ''), br(),
|
||||
label({ for: 'deadline' }, i18n.tribeVotationDeadline), br,
|
||||
|
|
@ -1128,12 +1187,7 @@ const renderForumSection = (tribe, items, query) => {
|
|||
t.description ? div({ class: 'forum-body' }, ...renderUrl((t.description || '').substring(0, 200))) : null,
|
||||
div({ class: 'forum-meta' },
|
||||
span({ class: 'forum-positive-votes' }, `▲: ${t.refeeds || 0}`),
|
||||
span({ class: 'forum-messages' }, `${(i18n.forumMessages || i18n.tribeForumReplies || 'MESSAGES').toUpperCase()}: ${replyCount}`),
|
||||
form({ method: 'GET', action: tribeUrl, class: 'visit-forum-form' },
|
||||
input({ type: 'hidden', name: 'section', value: 'forum' }),
|
||||
input({ type: 'hidden', name: 'thread', value: t.id }),
|
||||
button({ type: 'submit', class: 'filter-btn' }, i18n.forumVisitButton || 'VISIT')
|
||||
)
|
||||
span({ class: 'forum-messages' }, `${(i18n.forumMessages || i18n.tribeForumReplies || 'MESSAGES').toUpperCase()}: ${replyCount}`)
|
||||
),
|
||||
div({ class: 'forum-footer' },
|
||||
span({ class: 'date-link' }, `${new Date(t.createdAt).toLocaleString()} ${i18n.performed || ''}`),
|
||||
|
|
@ -1182,7 +1236,7 @@ const renderTribeMediaTypeSection = (tribe, items, query, mediaType) => {
|
|||
input({ type: 'hidden', name: 'mediaType', value: 'bookmark' }),
|
||||
input({ type: 'hidden', name: 'returnSection', value: sectionKey }),
|
||||
label({ for: 'title' }, i18n.tribeMediaTitle), br,
|
||||
input({ type: 'text', name: 'title', id: 'title', required: true, placeholder: i18n.tribeMediaTitle }), br(),
|
||||
input({ type: 'text', name: 'title', maxlength: '100', id: 'title', required: true, placeholder: i18n.tribeMediaTitle }), br(),
|
||||
label({ for: 'url' }, i18n.bookmarkUrlLabel || 'URL'), br,
|
||||
input({ type: 'url', name: 'url', id: 'url', required: true, placeholder: 'https://' }), br(),br(),
|
||||
label({ for: 'description' }, i18n.tribeMediaDescription), br,
|
||||
|
|
@ -1196,7 +1250,7 @@ const renderTribeMediaTypeSection = (tribe, items, query, mediaType) => {
|
|||
input({ type: 'hidden', name: 'mediaType', value: mediaType }),
|
||||
input({ type: 'hidden', name: 'returnSection', value: sectionKey }),
|
||||
label({ for: 'title' }, i18n.tribeMediaTitle), br,
|
||||
input({ type: 'text', name: 'title', id: 'title', required: true, placeholder: i18n.tribeMediaTitle }), br(),
|
||||
input({ type: 'text', name: 'title', maxlength: '100', id: 'title', required: true, placeholder: i18n.tribeMediaTitle }), br(),
|
||||
label({ for: 'description' }, i18n.tribeMediaDescription), br,
|
||||
textarea({ name: 'description', id: 'description', rows: 3, placeholder: i18n.tribeMediaDescription }, ''), br(),
|
||||
label({ for: 'media' }, i18n.tribeMediaUpload), br,
|
||||
|
|
@ -1542,6 +1596,7 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => {
|
|||
case 'events': sectionContent = renderEventsSection(tribe, sectionData, query); break;
|
||||
case 'tasks': sectionContent = renderTasksSection(tribe, sectionData, query); break;
|
||||
case 'votations': sectionContent = renderVotationsSection(tribe, sectionData, query); break;
|
||||
case 'polls': sectionContent = renderTribePollsSection(tribe, sectionData, query); break;
|
||||
case 'forum': sectionContent = renderForumSection(tribe, sectionData, query); break;
|
||||
case 'subtribes': sectionContent = renderSubTribesSection(tribe, sectionData, query); break;
|
||||
case 'search': sectionContent = renderTribeSearchSection(tribe, sectionData, query); break;
|
||||
|
|
@ -1636,6 +1691,15 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => {
|
|||
? a({ class: 'tribe-action-btn', href: inviteHref }, i18n.tribeEnterInvite)
|
||||
: null
|
||||
) : null,
|
||||
(isLarpHouse && larpHouseKey === 'academia' && isOutsider)
|
||||
? div({ class: 'tribe-side-actions' },
|
||||
form({ method: 'POST', action: '/larp/join' },
|
||||
input({ type: 'hidden', name: 'house', value: 'academia' }),
|
||||
input({ type: 'hidden', name: 'returnTo', value: `/tribe/${encodeURIComponent(tribe.id)}` }),
|
||||
button({ type: 'submit', class: 'tribe-action-btn' }, i18n.larpInviteRedeem || 'Join House')
|
||||
)
|
||||
)
|
||||
: null,
|
||||
(hasOpenInvite && canManageOpenInvite) ? div({ class: 'tribe-open-invite-panel' },
|
||||
div({ class: 'tribe-open-invite' },
|
||||
span({ class: 'tribe-members-count' }, i18n.tribeInviteCodeText),
|
||||
|
|
@ -1868,7 +1932,7 @@ const rulesBlock = (tribe, rules, isCreator) => div({},
|
|||
h3(i18n.tribeGovernanceAddRule || 'Add rule'),
|
||||
form({ method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/governance/rule/add` },
|
||||
label({}, i18n.tribeGovRuleTitle || 'Title'), br(),
|
||||
input({ type: 'text', name: 'title', required: true }), br(), br(),
|
||||
input({ type: 'text', name: 'title', maxlength: '100', required: true }), br(), br(),
|
||||
label({}, i18n.tribeGovRuleBody || 'Body'), br(),
|
||||
textarea({ name: 'body', rows: 4 }), br(), br(),
|
||||
button({ type: 'submit', class: 'create-button' }, i18n.save || 'Save')
|
||||
|
|
|
|||
|
|
@ -93,6 +93,32 @@ const stepContent = (key, lang, profile) => {
|
|||
)
|
||||
)
|
||||
}
|
||||
if (key === "ux") {
|
||||
let cfg = {}
|
||||
try { cfg = require("../configs/config-manager.js").getConfig() || {} } catch (_) {}
|
||||
const cur = cfg.ux?.current === "ainav" ? "ainav" : cfg.ux?.current === "chats" ? "chats" : "blocks"
|
||||
const chatsOn = cfg.modules?.chatsMod === "on"
|
||||
const aiOn = cfg.modules?.aiNavMod === "on"
|
||||
const uxCard = (value, title, image) => label({ class: "welcome-ux-option" },
|
||||
input({ type: "radio", name: "ux", value, ...(value === "blocks" ? { checked: true } : {}) }),
|
||||
img({ src: image, class: "welcome-ux-shot", alt: title }),
|
||||
span({ class: "welcome-ux-label" }, title)
|
||||
)
|
||||
return {
|
||||
title: i18n.welcomeStepUxTitle || "Choose your interface",
|
||||
text: i18n.welcomeStepUxText || "Pick how Oasis should look. You can change it later in Settings.",
|
||||
action: form({ method: "POST", action: "/welcome/ux", class: "welcome-ux-form" },
|
||||
div({ class: "welcome-ux-grid" },
|
||||
uxCard("blocks", i18n.uxModeMenus || "Blocks", "/assets/images/ux-blocks.png"),
|
||||
chatsOn ? uxCard("chats", i18n.chatsTitle || "Chats", "/assets/images/ux-chats.png") : null,
|
||||
aiOn ? uxCard("ainav", i18n.uxModeAINav || "AI", "/assets/images/ux-ainav.png") : null
|
||||
),
|
||||
div({ class: "welcome-action" },
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.welcomeStepUxAction || "Use this view")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (key === "backup") return {
|
||||
title: i18n.welcomeStepBackupTitle || "Backup your ID",
|
||||
text: i18n.welcomeStepBackupText || "Your identity is a key file on this device.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue