Foro: restaurar posts y temas borrados (moderadores)

- lib/forum.ts: getTopic/getPosts/countPosts aceptan includeDeleted (mods ven lo
  borrado); Post.deleted y TopicFull.deleted.
- API: PUT /api/forum/post restaura post (mod-only); moderate action 'restore'
  restaura tema (usa includeDeleted al leer el tema).
- UI: posts borrados atenuados con badge y botón Restaurar (PostActions);
  TopicModBar muestra banner + Restaurar tema cuando el tema está borrado.
- i18n es/en: Forum.restore, restoreTopic, deletedMark.

Verificado: build OK, PUT post y moderate restore 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:07:18 +00:00
parent 72fdead9f3
commit 8a3ba8ff40
8 changed files with 119 additions and 21 deletions
+13 -6
View File
@@ -37,6 +37,7 @@ export interface Post {
text: string
time: string | null
edited: boolean
deleted: boolean
}
export interface TopicFull {
@@ -46,6 +47,7 @@ export interface TopicFull {
poster_id: number | null
locked: boolean
sticky: boolean
deleted: boolean
}
export interface SearchResult {
@@ -148,10 +150,11 @@ export async function getTopics(forumId: number, offset = 0, limit = 20): Promis
}
}
export async function getTopic(id: number): Promise<TopicFull | null> {
export async function getTopic(id: number, includeDeleted = false): Promise<TopicFull | null> {
try {
const where = includeDeleted ? 'id = ?' : 'id = ? AND deleted = 0'
const [rows] = await db(DB.web).query<RowDataPacket[]>(
'SELECT id, name, forum_id, poster_id, locked, sticky FROM forum_topics WHERE id = ? AND deleted = 0',
`SELECT id, name, forum_id, poster_id, locked, sticky, deleted FROM forum_topics WHERE ${where}`,
[id],
)
const r = rows[0]
@@ -163,6 +166,7 @@ export async function getTopic(id: number): Promise<TopicFull | null> {
poster_id: r.poster_id ?? null,
locked: r.locked === 1,
sticky: r.sticky === 1,
deleted: r.deleted === 1,
}
: null
} catch {
@@ -170,10 +174,11 @@ export async function getTopic(id: number): Promise<TopicFull | null> {
}
}
export async function countPosts(topicId: number): Promise<number> {
export async function countPosts(topicId: number, includeDeleted = false): Promise<number> {
try {
const del = includeDeleted ? '' : 'AND deleted = 0'
const [rows] = await db(DB.web).query<RowDataPacket[]>(
'SELECT COUNT(*) AS n FROM forum_posts WHERE topic_id = ? AND deleted = 0',
`SELECT COUNT(*) AS n FROM forum_posts WHERE topic_id = ? ${del}`,
[topicId],
)
return Number(rows[0]?.n ?? 0)
@@ -182,10 +187,11 @@ export async function countPosts(topicId: number): Promise<number> {
}
}
export async function getPosts(topicId: number, offset = 0, limit = 15): Promise<Post[]> {
export async function getPosts(topicId: number, offset = 0, limit = 15, includeDeleted = false): Promise<Post[]> {
try {
const del = includeDeleted ? '' : 'AND deleted = 0'
const [rows] = await db(DB.web).query<RowDataPacket[]>(
'SELECT id, poster, poster_id, text, time, created_at, updated_at FROM forum_posts WHERE topic_id = ? AND deleted = 0 ORDER BY time ASC, id ASC LIMIT ? OFFSET ?',
`SELECT id, poster, poster_id, text, time, created_at, updated_at, deleted FROM forum_posts WHERE topic_id = ? ${del} ORDER BY time ASC, id ASC LIMIT ? OFFSET ?`,
[topicId, limit, offset],
)
return rows.map((r) => ({
@@ -198,6 +204,7 @@ export async function getPosts(topicId: number, offset = 0, limit = 15): Promise
edited: Boolean(
r.updated_at && r.created_at && new Date(r.updated_at).getTime() - new Date(r.created_at).getTime() > 2000,
),
deleted: r.deleted === 1,
}))
} catch {
return []