3a0b9dd5ee
Los nombres de categorías, foros, descripciones, clases e idiomas venían del seed solo en inglés. Ahora se muestran en el idioma de la página. Como es un conjunto fijo y conocido, se resuelve con un mapa de traducción en código (lib/forum-i18n.ts, inglés→español) que se aplica al vuelo, sin tocar el esquema (el inglés sigue siendo el valor guardado). Un texto no mapeado (p. ej. un foro nuevo creado desde el admin) se muestra tal cual. getForumIndex/getForum/getForumPath/getTopicPath aceptan el locale y localizan name/description/category (nunca el título de un tema, que es contenido de usuario). Las páginas pasan su locale. Verificado en producción: /es/forum en español (Noticias, Caballero de la Muerte, Foros por idioma) y /en/forum en inglés. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
180 lines
6.3 KiB
TypeScript
180 lines
6.3 KiB
TypeScript
import { notFound } from 'next/navigation'
|
|
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
|
import {
|
|
getTopic,
|
|
getTopicPath,
|
|
getPosts,
|
|
countPosts,
|
|
resolvePosters,
|
|
getUserPostCount,
|
|
} from '@/lib/forum'
|
|
import { formatForumDate } from '@/lib/forum-format'
|
|
import { getSession } from '@/lib/session'
|
|
import { forumIsModerator, canEditPost } from '@/lib/forum-perm'
|
|
import { PageShell } from '@/components/PageShell'
|
|
import { ForumBreadcrumb } from '@/components/ForumBreadcrumb'
|
|
import { Pagination } from '@/components/Pagination'
|
|
import { ReplyForm } from '@/components/ReplyForm'
|
|
import { EditPostForm } from '@/components/EditPostForm'
|
|
import { ForumModActions } from '@/components/ForumModActions'
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
const PER_PAGE = 5
|
|
|
|
export default async function TopicPage({
|
|
params,
|
|
searchParams,
|
|
}: {
|
|
params: Promise<{ locale: string; topicId: string }>
|
|
searchParams: Promise<{ page?: string }>
|
|
}) {
|
|
const { locale, topicId } = await params
|
|
setRequestLocale(locale)
|
|
const t = await getTranslations('Forum')
|
|
|
|
const id = Number(topicId)
|
|
const session = await getSession()
|
|
const isMod = await forumIsModerator(session)
|
|
const topic = await getTopic(id, isMod)
|
|
if (!topic) notFound()
|
|
|
|
const total = await countPosts(id, isMod)
|
|
const totalPages = Math.max(1, Math.ceil(total / PER_PAGE))
|
|
const page = Math.min(Math.max(1, Number((await searchParams).page) || 1), totalPages)
|
|
const posts = await getPosts(id, (page - 1) * PER_PAGE, PER_PAGE, isMod)
|
|
|
|
const posters = await resolvePosters(posts.map((p) => p.poster))
|
|
// Nº de posts por autor visible (para el panel lateral), en paralelo.
|
|
const uniquePosters = [...new Set(posts.map((p) => p.poster))]
|
|
const counts = new Map<number, number>(
|
|
await Promise.all(uniquePosters.map(async (pid) => [pid, await getUserPostCount(pid)] as const)),
|
|
)
|
|
|
|
const path = await getTopicPath(id, locale)
|
|
const canReply = Boolean(session.username) && (!topic.locked || isMod)
|
|
|
|
return (
|
|
<PageShell
|
|
title={
|
|
<>
|
|
{topic.sticky && `[${t('sticky')}] `}
|
|
{topic.locked && `[${t('locked')}] `}
|
|
{topic.deleted && `[${t('deleted')}] `}
|
|
{topic.name}
|
|
</>
|
|
}
|
|
>
|
|
<div className="main-wide">
|
|
{path && (
|
|
<ForumBreadcrumb
|
|
items={[
|
|
{ label: t('title'), href: '/forum' },
|
|
{ label: path.forum, href: `/forum/${path.forum_id}` },
|
|
{ label: path.topic },
|
|
]}
|
|
/>
|
|
)}
|
|
|
|
<div className="topic_header">
|
|
<div className="topic_title">
|
|
<h1>{topic.name}</h1>
|
|
<h3>{formatForumDate(topic.created, locale)}</h3>
|
|
</div>
|
|
<h4>
|
|
<b>{total}</b> {t('posts')}
|
|
</h4>
|
|
</div>
|
|
|
|
<div className="actions_c">
|
|
<span />
|
|
<Pagination page={page} totalPages={totalPages} hrefFor={(p) => `/forum/topic/${id}?page=${p}`} />
|
|
</div>
|
|
|
|
{posts.map((post) => {
|
|
const author = posters.get(post.poster)
|
|
const isStaff = author?.isStaff ?? false
|
|
return (
|
|
<div key={post.id} className={`topic_post${isStaff ? ' isStaff' : ''}${post.deleted ? ' post_deleted' : ''}`}>
|
|
<div className="left_side">
|
|
<div className="username_container">
|
|
<span className="username">{author?.name ?? `#${post.poster}`}</span>
|
|
</div>
|
|
<div className={`user_avatar${isStaff ? ' isStaff' : ''}`}>
|
|
<span
|
|
style={{
|
|
background: `url(${author?.avatar ?? '/forum/avatars/class-1.gif'}) no-repeat center`,
|
|
backgroundSize: 'cover',
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="user_info">
|
|
<h3 className="post_field">
|
|
<dt>
|
|
<i className="fa-solid fa-user-shield fa-fw" />
|
|
</dt>
|
|
<dd className={isStaff ? 'badge_staff' : undefined}>{isStaff ? t('staff') : t('member')}</dd>
|
|
</h3>
|
|
<h3 className="post_field">
|
|
<dt>
|
|
<i className="fa-solid fa-calendar fa-fw" />
|
|
</dt>
|
|
<dd>{formatForumDate(author?.joindate ?? null, locale) || '—'}</dd>
|
|
</h3>
|
|
<h3 className="post_field">
|
|
<dt>
|
|
<i className="fa-solid fa-comments fa-fw" />
|
|
</dt>
|
|
<dd>
|
|
{t('posts')}: {counts.get(post.poster) ?? 0}
|
|
</dd>
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
<div className="right_side">
|
|
<div className="post_container" dangerouslySetInnerHTML={{ __html: post.text }} />
|
|
<ul className="post_controls">
|
|
<li className="post_date">{formatForumDate(post.time, locale)}</li>
|
|
{canEditPost(session, isMod, post.poster) && (
|
|
<li>
|
|
<EditPostForm postId={post.id} initialText={post.text} />
|
|
</li>
|
|
)}
|
|
{isMod && (
|
|
<li>
|
|
<ForumModActions kind="post" id={post.id} deleted={post.deleted} />
|
|
</li>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
|
|
<div className="actions_c">
|
|
<span />
|
|
<Pagination page={page} totalPages={totalPages} hrefFor={(p) => `/forum/topic/${id}?page=${p}`} />
|
|
</div>
|
|
|
|
{isMod && (
|
|
<div className="forum-form-actions" style={{ marginBottom: 20 }}>
|
|
<ForumModActions kind="topic" id={id} deleted={topic.deleted} locked={topic.locked} />
|
|
</div>
|
|
)}
|
|
|
|
{canReply ? (
|
|
page === totalPages ? (
|
|
<ReplyForm topicId={id} />
|
|
) : (
|
|
<p className="forum-empty">
|
|
<a href={`/forum/topic/${id}?page=${totalPages}`}>{t('replyOnLastPage')}</a>
|
|
</p>
|
|
)
|
|
) : (
|
|
!session.username && <p className="forum-empty">{t('loginToPost')}</p>
|
|
)}
|
|
</div>
|
|
</PageShell>
|
|
)
|
|
}
|