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:
@@ -1,7 +1,7 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
import { getTopic, getPosts, countPosts } from '@/lib/forum'
|
||||
import { getTopic, getPosts, countPosts, listForumsForMove } from '@/lib/forum'
|
||||
import { getSession } from '@/lib/session'
|
||||
import { forumIsModerator, canEditPost } from '@/lib/forum-perm'
|
||||
import { ReplyForm } from '@/components/ReplyForm'
|
||||
@@ -33,6 +33,7 @@ export default async function TopicPage({
|
||||
const posts = await getPosts(id, (page - 1) * PER_PAGE, PER_PAGE)
|
||||
const session = await getSession()
|
||||
const isMod = await forumIsModerator(session)
|
||||
const moveForums = isMod ? (await listForumsForMove()).filter((f) => f.id !== topic!.forum_id) : []
|
||||
const canReply = Boolean(session.username) && (!topic!.locked || isMod)
|
||||
|
||||
return (
|
||||
@@ -48,7 +49,9 @@ export default async function TopicPage({
|
||||
{topic!.name}
|
||||
</h1>
|
||||
|
||||
{isMod && <TopicModBar topicId={id} locked={topic!.locked} sticky={topic!.sticky} />}
|
||||
{isMod && (
|
||||
<TopicModBar topicId={id} locked={topic!.locked} sticky={topic!.sticky} moveForums={moveForums} />
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{posts.map((post) => (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getSession } from '@/lib/session'
|
||||
import { getTopic } from '@/lib/forum'
|
||||
import { setTopicFlag } from '@/lib/forum-write'
|
||||
import { getTopic, getForum } from '@/lib/forum'
|
||||
import { setTopicFlag, moveTopic } from '@/lib/forum-write'
|
||||
import { forumIsModerator } from '@/lib/forum-perm'
|
||||
|
||||
type Action = 'lock' | 'unlock' | 'sticky' | 'unsticky' | 'delete'
|
||||
type Action = 'lock' | 'unlock' | 'sticky' | 'unsticky' | 'delete' | 'move'
|
||||
|
||||
/** Moderación de temas: bloquear, fijar, borrar. Solo moderadores. */
|
||||
export async function POST(request: Request) {
|
||||
@@ -36,6 +36,17 @@ export async function POST(request: Request) {
|
||||
case 'delete':
|
||||
await setTopicFlag(topicId, 'deleted', true)
|
||||
return Response.json({ success: true, forumId: topic.forum_id })
|
||||
case 'move': {
|
||||
const newForumId = Number(b.forumId)
|
||||
if (!newForumId || newForumId === topic.forum_id) {
|
||||
return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 })
|
||||
}
|
||||
if (!(await getForum(newForumId))) {
|
||||
return Response.json({ success: false, error: 'invalidForum' }, { status: 400 })
|
||||
}
|
||||
await moveTopic(topicId, newForumId)
|
||||
return Response.json({ success: true })
|
||||
}
|
||||
default:
|
||||
return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 })
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -71,6 +71,11 @@ export async function setTopicFlag(
|
||||
await db(DB.web).query(`UPDATE forum_topics SET ${field} = ? WHERE id = ?`, [value ? 1 : 0, topicId])
|
||||
}
|
||||
|
||||
/** Mueve un tema a otro foro (marca moved=1). Solo moderación. */
|
||||
export async function moveTopic(topicId: number, newForumId: number): Promise<void> {
|
||||
await db(DB.web).query('UPDATE forum_topics SET forum_id = ?, moved = 1 WHERE id = ?', [newForumId, topicId])
|
||||
}
|
||||
|
||||
/** Comprueba que un foro existe y es visible (para crear tema). */
|
||||
export async function forumIsPostable(forumId: number): Promise<boolean> {
|
||||
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
||||
|
||||
@@ -92,6 +92,18 @@ export async function getForumIndex(): Promise<Category[]> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Lista de foros visibles para el desplegable de «mover tema». */
|
||||
export async function listForumsForMove(): Promise<{ id: number; name: string }[]> {
|
||||
try {
|
||||
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
||||
'SELECT id, name FROM forums WHERE visibility = 1 AND deleted = 0 ORDER BY `order`, id',
|
||||
)
|
||||
return rows.map((r) => ({ id: r.id, name: r.name }))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function getForum(id: number): Promise<{ name: string; description: string } | null> {
|
||||
try {
|
||||
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
||||
|
||||
@@ -293,7 +293,8 @@
|
||||
"noResults": "No results for “{query}”.",
|
||||
"resultsCount": "{count} result(s) for “{query}”.",
|
||||
"in": "in",
|
||||
"replyOnLastPage": "Go to the last page to reply"
|
||||
"replyOnLastPage": "Go to the last page to reply",
|
||||
"moveTopic": "Move to"
|
||||
},
|
||||
"Admin": {
|
||||
"title": "Admin panel",
|
||||
|
||||
@@ -293,7 +293,8 @@
|
||||
"noResults": "No hay resultados para «{query}».",
|
||||
"resultsCount": "{count} resultado(s) para «{query}».",
|
||||
"in": "en",
|
||||
"replyOnLastPage": "Ir a la última página para responder"
|
||||
"replyOnLastPage": "Ir a la última página para responder",
|
||||
"moveTopic": "Mover a"
|
||||
},
|
||||
"Admin": {
|
||||
"title": "Panel de administración",
|
||||
|
||||
Reference in New Issue
Block a user