const { div, a, span, form, button, section, p, input, label, br, select, option, h2, textarea } = require("../server/node_modules/hyperaxe"); const moment = require("../server/node_modules/moment"); const { template, i18n } = require('./main_views'); const { config } = require('../server/SSB_server.js'); const { renderUrl } = require('../backend/renderUrl'); const userId = config.keys.id; const BASE_FILTERS = ['hot','all','mine','recent','top']; const CAT_BLOCK1 = ['GENERAL','OASIS','L.A.R.P.','POLITICS','TECH']; const CAT_BLOCK2 = ['SCIENCE','MUSIC','ART','GAMING','BOOKS','FILMS']; const CAT_BLOCK3 = ['PHILOSOPHY','SOCIETY','PRIVACY','CYBERWARFARE','SURVIVALISM']; const Z = 1.96; function wilsonScore(pos, neg) { const n = (pos||0)+(neg||0); if (n === 0) return 0; const phat = pos / n, z2 = Z * Z; return (phat + z2/(2*n) - Z*Math.sqrt((phat*(1-phat)+z2/(4*n))/n)) / (1+z2/n); } function getFilteredForums(filter, forums) { const now = Date.now(); if (filter === 'mine') return forums.filter(f => f.author === userId); if (filter === 'recent') return forums.filter(f => new Date(f.createdAt).getTime() >= now - 86400000); if (filter === 'top') return forums.slice().sort((a,b) => b.score - a.score); if (filter === 'hot') return forums .filter(f => new Date(f.createdAt).getTime() >= now - 86400000) .sort((a,b) => b.score - a.score); if ([...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3].includes(filter)) return forums.filter(f => f.category === filter); return forums; } const generateFilterButtons = (filters, currentFilter, action, i18nMap = {}) => div({ class: 'filter-group' }, filters.map(mode => form({ method: 'GET', action }, input({ type: 'hidden', name: 'filter', value: mode }), button({ type: 'submit', class: currentFilter === mode ? 'filter-btn active' : 'filter-btn' }, i18nMap[mode] || mode.toUpperCase() ) ) ) ); const renderCreateForumButton = () => div({ class: 'forum-create-col' }, form({ method: 'GET', action: '/forum' }, button({ type: 'submit', name: 'filter', value: 'create', class: 'create-button' }, i18n.forumCreateButton ) ) ); const renderVotes = (target, score, forumId) => div({ class: 'forum-score-box' }, form({ method: 'POST', action: `/forum/${encodeURIComponent(forumId)}/vote`, class: 'forum-score-form' }, button({ name: 'value', value: 1, class: 'score-btn' }, '▲'), div({ class: 'score-total' }, String(score || 0)), button({ name: 'value', value: -1, class: 'score-btn' }, '▼'), input({ type: 'hidden', name: 'target', value: target }), input({ type: 'hidden', name: 'forumId', value: forumId }) ) ); const renderForumForm = () => div({ class: 'forum-form' }, form({ action: '/forum/create', method: 'POST' }, label(i18n.forumCategoryLabel), br(), select({ name: 'category', required: true }, [...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3].map(cat => option({ value: cat }, cat) ) ), br(), br(), label(i18n.forumTitleLabel), br(), input({ type: 'text', name: 'title', required: true, placeholder: i18n.forumTitlePlaceholder }), br(), br(), label(i18n.forumMessageLabel), br(), textarea({ name: 'text', required: true, rows: 4, placeholder: i18n.forumMessagePlaceholder }), br(), br(), button({ type: 'submit' }, i18n.forumCreateButton) ) ); const renderThread = (nodes, level = 0, forumId) => { if (!Array.isArray(nodes)) return []; return [...nodes] .sort((a, b) => wilsonScore(b.positiveVotes, b.negativeVotes) - wilsonScore(a.positiveVotes, a.negativeVotes) ) .flatMap((m, i) => { const isTopLevelWinner = level === 0 && i === 0; const classList = [ 'forum-comment', `level-${level}`, isTopLevelWinner ? 'highlighted-reply' : '' ].filter(Boolean).join(' '); const commentBox = div( { class: classList }, div({ class: 'comment-header' }, span({ class: 'date-link' }, `${moment(m.timestamp).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`), a({ href: `/author/${encodeURIComponent(m.author)}`, class: 'user-link', style: 'margin-left:12px;' }, m.author), div({ class: 'comment-votes' }, span({ class: 'votes-count' }, `▲: ${m.positiveVotes || 0}`), span({ class: 'votes-count', style: 'margin-left:12px;' }, `▼: ${m.negativeVotes || 0}`) ) ), div({ class: 'comment-body-row' }, div({ class: 'comment-vote-col' }, renderVotes(m.key, m.score, forumId) ), div({ class: 'comment-text-col' }, div( ...(m.text || '').split('\n') .map(l => l.trim()) .filter(l => l) .map(l => p(...renderUrl(l))) ) ) ), div({ class: 'new-reply' }, form({ method: 'POST', action: `/forum/${forumId}/message`, class: 'comment-form' }, input({ type: 'hidden', name: 'parentId', value: m.key }), textarea({ name: 'message', rows: 2, required: true, placeholder: i18n.forumMessagePlaceholder, class: 'comment-textarea' }), button({ type: 'submit', class: 'forum-send-btn' }, 'Reply') ) ) ); return [ commentBox, ...renderThread(m.children || [], level + 1, forumId) ]; }); }; const renderForumList = (forums, currentFilter) => div({ class: 'forum-list' }, Array.isArray(forums) && forums.length ? forums.map(f => div({ class: 'forum-card' }, div({ class: 'forum-score-col' }, renderVotes(f.key, f.score, f.key) ), div({ class: 'forum-main-col' }, div({ class: 'forum-header-row' }, a({ class: 'forum-category', href: `/forum?filter=${encodeURIComponent(f.category)}` }, `[${f.category}]`), a({ class: 'forum-title', href: `/forum/${encodeURIComponent(f.key)}` }, f.title) ), div({ class: 'forum-body' }, ...renderUrl(f.text || '')), div({ class: 'forum-meta' }, span({ class: 'forum-positive-votes' }, `▲: ${f.positiveVotes || 0}`), span({ class: 'forum-negative-votes', style: 'margin-left:12px;' }, `▼: ${f.negativeVotes || 0}`), span({ class: 'forum-participants' }, `${i18n.forumParticipants.toUpperCase()}: ${f.participants?.length || 1}`), span({ class: 'forum-messages' }, `${i18n.forumMessages.toUpperCase()}: ${f.messagesCount - 1}`) ), div({ class: 'forum-footer' }, span({ class: 'date-link' }, `${moment(f.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`), a({ href: `/author/${encodeURIComponent(f.author)}`, class: 'user-link', style: 'margin-left:12px;' }, f.author) ), currentFilter === 'mine' && f.author === userId ? div({ class: 'forum-owner-actions' }, form({ method: 'POST', action: `/forum/delete/${f.key}`, class: 'forum-delete-form' }, button({ type: 'submit', class: 'delete-btn' }, i18n.forumDeleteButton) ) ) : null ) ) ) : p(i18n.noForums) ); exports.forumView = async (forums, currentFilter) => template(i18n.forumTitle, section( div({ class: 'tags-header' }, h2(currentFilter === 'create' ? i18n.forumCreateSectionTitle : i18n.forumTitle), p(i18n.forumDescription) ), div({ class: 'mode-buttons-cols' }, generateFilterButtons(BASE_FILTERS, currentFilter, '/forum', { hot: i18n.forumFilterHot, all: i18n.forumFilterAll, mine: i18n.forumFilterMine, recent: i18n.forumFilterRecent, top: i18n.forumFilterTop }), generateFilterButtons(CAT_BLOCK1, currentFilter, '/forum'), generateFilterButtons(CAT_BLOCK2, currentFilter, '/forum'), generateFilterButtons(CAT_BLOCK3, currentFilter, '/forum'), renderCreateForumButton() ), currentFilter === 'create' ? renderForumForm() : renderForumList( getFilteredForums(currentFilter || 'hot', forums), currentFilter ) ) ); exports.singleForumView = async (forum, messagesData, currentFilter) => template(forum.title, section( div({ class: 'tags-header' }, h2(i18n.forumTitle), p(i18n.forumDescription) ), div({ class: 'mode-buttons' }, generateFilterButtons(BASE_FILTERS, currentFilter, '/forum', { all: i18n.forumFilterAll, mine: i18n.forumFilterMine, recent: i18n.forumFilterRecent, top: i18n.forumFilterTop }), generateFilterButtons(CAT_BLOCK1, currentFilter, '/forum'), generateFilterButtons(CAT_BLOCK2, currentFilter, '/forum'), generateFilterButtons(CAT_BLOCK3, currentFilter, '/forum'), renderCreateForumButton() ) ), div({ class: 'forum-thread-container' }, div({ class: 'forum-card forum-thread-header', style: 'display:flex;align-items:flex-start;' }, div({ class: 'root-vote-col', style: 'width:60px;text-align:center;' }, renderVotes( forum.key, messagesData.totalScore, forum.key )), div({ class: 'forum-main-col', style: 'flex:1;padding-left:10px;' }, div({ class: 'forum-header-row' }, a({ class: 'forum-category', href: `/forum?filter=${encodeURIComponent(forum.category)}` }, `[${forum.category}]`), a({ class: 'forum-title', href: `/forum/${encodeURIComponent(forum.key)}` }, forum.title) ), div({ class: 'forum-footer' }, span({ class: 'date-link' }, `${moment(forum.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`), a({ href: `/author/${encodeURIComponent(forum.author)}`, class: 'user-link', style: 'margin-left:12px;' }, forum.author) ), div( ...(forum.text || '').split('\n') .map(l => l.trim()) .filter(l => l) .map(l => p(...renderUrl(l))) ), div({ class: 'forum-meta' }, span({ class: 'votes-count' }, `▲: ${messagesData.positiveVotes}`), span({ class: 'votes-count', style: 'margin-left:12px;' }, `▼: ${messagesData.negativeVotes}`), span({ class: 'forum-participants' }, `${i18n.forumParticipants.toUpperCase()}: ${forum.participants?.length || 1}`), span({ class: 'forum-messages' }, `${i18n.forumMessages.toUpperCase()}: ${messagesData.total}`) ) ) ), div({ class: 'new-message-wrapper', style: 'margin-top:12px;' }, form({ method: 'POST', action: `/forum/${forum.key}/message`, class: 'new-message-form' }, textarea({ name: 'message', rows: 4, required: true, placeholder: i18n.forumMessagePlaceholder, style: 'width:100%;' }), br(), button({ type: 'submit', class: 'forum-send-btn', style: 'margin-top:4px;' }, i18n.forumSendButton) ) ), ...renderThread(messagesData.messages, 0, forum.key) ) );