@meith/web 0.17.0 → 0.17.2
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/admin/api/marketplace/screenshot/route.ts +6 -3
- package/app/admin/forums/[id]/page.tsx +6 -3
- package/app/admin/settings/page.tsx +2 -2
- package/app/admin/system/page.tsx +1 -2
- package/app/admin/themes/page.tsx +6 -3
- package/bin/forum-web.mjs +104 -51
- package/bin/forum-web.test.ts +37 -0
- package/next.config.mjs +24 -63
- package/package.json +48 -47
- package/public/placeholder-logo.png +0 -0
- package/public/placeholder-logo.svg +1 -0
- package/public/placeholder-user.jpg +0 -0
- package/public/placeholder.jpg +0 -0
- package/public/placeholder.svg +1 -0
- package/public/sw.js +81 -0
- package/src/components/admin/theme-forms.tsx +1 -1
- package/src/components/admin/theme-palette.tsx +1 -1
- package/src/components/admin/user-bulk-toolbar.tsx +5 -3
- package/src/server/admin-settings-actions.ts +8 -8
- package/src/server/upgrade-notice.ts +1 -1
- package/src/server/user-admin-actions.ts +23 -21
- package/src/server/user-admin.ts +8 -7
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { NextRequest } from 'next/server'
|
|
2
2
|
|
|
3
|
+
import { readCappedBody } from '@meith/marketplace'
|
|
4
|
+
|
|
3
5
|
import { requireAdmin } from '@/server/admin'
|
|
4
6
|
import { marketplaceScreenshotUrl } from '@/server/marketplace-admin'
|
|
5
7
|
|
|
@@ -46,13 +48,14 @@ export async function GET(request: NextRequest): Promise<Response> {
|
|
|
46
48
|
const upstream = await fetch(url, {
|
|
47
49
|
signal: controller.signal,
|
|
48
50
|
headers: { accept: 'image/png' },
|
|
51
|
+
redirect: 'manual',
|
|
49
52
|
})
|
|
50
53
|
if (!upstream.ok) return fail(502, 'The catalog host did not answer with the screenshot.')
|
|
51
54
|
|
|
52
|
-
const bytes = await upstream
|
|
53
|
-
if (bytes
|
|
55
|
+
const bytes = await readCappedBody(upstream, MAX_BYTES)
|
|
56
|
+
if (bytes === null) return fail(502, 'The screenshot was larger than expected.')
|
|
54
57
|
|
|
55
|
-
return new Response(bytes, {
|
|
58
|
+
return new Response(bytes.buffer as ArrayBuffer, {
|
|
56
59
|
headers: {
|
|
57
60
|
'Content-Type': 'image/png',
|
|
58
61
|
'X-Content-Type-Options': 'nosniff',
|
|
@@ -30,9 +30,12 @@ export default async function AdminForumPage({ params }: { params: Promise<{ id:
|
|
|
30
30
|
const options = await repository?.readOptions(forum.id)
|
|
31
31
|
if (options === null || options === undefined || repository === null) notFound()
|
|
32
32
|
|
|
33
|
-
const t = await
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const [t, moderators, groupRows] = await Promise.all([
|
|
34
|
+
getTranslator(),
|
|
35
|
+
repository.listModerators(forum.id),
|
|
36
|
+
repository.listGroups(),
|
|
37
|
+
])
|
|
38
|
+
const groups = groupRows.map((group) => ({
|
|
36
39
|
groupId: group.id,
|
|
37
40
|
title: group.title,
|
|
38
41
|
}))
|
|
@@ -31,9 +31,9 @@ export default async function AdminSettingsPage({
|
|
|
31
31
|
if (query.group === undefined && query.q === undefined) {
|
|
32
32
|
redirect(settingsHref({ group: DEFAULT_SETTING_GROUP, advanced: query.advanced === '1' }))
|
|
33
33
|
}
|
|
34
|
-
const t = await getTranslator()
|
|
34
|
+
const [t, snapshot] = await Promise.all([getTranslator(), getSettings()])
|
|
35
35
|
const model = buildAdminSettingsModel({
|
|
36
|
-
snapshot
|
|
36
|
+
snapshot,
|
|
37
37
|
query: query.q,
|
|
38
38
|
group: query.group,
|
|
39
39
|
advanced: query.advanced === '1',
|
|
@@ -36,9 +36,8 @@ export default async function AdminSystemPage() {
|
|
|
36
36
|
if ((await adminPageContext()) === null) return null
|
|
37
37
|
|
|
38
38
|
const now = new Date()
|
|
39
|
-
const translator = await getTranslator()
|
|
39
|
+
const [translator, view] = await Promise.all([getTranslator(), buildSystemHealthView(now)])
|
|
40
40
|
const copy = systemFormsCopy(translator)
|
|
41
|
-
const view = await buildSystemHealthView(now)
|
|
42
41
|
|
|
43
42
|
if (view === null) {
|
|
44
43
|
return (
|
|
@@ -24,10 +24,13 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
24
24
|
export default async function AdminThemesPage() {
|
|
25
25
|
if ((await adminPageContext()) === null) return null
|
|
26
26
|
|
|
27
|
-
const themes = await
|
|
28
|
-
|
|
27
|
+
const [themes, lightKey, darkKey, translator] = await Promise.all([
|
|
28
|
+
themeListing(),
|
|
29
|
+
logoKey('light'),
|
|
30
|
+
logoKey('dark'),
|
|
31
|
+
getTranslator(),
|
|
32
|
+
])
|
|
29
33
|
const now = new Date()
|
|
30
|
-
const translator = await getTranslator()
|
|
31
34
|
const brandingCopy = brandingFormsCopy(translator)
|
|
32
35
|
const stateCopy = themeStateCopy(translator)
|
|
33
36
|
|
package/bin/forum-web.mjs
CHANGED
|
@@ -47,7 +47,15 @@
|
|
|
47
47
|
* through real `dependencies` entries a hoisted `node_modules` would need.
|
|
48
48
|
*/
|
|
49
49
|
import { spawn } from 'node:child_process'
|
|
50
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
cpSync,
|
|
52
|
+
existsSync,
|
|
53
|
+
mkdirSync,
|
|
54
|
+
readFileSync,
|
|
55
|
+
realpathSync,
|
|
56
|
+
rmSync,
|
|
57
|
+
writeFileSync,
|
|
58
|
+
} from 'node:fs'
|
|
51
59
|
import { createRequire } from 'node:module'
|
|
52
60
|
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
53
61
|
import { fileURLToPath } from 'node:url'
|
|
@@ -57,24 +65,20 @@ const packageRoot = join(here, '..')
|
|
|
57
65
|
const workspaceRoot = process.cwd()
|
|
58
66
|
const appDir = join(workspaceRoot, '.meith', 'app')
|
|
59
67
|
|
|
60
|
-
// `next dev|build` runs with `.meith/app` as its own cwd (see `run()` below),
|
|
61
|
-
// so a relative FORUM_WORKSPACE_ROOT / FORUM_ALIASES_FROM — the shape
|
|
62
|
-
// boards/stock/package.json's scripts write, relative to *this* process's
|
|
63
|
-
// own cwd (the board's own directory) — would resolve against the wrong
|
|
64
|
-
// directory if left for next.config.mjs to resolve itself. Rewriting them to
|
|
65
|
-
// absolute paths here, before spawning, means both env vars mean the same
|
|
66
|
-
// thing regardless of which process reads them.
|
|
67
68
|
for (const name of ['FORUM_WORKSPACE_ROOT', 'FORUM_ALIASES_FROM']) {
|
|
68
69
|
if (process.env[name]) process.env[name] = resolve(workspaceRoot, process.env[name])
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Files this package ships (see its `files` allowlist) that belong inside the
|
|
74
|
+
* materialized app. Board files (community.config.ts, board.plugins.json,
|
|
75
|
+
* community.plugins.ts) are never copied — they are read in place, from the
|
|
76
|
+
* workspace, through the generated tsconfig instead.
|
|
77
|
+
*/
|
|
75
78
|
const APP_ENTRIES = [
|
|
76
79
|
'app',
|
|
77
80
|
'src',
|
|
81
|
+
'public',
|
|
78
82
|
'next.config.mjs',
|
|
79
83
|
'postcss.config.mjs',
|
|
80
84
|
'components.json',
|
|
@@ -92,6 +96,24 @@ function toPosixRelative(from, to) {
|
|
|
92
96
|
return rel.startsWith('.') ? rel : `./${rel}`
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
export function rebaseGlobalsCssSources(css, cssDir, workspaceRootOverride) {
|
|
100
|
+
return css.replace(/@source "((?:\.\.\/)+)([^"]+)";/g, (_match, _dots, tail) => {
|
|
101
|
+
const target = join(workspaceRootOverride, ...tail.split('/'))
|
|
102
|
+
return `@source "${toPosixRelative(cssDir, target)}";`
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function rewriteGlobalsCssSourcePaths() {
|
|
107
|
+
const workspaceRootOverride = process.env.FORUM_WORKSPACE_ROOT
|
|
108
|
+
if (!workspaceRootOverride) return
|
|
109
|
+
|
|
110
|
+
const cssPath = join(appDir, 'src', 'styles', 'globals.css')
|
|
111
|
+
if (!existsSync(cssPath)) return
|
|
112
|
+
|
|
113
|
+
const css = readFileSync(cssPath, 'utf8')
|
|
114
|
+
writeFileSync(cssPath, rebaseGlobalsCssSources(css, dirname(cssPath), workspaceRootOverride))
|
|
115
|
+
}
|
|
116
|
+
|
|
95
117
|
/**
|
|
96
118
|
* A board outside this monorepo has a hoisted `node_modules` (see the module
|
|
97
119
|
* comment), so plain bare-specifier resolution reaches every `@meith/*`
|
|
@@ -110,7 +132,10 @@ function toPosixRelative(from, to) {
|
|
|
110
132
|
* materialized app's own generated tsconfig, rebased to be relative to
|
|
111
133
|
* `.meith/app` — the same aliases apps/community's own tsconfig.json
|
|
112
134
|
* hand-maintains for exactly this reason. Unset (the default, and the only
|
|
113
|
-
* path a real external board ever takes), this is a no-op.
|
|
135
|
+
* path a real external board ever takes), this is a no-op. `@board/config`
|
|
136
|
+
* and `@board/plugins` are excluded from the copy — they are this
|
|
137
|
+
* workspace's own seam, wired below to *this* board's files, never to
|
|
138
|
+
* whatever apps/community's own tsconfig happens to alias them to.
|
|
114
139
|
*/
|
|
115
140
|
function monorepoAliases() {
|
|
116
141
|
const configFile = process.env.FORUM_ALIASES_FROM
|
|
@@ -122,15 +147,20 @@ function monorepoAliases() {
|
|
|
122
147
|
|
|
123
148
|
const aliases = {}
|
|
124
149
|
for (const [alias, targets] of Object.entries(paths)) {
|
|
125
|
-
// `@board/config`/`@board/plugins` are this workspace's own seam, wired
|
|
126
|
-
// below to *this* board's files — never to whatever apps/community's own
|
|
127
|
-
// tsconfig happens to alias them to.
|
|
128
150
|
if (alias === '@board/config' || alias === '@board/plugins') continue
|
|
129
151
|
aliases[alias] = targets.map((target) => toPosixRelative(appDir, join(sourceDir, target)))
|
|
130
152
|
}
|
|
131
153
|
return aliases
|
|
132
154
|
}
|
|
133
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Replaces each shipped entry (`APP_ENTRIES`) but never `rm -rf`s the whole
|
|
158
|
+
* `.meith/app` directory: `.next` lives there too once a build has run, and
|
|
159
|
+
* `forum-web start` needs that build still on disk after this same function
|
|
160
|
+
* re-materializes the sources ahead of launching the standalone server. The
|
|
161
|
+
* `next-env.d.ts` it writes only needs to exist, not be complete — next
|
|
162
|
+
* regenerates it with the right content on first run.
|
|
163
|
+
*/
|
|
134
164
|
function materialize() {
|
|
135
165
|
const boardConfig = join(workspaceRoot, 'community.config.ts')
|
|
136
166
|
const boardPlugins = join(workspaceRoot, 'community.plugins.ts')
|
|
@@ -142,10 +172,6 @@ function materialize() {
|
|
|
142
172
|
)
|
|
143
173
|
}
|
|
144
174
|
|
|
145
|
-
// Replace each shipped entry, but never `rm -rf` the whole directory:
|
|
146
|
-
// `.next` lives here too once a build has run, and `forum-web start` needs
|
|
147
|
-
// that build still on disk after this same function re-materializes the
|
|
148
|
-
// sources ahead of launching the standalone server.
|
|
149
175
|
mkdirSync(appDir, { recursive: true })
|
|
150
176
|
|
|
151
177
|
for (const entry of APP_ENTRIES) {
|
|
@@ -156,6 +182,8 @@ function materialize() {
|
|
|
156
182
|
cpSync(source, target, { recursive: true })
|
|
157
183
|
}
|
|
158
184
|
|
|
185
|
+
rewriteGlobalsCssSourcePaths()
|
|
186
|
+
|
|
159
187
|
const tsconfig = {
|
|
160
188
|
compilerOptions: {
|
|
161
189
|
target: 'ES2022',
|
|
@@ -185,20 +213,20 @@ function materialize() {
|
|
|
185
213
|
|
|
186
214
|
writeFileSync(join(appDir, 'tsconfig.json'), `${JSON.stringify(tsconfig, null, 2)}\n`)
|
|
187
215
|
|
|
188
|
-
// next dev/build reads its own next-env.d.ts if present; an empty one is
|
|
189
|
-
// enough, and next regenerates it with the right content on first run.
|
|
190
216
|
writeFileSync(
|
|
191
217
|
join(appDir, 'next-env.d.ts'),
|
|
192
218
|
'/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n',
|
|
193
219
|
)
|
|
194
220
|
}
|
|
195
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Resolved from this package's own directory rather than the workspace root:
|
|
224
|
+
* `next` is `@meith/web`'s dependency, not necessarily the workspace
|
|
225
|
+
* manifest's, and resolving from here finds it either way — hoisted to the
|
|
226
|
+
* workspace root (npm, yarn classic) or nested under this package's own
|
|
227
|
+
* `node_modules` (pnpm's default, non-hoisted layout).
|
|
228
|
+
*/
|
|
196
229
|
function resolveNextBin() {
|
|
197
|
-
// Resolved from this package's own directory rather than the workspace
|
|
198
|
-
// root: `next` is @meith/web's dependency, not necessarily the workspace
|
|
199
|
-
// manifest's, and resolving from here finds it either way — hoisted to
|
|
200
|
-
// the workspace root (npm, yarn classic) or nested under this package's
|
|
201
|
-
// own node_modules (pnpm's default, non-hoisted layout).
|
|
202
230
|
const require = createRequire(join(packageRoot, 'package.json'))
|
|
203
231
|
try {
|
|
204
232
|
return require.resolve('next/dist/bin/next')
|
|
@@ -207,35 +235,60 @@ function resolveNextBin() {
|
|
|
207
235
|
}
|
|
208
236
|
}
|
|
209
237
|
|
|
210
|
-
function
|
|
238
|
+
function standaloneAppDir() {
|
|
239
|
+
return join(appDir, '.next', 'standalone', relative(workspaceRoot, appDir))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function stageStandaloneAssets() {
|
|
243
|
+
const targetAppDir = standaloneAppDir()
|
|
244
|
+
|
|
245
|
+
const staticTarget = join(targetAppDir, '.next', 'static')
|
|
246
|
+
rmSync(staticTarget, { recursive: true, force: true })
|
|
247
|
+
cpSync(join(appDir, '.next', 'static'), staticTarget, { recursive: true })
|
|
248
|
+
|
|
249
|
+
const publicSource = join(appDir, 'public')
|
|
250
|
+
const publicTarget = join(targetAppDir, 'public')
|
|
251
|
+
rmSync(publicTarget, { recursive: true, force: true })
|
|
252
|
+
if (existsSync(publicSource)) {
|
|
253
|
+
cpSync(publicSource, publicTarget, { recursive: true })
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function run(executable, args, cwd, onSuccess) {
|
|
211
258
|
const child = spawn(executable, args, { cwd, stdio: 'inherit' })
|
|
212
|
-
child.on('exit', (code, signal) =>
|
|
259
|
+
child.on('exit', (code, signal) => {
|
|
260
|
+
const exitCode = code ?? (signal ? 1 : 0)
|
|
261
|
+
if (exitCode === 0 && onSuccess) onSuccess()
|
|
262
|
+
process.exit(exitCode)
|
|
263
|
+
})
|
|
213
264
|
child.on('error', (error) => fail(error.message))
|
|
214
265
|
}
|
|
215
266
|
|
|
216
|
-
|
|
267
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
268
|
+
const [, , command, ...rest] = process.argv
|
|
217
269
|
|
|
218
|
-
if (!['dev', 'build', 'start'].includes(command ?? '')) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
270
|
+
if (!['dev', 'build', 'start'].includes(command ?? '')) {
|
|
271
|
+
console.error('Usage: forum-web <dev|build|start> [next arguments]')
|
|
272
|
+
process.exit(1)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
materialize()
|
|
222
276
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
277
|
+
if (command === 'start') {
|
|
278
|
+
const targetAppDir = standaloneAppDir()
|
|
279
|
+
const standaloneRoot = join(appDir, '.next', 'standalone')
|
|
280
|
+
const serverScript = join(targetAppDir, 'server.js')
|
|
281
|
+
if (!existsSync(serverScript)) {
|
|
282
|
+
fail(`no standalone build at ${serverScript} — run "forum-web build" first.`)
|
|
283
|
+
}
|
|
284
|
+
run(process.execPath, [serverScript, ...rest], standaloneRoot)
|
|
285
|
+
} else {
|
|
286
|
+
const nextBin = resolveNextBin()
|
|
287
|
+
run(
|
|
288
|
+
process.execPath,
|
|
289
|
+
[nextBin, command, ...rest],
|
|
290
|
+
appDir,
|
|
291
|
+
command === 'build' ? stageStandaloneAssets : undefined,
|
|
292
|
+
)
|
|
236
293
|
}
|
|
237
|
-
run(process.execPath, [serverScript, ...rest], standaloneRoot)
|
|
238
|
-
} else {
|
|
239
|
-
const nextBin = resolveNextBin()
|
|
240
|
-
run(process.execPath, [nextBin, command, ...rest], appDir)
|
|
241
294
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// @ts-expect-error forum-web.mjs ships untyped, imported directly for its pure export
|
|
4
|
+
import { rebaseGlobalsCssSources } from './forum-web.mjs'
|
|
5
|
+
|
|
6
|
+
describe('rebaseGlobalsCssSources', () => {
|
|
7
|
+
it('rebases every @source line against the real workspace root', () => {
|
|
8
|
+
const css = [
|
|
9
|
+
'@import "tailwindcss";',
|
|
10
|
+
'',
|
|
11
|
+
'@source "../../../../themes";',
|
|
12
|
+
'@source "../../../../plugins";',
|
|
13
|
+
'@source "../../../../examples";',
|
|
14
|
+
'@source "../../../../packages/ui/src";',
|
|
15
|
+
'',
|
|
16
|
+
'.foo { color: red; }',
|
|
17
|
+
].join('\n')
|
|
18
|
+
|
|
19
|
+
const cssDir = '/repo/boards/stock/.meith/app/src/styles'
|
|
20
|
+
const workspaceRoot = '/repo'
|
|
21
|
+
|
|
22
|
+
const rewritten = rebaseGlobalsCssSources(css, cssDir, workspaceRoot)
|
|
23
|
+
|
|
24
|
+
expect(rewritten).toContain('@source "../../../../../../themes";')
|
|
25
|
+
expect(rewritten).toContain('@source "../../../../../../plugins";')
|
|
26
|
+
expect(rewritten).toContain('@source "../../../../../../examples";')
|
|
27
|
+
expect(rewritten).toContain('@source "../../../../../../packages/ui/src";')
|
|
28
|
+
expect(rewritten).toContain('.foo { color: red; }')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('is a no-op for a file with no @source lines', () => {
|
|
32
|
+
const css = '@import "tailwindcss";\n.foo { color: red; }\n'
|
|
33
|
+
expect(rebaseGlobalsCssSources(css, '/repo/boards/stock/.meith/app/src/styles', '/repo')).toBe(
|
|
34
|
+
css,
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
})
|
package/next.config.mjs
CHANGED
|
@@ -4,35 +4,22 @@ import { fileURLToPath } from 'node:url'
|
|
|
4
4
|
|
|
5
5
|
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
// `boards/stock` (docker/Dockerfile) is neither: it is a workspace member of
|
|
15
|
-
// *this* monorepo's own pnpm install, which does not hoist, so its real
|
|
16
|
-
// dependencies (including `next` itself) resolve through this repository's
|
|
17
|
-
// root `node_modules`, two directories further up again — a fixed offset
|
|
18
|
-
// this file cannot compute from its own path alone, because pnpm resolves
|
|
19
|
-
// its dependencies through symlinks into a central store rather than by
|
|
20
|
-
// nesting a workspace member two directories below everything it needs.
|
|
21
|
-
// FORUM_WORKSPACE_ROOT lets the Dockerfile say so explicitly, without
|
|
22
|
-
// changing the default for every other consumer this computation already
|
|
23
|
-
// serves correctly.
|
|
7
|
+
/**
|
|
8
|
+
* Two directories up from this file's own location by default — correct at
|
|
9
|
+
* the depth every consumer places it at except `boards/stock`, which
|
|
10
|
+
* `FORUM_WORKSPACE_ROOT` overrides (see docs/architecture.md, "The stock
|
|
11
|
+
* board"; docs/development.md, "Consuming the board from a workspace").
|
|
12
|
+
*/
|
|
24
13
|
const workspaceRoot = process.env.FORUM_WORKSPACE_ROOT
|
|
25
14
|
? path.resolve(process.env.FORUM_WORKSPACE_ROOT)
|
|
26
15
|
: path.join(here, '../../')
|
|
27
16
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// not against `workspaceRoot` — an absolute glob there silently matches
|
|
35
|
-
// nothing useful once it is rejoined onto this directory downstream.
|
|
17
|
+
/**
|
|
18
|
+
* The relative equivalent of `workspaceRoot`, for `outputFileTracingIncludes`
|
|
19
|
+
* below — see docs/development.md, "Consuming the board from a workspace",
|
|
20
|
+
* for why that option needs a path relative to this file rather than an
|
|
21
|
+
* absolute one.
|
|
22
|
+
*/
|
|
36
23
|
const upToWorkspaceRoot = path.relative(here, workspaceRoot).split(path.sep).join('/')
|
|
37
24
|
|
|
38
25
|
const loadedEnvFiles = []
|
|
@@ -52,6 +39,10 @@ const nextConfig = {
|
|
|
52
39
|
poweredByHeader: false,
|
|
53
40
|
distDir: process.env.FORUM_DIST_DIR ?? '.next',
|
|
54
41
|
|
|
42
|
+
experimental: {
|
|
43
|
+
turbopackFileSystemCacheForDev: false,
|
|
44
|
+
},
|
|
45
|
+
|
|
55
46
|
serverExternalPackages: [
|
|
56
47
|
'@aws-sdk/client-s3',
|
|
57
48
|
'@aws-sdk/s3-request-presigner',
|
|
@@ -62,23 +53,11 @@ const nextConfig = {
|
|
|
62
53
|
'nodemailer',
|
|
63
54
|
],
|
|
64
55
|
outputFileTracingRoot: workspaceRoot,
|
|
65
|
-
|
|
66
|
-
// otherwise stops at this app's own directory — which for a materialized
|
|
67
|
-
// app (apps/community/bin/forum-web.mjs) is two directories below the
|
|
68
|
-
// real one. Left unset, Turbopack's own Node.js transform pool (postcss,
|
|
69
|
-
// for instance) cannot see the invoking workspace's node_modules at all,
|
|
70
|
-
// and fails with "Cannot find module" for a dependency that plain Node
|
|
71
|
-
// resolution finds without trouble.
|
|
56
|
+
/** See docs/development.md, "Consuming the board from a workspace", for why this is set. */
|
|
72
57
|
turbopack: {
|
|
73
58
|
root: workspaceRoot,
|
|
74
59
|
},
|
|
75
|
-
|
|
76
|
-
// package and misses the `esm/` variant next's own require-hook resolves at
|
|
77
|
-
// runtime, so the standalone build ships a `@swc/helpers` directory missing
|
|
78
|
-
// its esm/ half. next resolves the package from its own nested pnpm store
|
|
79
|
-
// entry (node_modules/.pnpm/next@…/node_modules/@swc/helpers), which is a
|
|
80
|
-
// symlink into node_modules/.pnpm/@swc+helpers@0.5.23/node_modules/@swc/helpers
|
|
81
|
-
// — so that is the path that has to be complete, not the app's own copy.
|
|
60
|
+
/** Works around a gap in Next's own tracing — see docs/development.md, "Consuming the board from a workspace". */
|
|
82
61
|
outputFileTracingIncludes: {
|
|
83
62
|
'/**': [
|
|
84
63
|
`${upToWorkspaceRoot}/node_modules/.pnpm/@swc+helpers@0.5.23/node_modules/@swc/helpers/**/*`,
|
|
@@ -87,24 +66,12 @@ const nextConfig = {
|
|
|
87
66
|
images: {
|
|
88
67
|
unoptimized: true,
|
|
89
68
|
},
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// seemingly arbitrary subset: only the packages some other resolution path
|
|
97
|
-
// happened to touch via node_modules ever needed it here.
|
|
98
|
-
//
|
|
99
|
-
// A materialized workspace's own generated tsconfig carries no such alias
|
|
100
|
-
// map (see apps/community/bin/forum-web.mjs) — only `@board/config` and
|
|
101
|
-
// `@board/plugins`, the seam itself. Every other `@meith/*` specifier
|
|
102
|
-
// resolves the ordinary way once this package is npm-installed, which
|
|
103
|
-
// means every one of them needs this same source-compilation treatment,
|
|
104
|
-
// or the build fails with "Unknown module type" on its `src/index.ts`.
|
|
105
|
-
// Computed by walking this app's own imports plus every `@meith/*`
|
|
106
|
-
// package's own "dependencies" transitively — see docs/development.md,
|
|
107
|
-
// "Consuming the board from a workspace".
|
|
69
|
+
/**
|
|
70
|
+
* Every `@meith/*` package this app's dependency graph reaches, not just the
|
|
71
|
+
* ones apps/community imports directly — see docs/development.md,
|
|
72
|
+
* "Consuming the board from a workspace", and docs/release.md, "They ship
|
|
73
|
+
* TypeScript source, deliberately".
|
|
74
|
+
*/
|
|
108
75
|
transpilePackages: [
|
|
109
76
|
'@meith/accounts',
|
|
110
77
|
'@meith/admin',
|
|
@@ -152,12 +119,6 @@ const nextConfig = {
|
|
|
152
119
|
'@meith/threads',
|
|
153
120
|
'@meith/ui',
|
|
154
121
|
'@meith/upgrade',
|
|
155
|
-
// Only reached through the `@meith/web/config` subpath, and only from a
|
|
156
|
-
// materialized workspace's own community.config.ts — inside this
|
|
157
|
-
// monorepo apps/community never imports itself by package name. Real
|
|
158
|
-
// once npm resolves this package into another workspace's node_modules,
|
|
159
|
-
// so it needs the same source-compilation treatment as everything else
|
|
160
|
-
// in this list.
|
|
161
122
|
'@meith/web',
|
|
162
123
|
],
|
|
163
124
|
async headers() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/web",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
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": "LGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"bin",
|
|
19
19
|
"app",
|
|
20
20
|
"src",
|
|
21
|
+
"public",
|
|
21
22
|
"next.config.mjs",
|
|
22
23
|
"postcss.config.mjs",
|
|
23
24
|
"components.json",
|
|
@@ -42,52 +43,52 @@
|
|
|
42
43
|
"typescript": "7.0.2",
|
|
43
44
|
"@types/react": "^19",
|
|
44
45
|
"@types/react-dom": "^19",
|
|
45
|
-
"@meith/
|
|
46
|
-
"@meith/
|
|
47
|
-
"@meith/
|
|
48
|
-
"@meith/
|
|
49
|
-
"@meith/
|
|
50
|
-
"@meith/
|
|
51
|
-
"@meith/avatars": "0.17.
|
|
52
|
-
"@meith/
|
|
53
|
-
"@meith/
|
|
54
|
-
"@meith/
|
|
55
|
-
"@meith/
|
|
56
|
-
"@meith/
|
|
57
|
-
"@meith/
|
|
58
|
-
"@meith/
|
|
59
|
-
"@meith/i18n": "0.17.
|
|
60
|
-
"@meith/
|
|
61
|
-
"@meith/
|
|
62
|
-
"@meith/
|
|
63
|
-
"@meith/markdown": "0.17.
|
|
64
|
-
"@meith/
|
|
65
|
-
"@meith/
|
|
66
|
-
"@meith/
|
|
67
|
-
"@meith/
|
|
68
|
-
"@meith/
|
|
69
|
-
"@meith/
|
|
70
|
-
"@meith/plugin-
|
|
71
|
-
"@meith/
|
|
72
|
-
"@meith/
|
|
73
|
-
"@meith/
|
|
74
|
-
"@meith/
|
|
75
|
-
"@meith/
|
|
76
|
-
"@meith/
|
|
77
|
-
"@meith/
|
|
78
|
-
"@meith/
|
|
79
|
-
"@meith/
|
|
80
|
-
"@meith/subscriptions": "0.17.
|
|
81
|
-
"@meith/
|
|
82
|
-
"@meith/theme-
|
|
83
|
-
"@meith/theme-
|
|
84
|
-
"@meith/theme-
|
|
85
|
-
"@meith/theme-
|
|
86
|
-
"@meith/theme-
|
|
87
|
-
"@meith/
|
|
88
|
-
"@meith/
|
|
89
|
-
"@meith/
|
|
90
|
-
"@meith/upgrade": "0.17.
|
|
46
|
+
"@meith/admin": "0.17.2",
|
|
47
|
+
"@meith/antispam": "0.17.2",
|
|
48
|
+
"@meith/attachments": "0.17.2",
|
|
49
|
+
"@meith/api": "0.17.2",
|
|
50
|
+
"@meith/authorization": "0.17.2",
|
|
51
|
+
"@meith/accounts": "0.17.2",
|
|
52
|
+
"@meith/avatars": "0.17.2",
|
|
53
|
+
"@meith/core": "0.17.2",
|
|
54
|
+
"@meith/db": "0.17.2",
|
|
55
|
+
"@meith/events": "0.17.2",
|
|
56
|
+
"@meith/demo": "0.17.2",
|
|
57
|
+
"@meith/drivers": "0.17.2",
|
|
58
|
+
"@meith/forums": "0.17.2",
|
|
59
|
+
"@meith/groups": "0.17.2",
|
|
60
|
+
"@meith/i18n": "0.17.2",
|
|
61
|
+
"@meith/drafts": "0.17.2",
|
|
62
|
+
"@meith/mail": "0.17.2",
|
|
63
|
+
"@meith/install": "0.17.2",
|
|
64
|
+
"@meith/markdown": "0.17.2",
|
|
65
|
+
"@meith/import": "0.17.2",
|
|
66
|
+
"@meith/marketplace": "0.17.2",
|
|
67
|
+
"@meith/moderation": "0.17.2",
|
|
68
|
+
"@meith/notifications": "0.17.2",
|
|
69
|
+
"@meith/messages": "0.17.2",
|
|
70
|
+
"@meith/polls": "0.17.2",
|
|
71
|
+
"@meith/plugin-dues": "0.17.2",
|
|
72
|
+
"@meith/posts": "0.17.2",
|
|
73
|
+
"@meith/plugin-kit": "0.17.2",
|
|
74
|
+
"@meith/profile-fields": "0.17.2",
|
|
75
|
+
"@meith/runtime": "0.17.2",
|
|
76
|
+
"@meith/search": "0.17.2",
|
|
77
|
+
"@meith/relations": "0.17.2",
|
|
78
|
+
"@meith/reputation": "0.17.2",
|
|
79
|
+
"@meith/signatures": "0.17.2",
|
|
80
|
+
"@meith/settings": "0.17.2",
|
|
81
|
+
"@meith/subscriptions": "0.17.2",
|
|
82
|
+
"@meith/tasks": "0.17.2",
|
|
83
|
+
"@meith/theme-clubhouse": "0.17.2",
|
|
84
|
+
"@meith/theme-default": "0.17.2",
|
|
85
|
+
"@meith/theme-kit": "0.17.2",
|
|
86
|
+
"@meith/theme-midnight": "0.17.2",
|
|
87
|
+
"@meith/theme-phasebook": "0.17.2",
|
|
88
|
+
"@meith/theme-raidframe": "0.17.2",
|
|
89
|
+
"@meith/threads": "0.17.2",
|
|
90
|
+
"@meith/ui": "0.17.2",
|
|
91
|
+
"@meith/upgrade": "0.17.2"
|
|
91
92
|
},
|
|
92
93
|
"scripts": {
|
|
93
94
|
"dev": "next dev",
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="215" height="48" fill="none"><path fill="#000" d="M57.588 9.6h6L73.828 38h-5.2l-2.36-6.88h-11.36L52.548 38h-5.2l10.24-28.4Zm7.16 17.16-4.16-12.16-4.16 12.16h8.32Zm23.694-2.24c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.486-7.72.12 3.4c.534-1.227 1.307-2.173 2.32-2.84 1.04-.693 2.267-1.04 3.68-1.04 1.494 0 2.76.387 3.8 1.16 1.067.747 1.827 1.813 2.28 3.2.507-1.44 1.294-2.52 2.36-3.24 1.094-.747 2.414-1.12 3.96-1.12 1.414 0 2.64.307 3.68.92s1.84 1.52 2.4 2.72c.56 1.2.84 2.667.84 4.4V38h-4.96V25.92c0-1.813-.293-3.187-.88-4.12-.56-.96-1.413-1.44-2.56-1.44-.906 0-1.68.213-2.32.64-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.84-.48 3.04V38h-4.56V25.92c0-1.2-.133-2.213-.4-3.04-.24-.827-.626-1.453-1.16-1.88-.506-.427-1.133-.64-1.88-.64-.906 0-1.68.227-2.32.68-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.827-.48 3V38h-4.96V16.8h4.48Zm26.723 10.6c0-2.24.427-4.187 1.28-5.84.854-1.68 2.067-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.84 0 3.494.413 4.96 1.24 1.467.827 2.64 2.08 3.52 3.76.88 1.653 1.347 3.693 1.4 6.12v1.32h-15.08c.107 1.813.614 3.227 1.52 4.24.907.987 2.134 1.48 3.68 1.48.987 0 1.88-.253 2.68-.76a4.803 4.803 0 0 0 1.84-2.2l5.08.36c-.64 2.027-1.84 3.64-3.6 4.84-1.733 1.173-3.733 1.76-6 1.76-2.08 0-3.906-.453-5.48-1.36-1.573-.907-2.786-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84Zm15.16-2.04c-.213-1.733-.76-3.013-1.64-3.84-.853-.827-1.893-1.24-3.12-1.24-1.44 0-2.6.453-3.48 1.36-.88.88-1.44 2.12-1.68 3.72h9.92ZM163.139 9.6V38h-5.04V9.6h5.04Zm8.322 7.2.24 5.88-.64-.36c.32-2.053 1.094-3.56 2.32-4.52 1.254-.987 2.787-1.48 4.6-1.48 2.32 0 4.107.733 5.36 2.2 1.254 1.44 1.88 3.387 1.88 5.84V38h-4.96V25.92c0-1.253-.12-2.28-.36-3.08-.24-.8-.64-1.413-1.2-1.84-.533-.427-1.253-.64-2.16-.64-1.44 0-2.573.48-3.4 1.44-.8.933-1.2 2.307-1.2 4.12V38h-4.96V16.8h4.48Zm30.003 7.72c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.443 8.16V38h-5.6v-5.32h5.6Z"/><path fill="#171717" fill-rule="evenodd" d="m7.839 40.783 16.03-28.054L20 6 0 40.783h7.839Zm8.214 0H40L27.99 19.894l-4.02 7.032 3.976 6.914H20.02l-3.967 6.943Z" clip-rule="evenodd"/></svg>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
|
package/public/sw.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const FALLBACK_HREF = '/notifications'
|
|
2
|
+
|
|
3
|
+
function readPayload(event) {
|
|
4
|
+
if (!event.data) return null
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
const payload = event.data.json()
|
|
8
|
+
if (payload === null || typeof payload !== 'object') return null
|
|
9
|
+
if (typeof payload.title !== 'string' || payload.title === '') return null
|
|
10
|
+
return payload
|
|
11
|
+
} catch (_error) {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fallbackUrl() {
|
|
17
|
+
return new URL(FALLBACK_HREF, self.location.origin).href
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function targetUrl(href) {
|
|
21
|
+
const wanted = typeof href === 'string' && href !== '' ? href : FALLBACK_HREF
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(wanted, self.location.origin)
|
|
25
|
+
return url.origin === self.location.origin ? url.href : fallbackUrl()
|
|
26
|
+
} catch (_error) {
|
|
27
|
+
return fallbackUrl()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
self.addEventListener('install', () => {
|
|
32
|
+
self.skipWaiting()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
self.addEventListener('activate', (event) => {
|
|
36
|
+
event.waitUntil(self.clients.claim())
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
self.addEventListener('push', (event) => {
|
|
40
|
+
const payload = readPayload(event)
|
|
41
|
+
if (payload === null) return
|
|
42
|
+
|
|
43
|
+
const options = {
|
|
44
|
+
body: typeof payload.body === 'string' ? payload.body : '',
|
|
45
|
+
tag: typeof payload.id === 'number' ? `meith-${payload.id}` : 'meith',
|
|
46
|
+
data: { href: targetUrl(payload.href) },
|
|
47
|
+
icon: '/apple-icon.png',
|
|
48
|
+
badge: '/icon-light-32x32.png',
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof payload.badge === 'number' && 'setAppBadge' in navigator) {
|
|
53
|
+
navigator.setAppBadge(payload.badge).catch(() => {})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
event.waitUntil(self.registration.showNotification(payload.title, options))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
self.addEventListener('notificationclick', (event) => {
|
|
60
|
+
event.notification.close()
|
|
61
|
+
|
|
62
|
+
const href = targetUrl(event.notification.data && event.notification.data.href)
|
|
63
|
+
|
|
64
|
+
event.waitUntil(
|
|
65
|
+
self.clients
|
|
66
|
+
.matchAll({ type: 'window', includeUncontrolled: true })
|
|
67
|
+
.then((clients) => {
|
|
68
|
+
for (const client of clients) {
|
|
69
|
+
if (client.url === href && 'focus' in client) return client.focus()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const open = clients[0]
|
|
73
|
+
if (open !== undefined && 'navigate' in open) {
|
|
74
|
+
return open.focus().then(() => open.navigate(href))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return self.clients.openWindow(href)
|
|
78
|
+
})
|
|
79
|
+
.catch(() => self.clients.openWindow(href)),
|
|
80
|
+
)
|
|
81
|
+
})
|
|
@@ -385,7 +385,7 @@ export function ThemeEditorForm({
|
|
|
385
385
|
<fieldset
|
|
386
386
|
key={group.titleKey}
|
|
387
387
|
hidden={hydrated && matches === 0}
|
|
388
|
-
className="flex flex-col gap-2 rounded-md border border-border p-3"
|
|
388
|
+
className="flex min-w-0 flex-col gap-2 rounded-md border border-border p-3"
|
|
389
389
|
>
|
|
390
390
|
<legend className="flex flex-wrap items-baseline gap-x-3 gap-y-1 px-1">
|
|
391
391
|
<span className="text-sm font-medium">{copy[group.titleKey]}</span>
|
|
@@ -104,7 +104,7 @@ export function PaletteGrid({
|
|
|
104
104
|
onSelect: (name: string) => void
|
|
105
105
|
}) {
|
|
106
106
|
return (
|
|
107
|
-
<div className="grid grid-cols-
|
|
107
|
+
<div className="grid grid-cols-2 gap-2 @lg:grid-cols-3">
|
|
108
108
|
{cells.map((cell) => (
|
|
109
109
|
<Cell
|
|
110
110
|
key={cell.token.name}
|
|
@@ -53,7 +53,7 @@ export function UserBulkToolbar({
|
|
|
53
53
|
<form
|
|
54
54
|
id="admin-user-bulk"
|
|
55
55
|
action={action}
|
|
56
|
-
className="flex flex-col gap-3 sm:flex-row sm:items-end"
|
|
56
|
+
className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end"
|
|
57
57
|
>
|
|
58
58
|
<label className="flex min-w-48 flex-col gap-1 text-sm">
|
|
59
59
|
<span className="font-medium">{labels.action}</span>
|
|
@@ -81,7 +81,7 @@ export function UserBulkToolbar({
|
|
|
81
81
|
</label>
|
|
82
82
|
<button
|
|
83
83
|
type="button"
|
|
84
|
-
className="inline-flex h-10 items-center justify-center rounded-md border border-border px-4 text-sm font-medium"
|
|
84
|
+
className="inline-flex h-10 shrink-0 items-center justify-center whitespace-nowrap rounded-md border border-border px-4 text-sm font-medium"
|
|
85
85
|
onClick={() => {
|
|
86
86
|
for (const checkbox of document.querySelectorAll<HTMLInputElement>(
|
|
87
87
|
'[data-admin-user-select]',
|
|
@@ -92,7 +92,9 @@ export function UserBulkToolbar({
|
|
|
92
92
|
>
|
|
93
93
|
{labels.selectPage}
|
|
94
94
|
</button>
|
|
95
|
-
<
|
|
95
|
+
<span className="min-w-48">
|
|
96
|
+
<SubmitButton>{labels.apply}</SubmitButton>
|
|
97
|
+
</span>
|
|
96
98
|
</form>
|
|
97
99
|
</div>
|
|
98
100
|
)
|
|
@@ -62,16 +62,16 @@ export async function saveAdminSettingsAction(
|
|
|
62
62
|
if (result.changed.length > 0) {
|
|
63
63
|
const tags = [CacheTags.settings(), ...result.invalidates]
|
|
64
64
|
await drivers().cache.invalidateTags(tags)
|
|
65
|
-
for (const tag of tags) await emitEvent('cache.invalidated', { tag }, {})
|
|
66
|
-
|
|
67
65
|
revalidatePath('/admin/settings')
|
|
68
66
|
|
|
69
|
-
await
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
await Promise.all([
|
|
68
|
+
...tags.map((tag) => emitEvent('cache.invalidated', { tag }, {})),
|
|
69
|
+
emitEvent('settings.saved', { keys: result.changed }, { adminId: admin.session.userId }),
|
|
70
|
+
recordAdminAction({
|
|
71
|
+
action: 'settings.changed',
|
|
72
|
+
detail: { keys: result.changed },
|
|
73
|
+
}),
|
|
74
|
+
])
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
return { notice: result.changed.length === 0 ? 'unchanged' : 'saved' }
|
|
@@ -6,7 +6,7 @@ import { planUpgrade, type UpgradeState, upgradeNotice } from '@meith/upgrade'
|
|
|
6
6
|
|
|
7
7
|
import { activeDefinitions } from './plugin-host'
|
|
8
8
|
|
|
9
|
-
export const CODE_VERSION = '0.17.
|
|
9
|
+
export const CODE_VERSION = '0.17.2'
|
|
10
10
|
|
|
11
11
|
export async function pendingUpgradeNotice(): Promise<string | null> {
|
|
12
12
|
if (env.DATA_SOURCE !== 'postgres') return null
|
|
@@ -179,26 +179,26 @@ export async function banMemberAction(_prev: FormState, form: FormData): Promise
|
|
|
179
179
|
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
180
180
|
})
|
|
181
181
|
|
|
182
|
-
await emitEvent(
|
|
183
|
-
'user.banned',
|
|
184
|
-
{ userId: id, expiresAt: expiresAt?.toISOString() ?? null },
|
|
185
|
-
{ moderatorId: context.session.userId, reason: trimmedText(form, 'reason') || null },
|
|
186
|
-
)
|
|
187
|
-
|
|
188
182
|
refreshMemberScreens()
|
|
189
|
-
await recordAdminAction({
|
|
190
|
-
action: 'user.banned',
|
|
191
|
-
detail: { userId: id, days: days === '' ? null : Number(days) },
|
|
192
|
-
})
|
|
193
183
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
184
|
+
const [, undo] = await Promise.all([
|
|
185
|
+
emitEvent(
|
|
186
|
+
'user.banned',
|
|
187
|
+
{ userId: id, expiresAt: expiresAt?.toISOString() ?? null },
|
|
188
|
+
{ moderatorId: context.session.userId, reason: trimmedText(form, 'reason') || null },
|
|
189
|
+
),
|
|
190
|
+
issueAdminUndo({
|
|
197
191
|
actorUserId: context.session.userId,
|
|
198
192
|
operation: 'user.ban',
|
|
199
193
|
snapshot: { userId: id },
|
|
200
194
|
}),
|
|
201
|
-
|
|
195
|
+
recordAdminAction({
|
|
196
|
+
action: 'user.banned',
|
|
197
|
+
detail: { userId: id, days: days === '' ? null : Number(days) },
|
|
198
|
+
}),
|
|
199
|
+
])
|
|
200
|
+
|
|
201
|
+
return { notice: 'banned', undo }
|
|
202
202
|
} catch (err) {
|
|
203
203
|
return toFormState(err)
|
|
204
204
|
}
|
|
@@ -556,14 +556,16 @@ export async function liftBanAction(_prev: FormState, form: FormData): Promise<F
|
|
|
556
556
|
|
|
557
557
|
await banService().lift(id)
|
|
558
558
|
|
|
559
|
-
await emitEvent(
|
|
560
|
-
'user.unbanned',
|
|
561
|
-
{ userId: id, expired: false },
|
|
562
|
-
{ moderatorId: context.session.userId, reason: null },
|
|
563
|
-
)
|
|
564
|
-
|
|
565
559
|
refreshMemberScreens()
|
|
566
|
-
|
|
560
|
+
|
|
561
|
+
await Promise.all([
|
|
562
|
+
emitEvent(
|
|
563
|
+
'user.unbanned',
|
|
564
|
+
{ userId: id, expired: false },
|
|
565
|
+
{ moderatorId: context.session.userId, reason: null },
|
|
566
|
+
),
|
|
567
|
+
recordAdminAction({ action: 'user.ban_lifted', detail: { userId: id } }),
|
|
568
|
+
])
|
|
567
569
|
|
|
568
570
|
return { notice: 'lifted' }
|
|
569
571
|
} catch (err) {
|
package/src/server/user-admin.ts
CHANGED
|
@@ -118,13 +118,14 @@ export async function buildMemberView(userId: number): Promise<MemberView | null
|
|
|
118
118
|
|
|
119
119
|
const prefix = member.lastIpPrefix ?? member.registrationIpPrefix ?? ''
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
const [secondaryGroupIds, groups, activeBan, sharedNetwork] = await Promise.all([
|
|
122
|
+
repository.readSecondaryGroups(userId),
|
|
123
|
+
repository.listGroups(),
|
|
124
|
+
bans.findActive(userId),
|
|
125
|
+
repository.sharingIpPrefix(prefix, userId),
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
return { member, secondaryGroupIds, groups, activeBan, sharedNetwork }
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
export function requireUserBulk(): PostgresUserBulkRepository {
|