@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.
- package/LICENSE.md +165 -0
- package/app/(auth)/layout.tsx +6 -0
- package/app/(auth)/login/page.tsx +106 -0
- package/app/(auth)/login/verify/page.tsx +55 -0
- package/app/(auth)/register/page.tsx +83 -0
- package/app/(auth)/reset/confirm/page.tsx +45 -0
- package/app/(auth)/reset/page.tsx +22 -0
- package/app/(auth)/verify/page.tsx +44 -0
- package/app/(auth)/verify/resend/page.tsx +51 -0
- package/app/(board)/[slug]/feed.xml/route.ts +48 -0
- package/app/(board)/[slug]/new/page.tsx +112 -0
- package/app/(board)/[slug]/page.tsx +403 -0
- package/app/(board)/discover/[view]/page.tsx +115 -0
- package/app/(board)/discover/page.tsx +5 -0
- package/app/(board)/layout.tsx +22 -0
- package/app/(board)/member/[id]/page.tsx +187 -0
- package/app/(board)/member/[id]/reputation/page.tsx +195 -0
- package/app/(board)/member/by-name/[name]/page.tsx +32 -0
- package/app/(board)/messages/[id]/page.tsx +103 -0
- package/app/(board)/messages/compose/page.tsx +60 -0
- package/app/(board)/messages/layout.tsx +5 -0
- package/app/(board)/messages/page.tsx +179 -0
- package/app/(board)/modcp/forums/page.tsx +59 -0
- package/app/(board)/modcp/ip/page.tsx +158 -0
- package/app/(board)/modcp/layout.tsx +5 -0
- package/app/(board)/modcp/log/page.tsx +98 -0
- package/app/(board)/modcp/page.tsx +112 -0
- package/app/(board)/moderation/layout.tsx +5 -0
- package/app/(board)/moderation/page.tsx +111 -0
- package/app/(board)/moderation/reports/page.tsx +193 -0
- package/app/(board)/moderation/warn/page.tsx +158 -0
- package/app/(board)/notifications/layout.tsx +5 -0
- package/app/(board)/notifications/page.tsx +144 -0
- package/app/(board)/notifications/preferences/page.tsx +65 -0
- package/app/(board)/online/page.tsx +119 -0
- package/app/(board)/page.tsx +195 -0
- package/app/(board)/plugins/[key]/[[...path]]/page.tsx +72 -0
- package/app/(board)/privacy/page.tsx +12 -0
- package/app/(board)/report/page.tsx +71 -0
- package/app/(board)/rules/page.tsx +12 -0
- package/app/(board)/search/[token]/page.tsx +130 -0
- package/app/(board)/search/page.tsx +289 -0
- package/app/(board)/stats/page.tsx +202 -0
- package/app/(board)/subscriptions/layout.tsx +5 -0
- package/app/(board)/subscriptions/page.tsx +142 -0
- package/app/(board)/terms/page.tsx +12 -0
- package/app/(board)/thread/[slug]/edit/page.tsx +107 -0
- package/app/(board)/thread/[slug]/feed.xml/route.ts +42 -0
- package/app/(board)/thread/[slug]/page.tsx +707 -0
- package/app/(board)/thread/[slug]/poll/page.tsx +41 -0
- package/app/(board)/thread/[slug]/post/[postId]/history/page.tsx +160 -0
- package/app/(board)/thread/[slug]/reply/page.tsx +136 -0
- package/app/(board)/unsubscribe/page.tsx +73 -0
- package/app/(board)/usercp/avatar/page.tsx +56 -0
- package/app/(board)/usercp/contacts/page.tsx +141 -0
- package/app/(board)/usercp/email/confirm/page.tsx +52 -0
- package/app/(board)/usercp/layout.tsx +23 -0
- package/app/(board)/usercp/options/page.tsx +56 -0
- package/app/(board)/usercp/page.tsx +71 -0
- package/app/(board)/usercp/profile/page.tsx +81 -0
- package/app/(board)/usercp/security/page.tsx +238 -0
- package/app/(board)/usercp/security/verify/page.tsx +92 -0
- package/app/(board)/usercp/signature/page.tsx +67 -0
- package/app/admin/antispam/page.tsx +155 -0
- package/app/admin/api/marketplace/screenshot/route.ts +68 -0
- package/app/admin/api/plugins/[key]/[...path]/route.ts +27 -0
- package/app/admin/api-tokens/page.tsx +129 -0
- package/app/admin/content/announcements/page.tsx +110 -0
- package/app/admin/content/attachments/page.tsx +199 -0
- package/app/admin/content/navigation/page.tsx +123 -0
- package/app/admin/content/page.tsx +170 -0
- package/app/admin/forums/[id]/page.tsx +96 -0
- package/app/admin/forums/[id]/permissions/page.tsx +108 -0
- package/app/admin/forums/page.tsx +45 -0
- package/app/admin/groups/[id]/page.tsx +133 -0
- package/app/admin/groups/memberships/page.tsx +56 -0
- package/app/admin/groups/page.tsx +104 -0
- package/app/admin/groups/promotions/page.tsx +143 -0
- package/app/admin/layout.tsx +86 -0
- package/app/admin/log/page.tsx +116 -0
- package/app/admin/page.tsx +206 -0
- package/app/admin/plugins/[key]/[[...path]]/page.tsx +276 -0
- package/app/admin/plugins/browse/page.tsx +70 -0
- package/app/admin/plugins/page.tsx +165 -0
- package/app/admin/security/page.tsx +124 -0
- package/app/admin/settings/page.tsx +190 -0
- package/app/admin/system/page.tsx +303 -0
- package/app/admin/themes/[key]/page.tsx +110 -0
- package/app/admin/themes/browse/page.tsx +70 -0
- package/app/admin/themes/page.tsx +163 -0
- package/app/admin/users/[id]/merge/page.tsx +136 -0
- package/app/admin/users/[id]/page.tsx +208 -0
- package/app/admin/users/mail/page.tsx +68 -0
- package/app/admin/users/page.tsx +260 -0
- package/app/admin/users/prune/page.tsx +186 -0
- package/app/api/health/route.ts +11 -0
- package/app/api/metrics/route.ts +66 -0
- package/app/api/plugins/[key]/[...path]/route.ts +27 -0
- package/app/api/read/all/route.ts +15 -0
- package/app/api/read/forum/[id]/route.ts +29 -0
- package/app/api/read/thread/[id]/route.ts +40 -0
- package/app/api/ready/route.ts +63 -0
- package/app/api/system/tick/route.ts +77 -0
- package/app/api/v1/[...path]/route.ts +208 -0
- package/app/api/v1/openapi.json/route.ts +12 -0
- package/app/apple-icon.ts +11 -0
- package/app/atom.xml/route.ts +7 -0
- package/app/attachment/[id]/route.ts +10 -0
- package/app/attachment/[id]/thumb/route.ts +10 -0
- package/app/auth/passkey/options/route.ts +145 -0
- package/app/auth/passkey/verify/route.ts +241 -0
- package/app/auth/resume/route.ts +55 -0
- package/app/auth/sso/[provider]/callback/route.ts +173 -0
- package/app/auth/sso/[provider]/go/route.ts +57 -0
- package/app/auth/sso/[provider]/route.ts +105 -0
- package/app/avatar/[id]/route.ts +31 -0
- package/app/brand/icon-192.png/route.ts +7 -0
- package/app/brand/icon-512.png/route.ts +7 -0
- package/app/brand/icon-maskable.png/route.ts +7 -0
- package/app/demo/stripe/[[...path]]/route.ts +27 -0
- package/app/error.tsx +15 -0
- package/app/feed.xml/route.ts +7 -0
- package/app/forumdisplay.php/page.tsx +11 -0
- package/app/group/[id]/badge/[scheme]/route.ts +26 -0
- package/app/icon.ts +13 -0
- package/app/install/page.tsx +221 -0
- package/app/jump/page.tsx +28 -0
- package/app/layout.tsx +124 -0
- package/app/logo/[scheme]/route.ts +24 -0
- package/app/manifest.ts +43 -0
- package/app/member.php/page.tsx +11 -0
- package/app/memberlist.php/page.tsx +11 -0
- package/app/not-found.tsx +14 -0
- package/app/opengraph-image.ts +11 -0
- package/app/redirect/page.tsx +44 -0
- package/app/robots.txt/route.ts +35 -0
- package/app/showthread.php/page.tsx +11 -0
- package/app/sitemap/[page]/route.ts +64 -0
- package/app/sitemap.xml/route.ts +31 -0
- package/app/twitter-image.ts +11 -0
- package/app/viewforum.php/page.tsx +11 -0
- package/app/viewtopic.php/page.tsx +11 -0
- package/bin/forum-web.mjs +241 -0
- package/components.json +21 -0
- package/instrumentation.ts +18 -0
- package/next.config.mjs +182 -0
- package/package.json +99 -0
- package/postcss.config.mjs +7 -0
- package/proxy.ts +160 -0
- package/src/components/account/active-sessions.tsx +87 -0
- package/src/components/account/avatar-form.tsx +119 -0
- package/src/components/account/credential-proof-form.tsx +76 -0
- package/src/components/account/logout-form.tsx +14 -0
- package/src/components/account/notification-forms.tsx +157 -0
- package/src/components/account/passkey-enrol.tsx +81 -0
- package/src/components/account/passkey-proof-button.tsx +47 -0
- package/src/components/account/push-device-form.tsx +193 -0
- package/src/components/account/relation-forms.tsx +69 -0
- package/src/components/account/reputation-forms.tsx +124 -0
- package/src/components/account/security-activity.tsx +46 -0
- package/src/components/account/sign-in-methods.tsx +166 -0
- package/src/components/account/subscription-forms.tsx +151 -0
- package/src/components/account/two-factor-forms.tsx +226 -0
- package/src/components/account/usercp-forms.tsx +387 -0
- package/src/components/account/usercp-nav.tsx +14 -0
- package/src/components/account/usercp-shell.tsx +34 -0
- package/src/components/admin/admin-forms.tsx +92 -0
- package/src/components/admin/admin-nav.tsx +52 -0
- package/src/components/admin/admin-undo.tsx +32 -0
- package/src/components/admin/api-token-forms.tsx +89 -0
- package/src/components/admin/badge-forms.tsx +97 -0
- package/src/components/admin/branding-forms.tsx +102 -0
- package/src/components/admin/content-forms.tsx +712 -0
- package/src/components/admin/form-bits.tsx +11 -0
- package/src/components/admin/forum-forms.tsx +524 -0
- package/src/components/admin/forum-tree.tsx +454 -0
- package/src/components/admin/group-forms.tsx +646 -0
- package/src/components/admin/mail-test-card.tsx +90 -0
- package/src/components/admin/marketplace-forms.tsx +30 -0
- package/src/components/admin/marketplace-listing.tsx +123 -0
- package/src/components/admin/navigation-forms.tsx +229 -0
- package/src/components/admin/navigation-tree.tsx +442 -0
- package/src/components/admin/oklch-picker.tsx +180 -0
- package/src/components/admin/plugin-forms.tsx +225 -0
- package/src/components/admin/settings-form.tsx +148 -0
- package/src/components/admin/system-forms.tsx +169 -0
- package/src/components/admin/theme-changes.tsx +354 -0
- package/src/components/admin/theme-forms.tsx +636 -0
- package/src/components/admin/theme-palette.tsx +119 -0
- package/src/components/admin/theme-preview.tsx +461 -0
- package/src/components/admin/user-bulk-toolbar.tsx +99 -0
- package/src/components/admin/user-forms.tsx +488 -0
- package/src/components/auth/auth-page.tsx +45 -0
- package/src/components/auth/form-controls.tsx +117 -0
- package/src/components/auth/login-form.tsx +47 -0
- package/src/components/auth/passkey-client.ts +171 -0
- package/src/components/auth/passkey-second-factor.tsx +69 -0
- package/src/components/auth/passkey-sign-in.tsx +61 -0
- package/src/components/auth/register-form.tsx +136 -0
- package/src/components/auth/resend-verification-form.tsx +34 -0
- package/src/components/auth/reset-confirm-form.tsx +43 -0
- package/src/components/auth/reset-request-form.tsx +35 -0
- package/src/components/auth/second-factor-form.tsx +51 -0
- package/src/components/auth/sso-buttons.tsx +47 -0
- package/src/components/board/live-region.tsx +74 -0
- package/src/components/board/search-off-notice.tsx +18 -0
- package/src/components/board/section-page.tsx +114 -0
- package/src/components/content/attachment-field.tsx +51 -0
- package/src/components/content/composer-field.ts +31 -0
- package/src/components/content/composer-ids.ts +6 -0
- package/src/components/content/composer-intents.tsx +27 -0
- package/src/components/content/composer-recovery.tsx +215 -0
- package/src/components/content/edit-post-form.tsx +121 -0
- package/src/components/content/markdown-editor.tsx +590 -0
- package/src/components/content/markdown-syntax.ts +53 -0
- package/src/components/content/mention-token.ts +23 -0
- package/src/components/content/multiquote-button.tsx +32 -0
- package/src/components/content/multiquote-selection.tsx +105 -0
- package/src/components/content/multiquote.ts +125 -0
- package/src/components/content/new-thread-form.tsx +169 -0
- package/src/components/content/poll-edit-form.tsx +96 -0
- package/src/components/content/poll.tsx +89 -0
- package/src/components/content/quote-in-place.tsx +51 -0
- package/src/components/content/reply-form.tsx +113 -0
- package/src/components/content/thanks-button.tsx +65 -0
- package/src/components/content/thread-rating.tsx +143 -0
- package/src/components/install/install-form.tsx +497 -0
- package/src/components/legal/legal-document.tsx +22 -0
- package/src/components/messages/message-forms.tsx +152 -0
- package/src/components/moderation/inline-moderation-form.tsx +158 -0
- package/src/components/moderation/modcp-nav.tsx +15 -0
- package/src/components/moderation/modcp-shell.tsx +41 -0
- package/src/components/moderation/queue-form.tsx +96 -0
- package/src/components/moderation/report-forms.tsx +120 -0
- package/src/components/moderation/signature-lock-form.tsx +83 -0
- package/src/components/moderation/thread-surgery-form.tsx +96 -0
- package/src/components/moderation/thread-tools-form.tsx +137 -0
- package/src/components/moderation/warning-forms.tsx +125 -0
- package/src/components/profile/custom-field.tsx +79 -0
- package/src/components/shell/appearance-icons.tsx +35 -0
- package/src/components/shell/board-notice.tsx +21 -0
- package/src/components/shell/copy-record.ts +20 -0
- package/src/components/shell/copy.tsx +17 -0
- package/src/components/shell/crash-notice.tsx +86 -0
- package/src/components/shell/demo-banner.tsx +49 -0
- package/src/components/shell/group-name-style.tsx +6 -0
- package/src/components/shell/measure.ts +1 -0
- package/src/components/shell/notification-menu.tsx +361 -0
- package/src/components/shell/offline-notice.tsx +29 -0
- package/src/components/shell/onboarding-banner.tsx +39 -0
- package/src/components/shell/page-shell.tsx +167 -0
- package/src/components/shell/panel-list.tsx +95 -0
- package/src/components/shell/panel-nav.tsx +40 -0
- package/src/components/shell/panel-overview.tsx +81 -0
- package/src/components/shell/panel-page.tsx +96 -0
- package/src/components/shell/panel-pagination.tsx +28 -0
- package/src/components/shell/panel-shell.tsx +31 -0
- package/src/components/shell/theme-runtime-style.tsx +6 -0
- package/src/components/shell/theme-switcher.tsx +97 -0
- package/src/components/shell/timezone-probe.tsx +45 -0
- package/src/components/shell/view-tabs.tsx +60 -0
- package/src/config.ts +13 -0
- package/src/server/absent-services.ts +29 -0
- package/src/server/admin-actions.ts +171 -0
- package/src/server/admin-settings-actions.ts +83 -0
- package/src/server/admin-undo-actions.ts +70 -0
- package/src/server/admin-undo.ts +75 -0
- package/src/server/admin.ts +112 -0
- package/src/server/announcements.ts +82 -0
- package/src/server/antispam.ts +310 -0
- package/src/server/api/content.ts +486 -0
- package/src/server/api/http.ts +119 -0
- package/src/server/api/members.ts +109 -0
- package/src/server/api/messages.ts +113 -0
- package/src/server/api/registry.ts +30 -0
- package/src/server/api/search.ts +98 -0
- package/src/server/api/subscriptions.ts +106 -0
- package/src/server/api-auth.ts +66 -0
- package/src/server/api-token-actions.ts +85 -0
- package/src/server/api-tokens-admin.ts +95 -0
- package/src/server/appearance-actions.ts +39 -0
- package/src/server/attachment-download.ts +43 -0
- package/src/server/attachment-embed.ts +63 -0
- package/src/server/attachment-upload-actions.ts +92 -0
- package/src/server/attachments.ts +257 -0
- package/src/server/auth-actions.ts +479 -0
- package/src/server/auth-config.ts +61 -0
- package/src/server/auth-events.ts +92 -0
- package/src/server/auth-form-state.ts +14 -0
- package/src/server/auth-mail.ts +104 -0
- package/src/server/avatars.ts +83 -0
- package/src/server/board-latest-actions.ts +7 -0
- package/src/server/board-latest.tsx +103 -0
- package/src/server/board-offline.ts +23 -0
- package/src/server/board-url.ts +14 -0
- package/src/server/brand-assets.ts +151 -0
- package/src/server/brand-mark.tsx +110 -0
- package/src/server/branding-actions.ts +53 -0
- package/src/server/branding.ts +101 -0
- package/src/server/cache-targets.ts +13 -0
- package/src/server/code-highlighting.ts +33 -0
- package/src/server/container.ts +402 -0
- package/src/server/content-actions.ts +459 -0
- package/src/server/content-admin-actions.ts +492 -0
- package/src/server/content-admin.ts +95 -0
- package/src/server/content-security-policy.ts +35 -0
- package/src/server/context.ts +32 -0
- package/src/server/cookies.ts +139 -0
- package/src/server/credential-proof-actions.ts +42 -0
- package/src/server/credential-proof.ts +35 -0
- package/src/server/current-location.ts +19 -0
- package/src/server/demo-stripe.ts +104 -0
- package/src/server/demo-uploads.ts +28 -0
- package/src/server/demo.ts +80 -0
- package/src/server/discovery.ts +104 -0
- package/src/server/embeds.ts +113 -0
- package/src/server/error-notice.tsx +41 -0
- package/src/server/federation-actions.ts +113 -0
- package/src/server/federation.ts +138 -0
- package/src/server/feed-builder.ts +72 -0
- package/src/server/feed-routes.ts +122 -0
- package/src/server/fixture-actor-source.ts +62 -0
- package/src/server/fixture-forum-repo.ts +59 -0
- package/src/server/fixture-member-profile-repo.ts +30 -0
- package/src/server/fixture-post-repo.ts +108 -0
- package/src/server/fixture-thread-repo.ts +98 -0
- package/src/server/form-state-reporter.ts +34 -0
- package/src/server/form-values.ts +23 -0
- package/src/server/forum-admin-actions.ts +353 -0
- package/src/server/forum-admin.ts +98 -0
- package/src/server/group-admin-actions.ts +341 -0
- package/src/server/group-admin.ts +98 -0
- package/src/server/group-badge-actions.ts +61 -0
- package/src/server/group-badge.ts +62 -0
- package/src/server/group-identity.ts +104 -0
- package/src/server/i18n-catalogs.ts +42 -0
- package/src/server/i18n.ts +69 -0
- package/src/server/image-upload.ts +109 -0
- package/src/server/inline-moderation-actions.ts +129 -0
- package/src/server/install-actions.ts +124 -0
- package/src/server/install.ts +281 -0
- package/src/server/legacy-redirect.ts +83 -0
- package/src/server/legal.ts +43 -0
- package/src/server/location-header.ts +37 -0
- package/src/server/mail-brand.ts +43 -0
- package/src/server/mail-health.ts +41 -0
- package/src/server/mail-send.ts +44 -0
- package/src/server/mail-test-actions.ts +67 -0
- package/src/server/mail-test.ts +56 -0
- package/src/server/markdown-pipeline.ts +31 -0
- package/src/server/marketplace-actions.ts +47 -0
- package/src/server/marketplace-admin.ts +260 -0
- package/src/server/mention-actions.ts +21 -0
- package/src/server/mention-search.ts +35 -0
- package/src/server/message-actions.ts +110 -0
- package/src/server/messages.ts +141 -0
- package/src/server/modcp.ts +125 -0
- package/src/server/moderation-actions.ts +165 -0
- package/src/server/navigation-admin-actions.ts +217 -0
- package/src/server/navigation.ts +112 -0
- package/src/server/nonce.ts +13 -0
- package/src/server/notification-actions.ts +84 -0
- package/src/server/notification-audience.ts +10 -0
- package/src/server/notification-menu-actions.ts +82 -0
- package/src/server/notification-menu.ts +96 -0
- package/src/server/notifications.ts +93 -0
- package/src/server/onboarding-actions.ts +27 -0
- package/src/server/onboarding.ts +89 -0
- package/src/server/page-metadata.ts +14 -0
- package/src/server/passkey-challenge.ts +42 -0
- package/src/server/plugin-admin-actions.ts +232 -0
- package/src/server/plugin-admin.ts +276 -0
- package/src/server/plugin-host.ts +143 -0
- package/src/server/plugin-pages.ts +192 -0
- package/src/server/plugin-panel.ts +49 -0
- package/src/server/plugin-routes.ts +298 -0
- package/src/server/plugin-view.tsx +58 -0
- package/src/server/poll-actions.ts +104 -0
- package/src/server/poll-scope.ts +52 -0
- package/src/server/post-notifications.ts +71 -0
- package/src/server/post-scope.ts +86 -0
- package/src/server/presence.ts +177 -0
- package/src/server/profile-fields.ts +112 -0
- package/src/server/push-actions.ts +113 -0
- package/src/server/push.ts +40 -0
- package/src/server/redirect-back.ts +15 -0
- package/src/server/registration.ts +7 -0
- package/src/server/relation-actions.ts +89 -0
- package/src/server/relations.ts +45 -0
- package/src/server/reply-core.ts +173 -0
- package/src/server/report-actions.ts +165 -0
- package/src/server/report-scope.ts +21 -0
- package/src/server/reputation-actions.ts +146 -0
- package/src/server/reputation-target.ts +49 -0
- package/src/server/reputation.ts +70 -0
- package/src/server/request-fingerprint.ts +65 -0
- package/src/server/safe-path.ts +16 -0
- package/src/server/same-origin.ts +45 -0
- package/src/server/search-page.ts +313 -0
- package/src/server/search.ts +45 -0
- package/src/server/secret-auth.ts +20 -0
- package/src/server/see-other.ts +11 -0
- package/src/server/seed-board.ts +417 -0
- package/src/server/session-actions.ts +78 -0
- package/src/server/session-cookies.ts +124 -0
- package/src/server/session-key.ts +17 -0
- package/src/server/settings.ts +47 -0
- package/src/server/signatures.ts +62 -0
- package/src/server/sso-handshake.ts +79 -0
- package/src/server/stats.ts +63 -0
- package/src/server/subscription-actions.ts +161 -0
- package/src/server/surgery-actions.ts +180 -0
- package/src/server/syndication.ts +77 -0
- package/src/server/system-admin-actions.ts +134 -0
- package/src/server/system-admin.ts +90 -0
- package/src/server/test-container.ts +114 -0
- package/src/server/theme-admin-actions.ts +242 -0
- package/src/server/theme-admin.ts +203 -0
- package/src/server/theme-runtime.ts +89 -0
- package/src/server/theme-style.ts +279 -0
- package/src/server/theme.ts +52 -0
- package/src/server/thread-core.ts +207 -0
- package/src/server/thread-rating-actions.ts +55 -0
- package/src/server/thread-tool-actions.ts +154 -0
- package/src/server/two-factor-actions.ts +178 -0
- package/src/server/two-factor.ts +86 -0
- package/src/server/upgrade-notice.ts +41 -0
- package/src/server/user-admin-actions.ts +572 -0
- package/src/server/user-admin.ts +165 -0
- package/src/server/usercp-actions.ts +263 -0
- package/src/server/usercp-mail.ts +120 -0
- package/src/server/viewer-preferences.ts +64 -0
- package/src/server/warning-actions.ts +122 -0
- package/src/styles/globals.css +969 -0
- package/src/theme/contract.fixture.ts +793 -0
- package/src/view/account-copy.ts +351 -0
- package/src/view/admin-content-copy.ts +66 -0
- package/src/view/admin-copy.ts +106 -0
- package/src/view/admin-forum-tree-copy.ts +40 -0
- package/src/view/admin-group-copy.ts +80 -0
- package/src/view/admin-log.ts +58 -0
- package/src/view/admin-nav.ts +164 -0
- package/src/view/admin-navigation-copy.ts +58 -0
- package/src/view/admin-panel-copy.ts +194 -0
- package/src/view/admin-settings.ts +173 -0
- package/src/view/admin-theme-copy.ts +229 -0
- package/src/view/admin-user-copy.ts +96 -0
- package/src/view/attachments.ts +51 -0
- package/src/view/auth-copy.ts +147 -0
- package/src/view/board-index.ts +167 -0
- package/src/view/board-latest.ts +101 -0
- package/src/view/breadcrumb.ts +50 -0
- package/src/view/contacts.ts +85 -0
- package/src/view/content-copy.ts +118 -0
- package/src/view/contrast.ts +286 -0
- package/src/view/copy.ts +101 -0
- package/src/view/count.ts +8 -0
- package/src/view/discovery-view.ts +74 -0
- package/src/view/editor-toolbar.ts +84 -0
- package/src/view/error-notice.ts +52 -0
- package/src/view/feed.ts +130 -0
- package/src/view/forum-cursor.ts +54 -0
- package/src/view/forum-display.ts +152 -0
- package/src/view/forum-jump.ts +60 -0
- package/src/view/forum-notice.ts +24 -0
- package/src/view/inline-moderation.ts +87 -0
- package/src/view/install-copy.ts +95 -0
- package/src/view/legal.ts +58 -0
- package/src/view/locale.ts +40 -0
- package/src/view/marketplace-panel.ts +51 -0
- package/src/view/member-identity.ts +23 -0
- package/src/view/member-profile.ts +65 -0
- package/src/view/messages.ts +222 -0
- package/src/view/metadata.ts +75 -0
- package/src/view/modcp-nav.ts +76 -0
- package/src/view/moderation-copy.ts +84 -0
- package/src/view/moderation-queue.ts +67 -0
- package/src/view/navigation-arrange.ts +188 -0
- package/src/view/navigation.ts +173 -0
- package/src/view/notification-menu.ts +202 -0
- package/src/view/notifications.ts +84 -0
- package/src/view/onboarding.ts +26 -0
- package/src/view/pager.ts +191 -0
- package/src/view/panel-nav.ts +203 -0
- package/src/view/plugin-panel.ts +42 -0
- package/src/view/post-form.ts +61 -0
- package/src/view/post-link.ts +29 -0
- package/src/view/presence.ts +99 -0
- package/src/view/promotion-rules.ts +52 -0
- package/src/view/redirect-notice.ts +21 -0
- package/src/view/reputation.ts +107 -0
- package/src/view/search-controls.ts +103 -0
- package/src/view/search-results.ts +291 -0
- package/src/view/security-activity.ts +73 -0
- package/src/view/setting-groups.ts +43 -0
- package/src/view/shell.ts +164 -0
- package/src/view/slug-id.ts +12 -0
- package/src/view/sso-hand-off.ts +44 -0
- package/src/view/sso-notices.ts +48 -0
- package/src/view/subscriptions.ts +76 -0
- package/src/view/theme-draft.ts +167 -0
- package/src/view/theme-preference.ts +30 -0
- package/src/view/theme-tokens.ts +333 -0
- package/src/view/thread-view.ts +280 -0
- package/src/view/time.ts +31 -0
- package/src/view/timezone.ts +35 -0
- package/src/view/two-factor.ts +6 -0
- package/src/view/usercp-nav.ts +103 -0
- package/src/view/usercp.ts +196 -0
- package/src/view/warnings.ts +128 -0
- package/src/view/word-filter.ts +5 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { PaginationModel } from '@meith/theme-kit'
|
|
2
|
+
|
|
3
|
+
export const TRAIL_PARAM = 'seen'
|
|
4
|
+
|
|
5
|
+
export const TRAIL_LIMIT = 40
|
|
6
|
+
|
|
7
|
+
const TRAIL_SEPARATOR = '~'
|
|
8
|
+
|
|
9
|
+
const WINDOW = 3
|
|
10
|
+
|
|
11
|
+
export type QueryParams = Readonly<Record<string, string | string[] | undefined>>
|
|
12
|
+
|
|
13
|
+
export type Cursor = Readonly<Record<string, string>>
|
|
14
|
+
|
|
15
|
+
export interface PagerInput {
|
|
16
|
+
readonly path: string
|
|
17
|
+
readonly params: QueryParams
|
|
18
|
+
/**
|
|
19
|
+
* The query parameters the cursor owns. They are rewritten on every step and
|
|
20
|
+
* never carried forward from the current address, which is what keeps a
|
|
21
|
+
* two-part cursor — a rank and an id — from being half of one page and half
|
|
22
|
+
* of the next.
|
|
23
|
+
*/
|
|
24
|
+
readonly cursorParams: readonly string[]
|
|
25
|
+
readonly nextCursor: Cursor | null
|
|
26
|
+
readonly pageSize?: number
|
|
27
|
+
readonly total?: number | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const PAGE_PARAM = 'page'
|
|
31
|
+
|
|
32
|
+
const NEIGHBOURS = 2
|
|
33
|
+
|
|
34
|
+
export interface OffsetPagerInput {
|
|
35
|
+
readonly path: string
|
|
36
|
+
readonly params: QueryParams
|
|
37
|
+
readonly pageParam?: string
|
|
38
|
+
readonly page: number
|
|
39
|
+
readonly pageSize: number
|
|
40
|
+
readonly total: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function one(params: QueryParams, key: string): string | undefined {
|
|
44
|
+
const raw = params[key]
|
|
45
|
+
const value = Array.isArray(raw) ? raw[0] : raw
|
|
46
|
+
return value === '' ? undefined : value
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function encodeCursor(cursor: Cursor): string {
|
|
50
|
+
return new URLSearchParams(Object.entries(cursor)).toString()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function decodeCursor(entry: string): Cursor {
|
|
54
|
+
return Object.fromEntries(new URLSearchParams(entry))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readTrail(params: QueryParams): Cursor[] {
|
|
58
|
+
const raw = one(params, TRAIL_PARAM)
|
|
59
|
+
if (raw === undefined) return []
|
|
60
|
+
|
|
61
|
+
return raw
|
|
62
|
+
.split(TRAIL_SEPARATOR)
|
|
63
|
+
.filter((entry) => entry !== '')
|
|
64
|
+
.slice(-TRAIL_LIMIT)
|
|
65
|
+
.map(decodeCursor)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function href(input: PagerInput, cursor: Cursor | null, trail: readonly Cursor[]): string {
|
|
69
|
+
const query = new URLSearchParams()
|
|
70
|
+
|
|
71
|
+
for (const [key, value] of Object.entries(input.params)) {
|
|
72
|
+
if (key === TRAIL_PARAM || input.cursorParams.includes(key)) continue
|
|
73
|
+
const text = Array.isArray(value) ? value[0] : value
|
|
74
|
+
if (text !== undefined && text !== '') query.set(key, text)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (cursor !== null) {
|
|
78
|
+
for (const [key, value] of Object.entries(cursor)) query.set(key, value)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (trail.length > 0) {
|
|
82
|
+
query.set(TRAIL_PARAM, trail.map(encodeCursor).join(TRAIL_SEPARATOR))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const search = query.toString()
|
|
86
|
+
return search === '' ? input.path : `${input.path}?${search}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pageHref(input: PagerInput, trail: readonly Cursor[], page: number): string {
|
|
90
|
+
if (page <= 1) return href(input, null, [])
|
|
91
|
+
return href(input, trail[page - 2] ?? null, trail.slice(0, page - 1))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildPager(input: PagerInput): PaginationModel {
|
|
95
|
+
const trail = readTrail(input.params)
|
|
96
|
+
const page = trail.length + 1
|
|
97
|
+
|
|
98
|
+
const size = input.pageSize ?? 0
|
|
99
|
+
const total = input.total ?? null
|
|
100
|
+
const exact = total !== null && size > 0
|
|
101
|
+
|
|
102
|
+
const pageCount = exact
|
|
103
|
+
? Math.max(1, Math.ceil(total / size))
|
|
104
|
+
: page + (input.nextCursor === null ? 0 : 1)
|
|
105
|
+
|
|
106
|
+
const numbers = new Set<number>([1, page])
|
|
107
|
+
for (let step = 1; step <= WINDOW; step += 1) {
|
|
108
|
+
if (page - step > 1) numbers.add(page - step)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const pages = [...numbers]
|
|
112
|
+
.sort((a, b) => a - b)
|
|
113
|
+
.map((number) => ({
|
|
114
|
+
page: number,
|
|
115
|
+
href: pageHref(input, trail, number),
|
|
116
|
+
isCurrent: number === page,
|
|
117
|
+
}))
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
page,
|
|
121
|
+
pageCount,
|
|
122
|
+
pageCountIsExact: exact,
|
|
123
|
+
pages,
|
|
124
|
+
previousHref: page === 1 ? null : pageHref(input, trail, page - 1),
|
|
125
|
+
nextHref:
|
|
126
|
+
input.nextCursor === null
|
|
127
|
+
? null
|
|
128
|
+
: href(input, input.nextCursor, [...trail, input.nextCursor].slice(-TRAIL_LIMIT)),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function readPage(params: QueryParams, pageParam: string = PAGE_PARAM): number {
|
|
133
|
+
const raw = one(params, pageParam)
|
|
134
|
+
if (raw === undefined) return 1
|
|
135
|
+
|
|
136
|
+
const page = Number(raw)
|
|
137
|
+
return Number.isSafeInteger(page) && page >= 1 ? page : 1
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function offsetOf(page: number, pageSize: number): number {
|
|
141
|
+
return (Math.max(page, 1) - 1) * pageSize
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function pageCountOf(total: number, pageSize: number): number {
|
|
145
|
+
return Math.max(1, Math.ceil(Math.max(total, 0) / Math.max(pageSize, 1)))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function numberedHref(input: OffsetPagerInput, page: number): string {
|
|
149
|
+
const pageParam = input.pageParam ?? PAGE_PARAM
|
|
150
|
+
const query = new URLSearchParams()
|
|
151
|
+
|
|
152
|
+
for (const [key, value] of Object.entries(input.params)) {
|
|
153
|
+
if (key === pageParam || key === TRAIL_PARAM) continue
|
|
154
|
+
const text = Array.isArray(value) ? value[0] : value
|
|
155
|
+
if (text !== undefined && text !== '') query.set(key, text)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (page > 1) query.set(pageParam, String(page))
|
|
159
|
+
|
|
160
|
+
const search = query.toString()
|
|
161
|
+
return search === '' ? input.path : `${input.path}?${search}`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function buildOffsetPager(input: OffsetPagerInput): PaginationModel {
|
|
165
|
+
const pageCount = pageCountOf(input.total, input.pageSize)
|
|
166
|
+
const page = Math.max(input.page, 1)
|
|
167
|
+
|
|
168
|
+
const anchor = Math.min(page, pageCount)
|
|
169
|
+
|
|
170
|
+
const numbers = new Set<number>([1, pageCount])
|
|
171
|
+
if (page <= pageCount) numbers.add(page)
|
|
172
|
+
for (let step = 1; step <= NEIGHBOURS; step += 1) {
|
|
173
|
+
if (anchor - step >= 1) numbers.add(anchor - step)
|
|
174
|
+
if (anchor + step <= pageCount) numbers.add(anchor + step)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
page,
|
|
179
|
+
pageCount,
|
|
180
|
+
pageCountIsExact: true,
|
|
181
|
+
pages: [...numbers]
|
|
182
|
+
.sort((a, b) => a - b)
|
|
183
|
+
.map((number) => ({
|
|
184
|
+
page: number,
|
|
185
|
+
href: numberedHref(input, number),
|
|
186
|
+
isCurrent: number === page,
|
|
187
|
+
})),
|
|
188
|
+
previousHref: page === 1 ? null : numberedHref(input, page > pageCount ? pageCount : page - 1),
|
|
189
|
+
nextHref: page >= pageCount ? null : numberedHref(input, page + 1),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { Translator } from '@meith/i18n'
|
|
2
|
+
import type {
|
|
3
|
+
PanelKind,
|
|
4
|
+
PanelNavCurrent,
|
|
5
|
+
PanelNavIcon,
|
|
6
|
+
PanelNavModel,
|
|
7
|
+
PanelNavSectionModel,
|
|
8
|
+
} from '@meith/theme-kit'
|
|
9
|
+
|
|
10
|
+
import { untranslated } from './time'
|
|
11
|
+
|
|
12
|
+
export interface PanelSubsection {
|
|
13
|
+
readonly href: string
|
|
14
|
+
readonly titleKey?: string
|
|
15
|
+
/** Set instead of `titleKey` where the name is data, such as a plugin's. */
|
|
16
|
+
readonly titleText?: string
|
|
17
|
+
readonly record?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PanelSection {
|
|
21
|
+
readonly href: string
|
|
22
|
+
readonly titleKey?: string
|
|
23
|
+
readonly titleText?: string
|
|
24
|
+
readonly blurbKey?: string
|
|
25
|
+
readonly blurbText?: string
|
|
26
|
+
readonly icon?: PanelNavIcon
|
|
27
|
+
readonly children?: readonly PanelSubsection[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function panelText(
|
|
31
|
+
t: Translator,
|
|
32
|
+
entry: { readonly titleKey?: string; readonly titleText?: string },
|
|
33
|
+
): string
|
|
34
|
+
export function panelText(
|
|
35
|
+
t: Translator,
|
|
36
|
+
entry: { readonly blurbKey?: string; readonly blurbText?: string },
|
|
37
|
+
field: 'blurb',
|
|
38
|
+
): string
|
|
39
|
+
export function panelText(
|
|
40
|
+
t: Translator,
|
|
41
|
+
entry: Readonly<Record<string, string | undefined>>,
|
|
42
|
+
field: 'title' | 'blurb' = 'title',
|
|
43
|
+
): string {
|
|
44
|
+
const text = entry[`${field}Text`]
|
|
45
|
+
if (text !== undefined) return text
|
|
46
|
+
|
|
47
|
+
const key = entry[`${field}Key`]
|
|
48
|
+
return key === undefined ? '' : t.t(key)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PanelSectionCopy {
|
|
52
|
+
readonly href: string
|
|
53
|
+
readonly title: string
|
|
54
|
+
readonly blurb: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function panelSectionCopy(nav: PanelNav, t: Translator): readonly PanelSectionCopy[] {
|
|
58
|
+
return nav.map((section) => ({
|
|
59
|
+
href: section.href,
|
|
60
|
+
title: panelText(t, section),
|
|
61
|
+
blurb: panelText(t, section, 'blurb'),
|
|
62
|
+
}))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type PanelNav = readonly PanelSection[]
|
|
66
|
+
|
|
67
|
+
function parts(href: string): { path: string; params: URLSearchParams } {
|
|
68
|
+
const cut = href.indexOf('?')
|
|
69
|
+
if (cut === -1) return { path: href, params: new URLSearchParams() }
|
|
70
|
+
return { path: href.slice(0, cut), params: new URLSearchParams(href.slice(cut + 1)) }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function paramsAgree(here: URLSearchParams, wanted: URLSearchParams): boolean {
|
|
74
|
+
for (const [key, value] of wanted) {
|
|
75
|
+
if (here.get(key) !== value) return false
|
|
76
|
+
}
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isUnder(location: string, href: string): boolean {
|
|
81
|
+
const here = parts(location)
|
|
82
|
+
const want = parts(href)
|
|
83
|
+
if (here.path !== want.path && !here.path.startsWith(`${want.path}/`)) return false
|
|
84
|
+
return paramsAgree(here.params, want.params)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isHere(location: string, href: string): boolean {
|
|
88
|
+
const here = parts(location)
|
|
89
|
+
const want = parts(href)
|
|
90
|
+
return here.path === want.path && paramsAgree(here.params, want.params)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function longest(location: string, hrefs: readonly string[]): string | null {
|
|
94
|
+
let best: string | null = null
|
|
95
|
+
for (const href of hrefs) {
|
|
96
|
+
if (!isUnder(location, href)) continue
|
|
97
|
+
if (best === null || href.length > best.length) best = href
|
|
98
|
+
}
|
|
99
|
+
return best
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function sectionHrefIn(nav: PanelNav, location: string): string | null {
|
|
103
|
+
return longest(
|
|
104
|
+
location,
|
|
105
|
+
nav.map((section) => section.href),
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function deepestHrefIn(nav: PanelNav, location: string): string | null {
|
|
110
|
+
return longest(
|
|
111
|
+
location,
|
|
112
|
+
nav.flatMap((section) => [
|
|
113
|
+
section.href,
|
|
114
|
+
...(section.children ?? []).map((child) => child.href),
|
|
115
|
+
]),
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function flattenNav(nav: PanelNav): readonly (PanelSection | PanelSubsection)[] {
|
|
120
|
+
return nav.flatMap((section) => [section, ...(section.children ?? [])])
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function visibleChildren(
|
|
124
|
+
section: PanelSection,
|
|
125
|
+
deepest: string | null,
|
|
126
|
+
): readonly PanelSubsection[] {
|
|
127
|
+
return (section.children ?? []).filter((child) => child.record !== true || child.href === deepest)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type PanelCounts = Readonly<Record<string, number>>
|
|
131
|
+
|
|
132
|
+
export function countFor(counts: PanelCounts | undefined, href: string): number | null {
|
|
133
|
+
const count = counts?.[href] ?? 0
|
|
134
|
+
return count > 0 ? count : null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function currentProps(
|
|
138
|
+
location: string,
|
|
139
|
+
href: string,
|
|
140
|
+
deepest: string | null,
|
|
141
|
+
): { readonly 'aria-current'?: 'page' | 'true' } {
|
|
142
|
+
if (href !== deepest) return {}
|
|
143
|
+
return { 'aria-current': isHere(location, href) ? 'page' : 'true' }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function currentFor(
|
|
147
|
+
location: string,
|
|
148
|
+
href: string,
|
|
149
|
+
deepest: string | null,
|
|
150
|
+
): PanelNavCurrent | null {
|
|
151
|
+
if (href !== deepest) return null
|
|
152
|
+
return isHere(location, href) ? 'here' : 'under'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function buildPanelNavModel(input: {
|
|
156
|
+
readonly panel: PanelKind
|
|
157
|
+
readonly nav: PanelNav
|
|
158
|
+
readonly overviewHref: string
|
|
159
|
+
readonly location: string
|
|
160
|
+
readonly fallbackHref?: string | undefined
|
|
161
|
+
readonly counts?: PanelCounts | undefined
|
|
162
|
+
readonly label?: string
|
|
163
|
+
readonly t?: Translator | undefined
|
|
164
|
+
}): PanelNavModel {
|
|
165
|
+
const t = input.t ?? untranslated()
|
|
166
|
+
const fallback = input.fallbackHref ?? null
|
|
167
|
+
const open = sectionHrefIn(input.nav, input.location) ?? fallback
|
|
168
|
+
const deepest = deepestHrefIn(input.nav, input.location) ?? fallback
|
|
169
|
+
|
|
170
|
+
const sections: PanelNavSectionModel[] = input.nav.map((section) => {
|
|
171
|
+
const isOpen = open === section.href
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
href: section.href,
|
|
175
|
+
title: panelText(t, section),
|
|
176
|
+
icon: section.icon ?? null,
|
|
177
|
+
count: countFor(input.counts, section.href),
|
|
178
|
+
current: currentFor(input.location, section.href, deepest),
|
|
179
|
+
isRecord: false,
|
|
180
|
+
isOpen,
|
|
181
|
+
isOverview: section.href === input.overviewHref,
|
|
182
|
+
children: isOpen
|
|
183
|
+
? visibleChildren(section, deepest).map((child) => ({
|
|
184
|
+
href: child.href,
|
|
185
|
+
title: panelText(t, child),
|
|
186
|
+
icon: null,
|
|
187
|
+
count: countFor(input.counts, child.href),
|
|
188
|
+
current: currentFor(input.location, child.href, deepest),
|
|
189
|
+
isRecord: child.record === true,
|
|
190
|
+
}))
|
|
191
|
+
: [],
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const here = flattenNav(input.nav).find((item) => item.href === deepest)
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
panel: input.panel,
|
|
199
|
+
label: input.label ?? t.t('panel.sections'),
|
|
200
|
+
sections,
|
|
201
|
+
currentTitle: here === undefined ? null : panelText(t, here),
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { pluginAdminPath } from '@meith/plugin-kit'
|
|
2
|
+
|
|
3
|
+
import type { PanelSubsection } from './panel-nav'
|
|
4
|
+
|
|
5
|
+
export interface PluginPage {
|
|
6
|
+
readonly path: string
|
|
7
|
+
readonly title: string
|
|
8
|
+
readonly href: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PluginPanelTab {
|
|
12
|
+
readonly href: string
|
|
13
|
+
readonly label: string
|
|
14
|
+
readonly isCurrent: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const PLUGIN_SETTINGS_TAB = 'Settings'
|
|
18
|
+
|
|
19
|
+
export function pluginPanelTabs(input: {
|
|
20
|
+
readonly pluginKey: string
|
|
21
|
+
readonly pages: readonly PluginPage[]
|
|
22
|
+
readonly current: string | null
|
|
23
|
+
}): readonly PluginPanelTab[] {
|
|
24
|
+
if (input.pages.length === 0) return []
|
|
25
|
+
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
href: pluginAdminPath(input.pluginKey, ''),
|
|
29
|
+
label: PLUGIN_SETTINGS_TAB,
|
|
30
|
+
isCurrent: input.current === null,
|
|
31
|
+
},
|
|
32
|
+
...input.pages.map((page) => ({
|
|
33
|
+
href: page.href,
|
|
34
|
+
label: page.title,
|
|
35
|
+
isCurrent: page.path === input.current,
|
|
36
|
+
})),
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function pluginNavChildren(pages: readonly PluginPage[]): readonly PanelSubsection[] {
|
|
41
|
+
return pages.map((page) => ({ href: page.href, titleText: page.title }))
|
|
42
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Translator } from '@meith/i18n'
|
|
2
|
+
import type { PostFormModel } from '@meith/theme-kit'
|
|
3
|
+
|
|
4
|
+
import { postLink } from './post-link'
|
|
5
|
+
import { untranslated } from './time'
|
|
6
|
+
|
|
7
|
+
export interface PostFormInput {
|
|
8
|
+
readonly forum: { readonly id: number; readonly title: string; readonly slug: string }
|
|
9
|
+
readonly errorMessage?: string | null
|
|
10
|
+
readonly t?: Translator
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildNewThreadView(input: PostFormInput): Omit<PostFormModel, 'regions'> {
|
|
14
|
+
const t = input.t ?? untranslated()
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
mode: 'thread',
|
|
18
|
+
heading: t.t('postForm.newThread', { forum: input.forum.title }),
|
|
19
|
+
cancelHref: `/${input.forum.id}-${input.forum.slug}`,
|
|
20
|
+
cancelLabel: t.t('postForm.backToForum', { forum: input.forum.title }),
|
|
21
|
+
errorMessage: input.errorMessage ?? null,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ReplyViewInput {
|
|
26
|
+
readonly thread: { readonly id: number; readonly title: string; readonly slug: string }
|
|
27
|
+
readonly errorMessage?: string | null
|
|
28
|
+
readonly t?: Translator
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildReplyView(input: ReplyViewInput): Omit<PostFormModel, 'regions'> {
|
|
32
|
+
const t = input.t ?? untranslated()
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
mode: 'reply',
|
|
36
|
+
heading: t.t('postForm.reply', { thread: input.thread.title }),
|
|
37
|
+
cancelHref: `/thread/${input.thread.id}-${input.thread.slug}`,
|
|
38
|
+
cancelLabel: t.t('postForm.backToThread'),
|
|
39
|
+
errorMessage: input.errorMessage ?? null,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface EditViewInput {
|
|
44
|
+
readonly thread: { readonly id: number; readonly title: string; readonly slug: string }
|
|
45
|
+
readonly postId: number
|
|
46
|
+
readonly isDeleted: boolean
|
|
47
|
+
readonly errorMessage?: string | null
|
|
48
|
+
readonly t?: Translator
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildEditView(input: EditViewInput): Omit<PostFormModel, 'regions'> {
|
|
52
|
+
const t = input.t ?? untranslated()
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
mode: 'edit',
|
|
56
|
+
heading: t.t(input.isDeleted ? 'postForm.deletedPost' : 'postForm.editPost'),
|
|
57
|
+
cancelHref: postLink(`/thread/${input.thread.id}-${input.thread.slug}`, input.postId),
|
|
58
|
+
cancelLabel: t.t('postForm.backToThread'),
|
|
59
|
+
errorMessage: input.errorMessage ?? null,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PostLocation } from '@meith/posts'
|
|
2
|
+
|
|
3
|
+
export function postAnchor(number: number): string {
|
|
4
|
+
return `post-${number}`
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function postLink(threadHref: string, postId: number): string {
|
|
8
|
+
const path = threadHref.split('#')[0] ?? ''
|
|
9
|
+
return `${path}${path.includes('?') ? '&' : '?'}post=${postId}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function locatedHref(
|
|
13
|
+
path: string,
|
|
14
|
+
query: Record<string, string | readonly string[] | undefined>,
|
|
15
|
+
location: PostLocation,
|
|
16
|
+
): string {
|
|
17
|
+
const params = new URLSearchParams()
|
|
18
|
+
for (const [key, value] of Object.entries(query)) {
|
|
19
|
+
if (value === undefined || key === 'post' || key === 'after' || key === 'page') continue
|
|
20
|
+
for (const one of typeof value === 'string' ? [value] : value) params.append(key, one)
|
|
21
|
+
}
|
|
22
|
+
if (location.afterId !== null) {
|
|
23
|
+
params.set('after', String(location.afterId))
|
|
24
|
+
params.set('page', String(location.page))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const search = params.toString()
|
|
28
|
+
return `${path}${search === '' ? '' : `?${search}`}#${postAnchor(location.number)}`
|
|
29
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Translator } from '@meith/i18n'
|
|
2
|
+
import type { BoardStatsModel, OnlineMemberModel, WhoIsOnlineModel } from '@meith/theme-kit'
|
|
3
|
+
|
|
4
|
+
import { count } from './count'
|
|
5
|
+
import { type MemberIdentity, nameClassOf } from './member-identity'
|
|
6
|
+
import { memberHref } from './member-profile'
|
|
7
|
+
import { formatTime, untranslated } from './time'
|
|
8
|
+
|
|
9
|
+
export interface OnlineRow {
|
|
10
|
+
readonly userId: number
|
|
11
|
+
readonly username: string
|
|
12
|
+
readonly invisible: boolean
|
|
13
|
+
readonly lastSeenAt: Date
|
|
14
|
+
readonly forumId: number | null
|
|
15
|
+
readonly forumTitle: string | null
|
|
16
|
+
readonly threadId: number | null
|
|
17
|
+
readonly threadTitle: string | null
|
|
18
|
+
readonly threadSlug: string | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface OnlineInput {
|
|
22
|
+
readonly members: readonly OnlineRow[]
|
|
23
|
+
readonly guestCount: number
|
|
24
|
+
readonly recordCount: number
|
|
25
|
+
readonly recordAt: Date | null
|
|
26
|
+
readonly now: Date
|
|
27
|
+
readonly t?: Translator | undefined
|
|
28
|
+
readonly identities?: ReadonlyMap<number, MemberIdentity>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function locationOf(
|
|
32
|
+
row: OnlineRow,
|
|
33
|
+
t: Translator = untranslated(),
|
|
34
|
+
): { label: string; href: string | null } {
|
|
35
|
+
if (row.threadId !== null && row.threadTitle !== null) {
|
|
36
|
+
return {
|
|
37
|
+
label: t.t('presence.reading', { title: row.threadTitle }),
|
|
38
|
+
href: `/thread/${row.threadId}-${row.threadSlug ?? ''}`,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (row.forumId !== null && row.forumTitle !== null) {
|
|
43
|
+
return { label: t.t('presence.viewing', { title: row.forumTitle }), href: `/${row.forumId}` }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { label: t.t('presence.somewhere'), href: null }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildWhoIsOnlineModel(input: OnlineInput): WhoIsOnlineModel {
|
|
50
|
+
const members: OnlineMemberModel[] = input.members.map((row) => ({
|
|
51
|
+
userId: row.userId,
|
|
52
|
+
username: row.username,
|
|
53
|
+
profileHref: memberHref(row.userId),
|
|
54
|
+
nameClass: nameClassOf(input.identities, row.userId),
|
|
55
|
+
location: locationOf(row, input.t ?? untranslated()),
|
|
56
|
+
isInvisible: row.invisible,
|
|
57
|
+
lastSeen: formatTime(row.lastSeenAt, input.now, input.t),
|
|
58
|
+
}))
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
guestCount: count(input.guestCount, input.t),
|
|
62
|
+
members,
|
|
63
|
+
memberCount: count(members.length, input.t),
|
|
64
|
+
total: count(members.length + input.guestCount, input.t),
|
|
65
|
+
recordCount: count(input.recordCount, input.t),
|
|
66
|
+
recordAt: input.recordAt === null ? null : formatTime(input.recordAt, input.now, input.t),
|
|
67
|
+
fullListHref: '/online',
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface StatsInput {
|
|
72
|
+
readonly threadCount: number
|
|
73
|
+
readonly postCount: number
|
|
74
|
+
readonly memberCount: number
|
|
75
|
+
readonly newestUserId: number | null
|
|
76
|
+
readonly newestUsername: string | null
|
|
77
|
+
readonly computedAt: Date | null
|
|
78
|
+
readonly now: Date
|
|
79
|
+
readonly t?: Translator | undefined
|
|
80
|
+
readonly identities?: ReadonlyMap<number, MemberIdentity>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function buildBoardStatsModel(input: StatsInput): BoardStatsModel {
|
|
84
|
+
return {
|
|
85
|
+
threadCount: count(input.threadCount, input.t),
|
|
86
|
+
postCount: count(input.postCount, input.t),
|
|
87
|
+
memberCount: count(input.memberCount, input.t),
|
|
88
|
+
newestMember:
|
|
89
|
+
input.newestUsername === null
|
|
90
|
+
? null
|
|
91
|
+
: {
|
|
92
|
+
userId: input.newestUserId,
|
|
93
|
+
username: input.newestUsername,
|
|
94
|
+
profileHref: input.newestUserId === null ? null : memberHref(input.newestUserId),
|
|
95
|
+
nameClass: nameClassOf(input.identities, input.newestUserId),
|
|
96
|
+
},
|
|
97
|
+
computedAt: input.computedAt === null ? null : formatTime(input.computedAt, input.now, input.t),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { PromotionRule } from '@meith/groups'
|
|
2
|
+
|
|
3
|
+
export interface PromotionRuleFormValues {
|
|
4
|
+
readonly id: number
|
|
5
|
+
readonly title: string
|
|
6
|
+
readonly enabled: boolean
|
|
7
|
+
readonly displayOrder: string
|
|
8
|
+
readonly minPostCount: string
|
|
9
|
+
readonly minReputation: string
|
|
10
|
+
readonly minDaysRegistered: string
|
|
11
|
+
readonly fromPrimaryGroupId: string
|
|
12
|
+
readonly toPrimaryGroupId: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function field(value: number | null | undefined): string {
|
|
16
|
+
return value === null || value === undefined ? '' : String(value)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function promotionRuleFormValues(rule: PromotionRule): PromotionRuleFormValues {
|
|
20
|
+
return {
|
|
21
|
+
id: rule.id,
|
|
22
|
+
title: rule.title,
|
|
23
|
+
enabled: rule.enabled,
|
|
24
|
+
displayOrder: String(rule.displayOrder),
|
|
25
|
+
minPostCount: field(rule.minPostCount),
|
|
26
|
+
minReputation: field(rule.minReputation),
|
|
27
|
+
minDaysRegistered: field(rule.minDaysRegistered),
|
|
28
|
+
fromPrimaryGroupId: field(rule.fromPrimaryGroupId),
|
|
29
|
+
toPrimaryGroupId: String(rule.toPrimaryGroupId),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function promotionRuleSummary(
|
|
34
|
+
rule: PromotionRule,
|
|
35
|
+
groupTitle: (id: number | null) => string,
|
|
36
|
+
): string {
|
|
37
|
+
const criteria: string[] = []
|
|
38
|
+
if (rule.minPostCount !== undefined) criteria.push(`${rule.minPostCount} posts`)
|
|
39
|
+
if (rule.minReputation !== undefined) criteria.push(`${rule.minReputation} reputation`)
|
|
40
|
+
if (rule.minDaysRegistered !== undefined) {
|
|
41
|
+
criteria.push(`${rule.minDaysRegistered} days registered`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const from =
|
|
45
|
+
rule.fromPrimaryGroupId === null || rule.fromPrimaryGroupId === undefined
|
|
46
|
+
? 'any group'
|
|
47
|
+
: groupTitle(rule.fromPrimaryGroupId)
|
|
48
|
+
|
|
49
|
+
const wants = criteria.length === 0 ? 'no criteria' : `at least ${criteria.join(', ')}`
|
|
50
|
+
|
|
51
|
+
return `${from} → ${groupTitle(rule.toPrimaryGroupId)} · ${wants}`
|
|
52
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RedirectNoticeModel } from '@meith/theme-kit'
|
|
2
|
+
|
|
3
|
+
export const REDIRECT_DELAY_SECONDS = 2
|
|
4
|
+
|
|
5
|
+
export function localHref(value: string | undefined): string {
|
|
6
|
+
if (value === undefined || !value.startsWith('/')) return '/'
|
|
7
|
+
const origin = 'https://forum.invalid'
|
|
8
|
+
const url = new URL(value, origin)
|
|
9
|
+
return url.origin === origin ? `${url.pathname}${url.search}${url.hash}` : '/'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildRedirectNotice(
|
|
13
|
+
target: string | undefined,
|
|
14
|
+
message: string | undefined,
|
|
15
|
+
): RedirectNoticeModel {
|
|
16
|
+
return {
|
|
17
|
+
targetHref: localHref(target),
|
|
18
|
+
message: message?.trim() || 'Continuing…',
|
|
19
|
+
delaySeconds: REDIRECT_DELAY_SECONDS,
|
|
20
|
+
}
|
|
21
|
+
}
|