@meith/web 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (511) hide show
  1. package/LICENSE.md +165 -0
  2. package/app/(auth)/layout.tsx +6 -0
  3. package/app/(auth)/login/page.tsx +106 -0
  4. package/app/(auth)/login/verify/page.tsx +55 -0
  5. package/app/(auth)/register/page.tsx +83 -0
  6. package/app/(auth)/reset/confirm/page.tsx +45 -0
  7. package/app/(auth)/reset/page.tsx +22 -0
  8. package/app/(auth)/verify/page.tsx +44 -0
  9. package/app/(auth)/verify/resend/page.tsx +51 -0
  10. package/app/(board)/[slug]/feed.xml/route.ts +48 -0
  11. package/app/(board)/[slug]/new/page.tsx +112 -0
  12. package/app/(board)/[slug]/page.tsx +403 -0
  13. package/app/(board)/discover/[view]/page.tsx +115 -0
  14. package/app/(board)/discover/page.tsx +5 -0
  15. package/app/(board)/layout.tsx +22 -0
  16. package/app/(board)/member/[id]/page.tsx +187 -0
  17. package/app/(board)/member/[id]/reputation/page.tsx +195 -0
  18. package/app/(board)/member/by-name/[name]/page.tsx +32 -0
  19. package/app/(board)/messages/[id]/page.tsx +103 -0
  20. package/app/(board)/messages/compose/page.tsx +60 -0
  21. package/app/(board)/messages/layout.tsx +5 -0
  22. package/app/(board)/messages/page.tsx +179 -0
  23. package/app/(board)/modcp/forums/page.tsx +59 -0
  24. package/app/(board)/modcp/ip/page.tsx +158 -0
  25. package/app/(board)/modcp/layout.tsx +5 -0
  26. package/app/(board)/modcp/log/page.tsx +98 -0
  27. package/app/(board)/modcp/page.tsx +112 -0
  28. package/app/(board)/moderation/layout.tsx +5 -0
  29. package/app/(board)/moderation/page.tsx +111 -0
  30. package/app/(board)/moderation/reports/page.tsx +193 -0
  31. package/app/(board)/moderation/warn/page.tsx +158 -0
  32. package/app/(board)/notifications/layout.tsx +5 -0
  33. package/app/(board)/notifications/page.tsx +144 -0
  34. package/app/(board)/notifications/preferences/page.tsx +65 -0
  35. package/app/(board)/online/page.tsx +119 -0
  36. package/app/(board)/page.tsx +195 -0
  37. package/app/(board)/plugins/[key]/[[...path]]/page.tsx +72 -0
  38. package/app/(board)/privacy/page.tsx +12 -0
  39. package/app/(board)/report/page.tsx +71 -0
  40. package/app/(board)/rules/page.tsx +12 -0
  41. package/app/(board)/search/[token]/page.tsx +130 -0
  42. package/app/(board)/search/page.tsx +289 -0
  43. package/app/(board)/stats/page.tsx +202 -0
  44. package/app/(board)/subscriptions/layout.tsx +5 -0
  45. package/app/(board)/subscriptions/page.tsx +142 -0
  46. package/app/(board)/terms/page.tsx +12 -0
  47. package/app/(board)/thread/[slug]/edit/page.tsx +107 -0
  48. package/app/(board)/thread/[slug]/feed.xml/route.ts +42 -0
  49. package/app/(board)/thread/[slug]/page.tsx +707 -0
  50. package/app/(board)/thread/[slug]/poll/page.tsx +41 -0
  51. package/app/(board)/thread/[slug]/post/[postId]/history/page.tsx +160 -0
  52. package/app/(board)/thread/[slug]/reply/page.tsx +136 -0
  53. package/app/(board)/unsubscribe/page.tsx +73 -0
  54. package/app/(board)/usercp/avatar/page.tsx +56 -0
  55. package/app/(board)/usercp/contacts/page.tsx +141 -0
  56. package/app/(board)/usercp/email/confirm/page.tsx +52 -0
  57. package/app/(board)/usercp/layout.tsx +23 -0
  58. package/app/(board)/usercp/options/page.tsx +56 -0
  59. package/app/(board)/usercp/page.tsx +71 -0
  60. package/app/(board)/usercp/profile/page.tsx +81 -0
  61. package/app/(board)/usercp/security/page.tsx +238 -0
  62. package/app/(board)/usercp/security/verify/page.tsx +92 -0
  63. package/app/(board)/usercp/signature/page.tsx +67 -0
  64. package/app/admin/antispam/page.tsx +155 -0
  65. package/app/admin/api/marketplace/screenshot/route.ts +68 -0
  66. package/app/admin/api/plugins/[key]/[...path]/route.ts +27 -0
  67. package/app/admin/api-tokens/page.tsx +129 -0
  68. package/app/admin/content/announcements/page.tsx +110 -0
  69. package/app/admin/content/attachments/page.tsx +199 -0
  70. package/app/admin/content/navigation/page.tsx +123 -0
  71. package/app/admin/content/page.tsx +170 -0
  72. package/app/admin/forums/[id]/page.tsx +96 -0
  73. package/app/admin/forums/[id]/permissions/page.tsx +108 -0
  74. package/app/admin/forums/page.tsx +45 -0
  75. package/app/admin/groups/[id]/page.tsx +133 -0
  76. package/app/admin/groups/memberships/page.tsx +56 -0
  77. package/app/admin/groups/page.tsx +104 -0
  78. package/app/admin/groups/promotions/page.tsx +143 -0
  79. package/app/admin/layout.tsx +86 -0
  80. package/app/admin/log/page.tsx +116 -0
  81. package/app/admin/page.tsx +206 -0
  82. package/app/admin/plugins/[key]/[[...path]]/page.tsx +276 -0
  83. package/app/admin/plugins/browse/page.tsx +70 -0
  84. package/app/admin/plugins/page.tsx +165 -0
  85. package/app/admin/security/page.tsx +124 -0
  86. package/app/admin/settings/page.tsx +190 -0
  87. package/app/admin/system/page.tsx +303 -0
  88. package/app/admin/themes/[key]/page.tsx +110 -0
  89. package/app/admin/themes/browse/page.tsx +70 -0
  90. package/app/admin/themes/page.tsx +163 -0
  91. package/app/admin/users/[id]/merge/page.tsx +136 -0
  92. package/app/admin/users/[id]/page.tsx +208 -0
  93. package/app/admin/users/mail/page.tsx +68 -0
  94. package/app/admin/users/page.tsx +260 -0
  95. package/app/admin/users/prune/page.tsx +186 -0
  96. package/app/api/health/route.ts +11 -0
  97. package/app/api/metrics/route.ts +66 -0
  98. package/app/api/plugins/[key]/[...path]/route.ts +27 -0
  99. package/app/api/read/all/route.ts +15 -0
  100. package/app/api/read/forum/[id]/route.ts +29 -0
  101. package/app/api/read/thread/[id]/route.ts +40 -0
  102. package/app/api/ready/route.ts +63 -0
  103. package/app/api/system/tick/route.ts +77 -0
  104. package/app/api/v1/[...path]/route.ts +208 -0
  105. package/app/api/v1/openapi.json/route.ts +12 -0
  106. package/app/apple-icon.ts +11 -0
  107. package/app/atom.xml/route.ts +7 -0
  108. package/app/attachment/[id]/route.ts +10 -0
  109. package/app/attachment/[id]/thumb/route.ts +10 -0
  110. package/app/auth/passkey/options/route.ts +145 -0
  111. package/app/auth/passkey/verify/route.ts +241 -0
  112. package/app/auth/resume/route.ts +55 -0
  113. package/app/auth/sso/[provider]/callback/route.ts +173 -0
  114. package/app/auth/sso/[provider]/go/route.ts +57 -0
  115. package/app/auth/sso/[provider]/route.ts +105 -0
  116. package/app/avatar/[id]/route.ts +31 -0
  117. package/app/brand/icon-192.png/route.ts +7 -0
  118. package/app/brand/icon-512.png/route.ts +7 -0
  119. package/app/brand/icon-maskable.png/route.ts +7 -0
  120. package/app/demo/stripe/[[...path]]/route.ts +27 -0
  121. package/app/error.tsx +15 -0
  122. package/app/feed.xml/route.ts +7 -0
  123. package/app/forumdisplay.php/page.tsx +11 -0
  124. package/app/group/[id]/badge/[scheme]/route.ts +26 -0
  125. package/app/icon.ts +13 -0
  126. package/app/install/page.tsx +221 -0
  127. package/app/jump/page.tsx +28 -0
  128. package/app/layout.tsx +124 -0
  129. package/app/logo/[scheme]/route.ts +24 -0
  130. package/app/manifest.ts +43 -0
  131. package/app/member.php/page.tsx +11 -0
  132. package/app/memberlist.php/page.tsx +11 -0
  133. package/app/not-found.tsx +14 -0
  134. package/app/opengraph-image.ts +11 -0
  135. package/app/redirect/page.tsx +44 -0
  136. package/app/robots.txt/route.ts +35 -0
  137. package/app/showthread.php/page.tsx +11 -0
  138. package/app/sitemap/[page]/route.ts +64 -0
  139. package/app/sitemap.xml/route.ts +31 -0
  140. package/app/twitter-image.ts +11 -0
  141. package/app/viewforum.php/page.tsx +11 -0
  142. package/app/viewtopic.php/page.tsx +11 -0
  143. package/bin/forum-web.mjs +241 -0
  144. package/components.json +21 -0
  145. package/instrumentation.ts +18 -0
  146. package/next.config.mjs +182 -0
  147. package/package.json +99 -0
  148. package/postcss.config.mjs +7 -0
  149. package/proxy.ts +160 -0
  150. package/src/components/account/active-sessions.tsx +87 -0
  151. package/src/components/account/avatar-form.tsx +119 -0
  152. package/src/components/account/credential-proof-form.tsx +76 -0
  153. package/src/components/account/logout-form.tsx +14 -0
  154. package/src/components/account/notification-forms.tsx +157 -0
  155. package/src/components/account/passkey-enrol.tsx +81 -0
  156. package/src/components/account/passkey-proof-button.tsx +47 -0
  157. package/src/components/account/push-device-form.tsx +193 -0
  158. package/src/components/account/relation-forms.tsx +69 -0
  159. package/src/components/account/reputation-forms.tsx +124 -0
  160. package/src/components/account/security-activity.tsx +46 -0
  161. package/src/components/account/sign-in-methods.tsx +166 -0
  162. package/src/components/account/subscription-forms.tsx +151 -0
  163. package/src/components/account/two-factor-forms.tsx +226 -0
  164. package/src/components/account/usercp-forms.tsx +387 -0
  165. package/src/components/account/usercp-nav.tsx +14 -0
  166. package/src/components/account/usercp-shell.tsx +34 -0
  167. package/src/components/admin/admin-forms.tsx +92 -0
  168. package/src/components/admin/admin-nav.tsx +52 -0
  169. package/src/components/admin/admin-undo.tsx +32 -0
  170. package/src/components/admin/api-token-forms.tsx +89 -0
  171. package/src/components/admin/badge-forms.tsx +97 -0
  172. package/src/components/admin/branding-forms.tsx +102 -0
  173. package/src/components/admin/content-forms.tsx +712 -0
  174. package/src/components/admin/form-bits.tsx +11 -0
  175. package/src/components/admin/forum-forms.tsx +524 -0
  176. package/src/components/admin/forum-tree.tsx +454 -0
  177. package/src/components/admin/group-forms.tsx +646 -0
  178. package/src/components/admin/mail-test-card.tsx +90 -0
  179. package/src/components/admin/marketplace-forms.tsx +30 -0
  180. package/src/components/admin/marketplace-listing.tsx +123 -0
  181. package/src/components/admin/navigation-forms.tsx +229 -0
  182. package/src/components/admin/navigation-tree.tsx +442 -0
  183. package/src/components/admin/oklch-picker.tsx +180 -0
  184. package/src/components/admin/plugin-forms.tsx +225 -0
  185. package/src/components/admin/settings-form.tsx +148 -0
  186. package/src/components/admin/system-forms.tsx +169 -0
  187. package/src/components/admin/theme-changes.tsx +354 -0
  188. package/src/components/admin/theme-forms.tsx +636 -0
  189. package/src/components/admin/theme-palette.tsx +119 -0
  190. package/src/components/admin/theme-preview.tsx +461 -0
  191. package/src/components/admin/user-bulk-toolbar.tsx +99 -0
  192. package/src/components/admin/user-forms.tsx +488 -0
  193. package/src/components/auth/auth-page.tsx +45 -0
  194. package/src/components/auth/form-controls.tsx +117 -0
  195. package/src/components/auth/login-form.tsx +47 -0
  196. package/src/components/auth/passkey-client.ts +171 -0
  197. package/src/components/auth/passkey-second-factor.tsx +69 -0
  198. package/src/components/auth/passkey-sign-in.tsx +61 -0
  199. package/src/components/auth/register-form.tsx +136 -0
  200. package/src/components/auth/resend-verification-form.tsx +34 -0
  201. package/src/components/auth/reset-confirm-form.tsx +43 -0
  202. package/src/components/auth/reset-request-form.tsx +35 -0
  203. package/src/components/auth/second-factor-form.tsx +51 -0
  204. package/src/components/auth/sso-buttons.tsx +47 -0
  205. package/src/components/board/live-region.tsx +74 -0
  206. package/src/components/board/search-off-notice.tsx +18 -0
  207. package/src/components/board/section-page.tsx +114 -0
  208. package/src/components/content/attachment-field.tsx +51 -0
  209. package/src/components/content/composer-field.ts +31 -0
  210. package/src/components/content/composer-ids.ts +6 -0
  211. package/src/components/content/composer-intents.tsx +27 -0
  212. package/src/components/content/composer-recovery.tsx +215 -0
  213. package/src/components/content/edit-post-form.tsx +121 -0
  214. package/src/components/content/markdown-editor.tsx +590 -0
  215. package/src/components/content/markdown-syntax.ts +53 -0
  216. package/src/components/content/mention-token.ts +23 -0
  217. package/src/components/content/multiquote-button.tsx +32 -0
  218. package/src/components/content/multiquote-selection.tsx +105 -0
  219. package/src/components/content/multiquote.ts +125 -0
  220. package/src/components/content/new-thread-form.tsx +169 -0
  221. package/src/components/content/poll-edit-form.tsx +96 -0
  222. package/src/components/content/poll.tsx +89 -0
  223. package/src/components/content/quote-in-place.tsx +51 -0
  224. package/src/components/content/reply-form.tsx +113 -0
  225. package/src/components/content/thanks-button.tsx +65 -0
  226. package/src/components/content/thread-rating.tsx +143 -0
  227. package/src/components/install/install-form.tsx +497 -0
  228. package/src/components/legal/legal-document.tsx +22 -0
  229. package/src/components/messages/message-forms.tsx +152 -0
  230. package/src/components/moderation/inline-moderation-form.tsx +158 -0
  231. package/src/components/moderation/modcp-nav.tsx +15 -0
  232. package/src/components/moderation/modcp-shell.tsx +41 -0
  233. package/src/components/moderation/queue-form.tsx +96 -0
  234. package/src/components/moderation/report-forms.tsx +120 -0
  235. package/src/components/moderation/signature-lock-form.tsx +83 -0
  236. package/src/components/moderation/thread-surgery-form.tsx +96 -0
  237. package/src/components/moderation/thread-tools-form.tsx +137 -0
  238. package/src/components/moderation/warning-forms.tsx +125 -0
  239. package/src/components/profile/custom-field.tsx +79 -0
  240. package/src/components/shell/appearance-icons.tsx +35 -0
  241. package/src/components/shell/board-notice.tsx +21 -0
  242. package/src/components/shell/copy-record.ts +20 -0
  243. package/src/components/shell/copy.tsx +17 -0
  244. package/src/components/shell/crash-notice.tsx +86 -0
  245. package/src/components/shell/demo-banner.tsx +49 -0
  246. package/src/components/shell/group-name-style.tsx +6 -0
  247. package/src/components/shell/measure.ts +1 -0
  248. package/src/components/shell/notification-menu.tsx +361 -0
  249. package/src/components/shell/offline-notice.tsx +29 -0
  250. package/src/components/shell/onboarding-banner.tsx +39 -0
  251. package/src/components/shell/page-shell.tsx +167 -0
  252. package/src/components/shell/panel-list.tsx +95 -0
  253. package/src/components/shell/panel-nav.tsx +40 -0
  254. package/src/components/shell/panel-overview.tsx +81 -0
  255. package/src/components/shell/panel-page.tsx +96 -0
  256. package/src/components/shell/panel-pagination.tsx +28 -0
  257. package/src/components/shell/panel-shell.tsx +31 -0
  258. package/src/components/shell/theme-runtime-style.tsx +6 -0
  259. package/src/components/shell/theme-switcher.tsx +97 -0
  260. package/src/components/shell/timezone-probe.tsx +45 -0
  261. package/src/components/shell/view-tabs.tsx +60 -0
  262. package/src/config.ts +13 -0
  263. package/src/server/absent-services.ts +29 -0
  264. package/src/server/admin-actions.ts +171 -0
  265. package/src/server/admin-settings-actions.ts +83 -0
  266. package/src/server/admin-undo-actions.ts +70 -0
  267. package/src/server/admin-undo.ts +75 -0
  268. package/src/server/admin.ts +112 -0
  269. package/src/server/announcements.ts +82 -0
  270. package/src/server/antispam.ts +310 -0
  271. package/src/server/api/content.ts +486 -0
  272. package/src/server/api/http.ts +119 -0
  273. package/src/server/api/members.ts +109 -0
  274. package/src/server/api/messages.ts +113 -0
  275. package/src/server/api/registry.ts +30 -0
  276. package/src/server/api/search.ts +98 -0
  277. package/src/server/api/subscriptions.ts +106 -0
  278. package/src/server/api-auth.ts +66 -0
  279. package/src/server/api-token-actions.ts +85 -0
  280. package/src/server/api-tokens-admin.ts +95 -0
  281. package/src/server/appearance-actions.ts +39 -0
  282. package/src/server/attachment-download.ts +43 -0
  283. package/src/server/attachment-embed.ts +63 -0
  284. package/src/server/attachment-upload-actions.ts +92 -0
  285. package/src/server/attachments.ts +257 -0
  286. package/src/server/auth-actions.ts +479 -0
  287. package/src/server/auth-config.ts +61 -0
  288. package/src/server/auth-events.ts +92 -0
  289. package/src/server/auth-form-state.ts +14 -0
  290. package/src/server/auth-mail.ts +104 -0
  291. package/src/server/avatars.ts +83 -0
  292. package/src/server/board-latest-actions.ts +7 -0
  293. package/src/server/board-latest.tsx +103 -0
  294. package/src/server/board-offline.ts +23 -0
  295. package/src/server/board-url.ts +14 -0
  296. package/src/server/brand-assets.ts +151 -0
  297. package/src/server/brand-mark.tsx +110 -0
  298. package/src/server/branding-actions.ts +53 -0
  299. package/src/server/branding.ts +101 -0
  300. package/src/server/cache-targets.ts +13 -0
  301. package/src/server/code-highlighting.ts +33 -0
  302. package/src/server/container.ts +402 -0
  303. package/src/server/content-actions.ts +459 -0
  304. package/src/server/content-admin-actions.ts +492 -0
  305. package/src/server/content-admin.ts +95 -0
  306. package/src/server/content-security-policy.ts +35 -0
  307. package/src/server/context.ts +32 -0
  308. package/src/server/cookies.ts +139 -0
  309. package/src/server/credential-proof-actions.ts +42 -0
  310. package/src/server/credential-proof.ts +35 -0
  311. package/src/server/current-location.ts +19 -0
  312. package/src/server/demo-stripe.ts +104 -0
  313. package/src/server/demo-uploads.ts +28 -0
  314. package/src/server/demo.ts +80 -0
  315. package/src/server/discovery.ts +104 -0
  316. package/src/server/embeds.ts +113 -0
  317. package/src/server/error-notice.tsx +41 -0
  318. package/src/server/federation-actions.ts +113 -0
  319. package/src/server/federation.ts +138 -0
  320. package/src/server/feed-builder.ts +72 -0
  321. package/src/server/feed-routes.ts +122 -0
  322. package/src/server/fixture-actor-source.ts +62 -0
  323. package/src/server/fixture-forum-repo.ts +59 -0
  324. package/src/server/fixture-member-profile-repo.ts +30 -0
  325. package/src/server/fixture-post-repo.ts +108 -0
  326. package/src/server/fixture-thread-repo.ts +98 -0
  327. package/src/server/form-state-reporter.ts +34 -0
  328. package/src/server/form-values.ts +23 -0
  329. package/src/server/forum-admin-actions.ts +353 -0
  330. package/src/server/forum-admin.ts +98 -0
  331. package/src/server/group-admin-actions.ts +341 -0
  332. package/src/server/group-admin.ts +98 -0
  333. package/src/server/group-badge-actions.ts +61 -0
  334. package/src/server/group-badge.ts +62 -0
  335. package/src/server/group-identity.ts +104 -0
  336. package/src/server/i18n-catalogs.ts +42 -0
  337. package/src/server/i18n.ts +69 -0
  338. package/src/server/image-upload.ts +109 -0
  339. package/src/server/inline-moderation-actions.ts +129 -0
  340. package/src/server/install-actions.ts +124 -0
  341. package/src/server/install.ts +281 -0
  342. package/src/server/legacy-redirect.ts +83 -0
  343. package/src/server/legal.ts +43 -0
  344. package/src/server/location-header.ts +37 -0
  345. package/src/server/mail-brand.ts +43 -0
  346. package/src/server/mail-health.ts +41 -0
  347. package/src/server/mail-send.ts +44 -0
  348. package/src/server/mail-test-actions.ts +67 -0
  349. package/src/server/mail-test.ts +56 -0
  350. package/src/server/markdown-pipeline.ts +31 -0
  351. package/src/server/marketplace-actions.ts +47 -0
  352. package/src/server/marketplace-admin.ts +260 -0
  353. package/src/server/mention-actions.ts +21 -0
  354. package/src/server/mention-search.ts +35 -0
  355. package/src/server/message-actions.ts +110 -0
  356. package/src/server/messages.ts +141 -0
  357. package/src/server/modcp.ts +125 -0
  358. package/src/server/moderation-actions.ts +165 -0
  359. package/src/server/navigation-admin-actions.ts +217 -0
  360. package/src/server/navigation.ts +112 -0
  361. package/src/server/nonce.ts +13 -0
  362. package/src/server/notification-actions.ts +84 -0
  363. package/src/server/notification-audience.ts +10 -0
  364. package/src/server/notification-menu-actions.ts +82 -0
  365. package/src/server/notification-menu.ts +96 -0
  366. package/src/server/notifications.ts +93 -0
  367. package/src/server/onboarding-actions.ts +27 -0
  368. package/src/server/onboarding.ts +89 -0
  369. package/src/server/page-metadata.ts +14 -0
  370. package/src/server/passkey-challenge.ts +42 -0
  371. package/src/server/plugin-admin-actions.ts +232 -0
  372. package/src/server/plugin-admin.ts +276 -0
  373. package/src/server/plugin-host.ts +143 -0
  374. package/src/server/plugin-pages.ts +192 -0
  375. package/src/server/plugin-panel.ts +49 -0
  376. package/src/server/plugin-routes.ts +298 -0
  377. package/src/server/plugin-view.tsx +58 -0
  378. package/src/server/poll-actions.ts +104 -0
  379. package/src/server/poll-scope.ts +52 -0
  380. package/src/server/post-notifications.ts +71 -0
  381. package/src/server/post-scope.ts +86 -0
  382. package/src/server/presence.ts +177 -0
  383. package/src/server/profile-fields.ts +112 -0
  384. package/src/server/push-actions.ts +113 -0
  385. package/src/server/push.ts +40 -0
  386. package/src/server/redirect-back.ts +15 -0
  387. package/src/server/registration.ts +7 -0
  388. package/src/server/relation-actions.ts +89 -0
  389. package/src/server/relations.ts +45 -0
  390. package/src/server/reply-core.ts +173 -0
  391. package/src/server/report-actions.ts +165 -0
  392. package/src/server/report-scope.ts +21 -0
  393. package/src/server/reputation-actions.ts +146 -0
  394. package/src/server/reputation-target.ts +49 -0
  395. package/src/server/reputation.ts +70 -0
  396. package/src/server/request-fingerprint.ts +65 -0
  397. package/src/server/safe-path.ts +16 -0
  398. package/src/server/same-origin.ts +45 -0
  399. package/src/server/search-page.ts +313 -0
  400. package/src/server/search.ts +45 -0
  401. package/src/server/secret-auth.ts +20 -0
  402. package/src/server/see-other.ts +11 -0
  403. package/src/server/seed-board.ts +417 -0
  404. package/src/server/session-actions.ts +78 -0
  405. package/src/server/session-cookies.ts +124 -0
  406. package/src/server/session-key.ts +17 -0
  407. package/src/server/settings.ts +47 -0
  408. package/src/server/signatures.ts +62 -0
  409. package/src/server/sso-handshake.ts +79 -0
  410. package/src/server/stats.ts +63 -0
  411. package/src/server/subscription-actions.ts +161 -0
  412. package/src/server/surgery-actions.ts +180 -0
  413. package/src/server/syndication.ts +77 -0
  414. package/src/server/system-admin-actions.ts +134 -0
  415. package/src/server/system-admin.ts +90 -0
  416. package/src/server/test-container.ts +114 -0
  417. package/src/server/theme-admin-actions.ts +242 -0
  418. package/src/server/theme-admin.ts +203 -0
  419. package/src/server/theme-runtime.ts +89 -0
  420. package/src/server/theme-style.ts +279 -0
  421. package/src/server/theme.ts +52 -0
  422. package/src/server/thread-core.ts +207 -0
  423. package/src/server/thread-rating-actions.ts +55 -0
  424. package/src/server/thread-tool-actions.ts +154 -0
  425. package/src/server/two-factor-actions.ts +178 -0
  426. package/src/server/two-factor.ts +86 -0
  427. package/src/server/upgrade-notice.ts +41 -0
  428. package/src/server/user-admin-actions.ts +572 -0
  429. package/src/server/user-admin.ts +165 -0
  430. package/src/server/usercp-actions.ts +263 -0
  431. package/src/server/usercp-mail.ts +120 -0
  432. package/src/server/viewer-preferences.ts +64 -0
  433. package/src/server/warning-actions.ts +122 -0
  434. package/src/styles/globals.css +969 -0
  435. package/src/theme/contract.fixture.ts +793 -0
  436. package/src/view/account-copy.ts +351 -0
  437. package/src/view/admin-content-copy.ts +66 -0
  438. package/src/view/admin-copy.ts +106 -0
  439. package/src/view/admin-forum-tree-copy.ts +40 -0
  440. package/src/view/admin-group-copy.ts +80 -0
  441. package/src/view/admin-log.ts +58 -0
  442. package/src/view/admin-nav.ts +164 -0
  443. package/src/view/admin-navigation-copy.ts +58 -0
  444. package/src/view/admin-panel-copy.ts +194 -0
  445. package/src/view/admin-settings.ts +173 -0
  446. package/src/view/admin-theme-copy.ts +229 -0
  447. package/src/view/admin-user-copy.ts +96 -0
  448. package/src/view/attachments.ts +51 -0
  449. package/src/view/auth-copy.ts +147 -0
  450. package/src/view/board-index.ts +167 -0
  451. package/src/view/board-latest.ts +101 -0
  452. package/src/view/breadcrumb.ts +50 -0
  453. package/src/view/contacts.ts +85 -0
  454. package/src/view/content-copy.ts +118 -0
  455. package/src/view/contrast.ts +286 -0
  456. package/src/view/copy.ts +101 -0
  457. package/src/view/count.ts +8 -0
  458. package/src/view/discovery-view.ts +74 -0
  459. package/src/view/editor-toolbar.ts +84 -0
  460. package/src/view/error-notice.ts +52 -0
  461. package/src/view/feed.ts +130 -0
  462. package/src/view/forum-cursor.ts +54 -0
  463. package/src/view/forum-display.ts +152 -0
  464. package/src/view/forum-jump.ts +60 -0
  465. package/src/view/forum-notice.ts +24 -0
  466. package/src/view/inline-moderation.ts +87 -0
  467. package/src/view/install-copy.ts +95 -0
  468. package/src/view/legal.ts +58 -0
  469. package/src/view/locale.ts +40 -0
  470. package/src/view/marketplace-panel.ts +51 -0
  471. package/src/view/member-identity.ts +23 -0
  472. package/src/view/member-profile.ts +65 -0
  473. package/src/view/messages.ts +222 -0
  474. package/src/view/metadata.ts +75 -0
  475. package/src/view/modcp-nav.ts +76 -0
  476. package/src/view/moderation-copy.ts +84 -0
  477. package/src/view/moderation-queue.ts +67 -0
  478. package/src/view/navigation-arrange.ts +188 -0
  479. package/src/view/navigation.ts +173 -0
  480. package/src/view/notification-menu.ts +202 -0
  481. package/src/view/notifications.ts +84 -0
  482. package/src/view/onboarding.ts +26 -0
  483. package/src/view/pager.ts +191 -0
  484. package/src/view/panel-nav.ts +203 -0
  485. package/src/view/plugin-panel.ts +42 -0
  486. package/src/view/post-form.ts +61 -0
  487. package/src/view/post-link.ts +29 -0
  488. package/src/view/presence.ts +99 -0
  489. package/src/view/promotion-rules.ts +52 -0
  490. package/src/view/redirect-notice.ts +21 -0
  491. package/src/view/reputation.ts +107 -0
  492. package/src/view/search-controls.ts +103 -0
  493. package/src/view/search-results.ts +291 -0
  494. package/src/view/security-activity.ts +73 -0
  495. package/src/view/setting-groups.ts +43 -0
  496. package/src/view/shell.ts +164 -0
  497. package/src/view/slug-id.ts +12 -0
  498. package/src/view/sso-hand-off.ts +44 -0
  499. package/src/view/sso-notices.ts +48 -0
  500. package/src/view/subscriptions.ts +76 -0
  501. package/src/view/theme-draft.ts +167 -0
  502. package/src/view/theme-preference.ts +30 -0
  503. package/src/view/theme-tokens.ts +333 -0
  504. package/src/view/thread-view.ts +280 -0
  505. package/src/view/time.ts +31 -0
  506. package/src/view/timezone.ts +35 -0
  507. package/src/view/two-factor.ts +6 -0
  508. package/src/view/usercp-nav.ts +103 -0
  509. package/src/view/usercp.ts +196 -0
  510. package/src/view/warnings.ts +128 -0
  511. package/src/view/word-filter.ts +5 -0
