@meith/web 0.23.1 → 0.23.3
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/(board)/[slug]/new/page.tsx +7 -5
- package/app/(board)/thread/[slug]/edit/page.tsx +7 -5
- package/app/(board)/thread/[slug]/page.tsx +19 -1
- package/app/(board)/thread/[slug]/reply/page.tsx +7 -5
- package/app/(board)/usercp/security/verify/page.tsx +3 -0
- package/app/admin/layout.tsx +4 -4
- package/package.json +48 -48
- package/src/components/account/credential-proof-form.tsx +14 -4
- package/src/components/account/two-factor-forms.tsx +4 -4
- package/src/components/admin/admin-forms.tsx +8 -13
- package/src/components/admin/badge-forms.tsx +6 -7
- package/src/components/admin/branding-forms.tsx +5 -10
- package/src/components/admin/form-bits.tsx +6 -4
- package/src/components/admin/forum-forms.tsx +180 -78
- package/src/components/admin/mail-test-card.tsx +2 -5
- package/src/components/admin/marketplace-forms.tsx +4 -5
- package/src/components/admin/marketplace-listing.tsx +1 -1
- package/src/components/admin/plugin-forms.tsx +2 -6
- package/src/components/admin/settings-form.tsx +2 -6
- package/src/components/admin/system-forms.tsx +2 -5
- package/src/components/auth/otp-field.tsx +142 -0
- package/src/components/auth/second-factor-form.tsx +4 -3
- package/src/components/content/composer-recovery.tsx +19 -1
- package/src/components/content/edit-post-form.tsx +3 -2
- package/src/components/content/markdown-editor.tsx +123 -104
- package/src/components/content/new-thread-form.tsx +3 -2
- package/src/components/content/reply-form.tsx +3 -2
- package/src/components/moderation/thread-surgery-form.tsx +51 -47
- package/src/components/moderation/thread-tools-form.tsx +67 -87
- package/src/server/marketplace-admin.ts +2 -2
- package/src/server/upgrade-notice.ts +1 -1
- package/src/theme/contract.fixture.ts +2 -2
- package/src/view/account-copy.ts +2 -0
- package/src/view/admin-copy.ts +9 -0
- package/src/view/admin-panel-copy.ts +2 -0
- package/src/view/auth-copy.ts +17 -10
- package/src/view/content-copy.ts +11 -6
- package/src/view/forum-display.ts +1 -0
|
@@ -90,12 +90,13 @@ export function ComposerRecovery({
|
|
|
90
90
|
const latest = useRef('')
|
|
91
91
|
const saved = useRef('')
|
|
92
92
|
const submitting = useRef(false)
|
|
93
|
+
const held = useRef(false)
|
|
93
94
|
const [recovery, setRecovery] = useState<Backup | null>(null)
|
|
94
95
|
const [saveState, setSaveState] = useState<SaveState>('idle')
|
|
95
96
|
|
|
96
97
|
const persist = useCallback(async () => {
|
|
97
98
|
const form = root.current?.closest('form')
|
|
98
|
-
if (form === null || form === undefined) return
|
|
99
|
+
if (form === null || form === undefined || held.current) return
|
|
99
100
|
const payload = readPayload(form, { ...scope, title: '', message: '', prefixId: null })
|
|
100
101
|
const next = signature(payload)
|
|
101
102
|
latest.current = next
|
|
@@ -134,6 +135,8 @@ export function ComposerRecovery({
|
|
|
134
135
|
|
|
135
136
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
136
137
|
const onInput = () => {
|
|
138
|
+
submitting.current = false
|
|
139
|
+
held.current = false
|
|
137
140
|
const payload = readPayload(form, { ...scope, title: '', message: '', prefixId: null })
|
|
138
141
|
latest.current = signature(payload)
|
|
139
142
|
try {
|
|
@@ -146,6 +149,12 @@ export function ComposerRecovery({
|
|
|
146
149
|
timer = setTimeout(() => void persist(), AUTOSAVE_DELAY_MS)
|
|
147
150
|
}
|
|
148
151
|
const onBlur = () => void persist()
|
|
152
|
+
const onPointerDown = (event: Event) => {
|
|
153
|
+
const target = event.target
|
|
154
|
+
if (target instanceof Element && target.closest('button[type="submit"]') !== null) {
|
|
155
|
+
held.current = true
|
|
156
|
+
}
|
|
157
|
+
}
|
|
149
158
|
const onSubmit = (event: SubmitEvent) => {
|
|
150
159
|
const submitter = event.submitter
|
|
151
160
|
submitting.current =
|
|
@@ -163,6 +172,7 @@ export function ComposerRecovery({
|
|
|
163
172
|
|
|
164
173
|
form.addEventListener('input', onInput)
|
|
165
174
|
form.addEventListener('blur', onBlur, true)
|
|
175
|
+
form.addEventListener('pointerdown', onPointerDown, true)
|
|
166
176
|
form.addEventListener('submit', onSubmit)
|
|
167
177
|
window.addEventListener('pagehide', onPageHide)
|
|
168
178
|
window.addEventListener('beforeunload', onBeforeUnload)
|
|
@@ -170,12 +180,20 @@ export function ComposerRecovery({
|
|
|
170
180
|
clearTimeout(timer)
|
|
171
181
|
form.removeEventListener('input', onInput)
|
|
172
182
|
form.removeEventListener('blur', onBlur, true)
|
|
183
|
+
form.removeEventListener('pointerdown', onPointerDown, true)
|
|
173
184
|
form.removeEventListener('submit', onSubmit)
|
|
174
185
|
window.removeEventListener('pagehide', onPageHide)
|
|
175
186
|
window.removeEventListener('beforeunload', onBeforeUnload)
|
|
176
187
|
}
|
|
177
188
|
}, [persist, scope, serverUpdatedAt, storageKey])
|
|
178
189
|
|
|
190
|
+
useEffect(
|
|
191
|
+
() => () => {
|
|
192
|
+
if (submitting.current) localStorage.removeItem(storageKey)
|
|
193
|
+
},
|
|
194
|
+
[storageKey],
|
|
195
|
+
)
|
|
196
|
+
|
|
179
197
|
function restore(): void {
|
|
180
198
|
const form = root.current?.closest('form')
|
|
181
199
|
if (form === null || form === undefined || recovery === null) return
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
3
4
|
import { useActionState } from 'react'
|
|
4
5
|
|
|
5
6
|
import { EMPTY_STATE } from '@/server/auth-form-state'
|
|
@@ -14,14 +15,14 @@ export function EditPostForm({
|
|
|
14
15
|
postId,
|
|
15
16
|
message,
|
|
16
17
|
reason,
|
|
17
|
-
toolbar
|
|
18
|
+
toolbar,
|
|
18
19
|
copy,
|
|
19
20
|
}: {
|
|
20
21
|
threadId: number
|
|
21
22
|
postId: number
|
|
22
23
|
message: string
|
|
23
24
|
reason: string | null
|
|
24
|
-
toolbar?:
|
|
25
|
+
toolbar?: ReactNode
|
|
25
26
|
copy: Copy
|
|
26
27
|
}) {
|
|
27
28
|
const [state, action] = useActionState(editPostAction, EMPTY_STATE)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
3
4
|
import { useCallback, useEffect, useId, useRef, useState } from 'react'
|
|
4
5
|
|
|
5
6
|
import type { MemberSuggestion } from '@meith/accounts'
|
|
@@ -84,7 +85,7 @@ export interface MarkdownEditorProps {
|
|
|
84
85
|
readonly hint?: React.ReactNode
|
|
85
86
|
readonly scope?: PreviewScope
|
|
86
87
|
readonly attachTo?: AttachmentTarget | undefined
|
|
87
|
-
readonly toolbar?:
|
|
88
|
+
readonly toolbar?: ReactNode
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
export function MarkdownEditor({
|
|
@@ -99,7 +100,7 @@ export function MarkdownEditor({
|
|
|
99
100
|
hint,
|
|
100
101
|
scope = 'post',
|
|
101
102
|
attachTo,
|
|
102
|
-
toolbar
|
|
103
|
+
toolbar,
|
|
103
104
|
}: MarkdownEditorProps) {
|
|
104
105
|
const field = useRef<HTMLTextAreaElement>(null)
|
|
105
106
|
const attachmentInput = useRef<HTMLInputElement>(null)
|
|
@@ -308,6 +309,9 @@ export function MarkdownEditor({
|
|
|
308
309
|
}
|
|
309
310
|
|
|
310
311
|
const writing = !enhanced || tab === 'write'
|
|
312
|
+
const onWriteTab = enhanced && tab === 'write'
|
|
313
|
+
const showThemedToolbar = toolbar != null && onWriteTab
|
|
314
|
+
const showBuiltInToolbar = toolbar == null && onWriteTab
|
|
311
315
|
|
|
312
316
|
const tabClass = (active: boolean): string =>
|
|
313
317
|
cn(
|
|
@@ -359,121 +363,136 @@ export function MarkdownEditor({
|
|
|
359
363
|
)}
|
|
360
364
|
</div>
|
|
361
365
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
>
|
|
369
|
-
{TOOLS.map((tool) => (
|
|
370
|
-
<button
|
|
371
|
-
key={tool.labelKey}
|
|
372
|
-
type="button"
|
|
373
|
-
onMouseDown={(event) => event.preventDefault()}
|
|
374
|
-
onClick={() => {
|
|
375
|
-
if (field.current !== null) runTool(field.current, tool, copy)
|
|
376
|
-
}}
|
|
377
|
-
title={
|
|
378
|
-
tool.shortcut === undefined
|
|
379
|
-
? fromCopy(copy, tool.labelKey)
|
|
380
|
-
: formatFromCopy(copy, 'composer.toolShortcut', {
|
|
381
|
-
label: fromCopy(copy, tool.labelKey),
|
|
382
|
-
key: tool.shortcut.toUpperCase(),
|
|
383
|
-
})
|
|
384
|
-
}
|
|
385
|
-
aria-label={fromCopy(copy, tool.labelKey)}
|
|
386
|
-
{...(tool.shortcut === undefined
|
|
387
|
-
? {}
|
|
388
|
-
: { 'aria-keyshortcuts': `Control+${tool.shortcut}` })}
|
|
389
|
-
className="min-w-8 rounded px-2 py-1 text-xs font-medium text-muted-foreground hover:bg-background hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
390
|
-
>
|
|
391
|
-
<span aria-hidden="true">{tool.glyph}</span>
|
|
392
|
-
</button>
|
|
393
|
-
))}
|
|
394
|
-
|
|
395
|
-
{attachTo !== undefined && (
|
|
396
|
-
<button
|
|
397
|
-
type="button"
|
|
398
|
-
disabled={attachmentUpload === 'uploading'}
|
|
399
|
-
onMouseDown={(event) => event.preventDefault()}
|
|
400
|
-
onClick={() => attachmentInput.current?.click()}
|
|
401
|
-
title={fromCopy(copy, 'composer.tool.attachment')}
|
|
402
|
-
aria-label={fromCopy(copy, 'composer.tool.attachment')}
|
|
403
|
-
className="min-w-8 rounded px-2 py-1 text-xs font-medium text-muted-foreground hover:bg-background hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:opacity-50"
|
|
404
|
-
>
|
|
405
|
-
<span aria-hidden="true">Attach</span>
|
|
406
|
-
</button>
|
|
407
|
-
)}
|
|
408
|
-
</div>
|
|
409
|
-
)}
|
|
410
|
-
|
|
411
|
-
<div
|
|
412
|
-
{...(enhanced
|
|
413
|
-
? { role: 'tabpanel', id: `${panelId}-write`, 'aria-labelledby': `${panelId}-write-tab` }
|
|
414
|
-
: {})}
|
|
415
|
-
hidden={!writing}
|
|
416
|
-
className="relative"
|
|
417
|
-
>
|
|
418
|
-
<textarea
|
|
419
|
-
ref={field}
|
|
420
|
-
id={id}
|
|
421
|
-
name={name}
|
|
422
|
-
rows={rows}
|
|
423
|
-
required={required === true && writing}
|
|
424
|
-
defaultValue={defaultValue}
|
|
425
|
-
{...(maxLength === undefined ? {} : { maxLength })}
|
|
426
|
-
onKeyDown={onKeyDown}
|
|
427
|
-
onPaste={onPaste}
|
|
428
|
-
onInput={(event) => {
|
|
429
|
-
fit()
|
|
430
|
-
syncMentionToken(event.currentTarget)
|
|
431
|
-
}}
|
|
432
|
-
onSelect={(event) => syncMentionToken(event.currentTarget)}
|
|
433
|
-
onBlur={() => setMentionToken(null)}
|
|
434
|
-
{...(enhanced && mentionToken !== null && mentionMatches.length > 0
|
|
435
|
-
? {
|
|
436
|
-
role: 'combobox',
|
|
437
|
-
'aria-expanded': true,
|
|
438
|
-
'aria-controls': `${panelId}-mentions`,
|
|
439
|
-
'aria-activedescendant': `${panelId}-mention-${mentionActive}`,
|
|
440
|
-
'aria-autocomplete': 'list' as const,
|
|
441
|
-
}
|
|
442
|
-
: {})}
|
|
443
|
-
className="w-full rounded-md border border-input bg-card px-3 py-2 font-normal text-sm leading-relaxed focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
444
|
-
/>
|
|
366
|
+
<div className="flex flex-col">
|
|
367
|
+
{showThemedToolbar && (
|
|
368
|
+
<div className="overflow-hidden rounded-t-md border border-b-0 border-input">
|
|
369
|
+
{toolbar}
|
|
370
|
+
</div>
|
|
371
|
+
)}
|
|
445
372
|
|
|
446
|
-
{
|
|
373
|
+
{showBuiltInToolbar && (
|
|
447
374
|
<div
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
aria-
|
|
451
|
-
className="
|
|
375
|
+
role="group"
|
|
376
|
+
aria-label={fromCopy(copy, 'composer.formatting')}
|
|
377
|
+
aria-controls={id}
|
|
378
|
+
className="flex flex-wrap gap-0.5 rounded-t-md border border-b-0 border-input bg-muted/40 p-1"
|
|
452
379
|
>
|
|
453
|
-
{
|
|
380
|
+
{TOOLS.map((tool) => (
|
|
454
381
|
<button
|
|
455
|
-
key={
|
|
382
|
+
key={tool.labelKey}
|
|
456
383
|
type="button"
|
|
457
|
-
id={`${panelId}-mention-${index}`}
|
|
458
|
-
role="option"
|
|
459
|
-
aria-selected={index === mentionActive}
|
|
460
384
|
onMouseDown={(event) => event.preventDefault()}
|
|
461
385
|
onClick={() => {
|
|
462
|
-
if (field.current !== null)
|
|
386
|
+
if (field.current !== null) runTool(field.current, tool, copy)
|
|
463
387
|
}}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
388
|
+
title={
|
|
389
|
+
tool.shortcut === undefined
|
|
390
|
+
? fromCopy(copy, tool.labelKey)
|
|
391
|
+
: formatFromCopy(copy, 'composer.toolShortcut', {
|
|
392
|
+
label: fromCopy(copy, tool.labelKey),
|
|
393
|
+
key: tool.shortcut.toUpperCase(),
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
aria-label={fromCopy(copy, tool.labelKey)}
|
|
397
|
+
{...(tool.shortcut === undefined
|
|
398
|
+
? {}
|
|
399
|
+
: { 'aria-keyshortcuts': `Control+${tool.shortcut}` })}
|
|
400
|
+
className="min-w-8 rounded px-2 py-1 text-xs font-medium text-muted-foreground hover:bg-background hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
471
401
|
>
|
|
472
|
-
|
|
402
|
+
<span aria-hidden="true">{tool.glyph}</span>
|
|
473
403
|
</button>
|
|
474
404
|
))}
|
|
405
|
+
|
|
406
|
+
{attachTo !== undefined && (
|
|
407
|
+
<button
|
|
408
|
+
type="button"
|
|
409
|
+
disabled={attachmentUpload === 'uploading'}
|
|
410
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
411
|
+
onClick={() => attachmentInput.current?.click()}
|
|
412
|
+
title={fromCopy(copy, 'composer.tool.attachment')}
|
|
413
|
+
aria-label={fromCopy(copy, 'composer.tool.attachment')}
|
|
414
|
+
className="min-w-8 rounded px-2 py-1 text-xs font-medium text-muted-foreground hover:bg-background hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:opacity-50"
|
|
415
|
+
>
|
|
416
|
+
<span aria-hidden="true">Attach</span>
|
|
417
|
+
</button>
|
|
418
|
+
)}
|
|
475
419
|
</div>
|
|
476
420
|
)}
|
|
421
|
+
|
|
422
|
+
<div
|
|
423
|
+
{...(enhanced
|
|
424
|
+
? {
|
|
425
|
+
role: 'tabpanel',
|
|
426
|
+
id: `${panelId}-write`,
|
|
427
|
+
'aria-labelledby': `${panelId}-write-tab`,
|
|
428
|
+
}
|
|
429
|
+
: {})}
|
|
430
|
+
hidden={!writing}
|
|
431
|
+
className="relative"
|
|
432
|
+
>
|
|
433
|
+
<textarea
|
|
434
|
+
ref={field}
|
|
435
|
+
id={id}
|
|
436
|
+
name={name}
|
|
437
|
+
rows={rows}
|
|
438
|
+
required={required === true && writing}
|
|
439
|
+
defaultValue={defaultValue}
|
|
440
|
+
{...(maxLength === undefined ? {} : { maxLength })}
|
|
441
|
+
onKeyDown={onKeyDown}
|
|
442
|
+
onPaste={onPaste}
|
|
443
|
+
onInput={(event) => {
|
|
444
|
+
fit()
|
|
445
|
+
syncMentionToken(event.currentTarget)
|
|
446
|
+
}}
|
|
447
|
+
onSelect={(event) => syncMentionToken(event.currentTarget)}
|
|
448
|
+
onBlur={() => setMentionToken(null)}
|
|
449
|
+
{...(enhanced && mentionToken !== null && mentionMatches.length > 0
|
|
450
|
+
? {
|
|
451
|
+
role: 'combobox',
|
|
452
|
+
'aria-expanded': true,
|
|
453
|
+
'aria-controls': `${panelId}-mentions`,
|
|
454
|
+
'aria-activedescendant': `${panelId}-mention-${mentionActive}`,
|
|
455
|
+
'aria-autocomplete': 'list' as const,
|
|
456
|
+
}
|
|
457
|
+
: {})}
|
|
458
|
+
className={cn(
|
|
459
|
+
'w-full rounded-md border border-input bg-card px-3 py-2 font-normal text-sm leading-relaxed focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
|
|
460
|
+
onWriteTab && 'rounded-t-none border-t-0',
|
|
461
|
+
)}
|
|
462
|
+
/>
|
|
463
|
+
|
|
464
|
+
{enhanced && mentionToken !== null && mentionMatches.length > 0 && (
|
|
465
|
+
<div
|
|
466
|
+
id={`${panelId}-mentions`}
|
|
467
|
+
role="listbox"
|
|
468
|
+
aria-label={fromCopy(copy, 'composer.mention.suggestions')}
|
|
469
|
+
className="absolute inset-x-0 top-full z-10 mt-1 max-h-48 overflow-y-auto rounded-md border border-border bg-card py-1 text-sm shadow-lg"
|
|
470
|
+
>
|
|
471
|
+
{mentionMatches.map((candidate, index) => (
|
|
472
|
+
<button
|
|
473
|
+
key={candidate.id}
|
|
474
|
+
type="button"
|
|
475
|
+
id={`${panelId}-mention-${index}`}
|
|
476
|
+
role="option"
|
|
477
|
+
aria-selected={index === mentionActive}
|
|
478
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
479
|
+
onClick={() => {
|
|
480
|
+
if (field.current !== null) applyMention(field.current, candidate)
|
|
481
|
+
}}
|
|
482
|
+
onMouseEnter={() => setMentionActive(index)}
|
|
483
|
+
className={cn(
|
|
484
|
+
'block w-full px-3 py-1.5 text-start',
|
|
485
|
+
index === mentionActive
|
|
486
|
+
? 'bg-muted text-foreground'
|
|
487
|
+
: 'text-foreground hover:bg-muted',
|
|
488
|
+
)}
|
|
489
|
+
>
|
|
490
|
+
@{candidate.username}
|
|
491
|
+
</button>
|
|
492
|
+
))}
|
|
493
|
+
</div>
|
|
494
|
+
)}
|
|
495
|
+
</div>
|
|
477
496
|
</div>
|
|
478
497
|
|
|
479
498
|
{attachTo !== undefined && (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
3
4
|
import { useActionState } from 'react'
|
|
4
5
|
|
|
5
6
|
import type { UploadLimits } from '@meith/attachments/limits'
|
|
@@ -29,7 +30,7 @@ export function NewThreadForm({
|
|
|
29
30
|
canPostPoll,
|
|
30
31
|
attachmentLimits,
|
|
31
32
|
draft,
|
|
32
|
-
toolbar
|
|
33
|
+
toolbar,
|
|
33
34
|
copy,
|
|
34
35
|
}: {
|
|
35
36
|
forumId: number
|
|
@@ -39,7 +40,7 @@ export function NewThreadForm({
|
|
|
39
40
|
canPostPoll: boolean
|
|
40
41
|
attachmentLimits: UploadLimits | null
|
|
41
42
|
draft: Draft | null
|
|
42
|
-
toolbar?:
|
|
43
|
+
toolbar?: ReactNode
|
|
43
44
|
copy: Copy
|
|
44
45
|
}) {
|
|
45
46
|
const [state, action] = useActionState(createThreadAction, EMPTY_STATE)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
3
4
|
import { useActionState } from 'react'
|
|
4
5
|
|
|
5
6
|
import type { UploadLimits } from '@meith/attachments/limits'
|
|
@@ -24,7 +25,7 @@ export function ReplyForm({
|
|
|
24
25
|
attachmentLimits,
|
|
25
26
|
draft,
|
|
26
27
|
collapsible = false,
|
|
27
|
-
toolbar
|
|
28
|
+
toolbar,
|
|
28
29
|
copy,
|
|
29
30
|
}: {
|
|
30
31
|
threadId: number
|
|
@@ -34,7 +35,7 @@ export function ReplyForm({
|
|
|
34
35
|
attachmentLimits: UploadLimits | null
|
|
35
36
|
draft: Draft | null
|
|
36
37
|
collapsible?: boolean
|
|
37
|
-
toolbar?:
|
|
38
|
+
toolbar?: ReactNode
|
|
38
39
|
copy: Copy
|
|
39
40
|
}) {
|
|
40
41
|
const [state, action] = useActionState(createReplyAction, EMPTY_STATE)
|
|
@@ -36,59 +36,63 @@ export function ThreadSurgeryForm({
|
|
|
36
36
|
return (
|
|
37
37
|
<>
|
|
38
38
|
{rights.split && splitPoints.length > 0 && (
|
|
39
|
-
<form action={splitAction} className="flex flex-
|
|
39
|
+
<form action={splitAction} className="flex flex-col gap-2">
|
|
40
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
41
|
+
<input type="hidden" name="threadId" value={threadId} />
|
|
42
|
+
<label className="flex items-center gap-2 text-xs">
|
|
43
|
+
<span className="sr-only">{fromCopy(copy, 'moderationForm.surgery.fromSr')}</span>
|
|
44
|
+
<select name="fromPostId" className={FIELD}>
|
|
45
|
+
{splitPoints.map((point) => (
|
|
46
|
+
<option key={point.id} value={point.id}>
|
|
47
|
+
{formatFromCopy(copy, 'moderationForm.surgery.splitPoint', {
|
|
48
|
+
number: point.number,
|
|
49
|
+
author: point.author,
|
|
50
|
+
})}
|
|
51
|
+
</option>
|
|
52
|
+
))}
|
|
53
|
+
</select>
|
|
54
|
+
</label>
|
|
55
|
+
<label className="flex items-center gap-2 text-xs">
|
|
56
|
+
<span className="sr-only">{fromCopy(copy, 'moderationForm.surgery.titleSr')}</span>
|
|
57
|
+
<input
|
|
58
|
+
type="text"
|
|
59
|
+
name="title"
|
|
60
|
+
required
|
|
61
|
+
minLength={3}
|
|
62
|
+
maxLength={150}
|
|
63
|
+
placeholder={fromCopy(copy, 'moderationForm.newThreadTitle')}
|
|
64
|
+
className={`${FIELD} w-48`}
|
|
65
|
+
/>
|
|
66
|
+
</label>
|
|
67
|
+
<button type="submit" className={BUTTON}>
|
|
68
|
+
{fromCopy(copy, 'moderationForm.surgery.split')}
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
40
71
|
<FormError message={splitState.error} />
|
|
41
|
-
<input type="hidden" name="threadId" value={threadId} />
|
|
42
|
-
<label className="flex items-center gap-2 text-xs">
|
|
43
|
-
<span className="sr-only">{fromCopy(copy, 'moderationForm.surgery.fromSr')}</span>
|
|
44
|
-
<select name="fromPostId" className={FIELD}>
|
|
45
|
-
{splitPoints.map((point) => (
|
|
46
|
-
<option key={point.id} value={point.id}>
|
|
47
|
-
{formatFromCopy(copy, 'moderationForm.surgery.splitPoint', {
|
|
48
|
-
number: point.number,
|
|
49
|
-
author: point.author,
|
|
50
|
-
})}
|
|
51
|
-
</option>
|
|
52
|
-
))}
|
|
53
|
-
</select>
|
|
54
|
-
</label>
|
|
55
|
-
<label className="flex items-center gap-2 text-xs">
|
|
56
|
-
<span className="sr-only">{fromCopy(copy, 'moderationForm.surgery.titleSr')}</span>
|
|
57
|
-
<input
|
|
58
|
-
type="text"
|
|
59
|
-
name="title"
|
|
60
|
-
required
|
|
61
|
-
minLength={3}
|
|
62
|
-
maxLength={150}
|
|
63
|
-
placeholder={fromCopy(copy, 'moderationForm.newThreadTitle')}
|
|
64
|
-
className={`${FIELD} w-48`}
|
|
65
|
-
/>
|
|
66
|
-
</label>
|
|
67
|
-
<button type="submit" className={BUTTON}>
|
|
68
|
-
{fromCopy(copy, 'moderationForm.surgery.split')}
|
|
69
|
-
</button>
|
|
70
72
|
</form>
|
|
71
73
|
)}
|
|
72
74
|
|
|
73
75
|
{rights.merge && (
|
|
74
|
-
<form action={mergeAction} className="flex flex-
|
|
76
|
+
<form action={mergeAction} className="flex flex-col gap-2">
|
|
77
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
78
|
+
<input type="hidden" name="threadId" value={threadId} />
|
|
79
|
+
<label className="flex items-center gap-2 text-xs">
|
|
80
|
+
<span className="sr-only">{fromCopy(copy, 'moderationForm.surgery.mergeSr')}</span>
|
|
81
|
+
<input
|
|
82
|
+
type="number"
|
|
83
|
+
name="targetThreadId"
|
|
84
|
+
required
|
|
85
|
+
min={1}
|
|
86
|
+
step={1}
|
|
87
|
+
placeholder={fromCopy(copy, 'moderationForm.surgery.mergePlaceholder')}
|
|
88
|
+
className={`${FIELD} w-40`}
|
|
89
|
+
/>
|
|
90
|
+
</label>
|
|
91
|
+
<button type="submit" className={`${BUTTON} border-destructive/40 text-destructive`}>
|
|
92
|
+
{fromCopy(copy, 'moderationForm.surgery.merge')}
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
75
95
|
<FormError message={mergeState.error} />
|
|
76
|
-
<input type="hidden" name="threadId" value={threadId} />
|
|
77
|
-
<label className="flex items-center gap-2 text-xs">
|
|
78
|
-
<span className="sr-only">{fromCopy(copy, 'moderationForm.surgery.mergeSr')}</span>
|
|
79
|
-
<input
|
|
80
|
-
type="number"
|
|
81
|
-
name="targetThreadId"
|
|
82
|
-
required
|
|
83
|
-
min={1}
|
|
84
|
-
step={1}
|
|
85
|
-
placeholder={fromCopy(copy, 'moderationForm.surgery.mergePlaceholder')}
|
|
86
|
-
className={`${FIELD} w-40`}
|
|
87
|
-
/>
|
|
88
|
-
</label>
|
|
89
|
-
<button type="submit" className={`${BUTTON} border-destructive/40 text-destructive`}>
|
|
90
|
-
{fromCopy(copy, 'moderationForm.surgery.merge')}
|
|
91
|
-
</button>
|
|
92
96
|
</form>
|
|
93
97
|
)}
|
|
94
98
|
</>
|