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 { notFound } from 'next/navigation'
|
||||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||||
import { Link } from '@/i18n/navigation'
|
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 { getSession } from '@/lib/session'
|
||||||
import { forumIsModerator, canEditPost } from '@/lib/forum-perm'
|
import { forumIsModerator, canEditPost } from '@/lib/forum-perm'
|
||||||
import { ReplyForm } from '@/components/ReplyForm'
|
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 posts = await getPosts(id, (page - 1) * PER_PAGE, PER_PAGE)
|
||||||
const session = await getSession()
|
const session = await getSession()
|
||||||
const isMod = await forumIsModerator(session)
|
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)
|
const canReply = Boolean(session.username) && (!topic!.locked || isMod)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,7 +49,9 @@ export default async function TopicPage({
|
|||||||
{topic!.name}
|
{topic!.name}
|
||||||
</h1>
|
</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">
|
<div className="space-y-4">
|
||||||
{posts.map((post) => (
|
{posts.map((post) => (
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { getSession } from '@/lib/session'
|
import { getSession } from '@/lib/session'
|
||||||
import { getTopic } from '@/lib/forum'
|
import { getTopic, getForum } from '@/lib/forum'
|
||||||
import { setTopicFlag } from '@/lib/forum-write'
|
import { setTopicFlag, moveTopic } from '@/lib/forum-write'
|
||||||
import { forumIsModerator } from '@/lib/forum-perm'
|
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. */
|
/** Moderación de temas: bloquear, fijar, borrar. Solo moderadores. */
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
@@ -36,6 +36,17 @@ export async function POST(request: Request) {
|
|||||||
case 'delete':
|
case 'delete':
|
||||||
await setTopicFlag(topicId, 'deleted', true)
|
await setTopicFlag(topicId, 'deleted', true)
|
||||||
return Response.json({ success: true, forumId: topic.forum_id })
|
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:
|
default:
|
||||||
return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 })
|
return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,18 @@ export function TopicModBar({
|
|||||||
topicId,
|
topicId,
|
||||||
locked,
|
locked,
|
||||||
sticky,
|
sticky,
|
||||||
|
moveForums = [],
|
||||||
}: {
|
}: {
|
||||||
topicId: number
|
topicId: number
|
||||||
locked: boolean
|
locked: boolean
|
||||||
sticky: boolean
|
sticky: boolean
|
||||||
|
moveForums?: { id: number; name: string }[]
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations('Forum')
|
const t = useTranslations('Forum')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [busy, setBusy] = useState(false)
|
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 (busy) return
|
||||||
if (confirmMsg && !confirm(confirmMsg)) return
|
if (confirmMsg && !confirm(confirmMsg)) return
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
@@ -26,11 +28,13 @@ export function TopicModBar({
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
body: JSON.stringify({ topicId, action }),
|
body: JSON.stringify({ topicId, action, ...extra }),
|
||||||
})
|
})
|
||||||
const data: { success?: boolean; forumId?: number } = await res.json()
|
const data: { success?: boolean; forumId?: number } = await res.json()
|
||||||
if (data.success && action === 'delete' && data.forumId) {
|
if (data.success && action === 'delete' && data.forumId) {
|
||||||
router.push(`/forum/${data.forumId}`)
|
router.push(`/forum/${data.forumId}`)
|
||||||
|
} else if (data.success && action === 'move') {
|
||||||
|
router.push(`/forum/${extra?.forumId}`)
|
||||||
} else {
|
} else {
|
||||||
router.refresh()
|
router.refresh()
|
||||||
}
|
}
|
||||||
@@ -58,9 +62,31 @@ export function TopicModBar({
|
|||||||
>
|
>
|
||||||
{sticky ? t('unpin') : t('pin')}
|
{sticky ? t('unpin') : t('pin')}
|
||||||
</button>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => act('delete', t('confirmDeleteTopic'))}
|
onClick={() => act('delete', undefined, t('confirmDeleteTopic'))}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
className="text-xs text-red-400 hover:text-red-300 disabled:opacity-60"
|
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])
|
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). */
|
/** Comprueba que un foro existe y es visible (para crear tema). */
|
||||||
export async function forumIsPostable(forumId: number): Promise<boolean> {
|
export async function forumIsPostable(forumId: number): Promise<boolean> {
|
||||||
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
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> {
|
export async function getForum(id: number): Promise<{ name: string; description: string } | null> {
|
||||||
try {
|
try {
|
||||||
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
||||||
|
|||||||
@@ -293,7 +293,8 @@
|
|||||||
"noResults": "No results for “{query}”.",
|
"noResults": "No results for “{query}”.",
|
||||||
"resultsCount": "{count} result(s) for “{query}”.",
|
"resultsCount": "{count} result(s) for “{query}”.",
|
||||||
"in": "in",
|
"in": "in",
|
||||||
"replyOnLastPage": "Go to the last page to reply"
|
"replyOnLastPage": "Go to the last page to reply",
|
||||||
|
"moveTopic": "Move to"
|
||||||
},
|
},
|
||||||
"Admin": {
|
"Admin": {
|
||||||
"title": "Admin panel",
|
"title": "Admin panel",
|
||||||
|
|||||||
@@ -293,7 +293,8 @@
|
|||||||
"noResults": "No hay resultados para «{query}».",
|
"noResults": "No hay resultados para «{query}».",
|
||||||
"resultsCount": "{count} resultado(s) para «{query}».",
|
"resultsCount": "{count} resultado(s) para «{query}».",
|
||||||
"in": "en",
|
"in": "en",
|
||||||
"replyOnLastPage": "Ir a la última página para responder"
|
"replyOnLastPage": "Ir a la última página para responder",
|
||||||
|
"moveTopic": "Mover a"
|
||||||
},
|
},
|
||||||
"Admin": {
|
"Admin": {
|
||||||
"title": "Panel de administración",
|
"title": "Panel de administración",
|
||||||
|
|||||||
Reference in New Issue
Block a user