@meith/web 0.36.2 → 0.37.1
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/app/fixtures/page.tsx +30 -0
- package/app/layout.tsx +0 -2
- package/next.config.mjs +1 -1
- package/package.json +51 -51
- package/src/server/auth-config.ts +2 -13
- package/src/server/board-latest.tsx +5 -3
- package/src/server/container.ts +16 -38
- package/src/server/federation-actions.ts +0 -3
- package/src/server/fixture-activity-repo.ts +127 -0
- package/src/server/fixture-content.json +3045 -0
- package/src/server/navigation.ts +6 -2
- package/src/server/plugin-routes.ts +1 -2
- package/src/server/request-fingerprint.ts +2 -15
- package/src/server/seed-board.ts +180 -29
- package/src/server/stats.ts +5 -4
- package/src/server/syndication.ts +1 -3
- package/src/server/two-factor-actions.ts +0 -3
- package/src/server/upgrade-notice.ts +1 -1
- package/src/server/user-admin-actions.ts +0 -4
- package/src/server/usercp-actions.ts +0 -5
- package/src/theme/contract.fixture.ts +6 -6
- package/src/theme/gallery.fixture.tsx +412 -0
- package/src/theme/preview-boundary.tsx +23 -0
- package/src/theme/preview.fixture.ts +304 -0
- package/app/demo/stripe/[[...path]]/route.ts +0 -27
- package/src/components/shell/demo-banner.tsx +0 -41
- package/src/server/demo-stripe.ts +0 -104
- package/src/server/demo-uploads.ts +0 -17
- package/src/server/demo.ts +0 -59
|
@@ -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.
|
|
3
|
+
"version": "0.37.1",
|
|
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": {
|
|
@@ -37,61 +37,61 @@
|
|
|
37
37
|
"@tailwindcss/postcss": "^4.3.3",
|
|
38
38
|
"@types/react": "^19",
|
|
39
39
|
"@types/react-dom": "^19",
|
|
40
|
-
"next": "16.3.
|
|
40
|
+
"next": "16.3.4",
|
|
41
41
|
"postcss": "^8.5",
|
|
42
42
|
"react": "19.2.8",
|
|
43
43
|
"react-dom": "19.2.8",
|
|
44
44
|
"tailwindcss": "^4.3.3",
|
|
45
45
|
"typescript": "7.0.2",
|
|
46
|
-
"@meith/accounts": "0.
|
|
47
|
-
"@meith/admin": "0.
|
|
48
|
-
"@meith/antispam": "0.
|
|
49
|
-
"@meith/api": "0.
|
|
50
|
-
"@meith/attachments": "0.
|
|
51
|
-
"@meith/authorization": "0.
|
|
52
|
-
"@meith/avatars": "0.
|
|
53
|
-
"@meith/backup": "0.
|
|
54
|
-
"@meith/board-digest": "0.
|
|
55
|
-
"@meith/core": "0.
|
|
56
|
-
"@meith/db": "0.
|
|
57
|
-
"@meith/
|
|
58
|
-
"@meith/
|
|
59
|
-
"@meith/
|
|
60
|
-
"@meith/
|
|
61
|
-
"@meith/
|
|
62
|
-
"@meith/
|
|
63
|
-
"@meith/
|
|
64
|
-
"@meith/
|
|
65
|
-
"@meith/
|
|
66
|
-
"@meith/
|
|
67
|
-
"@meith/
|
|
68
|
-
"@meith/
|
|
69
|
-
"@meith/
|
|
70
|
-
"@meith/
|
|
71
|
-
"@meith/
|
|
72
|
-
"@meith/plugin-calendar": "0.
|
|
73
|
-
"@meith/plugin-dues": "0.
|
|
74
|
-
"@meith/plugin-kit": "0.
|
|
75
|
-
"@meith/polls": "0.
|
|
76
|
-
"@meith/posts": "0.
|
|
77
|
-
"@meith/profile-fields": "0.
|
|
78
|
-
"@meith/relations": "0.
|
|
79
|
-
"@meith/reputation": "0.
|
|
80
|
-
"@meith/runtime": "0.
|
|
81
|
-
"@meith/search": "0.
|
|
82
|
-
"@meith/settings": "0.
|
|
83
|
-
"@meith/signatures": "0.
|
|
84
|
-
"@meith/subscriptions": "0.
|
|
85
|
-
"@meith/tasks": "0.
|
|
86
|
-
"@meith/theme-clubhouse": "0.
|
|
87
|
-
"@meith/theme-default": "0.
|
|
88
|
-
"@meith/theme-kit": "0.
|
|
89
|
-
"@meith/theme-midnight": "0.
|
|
90
|
-
"@meith/theme-phasebook": "0.
|
|
91
|
-
"@meith/theme-raidframe": "0.
|
|
92
|
-
"@meith/threads": "0.
|
|
93
|
-
"@meith/ui": "0.
|
|
94
|
-
"@meith/upgrade": "0.
|
|
46
|
+
"@meith/accounts": "0.37.1",
|
|
47
|
+
"@meith/admin": "0.37.1",
|
|
48
|
+
"@meith/antispam": "0.37.1",
|
|
49
|
+
"@meith/api": "0.37.1",
|
|
50
|
+
"@meith/attachments": "0.37.1",
|
|
51
|
+
"@meith/authorization": "0.37.1",
|
|
52
|
+
"@meith/avatars": "0.37.1",
|
|
53
|
+
"@meith/backup": "0.37.1",
|
|
54
|
+
"@meith/board-digest": "0.37.1",
|
|
55
|
+
"@meith/core": "0.37.1",
|
|
56
|
+
"@meith/db": "0.37.1",
|
|
57
|
+
"@meith/drafts": "0.37.1",
|
|
58
|
+
"@meith/drivers": "0.37.1",
|
|
59
|
+
"@meith/events": "0.37.1",
|
|
60
|
+
"@meith/forums": "0.37.1",
|
|
61
|
+
"@meith/groups": "0.37.1",
|
|
62
|
+
"@meith/i18n": "0.37.1",
|
|
63
|
+
"@meith/import": "0.37.1",
|
|
64
|
+
"@meith/install": "0.37.1",
|
|
65
|
+
"@meith/mail": "0.37.1",
|
|
66
|
+
"@meith/markdown": "0.37.1",
|
|
67
|
+
"@meith/marketplace": "0.37.1",
|
|
68
|
+
"@meith/messages": "0.37.1",
|
|
69
|
+
"@meith/moderation": "0.37.1",
|
|
70
|
+
"@meith/notifications": "0.37.1",
|
|
71
|
+
"@meith/plugin-awards": "0.37.1",
|
|
72
|
+
"@meith/plugin-calendar": "0.37.1",
|
|
73
|
+
"@meith/plugin-dues": "0.37.1",
|
|
74
|
+
"@meith/plugin-kit": "0.37.1",
|
|
75
|
+
"@meith/polls": "0.37.1",
|
|
76
|
+
"@meith/posts": "0.37.1",
|
|
77
|
+
"@meith/profile-fields": "0.37.1",
|
|
78
|
+
"@meith/relations": "0.37.1",
|
|
79
|
+
"@meith/reputation": "0.37.1",
|
|
80
|
+
"@meith/runtime": "0.37.1",
|
|
81
|
+
"@meith/search": "0.37.1",
|
|
82
|
+
"@meith/settings": "0.37.1",
|
|
83
|
+
"@meith/signatures": "0.37.1",
|
|
84
|
+
"@meith/subscriptions": "0.37.1",
|
|
85
|
+
"@meith/tasks": "0.37.1",
|
|
86
|
+
"@meith/theme-clubhouse": "0.37.1",
|
|
87
|
+
"@meith/theme-default": "0.37.1",
|
|
88
|
+
"@meith/theme-kit": "0.37.1",
|
|
89
|
+
"@meith/theme-midnight": "0.37.1",
|
|
90
|
+
"@meith/theme-phasebook": "0.37.1",
|
|
91
|
+
"@meith/theme-raidframe": "0.37.1",
|
|
92
|
+
"@meith/threads": "0.37.1",
|
|
93
|
+
"@meith/ui": "0.37.1",
|
|
94
|
+
"@meith/upgrade": "0.37.1"
|
|
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
|
|
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 |
|
|
25
|
-
return getContainer().dataSource === 'postgres'
|
|
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()
|
package/src/server/container.ts
CHANGED
|
@@ -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:
|
|
300
|
-
|
|
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
|
+
}
|