market_view.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, hr, table, tr, th, td } = require("../server/node_modules/hyperaxe");
  2. const { template, i18n } = require('./main_views');
  3. const moment = require("../server/node_modules/moment");
  4. const { config } = require('../server/SSB_server.js');
  5. const { renderUrl } = require('../backend/renderUrl');
  6. const userId = config.keys.id;
  7. const renderCardField = (labelText, value) =>
  8. div({ class: 'card-field' },
  9. span({ class: 'card-label' }, labelText),
  10. span({ class: 'card-value' }, ...renderUrl(value))
  11. );
  12. exports.marketView = async (items, filter, itemToEdit = null) => {
  13. const list = Array.isArray(items) ? items : [];
  14. let title = i18n.marketAllSectionTitle;
  15. switch (filter) {
  16. case 'mine':
  17. title = i18n.marketMineSectionTitle;
  18. break;
  19. case 'create':
  20. title = i18n.marketCreateSectionTitle;
  21. break;
  22. case 'edit':
  23. title = i18n.marketUpdateSectionTitle;
  24. break;
  25. }
  26. let filtered = [];
  27. switch (filter) {
  28. case 'all': filtered = list; break;
  29. case 'mine': filtered = list.filter(e => e.seller === userId); break;
  30. case 'exchange': filtered = list.filter(e => e.item_type === 'exchange' && e.status === 'FOR SALE'); break;
  31. case 'auctions': filtered = list.filter(e => e.item_type === 'auction' && e.status === 'FOR SALE'); break;
  32. case 'new': filtered = list.filter(e => e.item_status === 'NEW' && e.status === 'FOR SALE'); break;
  33. case 'used': filtered = list.filter(e => e.item_status === 'USED' && e.status === 'FOR SALE'); break;
  34. case 'broken': filtered = list.filter(e => e.item_status === 'BROKEN' && e.status === 'FOR SALE'); break;
  35. case 'for sale': filtered = list.filter(e => e.status === 'FOR SALE'); break;
  36. case 'sold': filtered = list.filter(e => e.status === 'SOLD'); break;
  37. case 'discarded': filtered = list.filter(e => e.status === 'DISCARDED'); break;
  38. case 'recent':
  39. const oneDayAgo = moment().subtract(1, 'days').toISOString();
  40. filtered = list.filter(e => e.status === 'FOR SALE' && e.createdAt >= oneDayAgo);
  41. break;
  42. default: break;
  43. }
  44. filtered = filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  45. return template(
  46. title,
  47. section(
  48. div({ class: "tags-header" },
  49. h2(i18n.marketTitle),
  50. p(i18n.marketDescription)
  51. ),
  52. div({ class: "filters" },
  53. form({ method: "GET", action: "/market" },
  54. button({ type: "submit", name: "filter", value: "all", class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterAll),
  55. button({ type: "submit", name: "filter", value: "mine", class: filter === 'mine' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterMine),
  56. button({ type: "submit", name: "filter", value: "exchange", class: filter === 'exchange' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterItems),
  57. button({ type: "submit", name: "filter", value: "auctions", class: filter === 'auctions' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterAuctions),
  58. button({ type: "submit", name: "filter", value: "new", class: filter === 'new' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterNew),
  59. button({ type: "submit", name: "filter", value: "used", class: filter === 'used' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterUsed),
  60. button({ type: "submit", name: "filter", value: "broken", class: filter === 'broken' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterBroken),
  61. button({ type: "submit", name: "filter", value: "for sale", class: filter === 'for sale' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterForSale),
  62. button({ type: "submit", name: "filter", value: "sold", class: filter === 'sold' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterSold),
  63. button({ type: "submit", name: "filter", value: "discarded", class: filter === 'discarded' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterDiscarded),
  64. button({ type: "submit", name: "filter", value: "recent", class: filter === 'recent' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterRecent),
  65. button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.marketCreateButton)
  66. )
  67. )
  68. ),
  69. section(
  70. (filter === 'create' || filter === 'edit') ? (
  71. div({ class: "market-form" },
  72. form({
  73. action: filter === 'edit' ? `/market/update/${encodeURIComponent(itemToEdit.id)}` : "/market/create",
  74. method: "POST",
  75. enctype: "multipart/form-data"
  76. },
  77. label(i18n.marketItemType), br(),
  78. select({ name: "item_type", id: "item_type", required: true },
  79. option({ value: "auction", selected: itemToEdit?.item_type === 'auction' ? true : false }, "Auction"),
  80. option({ value: "exchange", selected: itemToEdit?.item_type === 'exchange' ? true : false }, "Exchange")
  81. ), br(), br(),
  82. label(i18n.marketItemTitle), br(),
  83. input({ type: "text", name: "title", id: "title", value: itemToEdit?.title || '', required: true }), br(), br(),
  84. label(i18n.marketItemDescription), br(),
  85. textarea({ name: "description", id: "description", placeholder: i18n.marketItemDescriptionPlaceholder, rows: "6", innerHTML: itemToEdit?.description || '', required: true }), br(), br(),
  86. label(i18n.marketCreateFormImageLabel), br(),
  87. input({ type: "file", name: "image", id: "image", accept: "image/*" }), br(), br(),
  88. label(i18n.marketItemStatus), br(),
  89. select({ name: "item_status", id: "item_status" },
  90. option({ value: "BROKEN", selected: itemToEdit?.item_status === 'BROKEN' ? true : false }, "BROKEN"),
  91. option({ value: "USED", selected: itemToEdit?.item_status === 'USED' ? true : false }, "USED"),
  92. option({ value: "NEW", selected: itemToEdit?.item_status === 'NEW' ? true : false }, "NEW")
  93. ), br(), br(),
  94. label(i18n.marketItemStock), br(),
  95. input({
  96. type: "number",
  97. name: "stock",
  98. id: "stock",
  99. value: itemToEdit?.stock || 1,
  100. required: true,
  101. min: "1",
  102. step: "1"
  103. }), br(), br(),
  104. label(i18n.marketItemPrice), br(),
  105. input({ type: "number", name: "price", id: "price", value: itemToEdit?.price || '', required: true, step: "0.000001", min: "0.000001" }), br(), br(),
  106. label(i18n.marketItemTags), br(),
  107. input({ type: "text", name: "tags", id: "tags", placeholder: i18n.marketItemTagsPlaceholder, value: itemToEdit?.tags?.join(', ') || '' }), br(), br(),
  108. label(i18n.marketItemDeadline), br(),
  109. input({
  110. type: "datetime-local",
  111. name: "deadline",
  112. id: "deadline",
  113. required: true,
  114. min: moment().format("YYYY-MM-DDTHH:mm"),
  115. value: itemToEdit?.deadline ? moment(itemToEdit.deadline).format("YYYY-MM-DDTHH:mm") : ''
  116. }), br(), br(),
  117. label(i18n.marketItemIncludesShipping), br(),
  118. input({ type: "checkbox", name: "includesShipping", id: "includesShipping", checked: itemToEdit?.includesShipping }), br(), br(),
  119. button({ type: "submit" }, filter === 'edit' ? i18n.marketUpdateButton : i18n.marketCreateButton)
  120. )
  121. )
  122. ) : (
  123. div({ class: "market-grid" },
  124. filtered.length > 0
  125. ? filtered.map((item, index) =>
  126. div({ class: "market-item" },
  127. div({ class: "market-card left-col" },
  128. form({ method: "GET", action: `/market/${encodeURIComponent(item.id)}` },
  129. button({ class: "filter-btn", type: "submit" }, i18n.viewDetails)
  130. ),
  131. h2({ class: "market-card type" }, `${i18n.marketItemType}: ${item.item_type.toUpperCase()}`),
  132. h2(item.title),
  133. renderCardField(`${i18n.marketItemStatus}:`, item.status),
  134. item.deadline ? renderCardField(`${i18n.marketItemAvailable}:`, moment(item.deadline).format('YYYY/MM/DD HH:mm:ss')) : null,
  135. br, br,
  136. div({ class: "market-card image" },
  137. item.image
  138. ? img({ src: `/blob/${encodeURIComponent(item.image)}` })
  139. : img({ src: '/assets/images/default-market.png', alt: item.title })
  140. ),
  141. p(...renderUrl(item.description)),
  142. item.tags && item.tags.filter(Boolean).length
  143. ? div({ class: 'card-tags' }, item.tags.filter(Boolean).map(tag =>
  144. a({ class: "tag-link", href: `/search?query=%23${encodeURIComponent(tag)}` },
  145. `#${tag}`)
  146. ))
  147. : null,
  148. ),
  149. div({ class: "market-card right-col" },
  150. renderCardField(`${i18n.marketItemStock}:`, item.stock > 0 ? item.stock : i18n.marketOutOfStock),
  151. div({ class: "market-card price" },
  152. renderCardField(`${i18n.marketItemPrice}:`, `${item.price} ECO`),
  153. ),
  154. renderCardField(`${i18n.marketItemCondition}:`, item.item_status),
  155. renderCardField(`${i18n.marketItemIncludesShipping}:`, item.includesShipping ? i18n.YESLabel : i18n.NOLabel),
  156. renderCardField(`${i18n.marketItemSeller}:`),
  157. div({ class: "market-card image" },
  158. div({ class: 'card-field' },
  159. a({ class: 'user-link', href: `/author/${encodeURIComponent(item.seller)}` }, item.seller)
  160. )),
  161. item.item_type === 'auction' && item.auctions_poll.length > 0
  162. ? div({ class: "auction-info" },
  163. p({ class: "auction-bid-text" }, i18n.marketAuctionBids),
  164. table({ class: 'auction-bid-table' },
  165. tr(
  166. th(i18n.marketAuctionBidTime),
  167. th(i18n.marketAuctionUser),
  168. th(i18n.marketAuctionBidAmount)
  169. ),
  170. item.auctions_poll.map(bid => {
  171. const [userId, bidAmount, bidTime] = bid.split(':');
  172. return tr(
  173. td(moment(bidTime).format('YYYY-MM-DD HH:mm:ss')),
  174. td(a({ href: `/author/${encodeURIComponent(userId)}` }, userId)),
  175. td(`${parseFloat(bidAmount).toFixed(6)} ECO`)
  176. );
  177. })
  178. )
  179. )
  180. : null,
  181. div({ class: "market-card buttons" },
  182. (item.seller === userId) ? [
  183. form({ method: "POST", action: `/market/delete/${encodeURIComponent(item.id)}` },
  184. button({ class: "delete-btn", type: "submit" }, i18n.marketActionsDelete)
  185. ),
  186. (item.status !== 'SOLD' && item.status !== 'DISCARDED' && item.auctions_poll.length === 0)
  187. ? form({ method: "GET", action: `/market/edit/${encodeURIComponent(item.id)}` },
  188. button({ class: "update-btn", type: "submit" }, i18n.marketActionsUpdate)
  189. )
  190. : null,
  191. (item.status === 'FOR SALE')
  192. ? form({ method: "POST", action: `/market/sold/${encodeURIComponent(item.id)}` },
  193. button({ class: "sold-btn", type: "submit" }, i18n.marketActionsSold)
  194. )
  195. : null
  196. ] : [
  197. (item.status !== 'SOLD' && item.status !== 'DISCARDED' && item.item_type === 'auction')
  198. ? form({ method: "POST", action: `/market/bid/${encodeURIComponent(item.id)}` },
  199. input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }),
  200. br,
  201. button({ class: "buy-btn", type: "submit" }, i18n.marketPlaceBidButton)
  202. )
  203. : null,
  204. (item.status === 'FOR SALE' && item.item_type !== 'auction' && item.seller !== userId)
  205. ? form({ method: "POST", action: `/market/buy/${encodeURIComponent(item.id)}` },
  206. input({ type: "hidden", name: "buyerId", value: userId }),
  207. button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy)
  208. )
  209. : null
  210. ]
  211. )
  212. )
  213. )
  214. )
  215. : p(i18n.marketNoItems)
  216. )
  217. )
  218. )
  219. );
  220. };
  221. exports.singleMarketView = async (item, filter) => {
  222. return template(
  223. item.title,
  224. section(
  225. div({ class: "filters" },
  226. form({ method: 'GET', action: '/market' },
  227. button({ type: 'submit', name: 'filter', value: 'all', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterAll),
  228. button({ type: 'submit', name: 'filter', value: 'mine', class: filter === 'mine' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterMine),
  229. button({ type: 'submit', name: 'filter', value: 'exchange', class: filter === 'exchange' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterItems),
  230. button({ type: 'submit', name: 'filter', value: 'auctions', class: filter === 'auctions' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterAuctions),
  231. button({ type: 'submit', name: 'filter', value: 'new', class: filter === 'new' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterNew),
  232. button({ type: 'submit', name: 'filter', value: 'used', class: filter === 'used' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterUsed),
  233. button({ type: 'submit', name: 'filter', value: 'broken', class: filter === 'broken' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterBroken),
  234. button({ type: 'submit', name: 'filter', value: 'for sale', class: filter === 'for sale' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterForSale),
  235. button({ type: 'submit', name: 'filter', value: 'sold', class: filter === 'sold' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterSold),
  236. button({ type: 'submit', name: 'filter', value: 'discarded', class: filter === 'discarded' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterDiscarded),
  237. button({ type: 'submit', name: 'filter', value: 'recent', class: filter === 'recent' ? 'filter-btn active' : 'filter-btn' }, i18n.marketFilterRecent),
  238. button({ type: 'submit', name: 'filter', value: 'create', class: "create-button" }, i18n.marketCreateButton)
  239. )
  240. ),
  241. div({ class: "tags-header" },
  242. h2(item.title),
  243. renderCardField(`${i18n.marketItemType}:`, `${item.item_type.toUpperCase()}`),
  244. renderCardField(`${i18n.marketItemStatus}:`, item.status),
  245. renderCardField(`${i18n.marketItemCondition}:`, item.item_status),
  246. br,
  247. div({ class: "market-item image" },
  248. item.image
  249. ? img({ src: `/blob/${encodeURIComponent(item.image)}` })
  250. : img({ src: '/assets/images/default-market.png', alt: item.title })
  251. ),
  252. renderCardField(`${i18n.marketItemDescription}:`),
  253. p(...renderUrl(item.description)),
  254. item.tags && item.tags.length
  255. ? div({ class: 'card-tags' },
  256. item.tags.map(tag =>
  257. a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)
  258. )
  259. )
  260. : null,
  261. br,
  262. renderCardField(`${i18n.marketItemPrice}:`),
  263. br,
  264. div({ class: 'card-label' },
  265. h2(`${item.price} ECO`),
  266. ),
  267. br,
  268. renderCardField(`${i18n.marketItemStock}:`, item.stock > 0 ? item.stock : i18n.marketOutOfStock),
  269. renderCardField(`${i18n.marketItemIncludesShipping}:`, `${item.includesShipping ? i18n.YESLabel : i18n.NOLabel}`),
  270. item.deadline ? renderCardField(`${i18n.marketItemAvailable}:`, `${moment(item.deadline).format('YYYY/MM/DD HH:mm:ss')}`) : null,
  271. renderCardField(`${i18n.marketItemSeller}:`),
  272. br,
  273. div({ class: 'card-field' },
  274. a({ class: 'user-link', href: `/author/${encodeURIComponent(item.seller)}` }, item.seller)
  275. )
  276. ),
  277. item.item_type === 'auction'
  278. ? div({ class: "auction-info" },
  279. p({ class: "auction-bid-text" }, i18n.marketAuctionBids),
  280. table({ class: 'auction-bid-table' },
  281. tr(
  282. th(i18n.marketAuctionBidTime),
  283. th(i18n.marketAuctionUser),
  284. th(i18n.marketAuctionBidAmount)
  285. ),
  286. item.auctions_poll.map(bid => {
  287. const [userId, bidAmount, bidTime] = bid.split(':');
  288. return tr(
  289. td(moment(bidTime).format('YYYY-MM-DD HH:mm:ss')),
  290. td(a({ href: `/author/${encodeURIComponent(userId)}` }, userId)),
  291. td(`${parseFloat(bidAmount).toFixed(6)} ECO`)
  292. );
  293. })
  294. ),
  295. item.status !== 'SOLD' && item.status !== 'DISCARDED'
  296. ? form({ method: "POST", action: `/market/bid/${encodeURIComponent(item.id)}` },
  297. input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }),
  298. br(),
  299. button({ class: "buy-btn", type: "submit" }, i18n.marketPlaceBidButton)
  300. )
  301. : null
  302. )
  303. : null,
  304. div({ class: "market-item actions" },
  305. (item.seller === userId && item.status !== 'SOLD' && item.status !== 'DISCARDED' && item.item_type !== 'auction') ? [
  306. form({ method: "POST", action: `/market/delete/${encodeURIComponent(item.id)}` },
  307. button({ class: "delete-btn", type: "submit" }, i18n.marketActionsDelete)
  308. ),
  309. form({ method: "GET", action: `/market/edit/${encodeURIComponent(item.id)}` },
  310. button({ class: "update-btn", type: "submit" }, i18n.marketActionsUpdate)
  311. ),
  312. (item.status === 'FOR SALE')
  313. ? form({ method: "POST", action: `/market/sold/${encodeURIComponent(item.id)}` },
  314. button({ class: "sold-btn", type: "submit" }, i18n.marketActionsSold)
  315. )
  316. : null
  317. ] : null,
  318. (item.status === 'FOR SALE' && item.item_type !== 'auction' && item.seller !== userId)
  319. ? form({ method: "POST", action: `/market/buy/${encodeURIComponent(item.id)}` },
  320. button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy)
  321. )
  322. : null
  323. )
  324. )
  325. );
  326. };