@meith/web 0.36.2 → 0.37.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.
@@ -0,0 +1,30 @@
1
+ import type { Metadata } from 'next'
2
+ import { notFound } from 'next/navigation'
3
+
4
+ import { env } from '@meith/core'
5
+ import { isSlotName } from '@meith/theme-kit'
6
+
7
+ import { getTranslator } from '@/server/i18n'
8
+ import { FixtureGallery } from '@/theme/gallery.fixture'
9
+ import { fixtureVariants } from '@/theme/preview.fixture'
10
+
11
+ export async function generateMetadata(): Promise<Metadata> {
12
+ return {
13
+ title: (await getTranslator()).t('nav.themeFixtures'),
14
+ robots: { index: false, follow: false },
15
+ }
16
+ }
17
+
18
+ export default async function FixturesPage({
19
+ searchParams,
20
+ }: {
21
+ searchParams: Promise<Record<string, string | string[] | undefined>>
22
+ }) {
23
+ if (env.DATA_SOURCE !== 'fixture') notFound()
24
+ const query = await searchParams
25
+ const name = query.slot ?? 'BoardIndex'
26
+ if (typeof name !== 'string' || !isSlotName(name)) notFound()
27
+ const variant = query.variant ?? 'default'
28
+ if (typeof variant !== 'string' || !fixtureVariants(name).includes(variant)) notFound()
29
+ return <FixtureGallery name={name} variant={variant} />
30
+ }
package/app/layout.tsx CHANGED
@@ -9,7 +9,6 @@ import { NavTabsEnhancer } from '@meith/ui/nav-tabs-enhancer'
9
9
 
10
10
  import { CopyProvider } from '@/components/shell/copy'
11
11
  import { CrashNoticeProvider } from '@/components/shell/crash-notice'
12
- import { DemoBanner } from '@/components/shell/demo-banner'
13
12
  import { GroupNameStyle } from '@/components/shell/group-name-style'
14
13
  import { ServiceWorkerRegistrar } from '@/components/shell/service-worker'
15
14
  import { ThemeRuntimeStyle } from '@/components/shell/theme-runtime-style'
