e743d98777
- lib/forum.ts: getForumIndex (categorías + foros visibles con contadores y último tema, port de forum/db.py), getForum/getTopics, getTopic/getPosts (acore_web). - Páginas app/[locale]/forum (índice), /forum/[forumId] (temas), /forum/topic/[topicId] (mensajes; render del HTML ya saneado por Django). Enlace "Foros" en la cabecera. - Catálogo Forum + Nav.forum. Verificado: /forum 200, foros/temas inexistentes 404, enlace en la cabecera. Solo lectura; pendiente escribir (crear tema/responder + saneado nh3->TS) y moderación. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
135 lines
4.2 KiB
TypeScript
135 lines
4.2 KiB
TypeScript
import type { RowDataPacket } from 'mysql2'
|
|
import { db, DB } from './db'
|
|
|
|
export interface ForumInfo {
|
|
id: number
|
|
category_id: number
|
|
name: string
|
|
description: string
|
|
icon: string | null
|
|
topic_count: number
|
|
post_count: number
|
|
last_topic_id: number | null
|
|
last_topic_name: string | null
|
|
last_topic_poster: string | null
|
|
}
|
|
|
|
export interface Category {
|
|
id: number
|
|
name: string
|
|
forums: ForumInfo[]
|
|
}
|
|
|
|
export interface Topic {
|
|
id: number
|
|
name: string
|
|
poster: string
|
|
created: string | null
|
|
sticky: boolean
|
|
locked: boolean
|
|
views: number
|
|
}
|
|
|
|
export interface Post {
|
|
id: number
|
|
poster: string
|
|
text: string
|
|
time: string | null
|
|
}
|
|
|
|
/** Índice del foro: categorías con sus foros visibles (+ contadores y último tema). */
|
|
export async function getForumIndex(): Promise<Category[]> {
|
|
try {
|
|
const [cats] = await db(DB.web).query<RowDataPacket[]>(
|
|
'SELECT id, name FROM forum_categories ORDER BY `order`, id',
|
|
)
|
|
const [forums] = await db(DB.web).query<RowDataPacket[]>(
|
|
`SELECT f.id, f.category_id, f.name, f.description, f.icon,
|
|
(SELECT COUNT(*) FROM forum_topics t WHERE t.forum_id = f.id AND t.deleted = 0) AS topic_count,
|
|
(SELECT COUNT(*) FROM forum_posts p JOIN forum_topics t ON p.topic_id = t.id
|
|
WHERE t.forum_id = f.id AND p.deleted = 0 AND t.deleted = 0) AS post_count,
|
|
(SELECT t2.id FROM forum_topics t2 WHERE t2.forum_id = f.id AND t2.deleted = 0
|
|
ORDER BY t2.updated_at DESC, t2.id DESC LIMIT 1) AS last_topic_id,
|
|
(SELECT t3.name FROM forum_topics t3 WHERE t3.forum_id = f.id AND t3.deleted = 0
|
|
ORDER BY t3.updated_at DESC, t3.id DESC LIMIT 1) AS last_topic_name,
|
|
(SELECT t4.poster FROM forum_topics t4 WHERE t4.forum_id = f.id AND t4.deleted = 0
|
|
ORDER BY t4.updated_at DESC, t4.id DESC LIMIT 1) AS last_topic_poster
|
|
FROM forums f
|
|
WHERE f.visibility = 1 AND f.deleted = 0
|
|
ORDER BY f.\`order\`, f.id`,
|
|
)
|
|
const byCat = new Map<number, ForumInfo[]>()
|
|
for (const f of forums) {
|
|
const info = f as unknown as ForumInfo
|
|
if (!byCat.has(info.category_id)) byCat.set(info.category_id, [])
|
|
byCat.get(info.category_id)!.push(info)
|
|
}
|
|
return cats
|
|
.map((c) => ({ id: c.id, name: c.name, forums: byCat.get(c.id) ?? [] }))
|
|
.filter((c) => c.forums.length > 0)
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export async function getForum(id: number): Promise<{ name: string; description: string } | null> {
|
|
try {
|
|
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
|
'SELECT name, description FROM forums WHERE id = ? AND visibility = 1 AND deleted = 0',
|
|
[id],
|
|
)
|
|
return rows[0] ? { name: rows[0].name, description: rows[0].description } : null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function getTopics(forumId: number): Promise<Topic[]> {
|
|
try {
|
|
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
|
'SELECT id, name, poster, created, sticky, locked, views FROM forum_topics WHERE forum_id = ? AND deleted = 0 ORDER BY sticky DESC, updated_at DESC, id DESC',
|
|
[forumId],
|
|
)
|
|
return rows.map((r) => ({
|
|
id: r.id,
|
|
name: r.name,
|
|
poster: r.poster,
|
|
created: r.created ? new Date(r.created).toISOString() : null,
|
|
sticky: r.sticky === 1,
|
|
locked: r.locked === 1,
|
|
views: r.views ?? 0,
|
|
}))
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export async function getTopic(id: number): Promise<{ name: string; locked: boolean } | null> {
|
|
try {
|
|
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
|
'SELECT name, locked FROM forum_topics WHERE id = ? AND deleted = 0',
|
|
[id],
|
|
)
|
|
return rows[0] ? { name: rows[0].name, locked: rows[0].locked === 1 } : null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function getPosts(topicId: number): Promise<Post[]> {
|
|
try {
|
|
const [rows] = await db(DB.web).query<RowDataPacket[]>(
|
|
'SELECT id, poster, text, time FROM forum_posts WHERE topic_id = ? AND deleted = 0 ORDER BY time ASC, id ASC',
|
|
[topicId],
|
|
)
|
|
return rows.map((r) => ({
|
|
id: r.id,
|
|
poster: r.poster,
|
|
text: r.text,
|
|
time: r.time ? new Date(r.time).toISOString() : null,
|
|
}))
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|