image_view.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. const { form, button, div, h2, p, section, input, label, br, a, img, 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 userId = config.keys.id;
  7. const getFilteredImages = (filter, images, userId) => {
  8. const now = Date.now();
  9. let filtered =
  10. filter === 'mine' ? images.filter(img => img.author === userId) :
  11. filter === 'recent' ? images.filter(img => new Date(img.createdAt).getTime() >= now - 86400000) :
  12. filter === 'meme' ? images.filter(img => img.meme) :
  13. filter === 'top' ? [...images].sort((a, b) => {
  14. const sum = o => Object.values(o || {}).reduce((s, n) => s + n, 0);
  15. return sum(b.opinions) - sum(a.opinions);
  16. }) :
  17. images;
  18. return filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  19. };
  20. const renderImageActions = (filter, imgObj) => {
  21. return filter === 'mine' ? div({ class: "image-actions" },
  22. form({ method: "GET", action: `/images/edit/${encodeURIComponent(imgObj.key)}` },
  23. button({ class: "update-btn", type: "submit" }, i18n.imageUpdateButton)
  24. ),
  25. form({ method: "POST", action: `/images/delete/${encodeURIComponent(imgObj.key)}` },
  26. button({ class: "delete-btn", type: "submit" }, i18n.imageDeleteButton)
  27. )
  28. ) : null;
  29. };
  30. const renderImageList = (filteredImages, filter) => {
  31. return filteredImages.length > 0
  32. ? filteredImages.map(imgObj => {
  33. const commentCount = typeof imgObj.commentCount === 'number' ? imgObj.commentCount : 0;
  34. return div({ class: "tags-header" },
  35. renderImageActions(filter, imgObj),
  36. form({ method: "GET", action: `/images/${encodeURIComponent(imgObj.key)}` },
  37. button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
  38. ),
  39. imgObj.title ? h2(imgObj.title) : null,
  40. a({ href: `#img-${encodeURIComponent(imgObj.key)}` },
  41. img({ src: `/blob/${encodeURIComponent(imgObj.url)}` })
  42. ),
  43. imgObj.description ? p(...renderUrl(imgObj.description)) : null,
  44. imgObj.tags?.length
  45. ? div({ class: "card-tags" },
  46. imgObj.tags.map(tag =>
  47. a({
  48. href: `/search?query=%23${encodeURIComponent(tag)}`,
  49. class: "tag-link",
  50. style: "margin-right: 0.8em; margin-bottom: 0.5em;"
  51. },
  52. `#${tag}`
  53. )
  54. )
  55. )
  56. : null,
  57. div({ class: 'card-comments-summary' },
  58. span({ class: 'card-label' }, i18n.voteCommentsLabel + ':'),
  59. span({ class: 'card-value' }, String(commentCount)),
  60. br, br,
  61. form({ method: 'GET', action: `/images/${encodeURIComponent(imgObj.key)}` },
  62. button({ type: 'submit', class: 'filter-btn' }, i18n.voteCommentsForumButton)
  63. )
  64. ),
  65. br,
  66. p({ class: 'card-footer' },
  67. span(
  68. { class: 'date-link' },
  69. `${moment(imgObj.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `
  70. ),
  71. a(
  72. { href: `/author/${encodeURIComponent(imgObj.author)}`, class: 'user-link' },
  73. `${imgObj.author}`
  74. )
  75. ),
  76. div({ class: "voting-buttons" },
  77. ['interesting', 'necessary', 'funny', 'disgusting', 'sensible', 'propaganda', 'adultOnly', 'boring', 'confusing', 'inspiring', 'spam']
  78. .map(category =>
  79. form({ method: "POST", action: `/images/opinions/${encodeURIComponent(imgObj.key)}/${category}` },
  80. button(
  81. { class: "vote-btn" },
  82. `${i18n[`vote${category.charAt(0).toUpperCase() + category.slice(1)}`]} [${imgObj.opinions?.[category] || 0}]`
  83. )
  84. )
  85. )
  86. )
  87. );
  88. })
  89. : div(i18n.noImages);
  90. };
  91. const renderImageForm = (filter, imageId, imageToEdit) => {
  92. return div({ class: "div-center image-form" },
  93. form({
  94. action: filter === 'edit'
  95. ? `/images/update/${encodeURIComponent(imageId)}`
  96. : "/images/create",
  97. method: "POST", enctype: "multipart/form-data"
  98. },
  99. label(i18n.imageFileLabel), br(),
  100. input({ type: "file", name: "image", required: filter !== "edit" }), br(), br(),
  101. imageToEdit?.url ? img({ src: `/blob/${encodeURIComponent(imageToEdit.url)}`, class: "image-detail" }) : null,
  102. br(),
  103. label(i18n.imageTagsLabel), br(),
  104. input({ type: "text", name: "tags", placeholder: i18n.imageTagsPlaceholder, value: imageToEdit?.tags?.join(',') || '' }), br(), br(),
  105. label(i18n.imageTitleLabel), br(),
  106. input({ type: "text", name: "title", placeholder: i18n.imageTitlePlaceholder, value: imageToEdit?.title || '' }), br(), br(),
  107. label(i18n.imageDescriptionLabel), br(),
  108. textarea({ name: "description", placeholder: i18n.imageDescriptionPlaceholder, rows:"4", value: imageToEdit?.description || '' }), br(), br(),
  109. label(i18n.imageMemeLabel),
  110. input({ type: "checkbox", name: "meme", ...(imageToEdit?.meme ? { checked: true } : {}) }), br(), br(),
  111. button({ type: "submit" }, filter === 'edit' ? i18n.imageUpdateButton : i18n.imageCreateButton)
  112. )
  113. );
  114. };
  115. const renderGallery = (sortedImages) => {
  116. return div({ class: "gallery" },
  117. sortedImages.length
  118. ? sortedImages.map(imgObj =>
  119. a({ href: `#img-${encodeURIComponent(imgObj.key)}`, class: "gallery-item" },
  120. img({ src: `/blob/${encodeURIComponent(imgObj.url)}`, alt: imgObj.title || "", class: "gallery-image" })
  121. )
  122. )
  123. : div(i18n.noImages)
  124. );
  125. };
  126. const renderLightbox = (sortedImages) => {
  127. return sortedImages.map(imgObj =>
  128. div(
  129. { id: `img-${encodeURIComponent(imgObj.key)}`, class: "lightbox" },
  130. a({ href: "#", class: "lightbox-close" }, "×"),
  131. img({ src: `/blob/${encodeURIComponent(imgObj.url)}`, class: "lightbox-image", alt: imgObj.title || "" })
  132. )
  133. );
  134. };
  135. const renderImageCommentsSection = (imageId, comments = []) => {
  136. const commentsCount = Array.isArray(comments) ? comments.length : 0;
  137. return div({ class: 'vote-comments-section' },
  138. div({ class: 'comments-count' },
  139. span({ class: 'card-label' }, i18n.voteCommentsLabel + ': '),
  140. span({ class: 'card-value' }, String(commentsCount))
  141. ),
  142. div({ class: 'comment-form-wrapper' },
  143. h2({ class: 'comment-form-title' }, i18n.voteNewCommentLabel),
  144. form({
  145. method: 'POST',
  146. action: `/images/${encodeURIComponent(imageId)}/comments`,
  147. class: 'comment-form'
  148. },
  149. textarea({
  150. id: 'comment-text',
  151. name: 'text',
  152. required: true,
  153. rows: 4,
  154. class: 'comment-textarea',
  155. placeholder: i18n.voteNewCommentPlaceholder
  156. }),
  157. br(),
  158. button({ type: 'submit', class: 'comment-submit-btn' }, i18n.voteNewCommentButton)
  159. )
  160. ),
  161. comments && comments.length
  162. ? div({ class: 'comments-list' },
  163. comments.map(c => {
  164. const author = c.value && c.value.author ? c.value.author : '';
  165. const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp;
  166. const absDate = ts ? moment(ts).format('YYYY/MM/DD HH:mm:ss') : '';
  167. const relDate = ts ? moment(ts).fromNow() : '';
  168. const userName = author && author.includes('@') ? author.split('@')[1] : author;
  169. return div({ class: 'votations-comment-card' },
  170. span({ class: 'created-at' },
  171. span(i18n.createdBy),
  172. author
  173. ? a(
  174. { href: `/author/${encodeURIComponent(author)}` },
  175. `@${userName}`
  176. )
  177. : span('(unknown)'),
  178. absDate ? span(' | ') : '',
  179. absDate ? span({ class: 'votations-comment-date' }, absDate) : '',
  180. relDate ? span({ class: 'votations-comment-date' }, ' | ', i18n.sendTime) : '',
  181. relDate
  182. ? a(
  183. {
  184. href: `/thread/${encodeURIComponent(c.value.content.fork || c.value.content.root)}#${encodeURIComponent(c.key)}`
  185. },
  186. relDate
  187. )
  188. : ''
  189. ),
  190. p({
  191. class: 'votations-comment-text',
  192. innerHTML: (c.value && c.value.content && c.value.content.text) || ''
  193. })
  194. );
  195. })
  196. )
  197. : p({ class: 'votations-no-comments' }, i18n.voteNoCommentsYet)
  198. );
  199. };
  200. exports.imageView = async (images, filter, imageId) => {
  201. const title = filter === 'mine' ? i18n.imageMineSectionTitle :
  202. filter === 'create' ? i18n.imageCreateSectionTitle :
  203. filter === 'edit' ? i18n.imageUpdateSectionTitle :
  204. filter === 'gallery' ? i18n.imageGallerySectionTitle :
  205. filter === 'meme' ? i18n.imageMemeSectionTitle :
  206. filter === 'recent' ? i18n.imageRecentSectionTitle :
  207. filter === 'top' ? i18n.imageTopSectionTitle :
  208. i18n.imageAllSectionTitle;
  209. const filteredImages = getFilteredImages(filter, images, userId);
  210. const imageToEdit = images.find(img => img.key === imageId);
  211. return template(
  212. title,
  213. section(
  214. div({ class: "tags-header" },
  215. h2(i18n.imageCreateSectionTitle),
  216. p(i18n.imageDescription)
  217. ),
  218. div({ class: "filters" },
  219. form({ method: "GET", action: "/images" },
  220. ["all", "mine", "recent", "top", "gallery", "meme"].map(f =>
  221. button({
  222. type: "submit", name: "filter", value: f,
  223. class: filter === f ? "filter-btn active" : "filter-btn"
  224. },
  225. i18n[`imageFilter${f.charAt(0).toUpperCase() + f.slice(1)}`]
  226. )
  227. ),
  228. button({ type: "submit", name: "filter", value: "create", class: "create-button" },
  229. i18n.imageCreateButton)
  230. )
  231. )
  232. ),
  233. section(
  234. (filter === 'create' || filter === 'edit')
  235. ? renderImageForm(filter, imageId, imageToEdit)
  236. : filter === 'gallery'
  237. ? renderGallery(filteredImages)
  238. : renderImageList(filteredImages, filter)
  239. ),
  240. ...renderLightbox(filteredImages)
  241. );
  242. };
  243. exports.singleImageView = async (image, filter, comments = []) => {
  244. const isAuthor = image.author === userId;
  245. const hasOpinions = Object.keys(image.opinions || {}).length > 0;
  246. return template(
  247. i18n.imageTitle,
  248. section(
  249. div({ class: "filters" },
  250. form({ method: "GET", action: "/images" },
  251. button({ type: "submit", name: "filter", value: "all", class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, i18n.imageFilterAll),
  252. button({ type: "submit", name: "filter", value: "mine", class: filter === 'mine' ? 'filter-btn active' : 'filter-btn' }, i18n.imageFilterMine),
  253. button({ type: "submit", name: "filter", value: "meme", class: filter === 'meme' ? 'filter-btn active' : 'filter-btn' }, i18n.imageFilterMeme),
  254. button({ type: "submit", name: "filter", value: "top", class: filter === 'top' ? 'filter-btn active' : 'filter-btn' }, i18n.imageFilterTop),
  255. button({ type: "submit", name: "filter", value: "recent", class: filter === 'recent' ? 'filter-btn active' : 'filter-btn' }, i18n.imageFilterRecent),
  256. button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.imageCreateButton)
  257. )
  258. ),
  259. div({ class: "tags-header" },
  260. isAuthor ? div({ class: "image-actions" },
  261. !hasOpinions
  262. ? form({ method: "GET", action: `/images/edit/${encodeURIComponent(image.key)}` },
  263. button({ class: "update-btn", type: "submit" }, i18n.imageUpdateButton)
  264. )
  265. : null,
  266. form({ method: "POST", action: `/images/delete/${encodeURIComponent(image.key)}` },
  267. button({ class: "delete-btn", type: "submit" }, i18n.imageDeleteButton)
  268. )
  269. ) : null,
  270. h2(image.title),
  271. image.url ? img({ src: `/blob/${encodeURIComponent(image.url)}` }) : null,
  272. p(...renderUrl(image.description)),
  273. image.tags?.length
  274. ? div({ class: "card-tags" },
  275. image.tags.map(tag =>
  276. a({
  277. href: `/search?query=%23${encodeURIComponent(tag)}`,
  278. class: "tag-link",
  279. style: "margin-right: 0.8em; margin-bottom: 0.5em;"
  280. },
  281. `#${tag}`
  282. )
  283. )
  284. )
  285. : null,
  286. br,
  287. p({ class: 'card-footer' },
  288. span(
  289. { class: 'date-link' },
  290. `${moment(image.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `
  291. ),
  292. a(
  293. { href: `/author/${encodeURIComponent(image.author)}`, class: 'user-link' },
  294. `${image.author}`
  295. )
  296. )
  297. ),
  298. div({ class: "voting-buttons" },
  299. ['interesting', 'necessary', 'funny', 'disgusting', 'sensible', 'propaganda', 'adultOnly', 'boring', 'confusing', 'inspiring', 'spam']
  300. .map(category =>
  301. form({ method: "POST", action: `/images/opinions/${encodeURIComponent(image.key)}/${category}` },
  302. button(
  303. { class: "vote-btn" },
  304. `${i18n[`vote${category.charAt(0).toUpperCase() + category.slice(1)}`]} [${image.opinions?.[category] || 0}]`
  305. )
  306. )
  307. )
  308. ),
  309. renderImageCommentsSection(image.key, comments)
  310. )
  311. );
  312. };