document_view.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. const { form, button, div, h2, p, section, input, label, br, a, span, textarea } = require("../server/node_modules/hyperaxe");
  2. const moment = require("../server/node_modules/moment");
  3. const { template, i18n } = require('./main_views');
  4. const { config } = require('../server/SSB_server.js');
  5. const { renderUrl } = require('../backend/renderUrl');
  6. const opinionCategories = require('../backend/opinion_categories');
  7. const userId = config.keys.id;
  8. const getFilteredDocuments = (filter, documents, userId) => {
  9. const now = Date.now();
  10. let filtered =
  11. filter === 'mine' ? documents.filter(d => d.author === userId) :
  12. filter === 'recent' ? documents.filter(d => new Date(d.createdAt).getTime() >= now - 86400000) :
  13. filter === 'top' ? [...documents].sort((a, b) => {
  14. const sumA = Object.values(a.opinions || {}).reduce((s, n) => s + (n || 0), 0);
  15. const sumB = Object.values(b.opinions || {}).reduce((s, n) => s + (n || 0), 0);
  16. return sumB - sumA;
  17. }) :
  18. documents;
  19. if (filter !== 'top') {
  20. filtered = filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  21. }
  22. return filtered;
  23. };
  24. const renderDocumentCommentsSection = (documentId, comments = []) => {
  25. const commentsCount = Array.isArray(comments) ? comments.length : 0;
  26. return div({ class: 'vote-comments-section' },
  27. div({ class: 'comments-count' },
  28. span({ class: 'card-label' }, i18n.voteCommentsLabel + ': '),
  29. span({ class: 'card-value' }, String(commentsCount))
  30. ),
  31. div({ class: 'comment-form-wrapper' },
  32. h2({ class: 'comment-form-title' }, i18n.voteNewCommentLabel),
  33. form({
  34. method: 'POST',
  35. action: `/documents/${encodeURIComponent(documentId)}/comments`,
  36. class: 'comment-form'
  37. },
  38. textarea({
  39. id: 'comment-text',
  40. name: 'text',
  41. required: true,
  42. rows: 4,
  43. class: 'comment-textarea',
  44. placeholder: i18n.voteNewCommentPlaceholder
  45. }),
  46. br(),
  47. button({ type: 'submit', class: 'comment-submit-btn' }, i18n.voteNewCommentButton)
  48. )
  49. ),
  50. comments && comments.length
  51. ? div({ class: 'comments-list' },
  52. comments.map(c => {
  53. const author = c.value && c.value.author ? c.value.author : '';
  54. const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp;
  55. const absDate = ts ? moment(ts).format('YYYY/MM/DD HH:mm:ss') : '';
  56. const relDate = ts ? moment(ts).fromNow() : '';
  57. const userName = author && author.includes('@') ? author.split('@')[1] : author;
  58. return div({ class: 'votations-comment-card' },
  59. span({ class: 'created-at' },
  60. span(i18n.createdBy),
  61. author
  62. ? a(
  63. { href: `/author/${encodeURIComponent(author)}` },
  64. `@${userName}`
  65. )
  66. : span('(unknown)'),
  67. absDate ? span(' | ') : '',
  68. absDate ? span({ class: 'votations-comment-date' }, absDate) : '',
  69. relDate ? span({ class: 'votations-comment-date' }, ' | ', i18n.sendTime) : '',
  70. relDate
  71. ? a(
  72. {
  73. href: `/thread/${encodeURIComponent(c.value.content.fork || c.value.content.root)}#${encodeURIComponent(c.key)}`
  74. },
  75. relDate
  76. )
  77. : ''
  78. ),
  79. p({
  80. class: 'votations-comment-text',
  81. innerHTML: (c.value && c.value.content && c.value.content.text) || ''
  82. })
  83. );
  84. })
  85. )
  86. : p({ class: 'votations-no-comments' }, i18n.voteNoCommentsYet)
  87. );
  88. };
  89. const renderDocumentActions = (filter, doc) => {
  90. return filter === 'mine' ? div({ class: "document-actions" },
  91. form({ method: "GET", action: `/documents/edit/${encodeURIComponent(doc.key)}` },
  92. button({ class: "update-btn", type: "submit" }, i18n.documentUpdateButton)
  93. ),
  94. form({ method: "POST", action: `/documents/delete/${encodeURIComponent(doc.key)}` },
  95. button({ class: "delete-btn", type: "submit" }, i18n.documentDeleteButton)
  96. )
  97. ) : null;
  98. };
  99. const renderDocumentList = (filteredDocs, filter) => {
  100. const seen = new Set();
  101. const unique = [];
  102. for (const doc of filteredDocs) {
  103. if (seen.has(doc.title)) continue;
  104. seen.add(doc.title);
  105. unique.push(doc);
  106. }
  107. return unique.length > 0
  108. ? unique.map(doc => {
  109. const commentCount = typeof doc.commentCount === 'number' ? doc.commentCount : 0;
  110. return div({ class: "tags-header" },
  111. renderDocumentActions(filter, doc),
  112. form({ method: "GET", action: `/documents/${encodeURIComponent(doc.key)}` },
  113. button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
  114. ),
  115. doc.title?.trim() ? h2(doc.title) : null,
  116. div({
  117. id: `pdf-container-${doc.key}`,
  118. class: 'pdf-viewer-container',
  119. 'data-pdf-url': `/blob/${encodeURIComponent(doc.url)}`
  120. }),
  121. doc.description?.trim() ? p(...renderUrl(doc.description)) : null,
  122. doc.tags.length
  123. ? div({ class: "card-tags" }, doc.tags.map(tag =>
  124. a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
  125. ))
  126. : null,
  127. div({ class: 'card-comments-summary' },
  128. span({ class: 'card-label' }, i18n.voteCommentsLabel + ':'),
  129. span({ class: 'card-value' }, String(commentCount)),
  130. br(),
  131. br(),
  132. form({ method: 'GET', action: `/documents/${encodeURIComponent(doc.key)}` },
  133. button({ type: 'submit', class: 'filter-btn' }, i18n.voteCommentsForumButton)
  134. )
  135. ),
  136. br(),
  137. p({ class: 'card-footer' },
  138. span({ class: 'date-link' }, `${moment(doc.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
  139. a({ href: `/author/${encodeURIComponent(doc.author)}`, class: 'user-link' }, doc.author)
  140. ),
  141. div({ class: "voting-buttons" },
  142. opinionCategories.map(category =>
  143. form({ method: "POST", action: `/documents/opinions/${encodeURIComponent(doc.key)}/${category}` },
  144. button({ class: "vote-btn" },
  145. `${i18n[`vote${category.charAt(0).toUpperCase() + category.slice(1)}`] || category} [${doc.opinions?.[category] || 0}]`
  146. )
  147. )
  148. )
  149. )
  150. );
  151. })
  152. : div(i18n.noDocuments);
  153. };
  154. const renderDocumentForm = (filter, documentId, docToEdit) => {
  155. return div({ class: "div-center document-form" },
  156. form({
  157. action: filter === 'edit' ? `/documents/update/${encodeURIComponent(documentId)}` : "/documents/create",
  158. method: "POST", enctype: "multipart/form-data"
  159. },
  160. label(i18n.documentFileLabel), br(),
  161. input({ type: "file", name: "document", accept: "application/pdf", required: filter !== "edit" }), br(), br(),
  162. label(i18n.documentTagsLabel), br(),
  163. input({ type: "text", name: "tags", placeholder: i18n.documentTagsPlaceholder, value: docToEdit?.tags?.join(', ') || '' }), br(), br(),
  164. label(i18n.documentTitleLabel), br(),
  165. input({ type: "text", name: "title", placeholder: i18n.documentTitlePlaceholder, value: docToEdit?.title || '' }), br(), br(),
  166. label(i18n.documentDescriptionLabel), br(),
  167. textarea({ name: "description", placeholder: i18n.documentDescriptionPlaceholder, rows: "4", value: docToEdit?.description || '' }), br(), br(),
  168. button({ type: "submit" }, filter === 'edit' ? i18n.documentUpdateButton : i18n.documentCreateButton)
  169. )
  170. );
  171. };
  172. exports.documentView = async (documents, filter, documentId) => {
  173. const title = filter === 'mine' ? i18n.documentMineSectionTitle :
  174. filter === 'create' ? i18n.documentCreateSectionTitle :
  175. filter === 'edit' ? i18n.documentUpdateSectionTitle :
  176. filter === 'recent' ? i18n.documentRecentSectionTitle :
  177. filter === 'top' ? i18n.documentTopSectionTitle :
  178. i18n.documentAllSectionTitle;
  179. const filteredDocs = getFilteredDocuments(filter, documents, userId);
  180. const docToEdit = documents.find(d => d.key === documentId);
  181. const isDocView = ['mine', 'create', 'edit', 'all', 'recent', 'top'].includes(filter);
  182. const tpl = template(
  183. title,
  184. section(
  185. div({ class: "tags-header" },
  186. h2(title),
  187. p(i18n.documentDescription)
  188. ),
  189. div({ class: "filters" },
  190. form({ method: "GET", action: "/documents" },
  191. button({ type: "submit", name: "filter", value: "all", class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, i18n.documentFilterAll),
  192. button({ type: "submit", name: "filter", value: "mine", class: filter === 'mine' ? 'filter-btn active' : 'filter-btn' }, i18n.documentFilterMine),
  193. button({ type: "submit", name: "filter", value: "recent", class: filter === 'recent' ? 'filter-btn active' : 'filter-btn' }, i18n.documentFilterRecent),
  194. button({ type: "submit", name: "filter", value: "top", class: filter === 'top' ? 'filter-btn active' : 'filter-btn' }, i18n.documentFilterTop),
  195. button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.documentCreateButton)
  196. )
  197. )
  198. ),
  199. section(
  200. (filter === 'create' || filter === 'edit')
  201. ? renderDocumentForm(filter, documentId, docToEdit)
  202. : renderDocumentList(filteredDocs, filter)
  203. )
  204. );
  205. return `${tpl}${isDocView
  206. ? `<script type="module" src="/js/pdf.min.mjs"></script>
  207. <script src="/js/pdf-viewer.js"></script>`
  208. : ''}`;
  209. };
  210. exports.singleDocumentView = async (doc, filter, comments = []) => {
  211. const isAuthor = doc.author === userId;
  212. const hasOpinions = Object.keys(doc.opinions || {}).length > 0;
  213. const tpl = template(
  214. i18n.documentTitle,
  215. section(
  216. div({ class: "filters" },
  217. form({ method: "GET", action: "/documents" },
  218. button({ type: "submit", name: "filter", value: "all", class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, i18n.documentFilterAll),
  219. button({ type: "submit", name: "filter", value: "mine", class: filter === 'mine' ? 'filter-btn active' : 'filter-btn' }, i18n.documentFilterMine),
  220. button({ type: "submit", name: "filter", value: "recent", class: filter === 'recent' ? 'filter-btn active' : 'filter-btn' }, i18n.documentFilterRecent),
  221. button({ type: "submit", name: "filter", value: "top", class: filter === 'top' ? 'filter-btn active' : 'filter-btn' }, i18n.documentFilterTop),
  222. button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.documentCreateButton)
  223. )
  224. ),
  225. div({ class: "tags-header" },
  226. isAuthor ? div({ class: "document-actions" },
  227. !hasOpinions
  228. ? form({ method: "GET", action: `/documents/edit/${encodeURIComponent(doc.key)}` },
  229. button({ class: "update-btn", type: "submit" }, i18n.documentUpdateButton)
  230. )
  231. : null,
  232. form({ method: "POST", action: `/documents/delete/${encodeURIComponent(doc.key)}` },
  233. button({ class: "delete-btn", type: "submit" }, i18n.documentDeleteButton)
  234. )
  235. ) : null,
  236. h2(doc.title),
  237. div({
  238. id: `pdf-container-${doc.key}`,
  239. class: 'pdf-viewer-container',
  240. 'data-pdf-url': `/blob/${encodeURIComponent(doc.url)}`
  241. }),
  242. p(...renderUrl(doc.description)),
  243. doc.tags.length
  244. ? div({ class: "card-tags" }, doc.tags.map(tag =>
  245. a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
  246. ))
  247. : null,
  248. br(),
  249. p({ class: 'card-footer' },
  250. span({ class: 'date-link' }, `${moment(doc.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
  251. a({ href: `/author/${encodeURIComponent(doc.author)}`, class: 'user-link' }, `${doc.author}`)
  252. )
  253. ),
  254. div({ class: "voting-buttons" },
  255. opinionCategories.map(category =>
  256. form({ method: "POST", action: `/documents/opinions/${encodeURIComponent(doc.key)}/${category}` },
  257. button({ class: "vote-btn" }, `${i18n[`vote${category.charAt(0).toUpperCase() + category.slice(1)}`] || category} [${doc.opinions?.[category] || 0}]`)
  258. )
  259. )
  260. ),
  261. renderDocumentCommentsSection(doc.key, comments)
  262. )
  263. );
  264. return `${tpl}<script type="module" src="/js/pdf.min.mjs"></script>
  265. <script src="/js/pdf-viewer.js"></script>`;
  266. };