Foro: mover tema entre foros (moderadores)

- lib/forum-write.ts: moveTopic (forum_id + moved=1). lib/forum.ts: listForumsForMove.
- API /api/forum/moderate: acción 'move' con forumId, validando foro destino visible
  y distinto del actual.
- TopicModBar: selector «Mover a» con los foros visibles (excl. el actual); tras mover,
  redirige al foro destino.
- i18n es/en: Forum.moveTopic.

Verificado: build OK, move 401 sin sesión, /forum 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 01:01:48 +00:00
parent 0247c6cc38
commit 7c1cd195ff
7 changed files with 69 additions and 10 deletions
+29 -3
View File
@@ -8,16 +8,18 @@ export function TopicModBar({
topicId,
locked,
sticky,
moveForums = [],
}: {
topicId: number
locked: boolean
sticky: boolean
moveForums?: { id: number; name: string }[]
}) {
const t = useTranslations('Forum')
const router = useRouter()
const [busy, setBusy] = useState(false)
async function act(action: string, confirmMsg?: string) {
async function act(action: string, extra?: Record<string, unknown>, confirmMsg?: string) {
if (busy) return
if (confirmMsg && !confirm(confirmMsg)) return
setBusy(true)
@@ -26,11 +28,13 @@ export function TopicModBar({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ topicId, action }),
body: JSON.stringify({ topicId, action, ...extra }),
})
const data: { success?: boolean; forumId?: number } = await res.json()
if (data.success && action === 'delete' && data.forumId) {
router.push(`/forum/${data.forumId}`)
} else if (data.success && action === 'move') {
router.push(`/forum/${extra?.forumId}`)
} else {
router.refresh()
}
@@ -58,9 +62,31 @@ export function TopicModBar({
>
{sticky ? t('unpin') : t('pin')}
</button>
{moveForums.length > 0 && (
<select
defaultValue=""
disabled={busy}
onChange={(e) => {
const forumId = Number(e.target.value)
e.target.value = ''
if (forumId) act('move', { forumId })
}}
className="nw-input inline-block w-auto py-1 text-xs disabled:opacity-60"
aria-label={t('moveTopic')}
>
<option value="" disabled>
{t('moveTopic')}
</option>
{moveForums.map((f) => (
<option key={f.id} value={f.id}>
{f.name}
</option>
))}
</select>
)}
<button
type="button"
onClick={() => act('delete', t('confirmDeleteTopic'))}
onClick={() => act('delete', undefined, t('confirmDeleteTopic'))}
disabled={busy}
className="text-xs text-red-400 hover:text-red-300 disabled:opacity-60"
>