@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,712 @@
1
+ 'use client'
2
+
3
+ import { useActionState } from 'react'
4
+
5
+ import { EMPTY_STATE } from '@/server/auth-form-state'
6
+ import {
7
+ createAnnouncementAction,
8
+ createCaptchaQuestionAction,
9
+ createDirectiveAction,
10
+ createPrefixAction,
11
+ createSmileyAction,
12
+ createWordFilterAction,
13
+ deleteAnnouncementAction,
14
+ deleteAttachmentAction,
15
+ deleteCaptchaQuestionAction,
16
+ deleteDirectiveAction,
17
+ deletePrefixAction,
18
+ deleteSmileyAction,
19
+ deleteWordFilterAction,
20
+ updateAnnouncementAction,
21
+ updateCaptchaQuestionAction,
22
+ updateDirectiveAction,
23
+ updateSmileyAction,
24
+ updateWordFilterAction,
25
+ } from '@/server/content-admin-actions'
26
+
27
+ import { FormError, SubmitButton } from '../auth/form-controls'
28
+ import { type Copy, fromCopy } from '../shell/copy'
29
+ import { INPUT, Saved } from './form-bits'
30
+
31
+ export interface WordFilterValues {
32
+ readonly id: number
33
+ readonly pattern: string
34
+ readonly replacement: string
35
+ readonly wholeWord: boolean
36
+ readonly enabled: boolean
37
+ }
38
+
39
+ export function WordFilterRowForm({ filter, copy }: { filter: WordFilterValues; copy: Copy }) {
40
+ const [state, action] = useActionState(updateWordFilterAction, EMPTY_STATE)
41
+ const [removeState, removeAction] = useActionState(deleteWordFilterAction, EMPTY_STATE)
42
+
43
+ return (
44
+ <div className="flex flex-col gap-2 py-3">
45
+ <FormError message={state.error ?? removeState.error} />
46
+ <Saved when={state.notice === 'saved'}>{fromCopy(copy, 'admin.saved')}</Saved>
47
+
48
+ <form action={action} className="flex flex-wrap items-end gap-3" noValidate>
49
+ <input type="hidden" name="id" value={filter.id} />
50
+
51
+ <label className="flex min-w-40 flex-1 flex-col gap-1 text-sm">
52
+ <span className="font-medium">{fromCopy(copy, 'adminContent.filter.match')}</span>
53
+ <input name="pattern" defaultValue={filter.pattern} className={INPUT} required />
54
+ </label>
55
+
56
+ <label className="flex min-w-40 flex-1 flex-col gap-1 text-sm">
57
+ <span className="font-medium">{fromCopy(copy, 'adminContent.filter.showInstead')}</span>
58
+ <input name="replacement" defaultValue={filter.replacement} className={INPUT} />
59
+ </label>
60
+
61
+ <label className="flex items-center gap-2 text-sm">
62
+ <input
63
+ type="checkbox"
64
+ name="wholeWord"
65
+ value="1"
66
+ defaultChecked={filter.wholeWord}
67
+ className="size-4"
68
+ />
69
+ <span>{fromCopy(copy, 'adminContent.filter.wholeWords')}</span>
70
+ </label>
71
+
72
+ <label className="flex items-center gap-2 text-sm">
73
+ <input
74
+ type="checkbox"
75
+ name="enabled"
76
+ value="1"
77
+ defaultChecked={filter.enabled}
78
+ className="size-4"
79
+ />
80
+ <span>{fromCopy(copy, 'adminContent.filter.active')}</span>
81
+ </label>
82
+
83
+ <span className="min-w-24">
84
+ <SubmitButton>{fromCopy(copy, 'adminContent.save')}</SubmitButton>
85
+ </span>
86
+ </form>
87
+
88
+ <form action={removeAction}>
89
+ <input type="hidden" name="id" value={filter.id} />
90
+ <button type="submit" className="text-xs text-muted-foreground hover:underline">
91
+ {fromCopy(copy, 'adminContent.filter.removeThis')}
92
+ </button>
93
+ </form>
94
+ </div>
95
+ )
96
+ }
97
+
98
+ export function NewWordFilterForm({ copy }: { copy: Copy }) {
99
+ const [state, action] = useActionState(createWordFilterAction, EMPTY_STATE)
100
+
101
+ return (
102
+ <form action={action} className="flex flex-wrap items-end gap-3" noValidate>
103
+ <FormError message={state.error} />
104
+ <Saved when={state.notice === 'saved'}>{fromCopy(copy, 'adminContent.added')}</Saved>
105
+
106
+ <label className="flex min-w-40 flex-1 flex-col gap-1 text-sm">
107
+ <span className="font-medium">{fromCopy(copy, 'adminContent.filter.match')}</span>
108
+ <input name="pattern" className={INPUT} required />
109
+ </label>
110
+
111
+ <label className="flex min-w-40 flex-1 flex-col gap-1 text-sm">
112
+ <span className="font-medium">{fromCopy(copy, 'adminContent.filter.showInstead')}</span>
113
+ <input name="replacement" className={INPUT} />
114
+ <span className="text-xs text-muted-foreground">
115
+ {fromCopy(copy, 'adminContent.filter.showInsteadHint')}
116
+ </span>
117
+ </label>
118
+
119
+ <label className="flex items-center gap-2 text-sm">
120
+ <input type="checkbox" name="wholeWord" value="1" defaultChecked className="size-4" />
121
+ <span>{fromCopy(copy, 'adminContent.filter.wholeWords')}</span>
122
+ </label>
123
+
124
+ <span className="min-w-28">
125
+ <SubmitButton>{fromCopy(copy, 'adminContent.add')}</SubmitButton>
126
+ </span>
127
+ </form>
128
+ )
129
+ }
130
+
131
+ export interface PrefixValues {
132
+ readonly id: number
133
+ readonly label: string
134
+ readonly token: string | null
135
+ readonly displayOrder: number
136
+ readonly forumPathPrefix: string | null
137
+ }
138
+
139
+ export function DeletePrefixForm({ prefix, copy }: { prefix: PrefixValues; copy: Copy }) {
140
+ const [state, action] = useActionState(deletePrefixAction, EMPTY_STATE)
141
+
142
+ return (
143
+ <form action={action} className="flex items-center gap-2">
144
+ <FormError message={state.error} />
145
+ <input type="hidden" name="id" value={prefix.id} />
146
+ <button type="submit" className="text-xs text-muted-foreground hover:underline">
147
+ {fromCopy(copy, 'admin.remove')}
148
+ </button>
149
+ </form>
150
+ )
151
+ }
152
+
153
+ export function NewPrefixForm({ copy }: { copy: Copy }) {
154
+ const [state, action] = useActionState(createPrefixAction, EMPTY_STATE)
155
+
156
+ return (
157
+ <form action={action} className="flex flex-col gap-3" noValidate>
158
+ <FormError message={state.error} />
159
+ <Saved when={state.notice === 'saved'}>{fromCopy(copy, 'adminContent.added')}</Saved>
160
+
161
+ <div className="grid gap-3 sm:grid-cols-2">
162
+ <label className="flex flex-col gap-1 text-sm">
163
+ <span className="font-medium">{fromCopy(copy, 'adminContent.prefix.label')}</span>
164
+ <input name="label" className={INPUT} required />
165
+ </label>
166
+
167
+ <label className="flex flex-col gap-1 text-sm">
168
+ <span className="font-medium">{fromCopy(copy, 'adminContent.prefix.styleToken')}</span>
169
+ <input name="token" className={INPUT} />
170
+ <span className="text-xs text-muted-foreground">
171
+ {fromCopy(copy, 'adminContent.prefix.styleTokenHint')}
172
+ </span>
173
+ </label>
174
+
175
+ <label className="flex flex-col gap-1 text-sm">
176
+ <span className="font-medium">{fromCopy(copy, 'adminContent.prefix.displayOrder')}</span>
177
+ <input type="number" name="displayOrder" min={0} defaultValue={0} className={INPUT} />
178
+ </label>
179
+
180
+ <label className="flex flex-col gap-1 text-sm">
181
+ <span className="font-medium">{fromCopy(copy, 'adminContent.prefix.onlyBranch')}</span>
182
+ <input name="forumPathPrefix" className={INPUT} />
183
+ <span className="text-xs text-muted-foreground">
184
+ {fromCopy(copy, 'adminContent.prefix.onlyBranchHint')}
185
+ </span>
186
+ </label>
187
+ </div>
188
+
189
+ <div>
190
+ <SubmitButton>{fromCopy(copy, 'adminContent.prefix.add')}</SubmitButton>
191
+ </div>
192
+ </form>
193
+ )
194
+ }
195
+
196
+ export interface SmileyValues {
197
+ readonly id: number
198
+ readonly code: string
199
+ readonly src: string
200
+ readonly alt: string | null
201
+ readonly enabled: boolean
202
+ }
203
+
204
+ export function SmileyRowForm({ smiley, copy }: { smiley: SmileyValues; copy: Copy }) {
205
+ const [state, action] = useActionState(updateSmileyAction, EMPTY_STATE)
206
+ const [removeState, removeAction] = useActionState(deleteSmileyAction, EMPTY_STATE)
207
+
208
+ return (
209
+ <div className="flex flex-col gap-2 py-3">
210
+ <FormError message={state.error ?? removeState.error} />
211
+ <Saved when={state.notice === 'saved'}>{fromCopy(copy, 'admin.saved')}</Saved>
212
+
213
+ <form action={action} className="flex flex-wrap items-end gap-3" noValidate>
214
+ <input type="hidden" name="id" value={smiley.id} />
215
+
216
+ <img
217
+ src={smiley.src}
218
+ alt={smiley.alt ?? smiley.code}
219
+ className="size-6 shrink-0 self-center"
220
+ />
221
+
222
+ <label className="flex w-28 flex-col gap-1 text-sm">
223
+ <span className="font-medium">{fromCopy(copy, 'adminContent.smiley.code')}</span>
224
+ <input name="code" defaultValue={smiley.code} className={INPUT} required />
225
+ </label>
226
+
227
+ <label className="flex min-w-40 flex-1 flex-col gap-1 text-sm">
228
+ <span className="font-medium">{fromCopy(copy, 'adminContent.smiley.image')}</span>
229
+ <input name="src" defaultValue={smiley.src} className={INPUT} required />
230
+ </label>
231
+
232
+ <label className="flex w-32 flex-col gap-1 text-sm">
233
+ <span className="font-medium">{fromCopy(copy, 'adminContent.smiley.altText')}</span>
234
+ <input name="alt" defaultValue={smiley.alt ?? ''} className={INPUT} />
235
+ </label>
236
+
237
+ <label className="flex items-center gap-2 text-sm">
238
+ <input
239
+ type="checkbox"
240
+ name="enabled"
241
+ value="1"
242
+ defaultChecked={smiley.enabled}
243
+ className="size-4"
244
+ />
245
+ <span>{fromCopy(copy, 'adminContent.enabled')}</span>
246
+ </label>
247
+
248
+ <span className="min-w-24">
249
+ <SubmitButton>{fromCopy(copy, 'adminContent.save')}</SubmitButton>
250
+ </span>
251
+ </form>
252
+
253
+ <form action={removeAction}>
254
+ <input type="hidden" name="id" value={smiley.id} />
255
+ <button type="submit" className="text-xs text-destructive hover:underline">
256
+ {fromCopy(copy, 'admin.remove')}
257
+ </button>
258
+ </form>
259
+ </div>
260
+ )
261
+ }
262
+
263
+ export function NewSmileyForm({ copy }: { copy: Copy }) {
264
+ const [state, action] = useActionState(createSmileyAction, EMPTY_STATE)
265
+
266
+ return (
267
+ <form action={action} className="flex flex-wrap items-end gap-3" noValidate>
268
+ <FormError message={state.error} />
269
+ <Saved when={state.notice === 'saved'}>{fromCopy(copy, 'adminContent.added')}</Saved>
270
+
271
+ <label className="flex w-28 flex-col gap-1 text-sm">
272
+ <span className="font-medium">{fromCopy(copy, 'adminContent.smiley.code')}</span>
273
+ <input
274
+ name="code"
275
+ className={INPUT}
276
+ placeholder={fromCopy(copy, 'adminContent.smiley.codePlaceholder')}
277
+ required
278
+ />
279
+ </label>
280
+
281
+ <label className="flex min-w-40 flex-1 flex-col gap-1 text-sm">
282
+ <span className="font-medium">{fromCopy(copy, 'adminContent.smiley.image')}</span>
283
+ <input
284
+ name="src"
285
+ className={INPUT}
286
+ placeholder={fromCopy(copy, 'adminContent.smiley.imagePlaceholder')}
287
+ required
288
+ />
289
+ </label>
290
+
291
+ <label className="flex w-32 flex-col gap-1 text-sm">
292
+ <span className="font-medium">{fromCopy(copy, 'adminContent.smiley.altText')}</span>
293
+ <input name="alt" className={INPUT} />
294
+ <span className="text-xs text-muted-foreground">
295
+ {fromCopy(copy, 'adminContent.smiley.altTextHint')}
296
+ </span>
297
+ </label>
298
+
299
+ <span className="min-w-28">
300
+ <SubmitButton>{fromCopy(copy, 'adminContent.add')}</SubmitButton>
301
+ </span>
302
+ </form>
303
+ )
304
+ }
305
+
306
+ export interface DirectiveValues {
307
+ readonly id: number
308
+ readonly name: string
309
+ readonly block: boolean
310
+ readonly description: string | null
311
+ readonly enabled: boolean
312
+ }
313
+
314
+ export function DirectiveRowForm({ directive, copy }: { directive: DirectiveValues; copy: Copy }) {
315
+ const [state, action] = useActionState(updateDirectiveAction, EMPTY_STATE)
316
+ const [removeState, removeAction] = useActionState(deleteDirectiveAction, EMPTY_STATE)
317
+
318
+ return (
319
+ <div className="flex flex-col gap-2 py-3">
320
+ <FormError message={state.error ?? removeState.error} />
321
+ <Saved when={state.notice === 'saved'}>{fromCopy(copy, 'admin.saved')}</Saved>
322
+
323
+ <form action={action} className="flex flex-wrap items-end gap-3" noValidate>
324
+ <input type="hidden" name="id" value={directive.id} />
325
+
326
+ <label className="flex w-36 flex-col gap-1 text-sm">
327
+ <span className="font-medium">{fromCopy(copy, 'adminContent.directive.name')}</span>
328
+ <input name="name" defaultValue={directive.name} className={INPUT} required />
329
+ </label>
330
+
331
+ <label className="flex min-w-40 flex-1 flex-col gap-1 text-sm">
332
+ <span className="font-medium">{fromCopy(copy, 'adminContent.directive.note')}</span>
333
+ <input name="description" defaultValue={directive.description ?? ''} className={INPUT} />
334
+ </label>
335
+
336
+ <label className="flex items-center gap-2 text-sm">
337
+ <input
338
+ type="checkbox"
339
+ name="block"
340
+ value="1"
341
+ defaultChecked={directive.block}
342
+ className="size-4"
343
+ />
344
+ <span>{fromCopy(copy, 'adminContent.directive.block')}</span>
345
+ </label>
346
+
347
+ <label className="flex items-center gap-2 text-sm">
348
+ <input
349
+ type="checkbox"
350
+ name="enabled"
351
+ value="1"
352
+ defaultChecked={directive.enabled}
353
+ className="size-4"
354
+ />
355
+ <span>{fromCopy(copy, 'adminContent.enabled')}</span>
356
+ </label>
357
+
358
+ <span className="min-w-24">
359
+ <SubmitButton>{fromCopy(copy, 'adminContent.save')}</SubmitButton>
360
+ </span>
361
+ </form>
362
+
363
+ <form action={removeAction}>
364
+ <input type="hidden" name="id" value={directive.id} />
365
+ <button type="submit" className="text-xs text-destructive hover:underline">
366
+ {fromCopy(copy, 'admin.remove')}
367
+ </button>
368
+ </form>
369
+ </div>
370
+ )
371
+ }
372
+
373
+ export function NewDirectiveForm({ copy }: { copy: Copy }) {
374
+ const [state, action] = useActionState(createDirectiveAction, EMPTY_STATE)
375
+
376
+ return (
377
+ <form action={action} className="flex flex-wrap items-end gap-3" noValidate>
378
+ <FormError message={state.error} />
379
+ <Saved when={state.notice === 'saved'}>{fromCopy(copy, 'adminContent.added')}</Saved>
380
+
381
+ <label className="flex w-36 flex-col gap-1 text-sm">
382
+ <span className="font-medium">{fromCopy(copy, 'adminContent.directive.name')}</span>
383
+ <input
384
+ name="name"
385
+ className={INPUT}
386
+ placeholder={fromCopy(copy, 'adminContent.directive.namePlaceholder')}
387
+ required
388
+ />
389
+ <span className="text-xs text-muted-foreground">
390
+ {copy['adminContent.directive.nameHintLead']}
391
+ <code>:::note</code>
392
+ {copy['adminContent.directive.nameHintMid']}
393
+ <code>:note[…]</code>
394
+ {copy['adminContent.directive.nameHintTail']}
395
+ </span>
396
+ </label>
397
+
398
+ <label className="flex min-w-40 flex-1 flex-col gap-1 text-sm">
399
+ <span className="font-medium">{fromCopy(copy, 'adminContent.directive.note')}</span>
400
+ <input name="description" className={INPUT} />
401
+ <span className="text-xs text-muted-foreground">
402
+ {fromCopy(copy, 'adminContent.directive.noteHint')}
403
+ </span>
404
+ </label>
405
+
406
+ <label className="flex items-center gap-2 text-sm">
407
+ <input type="checkbox" name="block" value="1" className="size-4" />
408
+ <span>{fromCopy(copy, 'adminContent.directive.block')}</span>
409
+ </label>
410
+
411
+ <span className="min-w-28">
412
+ <SubmitButton>{fromCopy(copy, 'adminContent.add')}</SubmitButton>
413
+ </span>
414
+ </form>
415
+ )
416
+ }
417
+
418
+ export function DeleteAttachmentForm({ attachmentId, copy }: { attachmentId: number; copy: Copy }) {
419
+ const [state, action] = useActionState(deleteAttachmentAction, EMPTY_STATE)
420
+
421
+ return (
422
+ <form action={action} className="shrink-0">
423
+ <FormError message={state.error} />
424
+ <input type="hidden" name="id" value={attachmentId} />
425
+ <button type="submit" className="text-xs text-destructive hover:underline">
426
+ {fromCopy(copy, 'adminContent.attachment.delete')}
427
+ </button>
428
+ </form>
429
+ )
430
+ }
431
+
432
+ export interface AnnouncementValues {
433
+ readonly id: number
434
+ readonly forumId: number | null
435
+ readonly title: string
436
+ readonly message: string
437
+ readonly startsAtInput: string
438
+ readonly endsAtInput: string
439
+ readonly enabled: boolean
440
+ }
441
+
442
+ export interface ForumChoice {
443
+ readonly id: number
444
+ readonly label: string
445
+ }
446
+
447
+ function AnnouncementFields({
448
+ forums,
449
+ values,
450
+ copy,
451
+ }: {
452
+ forums: readonly ForumChoice[]
453
+ values?: AnnouncementValues
454
+ copy: Copy
455
+ }) {
456
+ return (
457
+ <>
458
+ <label className="flex flex-col gap-1 text-sm">
459
+ <span className="font-medium">{fromCopy(copy, 'adminContent.announcement.title')}</span>
460
+ <input name="title" defaultValue={values?.title ?? ''} className={INPUT} required />
461
+ </label>
462
+
463
+ <label className="flex flex-col gap-1 text-sm">
464
+ <span className="font-medium">{fromCopy(copy, 'adminContent.announcement.message')}</span>
465
+ <textarea
466
+ name="message"
467
+ rows={5}
468
+ defaultValue={values?.message ?? ''}
469
+ className={INPUT}
470
+ required
471
+ />
472
+ <span className="text-xs text-muted-foreground">
473
+ {fromCopy(copy, 'adminContent.announcement.messageHint')}
474
+ </span>
475
+ </label>
476
+
477
+ <label className="flex flex-col gap-1 text-sm">
478
+ <span className="font-medium">{fromCopy(copy, 'adminContent.announcement.where')}</span>
479
+ <select
480
+ name="forumId"
481
+ defaultValue={
482
+ values?.forumId === null || values === undefined ? '' : String(values.forumId)
483
+ }
484
+ className={INPUT}
485
+ >
486
+ <option value="">{fromCopy(copy, 'adminContent.announcement.wholeBoard')}</option>
487
+ {forums.map((forum) => (
488
+ <option key={forum.id} value={forum.id}>
489
+ {forum.label}
490
+ </option>
491
+ ))}
492
+ </select>
493
+ <span className="text-xs text-muted-foreground">
494
+ {fromCopy(copy, 'adminContent.announcement.whereHint')}
495
+ </span>
496
+ </label>
497
+
498
+ <div className="flex flex-wrap gap-3">
499
+ <label className="flex flex-1 flex-col gap-1 text-sm">
500
+ <span className="font-medium">{fromCopy(copy, 'adminContent.announcement.from')}</span>
501
+ <input
502
+ type="datetime-local"
503
+ name="startsAt"
504
+ defaultValue={values?.startsAtInput ?? ''}
505
+ className={INPUT}
506
+ />
507
+ </label>
508
+
509
+ <label className="flex flex-1 flex-col gap-1 text-sm">
510
+ <span className="font-medium">{fromCopy(copy, 'adminContent.announcement.until')}</span>
511
+ <input
512
+ type="datetime-local"
513
+ name="endsAt"
514
+ defaultValue={values?.endsAtInput ?? ''}
515
+ className={INPUT}
516
+ />
517
+ <span className="text-xs text-muted-foreground">
518
+ {fromCopy(copy, 'adminContent.announcement.untilHint')}
519
+ </span>
520
+ </label>
521
+ </div>
522
+
523
+ <p className="text-xs text-muted-foreground">
524
+ {fromCopy(copy, 'adminContent.announcement.timesUtc')}
525
+ </p>
526
+ </>
527
+ )
528
+ }
529
+
530
+ export function AnnouncementRowForm({
531
+ announcement,
532
+ forums,
533
+ copy,
534
+ }: {
535
+ announcement: AnnouncementValues
536
+ forums: readonly ForumChoice[]
537
+ copy: Copy
538
+ }) {
539
+ const [state, action] = useActionState(updateAnnouncementAction, EMPTY_STATE)
540
+ const [removeState, removeAction] = useActionState(deleteAnnouncementAction, EMPTY_STATE)
541
+
542
+ return (
543
+ <div className="flex flex-col gap-3 py-4">
544
+ <FormError message={state.error ?? removeState.error} />
545
+ <Saved when={state.notice === 'saved'}>{fromCopy(copy, 'admin.saved')}</Saved>
546
+
547
+ <form action={action} className="flex flex-col gap-3" noValidate>
548
+ <input type="hidden" name="id" value={announcement.id} />
549
+ <AnnouncementFields forums={forums} values={announcement} copy={copy} />
550
+
551
+ <div className="flex flex-wrap items-center gap-4">
552
+ <label className="flex items-center gap-2 text-sm">
553
+ <input
554
+ type="checkbox"
555
+ name="enabled"
556
+ value="1"
557
+ defaultChecked={announcement.enabled}
558
+ className="size-4"
559
+ />
560
+ <span>{fromCopy(copy, 'adminContent.enabled')}</span>
561
+ </label>
562
+
563
+ <span className="min-w-28">
564
+ <SubmitButton>{fromCopy(copy, 'adminContent.save')}</SubmitButton>
565
+ </span>
566
+ </div>
567
+ </form>
568
+
569
+ <form action={removeAction}>
570
+ <input type="hidden" name="id" value={announcement.id} />
571
+ <button type="submit" className="text-xs text-destructive hover:underline">
572
+ {fromCopy(copy, 'admin.remove')}
573
+ </button>
574
+ </form>
575
+ </div>
576
+ )
577
+ }
578
+
579
+ export function NewAnnouncementForm({
580
+ forums,
581
+ copy,
582
+ }: {
583
+ forums: readonly ForumChoice[]
584
+ copy: Copy
585
+ }) {
586
+ const [state, action] = useActionState(createAnnouncementAction, EMPTY_STATE)
587
+
588
+ return (
589
+ <form action={action} className="flex flex-col gap-3" noValidate>
590
+ <FormError message={state.error} />
591
+ <Saved when={state.notice === 'saved'}>{fromCopy(copy, 'adminContent.added')}</Saved>
592
+
593
+ <AnnouncementFields forums={forums} copy={copy} />
594
+
595
+ <div className="flex flex-wrap items-center gap-4">
596
+ <label className="flex items-center gap-2 text-sm">
597
+ <input type="checkbox" name="enabled" value="1" defaultChecked className="size-4" />
598
+ <span>{fromCopy(copy, 'adminContent.enabled')}</span>
599
+ </label>
600
+
601
+ <span className="min-w-28">
602
+ <SubmitButton>{fromCopy(copy, 'adminContent.add')}</SubmitButton>
603
+ </span>
604
+ </div>
605
+ </form>
606
+ )
607
+ }
608
+
609
+ export interface CaptchaQuestionValues {
610
+ readonly id: number
611
+ readonly question: string
612
+ readonly answers: readonly string[]
613
+ readonly enabled: boolean
614
+ }
615
+
616
+ export function CaptchaQuestionRowForm({
617
+ question,
618
+ copy,
619
+ }: {
620
+ question: CaptchaQuestionValues
621
+ copy: Copy
622
+ }) {
623
+ const [state, action] = useActionState(updateCaptchaQuestionAction, EMPTY_STATE)
624
+ const [removeState, removeAction] = useActionState(deleteCaptchaQuestionAction, EMPTY_STATE)
625
+
626
+ return (
627
+ <div className="flex flex-col gap-2 py-3">
628
+ <FormError message={state.error ?? removeState.error} />
629
+ <Saved when={state.notice === 'saved'}>{fromCopy(copy, 'admin.saved')}</Saved>
630
+
631
+ <form action={action} className="flex flex-col gap-3" noValidate>
632
+ <input type="hidden" name="id" value={question.id} />
633
+
634
+ <label className="flex flex-col gap-1 text-sm">
635
+ <span className="font-medium">{fromCopy(copy, 'adminContent.captcha.question')}</span>
636
+ <input name="question" defaultValue={question.question} className={INPUT} required />
637
+ </label>
638
+
639
+ <label className="flex flex-col gap-1 text-sm">
640
+ <span className="font-medium">{fromCopy(copy, 'adminContent.captcha.answers')}</span>
641
+ <textarea
642
+ name="answers"
643
+ rows={3}
644
+ defaultValue={question.answers.join('\n')}
645
+ className={INPUT}
646
+ required
647
+ />
648
+ <span className="text-xs text-muted-foreground">
649
+ {fromCopy(copy, 'adminContent.captcha.answersHint')}
650
+ </span>
651
+ </label>
652
+
653
+ <div className="flex flex-wrap items-center gap-4">
654
+ <label className="flex items-center gap-2 text-sm">
655
+ <input
656
+ type="checkbox"
657
+ name="enabled"
658
+ value="1"
659
+ defaultChecked={question.enabled}
660
+ className="size-4"
661
+ />
662
+ <span>{fromCopy(copy, 'adminContent.enabled')}</span>
663
+ </label>
664
+
665
+ <span className="min-w-24">
666
+ <SubmitButton>{fromCopy(copy, 'adminContent.save')}</SubmitButton>
667
+ </span>
668
+ </div>
669
+ </form>
670
+
671
+ <form action={removeAction}>
672
+ <input type="hidden" name="id" value={question.id} />
673
+ <button type="submit" className="text-xs text-destructive hover:underline">
674
+ {fromCopy(copy, 'admin.remove')}
675
+ </button>
676
+ </form>
677
+ </div>
678
+ )
679
+ }
680
+
681
+ export function NewCaptchaQuestionForm({ copy }: { copy: Copy }) {
682
+ const [state, action] = useActionState(createCaptchaQuestionAction, EMPTY_STATE)
683
+
684
+ return (
685
+ <form action={action} className="flex flex-col gap-3" noValidate>
686
+ <FormError message={state.error} />
687
+ <Saved when={state.notice === 'saved'}>{fromCopy(copy, 'adminContent.added')}</Saved>
688
+
689
+ <label className="flex flex-col gap-1 text-sm">
690
+ <span className="font-medium">{fromCopy(copy, 'adminContent.captcha.question')}</span>
691
+ <input
692
+ name="question"
693
+ className={INPUT}
694
+ placeholder={fromCopy(copy, 'adminContent.captcha.questionPlaceholder')}
695
+ required
696
+ />
697
+ </label>
698
+
699
+ <label className="flex flex-col gap-1 text-sm">
700
+ <span className="font-medium">{fromCopy(copy, 'adminContent.captcha.answers')}</span>
701
+ <textarea name="answers" rows={3} className={INPUT} required />
702
+ <span className="text-xs text-muted-foreground">
703
+ {fromCopy(copy, 'adminContent.captcha.answersHint')}
704
+ </span>
705
+ </label>
706
+
707
+ <span className="min-w-28">
708
+ <SubmitButton>{fromCopy(copy, 'adminContent.add')}</SubmitButton>
709
+ </span>
710
+ </form>
711
+ )
712
+ }