@@ -0,0 +1,222 @@
1
+ import type { Translator } from '@meith/i18n'
2
+ import type { FolderCounts, MessageDetail, MessageFolder, MessageListRow } from '@meith/messages'
3
+ import type { LinkModel, TimeModel } from '@meith/theme-kit'
4
+
5
+ import { formatTime, untranslated } from './time'
6
+
7
+ export const MESSAGE_FORM_ID = 'message-actions'
8
+
9
+ export interface FolderTab {
10
+ readonly folder: MessageFolder
11
+ readonly label: string
12
+ readonly href: string
13
+ readonly count: number
14
+ readonly isCurrent: boolean
15
+ }
16
+
17
+ export interface MessageRowView {
18
+ readonly copyId: number
19
+ readonly href: string
20
+ readonly subject: string
21
+ readonly people: string
22
+ readonly peopleLabel: string
23
+ readonly at: TimeModel
24
+ readonly isUnread: boolean
25
+ }
26
+
27
+ export interface QuotaView {
28
+ readonly stored: number
29
+ readonly quota: number
30
+ readonly label: string
31
+ readonly isFull: boolean
32
+ readonly isNearlyFull: boolean
33
+ }
34
+
35
+ export interface MessageFolderView {
36
+ readonly folder: MessageFolder
37
+ readonly tabs: readonly FolderTab[]
38
+ readonly rows: readonly MessageRowView[]
39
+ readonly nextHref: string | null
40
+ readonly quota: QuotaView
41
+ readonly composeHref: string
42
+ }
43
+
44
+ const FOLDER_LABELS: Readonly<Record<MessageFolder, string>> = {
45
+ inbox: 'Inbox',
46
+ sent: 'Sent',
47
+ trash: 'Trash',
48
+ }
49
+
50
+ export function folderHref(folder: MessageFolder): string {
51
+ return folder === 'inbox' ? '/messages' : `/messages?folder=${folder}`
52
+ }
53
+
54
+ export function quotaView(counts: FolderCounts, quota: number): QuotaView {
55
+ if (quota <= 0) {
56
+ return {
57
+ stored: counts.stored,
58
+ quota: 0,
59
+ label: `${counts.stored} stored`,
60
+ isFull: false,
61
+ isNearlyFull: false,
62
+ }
63
+ }
64
+
65
+ return {
66
+ stored: counts.stored,
67
+ quota,
68
+ label: `${counts.stored} of ${quota} stored`,
69
+ isFull: counts.stored >= quota,
70
+ isNearlyFull: counts.stored >= Math.floor(quota * 0.9),
71
+ }
72
+ }
73
+
74
+ export function buildMessageFolderView(input: {
75
+ readonly folder: MessageFolder
76
+ readonly rows: readonly MessageListRow[]
77
+ readonly counts: FolderCounts
78
+ readonly quota: number
79
+ readonly nextBefore: number | null
80
+ readonly now: Date
81
+ readonly t?: Translator
82
+ }): MessageFolderView {
83
+ const t = input.t ?? untranslated()
84
+ const tabs: readonly FolderTab[] = (['inbox', 'sent', 'trash'] as const).map((folder) => ({
85
+ folder,
86
+ label: FOLDER_LABELS[folder],
87
+ href: folderHref(folder),
88
+ count: folder === 'inbox' ? input.counts.unread : input.counts[folder],
89
+ isCurrent: folder === input.folder,
90
+ }))
91
+
92
+ return {
93
+ folder: input.folder,
94
+ tabs,
95
+ rows: input.rows.map((row) => ({
96
+ copyId: row.copyId,
97
+ href: `/messages/${row.messageId}`,
98
+ subject: row.subject === '' ? t.t('message.noSubject') : row.subject,
99
+ people: peopleLabel(row),
100
+ peopleLabel: row.folder === 'sent' ? 'To' : 'From',
101
+ at: formatTime(row.sentAt, input.now, input.t),
102
+ isUnread: row.readAt === null && row.role !== 'author',
103
+ })),
104
+ nextHref:
105
+ input.nextBefore === null
106
+ ? null
107
+ : `${folderHref(input.folder)}${input.folder === 'inbox' ? '?' : '&'}before=${input.nextBefore}`,
108
+ quota: quotaView(input.counts, input.quota),
109
+ composeHref: '/messages/compose',
110
+ }
111
+ }
112
+
113
+ function peopleLabel(row: MessageListRow): string {
114
+ const names = [...row.counterparties]
115
+ if (names.length === 0)
116
+ return row.moreCounterparties > 0 ? `${row.moreCounterparties} people` : '—'
117
+
118
+ if (row.moreCounterparties > 0) {
119
+ return `${names.join(', ')} and ${row.moreCounterparties} ${
120
+ row.moreCounterparties === 1 ? 'other' : 'others'
121
+ }`
122
+ }
123
+
124
+ if (names.length === 1) return names[0] as string
125
+ const last = names.pop() as string
126
+ return `${names.join(', ')} and ${last}`
127
+ }
128
+
129
+ export interface ParticipantView {
130
+ readonly username: string
131
+ readonly role: 'author' | 'to' | 'bcc'
132
+ readonly readLabel: string | null
133
+ }
134
+
135
+ export interface MessageView {
136
+ readonly id: number
137
+ readonly subject: string
138
+ readonly author: string
139
+ readonly at: TimeModel
140
+ readonly bodyHtml: string
141
+ readonly folder: MessageFolder
142
+ readonly participants: readonly ParticipantView[]
143
+ readonly tracking: readonly ParticipantView[]
144
+ readonly actions: readonly LinkModel[]
145
+ readonly reportHref: string | null
146
+ }
147
+
148
+ export function buildMessageView(input: {
149
+ readonly detail: MessageDetail
150
+ readonly bodyHtml: string
151
+ readonly viewerUserId: number
152
+ readonly now: Date
153
+ readonly t?: Translator
154
+ }): MessageView {
155
+ const t = input.t ?? untranslated()
156
+ const { detail } = input
157
+ const isAuthor = detail.copy.role === 'author'
158
+
159
+ const participants: readonly ParticipantView[] = detail.participants.map((participant) => ({
160
+ username: participant.username,
161
+ role: participant.role,
162
+ readLabel:
163
+ participant.role === 'author'
164
+ ? null
165
+ : participant.readAt === null
166
+ ? t.t('message.notRead')
167
+ : `Read ${formatTime(participant.readAt, input.now, input.t).label}`,
168
+ }))
169
+
170
+ return {
171
+ id: detail.message.id,
172
+ subject: detail.message.subject === '' ? t.t('message.noSubject') : detail.message.subject,
173
+ author:
174
+ detail.message.authorUsername === ''
175
+ ? t.t('message.deletedMember')
176
+ : detail.message.authorUsername,
177
+ at: formatTime(detail.message.sentAt, input.now, input.t),
178
+ bodyHtml: input.bodyHtml,
179
+ folder: detail.copy.folder,
180
+ participants,
181
+ tracking: isAuthor ? participants.filter((p) => p.role !== 'author') : [],
182
+ actions: [
183
+ ...(isAuthor
184
+ ? []
185
+ : [{ label: 'Reply', href: `/messages/compose?reply=${detail.message.id}` }]),
186
+ { label: 'Forward', href: `/messages/compose?forward=${detail.message.id}` },
187
+ ],
188
+ reportHref: isAuthor ? null : `/report?kind=private_message&id=${detail.message.id}`,
189
+ }
190
+ }
191
+
192
+ export function messageNotice(
193
+ query: {
194
+ readonly sent?: string | undefined
195
+ readonly moved?: string | undefined
196
+ readonly deleted?: string | undefined
197
+ readonly marked?: string | undefined
198
+ readonly emptied?: string | undefined
199
+ },
200
+ t: Translator = untranslated(),
201
+ ): { kind: 'info'; message: string } | null {
202
+ if (query.sent !== undefined) return { kind: 'info', message: t.t('message.sent') }
203
+ if (query.moved !== undefined) {
204
+ return { kind: 'info', message: `${plural(query.moved, 'message')} moved.` }
205
+ }
206
+ if (query.deleted !== undefined) {
207
+ return { kind: 'info', message: `${plural(query.deleted, 'message')} deleted.` }
208
+ }
209
+ if (query.marked !== undefined) {
210
+ return { kind: 'info', message: `${plural(query.marked, 'message')} updated.` }
211
+ }
212
+ if (query.emptied !== undefined) {
213
+ return { kind: 'info', message: `Trash emptied: ${plural(query.emptied, 'message')} deleted.` }
214
+ }
215
+ return null
216
+ }
217
+
218
+ function plural(raw: string, noun: string): string {
219
+ const n = Number(raw)
220
+ const count = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0
221
+ return `${count} ${count === 1 ? noun : `${noun}s`}`
222
+ }
@@ -0,0 +1,75 @@
1
+ import { type CompiledWordFilter, summarise } from '@meith/markdown'
2
+
3
+ import { filterWords } from './word-filter'
4
+
5
+ export interface CanonicalInput {
6
+ readonly path: string
7
+ readonly page: number
8
+ }
9
+
10
+ export function canonicalPath(input: CanonicalInput): string {
11
+ return input.page <= 1 ? input.path : `${input.path}?page=${input.page}`
12
+ }
13
+
14
+ export interface PageLinks {
15
+ readonly canonical: string
16
+ readonly previous: string | null
17
+ readonly next: string | null
18
+ }
19
+
20
+ export function pageLinks(input: CanonicalInput & { readonly hasNext: boolean }): PageLinks {
21
+ return {
22
+ canonical: canonicalPath(input),
23
+ previous: input.page <= 1 ? null : canonicalPath({ path: input.path, page: input.page - 1 }),
24
+ next: input.hasNext ? canonicalPath({ path: input.path, page: input.page + 1 }) : null,
25
+ }
26
+ }
27
+
28
+ export function cardDescription(
29
+ source: string | null,
30
+ fallback: string,
31
+ wordFilter?: CompiledWordFilter | undefined,
32
+ ): string {
33
+ if (source === null) return fallback
34
+ const flat = filterWords(summarise(source, 200), wordFilter)
35
+ return flat === '' ? fallback : flat
36
+ }
37
+
38
+ export interface ThreadJsonLd {
39
+ readonly title: string
40
+ readonly url: string
41
+ readonly author: string
42
+ readonly published: Date
43
+ readonly modified: Date
44
+ readonly replyCount: number
45
+ readonly forumTitle: string
46
+ readonly description: string
47
+ }
48
+
49
+ export function jsonLdScript(record: Record<string, unknown>): string {
50
+ return JSON.stringify(record)
51
+ .replace(/</g, '\\u003c')
52
+ .replace(/>/g, '\\u003e')
53
+ .replace(/&/g, '\\u0026')
54
+ .replace(/\u2028/g, '\\u2028')
55
+ .replace(/\u2029/g, '\\u2029')
56
+ }
57
+
58
+ export function threadJsonLd(input: ThreadJsonLd): Record<string, unknown> {
59
+ return {
60
+ '@context': 'https://schema.org',
61
+ '@type': 'DiscussionForumPosting',
62
+ headline: input.title,
63
+ url: input.url,
64
+ datePublished: input.published.toISOString(),
65
+ dateModified: input.modified.toISOString(),
66
+ author: { '@type': 'Person', name: input.author },
67
+ articleSection: input.forumTitle,
68
+ description: input.description,
69
+ interactionStatistic: {
70
+ '@type': 'InteractionCounter',
71
+ interactionType: 'https://schema.org/CommentAction',
72
+ userInteractionCount: input.replyCount,
73
+ },
74
+ }
75
+ }
@@ -0,0 +1,76 @@
1
+ import {
2
+ currentProps,
3
+ deepestHrefIn,
4
+ isUnder,
5
+ type PanelNav,
6
+ type PanelSection,
7
+ sectionHrefIn,
8
+ } from './panel-nav'
9
+
10
+ export { currentProps, isUnder }
11
+
12
+ export interface ModCpNavAccess {
13
+ readonly canWarn: boolean
14
+ readonly canLookUpIp: boolean
15
+ }
16
+
17
+ export const MODCP_OVERVIEW: PanelSection = {
18
+ href: '/modcp',
19
+ titleKey: 'modCpNav.modcp.title',
20
+ icon: 'overview',
21
+ blurbKey: 'modCpNav.modcp.blurb',
22
+ }
23
+
24
+ export function modCpSections(access: ModCpNavAccess): PanelNav {
25
+ return [
26
+ {
27
+ href: '/moderation',
28
+ titleKey: 'modCpNav.moderation.title',
29
+ icon: 'queue',
30
+ blurbKey: 'modCpNav.moderation.blurb',
31
+ children: access.canWarn
32
+ ? [{ href: '/moderation/warn', titleKey: 'modCpNav.moderation-warn.title', record: true }]
33
+ : [],
34
+ },
35
+ {
36
+ href: '/moderation/reports',
37
+ titleKey: 'modCpNav.moderation-reports.title',
38
+ icon: 'reports',
39
+ blurbKey: 'modCpNav.moderation-reports.blurb',
40
+ },
41
+ {
42
+ href: '/modcp/forums',
43
+ titleKey: 'modCpNav.modcp-forums.title',
44
+ icon: 'forums',
45
+ blurbKey: 'modCpNav.modcp-forums.blurb',
46
+ },
47
+ {
48
+ href: '/modcp/log',
49
+ titleKey: 'modCpNav.modcp-log.title',
50
+ icon: 'log',
51
+ blurbKey: 'modCpNav.modcp-log.blurb',
52
+ },
53
+ ...(access.canLookUpIp
54
+ ? ([
55
+ {
56
+ href: '/modcp/ip',
57
+ titleKey: 'modCpNav.modcp-ip.title',
58
+ icon: 'ip',
59
+ blurbKey: 'modCpNav.modcp-ip.blurb',
60
+ },
61
+ ] satisfies PanelNav)
62
+ : []),
63
+ ]
64
+ }
65
+
66
+ export function modCpNav(access: ModCpNavAccess): PanelNav {
67
+ return [MODCP_OVERVIEW, ...modCpSections(access)]
68
+ }
69
+
70
+ export function activeSectionHref(access: ModCpNavAccess, pathname: string): string | null {
71
+ return sectionHrefIn(modCpNav(access), pathname)
72
+ }
73
+
74
+ export function deepestNavHref(access: ModCpNavAccess, pathname: string): string | null {
75
+ return deepestHrefIn(modCpNav(access), pathname)
76
+ }
@@ -0,0 +1,84 @@
1
+ import type { Translator } from '@meith/i18n'
2
+
3
+ import { copyFor, patternCopy, splitAround } from './copy'
4
+ import { untranslated } from './time'
5
+
6
+ export function moderationFormsCopy(
7
+ t: Translator = untranslated(),
8
+ ): Readonly<Record<string, string>> {
9
+ const [byLead, byTail] = splitAround(t, 'moderationForm.queue.by', 'author')
10
+ return {
11
+ ...copyFor(
12
+ [
13
+ 'moderationForm.queue.approve',
14
+ 'moderationForm.queue.reject',
15
+ 'moderationForm.report.label',
16
+ 'moderationForm.report.hint',
17
+ 'moderationForm.report.submit',
18
+ 'moderationForm.report.putBack',
19
+ 'moderationForm.report.take',
20
+ 'moderationForm.report.note',
21
+ 'moderationForm.report.resolve',
22
+ 'moderationForm.report.dismiss',
23
+ 'moderationForm.inline.withThreads',
24
+ 'moderationForm.inline.withPosts',
25
+ 'moderationForm.inline.moderateThreads',
26
+ 'moderationForm.inline.moderatePosts',
27
+ 'moderationForm.inline.approve',
28
+ 'moderationForm.inline.restore',
29
+ 'moderationForm.inline.splitTitleSr',
30
+ 'moderationForm.inline.splitOut',
31
+ 'moderationForm.inline.delete',
32
+ 'moderationForm.newThreadTitle',
33
+ 'moderationForm.moveTo',
34
+ 'moderationForm.tool.lock',
35
+ 'moderationForm.tool.unlock',
36
+ 'moderationForm.tool.pin',
37
+ 'moderationForm.tool.unpin',
38
+ 'moderationForm.tool.move',
39
+ 'moderationForm.tool.copy',
40
+ 'moderationForm.tool.deleteThread',
41
+ 'moderationForm.lock.why',
42
+ 'moderationForm.lock.signature',
43
+ 'moderationForm.lock.unlockSignature',
44
+ 'moderationForm.lock.avatar',
45
+ 'moderationForm.lock.unlockAvatar',
46
+ 'moderationForm.surgery.fromSr',
47
+ 'moderationForm.surgery.titleSr',
48
+ 'moderationForm.surgery.split',
49
+ 'moderationForm.surgery.mergeSr',
50
+ 'moderationForm.surgery.mergePlaceholder',
51
+ 'moderationForm.surgery.merge',
52
+ 'moderationForm.warn.revokeSr',
53
+ 'moderationForm.warn.revokePlaceholder',
54
+ 'moderationForm.warn.revoke',
55
+ ],
56
+ t,
57
+ ),
58
+ ...patternCopy(['moderationForm.queue.inForum', 'moderationForm.surgery.splitPoint'], t),
59
+ 'moderationForm.queue.byLead': byLead,
60
+ 'moderationForm.queue.byTail': byTail,
61
+ }
62
+ }
63
+
64
+ export function issueWarningFormCopy(
65
+ username: string,
66
+ t: Translator = untranslated(),
67
+ ): Readonly<Record<string, string>> {
68
+ return {
69
+ ...copyFor(
70
+ [
71
+ 'moderationForm.warn.reason',
72
+ 'moderationForm.warn.somethingElse',
73
+ 'moderationForm.warn.titleLabel',
74
+ 'moderationForm.warn.titleHint',
75
+ 'moderationForm.warn.points',
76
+ 'moderationForm.warn.what',
77
+ 'moderationForm.warn.submit',
78
+ ],
79
+ t,
80
+ ),
81
+ 'moderationForm.warn.title': t.t('moderationForm.warn.title', { username }),
82
+ 'moderationForm.warn.whatHint': t.t('moderationForm.warn.whatHint', { username }),
83
+ }
84
+ }
@@ -0,0 +1,67 @@
1
+ import type { Translator } from '@meith/i18n'
2
+ import type { QueueItem } from '@meith/moderation'
3
+ import type { TimeModel } from '@meith/theme-kit'
4
+
5
+ import { postLink } from './post-link'
6
+ import { formatTime, untranslated } from './time'
7
+
8
+ export interface QueueRowModel {
9
+ readonly value: string
10
+ readonly kind: 'thread' | 'post'
11
+ readonly kindLabel: string
12
+ readonly forumTitle: string
13
+ readonly threadTitle: string
14
+ readonly href: string
15
+ readonly authorUsername: string
16
+ readonly authorHref: string | null
17
+ readonly excerpt: string
18
+ readonly postedAt: TimeModel
19
+ }
20
+
21
+ export interface QueueViewModel {
22
+ readonly rows: readonly QueueRowModel[]
23
+ readonly pending: number
24
+ readonly nextHref: string | null
25
+ readonly emptyReason: 'nothing-moderated' | 'queue-empty' | null
26
+ }
27
+
28
+ export interface QueueViewInput {
29
+ readonly items: readonly QueueItem[]
30
+ readonly pending: number
31
+ readonly nextCursor?: string | undefined
32
+ readonly moderatesAnything: boolean
33
+ readonly now: Date
34
+ readonly t?: Translator
35
+ }
36
+
37
+ function row(item: QueueItem, now: Date, t: Translator | undefined): QueueRowModel {
38
+ const thread = `/thread/${item.threadId}-${item.threadSlug}`
39
+ return {
40
+ value: `${item.kind}:${item.id}`,
41
+ kind: item.kind,
42
+ kindLabel: (t ?? untranslated()).t(item.kind === 'thread' ? 'queue.newThread' : 'queue.reply'),
43
+ forumTitle: item.forumTitle,
44
+ threadTitle: item.threadTitle,
45
+ href: item.kind === 'thread' ? `/${item.forumId}` : postLink(thread, item.id),
46
+ authorUsername: item.authorUsername,
47
+ authorHref: item.authorUserId === null ? null : `/member/${item.authorUserId}`,
48
+ excerpt: item.excerpt,
49
+ postedAt: formatTime(item.createdAt, now, t),
50
+ }
51
+ }
52
+
53
+ export function buildQueueView(input: QueueViewInput): QueueViewModel {
54
+ return {
55
+ rows: input.items.map((item) => row(item, input.now, input.t)),
56
+ pending: input.pending,
57
+ nextHref:
58
+ input.nextCursor === undefined
59
+ ? null
60
+ : `/moderation?after=${encodeURIComponent(input.nextCursor)}`,
61
+ emptyReason: !input.moderatesAnything
62
+ ? 'nothing-moderated'
63
+ : input.items.length === 0
64
+ ? 'queue-empty'
65
+ : null,
66
+ }
67
+ }