@@ -110,7 +109,6 @@ export default async function RootLayout({
110
109
  </head>
111
110
  <body className="font-sans antialiased">
112
111
  <NavTabsEnhancer />
113
- <DemoBanner />
114
112
  <CrashNoticeProvider
115
113
  notice={notice}
116
114
  requestId={currentRequestId() ?? null}
package/next.config.mjs CHANGED
@@ -63,7 +63,6 @@ const nextConfig = {
63
63
  '@meith/board-digest',
64
64
  '@meith/core',
65
65
  '@meith/db',
66
- '@meith/demo',
67
66
  '@meith/drafts',
68
67
  '@meith/drivers',
69
68
  '@meith/events',
@@ -78,6 +77,7 @@ const nextConfig = {
78
77
  '@meith/messages',
79
78
  '@meith/moderation',
80
79
  '@meith/notifications',
80
+ '@meith/plugin-awards',
81
81
  '@meith/plugin-calendar',
82
82
  '@meith/plugin-dues',
83
83
  '@meith/plugin-kit',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/web",
3
- "version": "0.36.2",
3
+ "version": "0.37.0",
4
4
  "description": "The board itself: the Next.js app, and the forum-web bin that materializes it into an external board workspace.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -43,55 +43,55 @@
43
43
  "react-dom": "19.2.8",
44
44
  "tailwindcss": "^4.3.3",
45
45
  "typescript": "7.0.2",
46
- "@meith/accounts": "0.36.2",
47
- "@meith/admin": "0.36.2",
48
- "@meith/antispam": "0.36.2",
49
- "@meith/api": "0.36.2",
50
- "@meith/attachments": "0.36.2",
51
- "@meith/authorization": "0.36.2",
52
- "@meith/avatars": "0.36.2",
53
- "@meith/backup": "0.36.2",
54
- "@meith/board-digest": "0.36.2",
55
- "@meith/core": "0.36.2",
56
- "@meith/db": "0.36.2",
57
- "@meith/demo": "0.36.2",
58
- "@meith/drafts": "0.36.2",
59
- "@meith/drivers": "0.36.2",
60
- "@meith/events": "0.36.2",
61
- "@meith/forums": "0.36.2",
62
- "@meith/groups": "0.36.2",
63
- "@meith/i18n": "0.36.2",
64
- "@meith/import": "0.36.2",
65
- "@meith/install": "0.36.2",
66
- "@meith/mail": "0.36.2",
67
- "@meith/markdown": "0.36.2",
68
- "@meith/marketplace": "0.36.2",
69
- "@meith/messages": "0.36.2",
70
- "@meith/moderation": "0.36.2",
71
- "@meith/notifications": "0.36.2",
72
- "@meith/plugin-calendar": "0.36.2",
73
- "@meith/plugin-dues": "0.36.2",
74
- "@meith/plugin-kit": "0.36.2",
75
- "@meith/polls": "0.36.2",
76
- "@meith/posts": "0.36.2",
77
- "@meith/profile-fields": "0.36.2",
78
- "@meith/relations": "0.36.2",
79
- "@meith/reputation": "0.36.2",
80
- "@meith/runtime": "0.36.2",
81
- "@meith/search": "0.36.2",
82
- "@meith/settings": "0.36.2",
83
- "@meith/signatures": "0.36.2",
84
- "@meith/subscriptions": "0.36.2",
85
- "@meith/tasks": "0.36.2",
86
- "@meith/theme-clubhouse": "0.36.2",
87
- "@meith/theme-default": "0.36.2",
88
- "@meith/theme-kit": "0.36.2",
89
- "@meith/theme-midnight": "0.36.2",
90
- "@meith/theme-phasebook": "0.36.2",
91
- "@meith/theme-raidframe": "0.36.2",
92
- "@meith/threads": "0.36.2",
93
- "@meith/ui": "0.36.2",
94
- "@meith/upgrade": "0.36.2"
46
+ "@meith/accounts": "0.37.0",
47
+ "@meith/admin": "0.37.0",
48
+ "@meith/antispam": "0.37.0",
49
+ "@meith/api": "0.37.0",
50
+ "@meith/attachments": "0.37.0",
51
+ "@meith/authorization": "0.37.0",
52
+ "@meith/avatars": "0.37.0",
53
+ "@meith/backup": "0.37.0",
54
+ "@meith/board-digest": "0.37.0",
55
+ "@meith/core": "0.37.0",
56
+ "@meith/db": "0.37.0",
57
+ "@meith/drafts": "0.37.0",
58
+ "@meith/drivers": "0.37.0",
59
+ "@meith/events": "0.37.0",
60
+ "@meith/forums": "0.37.0",
61
+ "@meith/i18n": "0.37.0",
62
+ "@meith/groups": "0.37.0",
63
+ "@meith/import": "0.37.0",
64
+ "@meith/install": "0.37.0",
65
+ "@meith/mail": "0.37.0",
66
+ "@meith/markdown": "0.37.0",
67
+ "@meith/marketplace": "0.37.0",
68
+ "@meith/messages": "0.37.0",
69
+ "@meith/moderation": "0.37.0",
70
+ "@meith/notifications": "0.37.0",
71
+ "@meith/plugin-awards": "0.37.0",
72
+ "@meith/plugin-calendar": "0.37.0",
73
+ "@meith/plugin-dues": "0.37.0",
74
+ "@meith/plugin-kit": "0.37.0",
75
+ "@meith/polls": "0.37.0",
76
+ "@meith/posts": "0.37.0",
77
+ "@meith/profile-fields": "0.37.0",
78
+ "@meith/relations": "0.37.0",
79
+ "@meith/reputation": "0.37.0",
80
+ "@meith/runtime": "0.37.0",
81
+ "@meith/search": "0.37.0",
82
+ "@meith/settings": "0.37.0",
83
+ "@meith/signatures": "0.37.0",
84
+ "@meith/subscriptions": "0.37.0",
85
+ "@meith/theme-clubhouse": "0.37.0",
86
+ "@meith/tasks": "0.37.0",
87
+ "@meith/theme-default": "0.37.0",
88
+ "@meith/theme-kit": "0.37.0",
89
+ "@meith/theme-midnight": "0.37.0",
90
+ "@meith/theme-phasebook": "0.37.0",
91
+ "@meith/theme-raidframe": "0.37.0",
92
+ "@meith/threads": "0.37.0",
93
+ "@meith/ui": "0.37.0",
94
+ "@meith/upgrade": "0.37.0"
95
95
  },
96
96
  "scripts": {
97
97
  "dev": "next dev",
@@ -25,10 +25,10 @@ export async function boardAuthConfig(): Promise<AuthConfig> {
25
25
  if (env.DATA_SOURCE !== 'postgres') return AUTH_CONFIG
26
26
 
27
27
  const settings = await getSettings()
28
- return demoOverrides({
28
+ return {
29
29
  ...AUTH_CONFIG,
30
30
  ...resolveAuthPolicy((key) => settings.get(key as never), AUTH_CONFIG),
31
- })
31
+ }
32
32
  }
33
33
 
34
34
  export async function boardSessionConfig(): Promise<BoardSessionConfig> {
@@ -37,14 +37,3 @@ export async function boardSessionConfig(): Promise<BoardSessionConfig> {
37
37
  sessionLifetimeDays: (await boardAuthConfig()).sessionLifetimeDays,
38
38
  }
39
39
  }
40
-
41
- function demoOverrides(config: AuthConfig): AuthConfig {
42
- if (!env.DEMO_MODE) return config
43
-
44
- return {
45
- ...config,
46
- maxLoginAttempts: 50,
47
- maxAccountLoginAttempts: 500,
48
- lockoutMinutes: 1,
49
- }
50
- }
@@ -12,6 +12,7 @@ import { distinctUserIds } from '@/view/member-identity'
12
12
  import { getContainer } from './container'
13
13
  import { activeWordFilter } from './content-admin'
14
14
  import { getActor } from './context'
15
+ import { FixtureActivityRepository } from './fixture-activity-repo'
15
16
  import { identitiesFor } from './group-identity'
16
17
  import { filterView, viewerRef } from './plugin-view'
17
18
  import { currentTheme } from './theme'
@@ -21,8 +22,10 @@ export const LATEST_ROWS = 5
21
22
 
22
23
  export const LATEST_REFRESH_SECONDS = 60
23
24
 
24
- export function latestRepository(): PostgresLatestRepository | null {
25
- return getContainer().dataSource === 'postgres' ? new PostgresLatestRepository(getDb()) : null
25
+ export function latestRepository(): PostgresLatestRepository | FixtureActivityRepository {
26
+ return getContainer().dataSource === 'postgres'
27
+ ? new PostgresLatestRepository(getDb())
28
+ : new FixtureActivityRepository()
26
29
  }
27
30
 
28
31
  export async function latestScopeFor(actor: Actor): Promise<LatestScope> {
@@ -37,7 +40,6 @@ export async function latestScopeFor(actor: Actor): Promise<LatestScope> {
37
40
 
38
41
  export async function renderLatestPanels(): Promise<React.ReactNode> {
39
42
  const repo = latestRepository()
40
- if (repo === null) return null
41
43
 
42
44
  const actor = await getActor()
43
45
  const now = new Date()
@@ -63,7 +63,6 @@ import {
63
63
  PostgresThreadWriteRepository,
64
64
  PostgresWarningRepository,
65
65
  } from '@meith/db'
66
- import { demoResetTask } from '@meith/demo'
67
66
  import type { DraftRepository } from '@meith/drafts'
68
67
  import { drivers } from '@meith/drivers'
69
68
  import { imageProcessor } from '@meith/drivers/images'
@@ -103,7 +102,6 @@ import {
103
102
  REMEMBER_DAYS,
104
103
  SESSION_LIFETIME_DAYS,
105
104
  } from './auth-config'
106
- import { clearUploadedFiles } from './demo-uploads'
107
105
  import { FixtureActorSource } from './fixture-actor-source'
108
106
  import { FixtureForumRepository } from './fixture-forum-repo'
109
107
  import { FixtureMemberProfileRepository } from './fixture-member-profile-repo'
@@ -296,47 +294,27 @@ function buildPostgres(onBypass: (e: BypassEvent) => void): Container {
296
294
  fixtureDataVersion: null,
297
295
  accountStore: store,
298
296
  ...identityServices(store, new PostgresBanFilterRepository(db), new PostgresBanRepository(db)),
299
- scheduler: withDemoReset(
300
- buildSchedulerBundle({
301
- queue: drivers().queue,
302
- db,
303
- mail: drivers().mail,
304
- themeKey: forumConfig.defaultTheme,
305
- themeTokens: Object.fromEntries(
306
- Object.values(forumConfig.themes).map((theme) => [theme.key, theme.tokens]),
307
- ),
308
- themeVersions: Object.values(forumConfig.themes).map((theme) => ({
309
- key: theme.key,
310
- version: theme.theme?.version ?? null,
311
- })),
312
- files: drivers().files,
313
- images: imageProcessor,
314
- plugins: activeDefinitions(),
315
- translatorForLocale,
316
- }),
297
+ scheduler: buildSchedulerBundle({
298
+ queue: drivers().queue,
317
299
  db,
318
- ),
300
+ mail: drivers().mail,
301
+ themeKey: forumConfig.defaultTheme,
302
+ themeTokens: Object.fromEntries(
303
+ Object.values(forumConfig.themes).map((theme) => [theme.key, theme.tokens]),
304
+ ),
305
+ themeVersions: Object.values(forumConfig.themes).map((theme) => ({
306
+ key: theme.key,
307
+ version: theme.theme?.version ?? null,
308
+ })),
309
+ files: drivers().files,
310
+ images: imageProcessor,
311
+ plugins: activeDefinitions(),
312
+ translatorForLocale,
313
+ }),
319
314
  dataSource: 'postgres',
320
315
  }
321
316
  }
322
317
 
323
- function withDemoReset(bundle: SchedulerBundle, db: ReturnType<typeof getDb>): SchedulerBundle {
324
- if (!env.DEMO_MODE) return bundle
325
-
326
- return {
327
- ...bundle,
328
- tasks: [
329
- ...bundle.tasks,
330
- demoResetTask({
331
- db,
332
- cache: drivers().cache,
333
- clearUploads: () => clearUploadedFiles(),
334
- plugins: activeDefinitions(),
335
- }),
336
- ],
337
- }
338
- }
339
-
340
318
  export function getContainer(): Container {
341
319
  const g = globalThis as GlobalWithContainer
342
320
  const cached = g[GLOBAL_KEY] as Partial<Container> | undefined
@@ -10,7 +10,6 @@ import type { FormState } from './auth-form-state'
10
10
  import { getContainer } from './container'
11
11
  import { getActor } from './context'
12
12
  import { requireFreshCredentialProof } from './credential-proof'
13
- import { assertDemoAccountChangeable } from './demo'
14
13
  import {
15
14
  federationService,
16
15
  memberManagedSignIns,
@@ -53,8 +52,6 @@ async function requireOwnAccount(): Promise<{
53
52
  throw new ForbiddenError(msg('error.app.board-manages-sign-ins-for-its'))
54
53
  }
55
54
 
56
- await assertDemoAccountChangeable(actor.userId, 'password')
57
-
58
55
  const account = await getContainer().accountStore.accounts.findById(actor.userId)
59
56
  if (account === null) throw new ForbiddenError(msg('error.app.account-longer-exists'))
60
57
 
@@ -0,0 +1,127 @@
1
+ import 'server-only'
2
+
3
+ import { audienceFilterIn, authorFilterAdmits } from '@meith/core'
4
+ import type {
5
+ BoardTotals,
6
+ LatestPostRow,
7
+ LatestScope,
8
+ LatestThreadRow,
9
+ TopPoster,
10
+ TopThread,
11
+ } from '@meith/db'
12
+
13
+ import {
14
+ SEED_FORUM_ROWS,
15
+ SEED_MEMBER_PROFILES,
16
+ SEED_POST_ROWS,
17
+ SEED_THREAD_ROWS,
18
+ } from './seed-board'
19
+
20
+ const forums = new Map(SEED_FORUM_ROWS.map((row) => [row.id, row]))
21
+ const threads = new Map(SEED_THREAD_ROWS.map((row) => [row.id, row]))
22
+ const firstPosts = new Map(
23
+ SEED_POST_ROWS.filter((post) => post.isFirstPost).map((post) => [post.threadId, post]),
24
+ )
25
+
26
+ function visibleThreads(scope: LatestScope) {
27
+ return SEED_THREAD_ROWS.filter(
28
+ (thread) =>
29
+ scope.content.states.includes(thread.visibility) &&
30
+ authorFilterAdmits(audienceFilterIn(scope, thread.forumId), thread.authorUserId),
31
+ )
32
+ }
33
+
34
+ export class FixtureActivityRepository {
35
+ async threads(limit: number, scope: LatestScope): Promise<readonly LatestThreadRow[]> {
36
+ return visibleThreads(scope)
37
+ .map((thread) => ({
38
+ threadId: thread.id,
39
+ title: thread.title,
40
+ slug: thread.slug,
41
+ forumId: thread.forumId,
42
+ forumTitle: forums.get(thread.forumId)!.title,
43
+ forumSlug: forums.get(thread.forumId)!.slug,
44
+ authorUserId: thread.authorUserId,
45
+ authorUsername: thread.authorUsername,
46
+ replyCount: thread.replyCount,
47
+ createdAt: firstPosts.get(thread.id)!.createdAt,
48
+ }))
49
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || b.threadId - a.threadId)
50
+ .slice(0, limit)
51
+ }
52
+
53
+ async posts(limit: number, scope: LatestScope): Promise<readonly LatestPostRow[]> {
54
+ const visible = new Set(visibleThreads(scope).map((thread) => thread.id))
55
+ return SEED_POST_ROWS.filter(
56
+ (post) => visible.has(post.threadId) && scope.content.states.includes(post.visibility),
57
+ )
58
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || b.id - a.id)
59
+ .slice(0, limit)
60
+ .map((post) => ({
61
+ postId: post.id,
62
+ threadId: post.threadId,
63
+ threadTitle: threads.get(post.threadId)!.title,
64
+ threadSlug: threads.get(post.threadId)!.slug,
65
+ forumId: post.forumId,
66
+ forumTitle: forums.get(post.forumId)!.title,
67
+ forumSlug: forums.get(post.forumId)!.slug,
68
+ authorUserId: post.authorUserId,
69
+ authorUsername: post.authorUsername,
70
+ createdAt: post.createdAt,
71
+ messageSource: post.message.slice(0, 300),
72
+ }))
73
+ }
74
+
75
+ async readTotals(): Promise<BoardTotals> {
76
+ const newest = [...SEED_MEMBER_PROFILES].sort(
77
+ (a, b) => b.createdAt.getTime() - a.createdAt.getTime() || b.id - a.id,
78
+ )[0]
79
+ return {
80
+ threadCount: SEED_THREAD_ROWS.length,
81
+ postCount: SEED_POST_ROWS.length,
82
+ memberCount: SEED_MEMBER_PROFILES.length,
83
+ newestUserId: newest?.id ?? null,
84
+ newestUsername: newest?.username ?? null,
85
+ computedAt: newest?.lastActiveAt ?? null,
86
+ }
87
+ }
88
+
89
+ async topPosters(limit: number): Promise<readonly TopPoster[]> {
90
+ return [...SEED_MEMBER_PROFILES]
91
+ .filter((member) => member.postCount > 0)
92
+ .sort((a, b) => b.postCount - a.postCount || a.id - b.id)
93
+ .slice(0, limit)
94
+ .map((member) => ({
95
+ userId: member.id,
96
+ username: member.username,
97
+ postCount: member.postCount,
98
+ }))
99
+ }
100
+
101
+ async mostViewed(limit: number, scope: LatestScope): Promise<readonly TopThread[]> {
102
+ return this.topThreads(limit, scope, 'viewCount')
103
+ }
104
+
105
+ async mostReplied(limit: number, scope: LatestScope): Promise<readonly TopThread[]> {
106
+ return this.topThreads(limit, scope, 'replyCount')
107
+ }
108
+
109
+ private topThreads(
110
+ limit: number,
111
+ scope: LatestScope,
112
+ key: 'viewCount' | 'replyCount',
113
+ ): readonly TopThread[] {
114
+ return visibleThreads(scope)
115
+ .sort((a, b) => b[key] - a[key] || a.id - b.id)
116
+ .slice(0, limit)
117
+ .map((thread) => ({
118
+ threadId: thread.id,
119
+ title: thread.title,
120
+ slug: thread.slug,
121
+ forumId: thread.forumId,
122
+ forumTitle: forums.get(thread.forumId)!.title,
123
+ viewCount: thread.viewCount,
124
+ replyCount: thread.replyCount,
125
+ }))
126
+ }
127
+ }