Foro: perfil de usuario

- lib/forum.ts: getForumProfile (post_count, topic_count, joindate, gmlevel->status
  admin/gm, y últimos 5 temas), portado de forum/views.py profile().
- Página /forum/user/[username] con contadores, antigüedad, rango y temas recientes.
- Los nombres de autor en la vista del tema enlazan al perfil.
- i18n es/en: Forum.memberSince, recentTopics, roleAdmin, roleGm.

Verificado: build OK, /forum/user 200.

Foro COMPLETO: índice, foro/tema, crear, responder, editar/borrar, moderación
(lock/pin/mover/borrar/restaurar), búsqueda, paginación, editor rico y perfiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 01:08:49 +00:00
parent 8a3ba8ff40
commit 00c03d84e1
5 changed files with 142 additions and 3 deletions
+61
View File
@@ -211,6 +211,67 @@ export async function getPosts(topicId: number, offset = 0, limit = 15, includeD
}
}
export interface ForumProfile {
username: string
postCount: number
topicCount: number
joindate: string | null
gmlevel: number
status: 'admin' | 'gm' | null
recentTopics: { id: number; name: string; forum_id: number }[]
}
/** Perfil público de un usuario del foro (contadores + antigüedad + rango). */
export async function getForumProfile(username: string): Promise<ForumProfile> {
const profile: ForumProfile = {
username,
postCount: 0,
topicCount: 0,
joindate: null,
gmlevel: 0,
status: null,
recentTopics: [],
}
try {
const [pc] = await db(DB.web).query<RowDataPacket[]>(
'SELECT COUNT(*) AS n FROM forum_posts WHERE poster = ? AND deleted = 0',
[username],
)
profile.postCount = Number(pc[0]?.n ?? 0)
const [tc] = await db(DB.web).query<RowDataPacket[]>(
'SELECT COUNT(*) AS n FROM forum_topics WHERE poster = ? AND deleted = 0',
[username],
)
profile.topicCount = Number(tc[0]?.n ?? 0)
const [rt] = await db(DB.web).query<RowDataPacket[]>(
'SELECT id, name, forum_id FROM forum_topics WHERE poster = ? AND deleted = 0 ORDER BY updated_at DESC, id DESC LIMIT 5',
[username],
)
profile.recentTopics = rt.map((r) => ({ id: r.id, name: r.name, forum_id: r.forum_id }))
} catch {
/* tablas de foro no pobladas */
}
try {
const [acc] = await db(DB.auth).query<RowDataPacket[]>(
'SELECT id, joindate FROM account WHERE username = ?',
[username],
)
if (acc[0]) {
profile.joindate = acc[0].joindate ? new Date(acc[0].joindate).toISOString() : null
const [ga] = await db(DB.auth).query<RowDataPacket[]>(
'SELECT MAX(gmlevel) AS lvl FROM account_access WHERE id = ?',
[acc[0].id],
)
profile.gmlevel = ga[0]?.lvl != null ? Number(ga[0].lvl) : 0
}
} catch {
/* account_access puede no existir */
}
if (profile.gmlevel >= 16) profile.status = 'admin'
else if (profile.gmlevel >= 1) profile.status = 'gm'
return profile
}
/** Nº de temas que coinciden con la búsqueda (foros visibles). */
export async function countSearchTopics(query: string): Promise<number> {
const like = `%${query}%`