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:
@@ -65,7 +65,9 @@ export default async function TopicPage({
|
||||
<article key={post.id} className={`nw-card ${post.deleted ? 'opacity-50 ring-1 ring-red-900/50' : ''}`}>
|
||||
<div className="mb-2 flex items-center justify-between border-b border-amber-900/40 pb-2 text-sm">
|
||||
<span className="font-semibold text-amber-400">
|
||||
{post.poster}
|
||||
<Link href={`/forum/user/${encodeURIComponent(post.poster)}`} className="hover:text-amber-300 hover:underline">
|
||||
{post.poster}
|
||||
</Link>
|
||||
{post.deleted && <span className="ml-2 text-xs text-red-400">[{t('deletedMark')}]</span>}
|
||||
</span>
|
||||
{post.time && (
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
import { getForumProfile } from '@/lib/forum'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function ForumProfilePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string; username: string }>
|
||||
}) {
|
||||
const { locale, username } = await params
|
||||
setRequestLocale(locale)
|
||||
const t = await getTranslations('Forum')
|
||||
const p = await getForumProfile(decodeURIComponent(username))
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-2xl px-4 py-8">
|
||||
<p className="mb-2 text-sm">
|
||||
<Link href="/forum" className="text-sky-400 hover:underline">
|
||||
← {t('backToForum')}
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div className="nw-card">
|
||||
<h1 className="flex items-center gap-2 text-2xl font-bold text-amber-500">
|
||||
{p.username}
|
||||
{p.status && (
|
||||
<span className="rounded bg-nw-gold/15 px-2 py-0.5 text-xs text-nw-gold-light">
|
||||
{p.status === 'admin' ? t('roleAdmin') : t('roleGm')}
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
<div className="mt-4 grid grid-cols-2 gap-3 text-sm sm:grid-cols-3">
|
||||
<div>
|
||||
<p className="text-nw-muted">{t('posts')}</p>
|
||||
<p className="text-lg font-semibold text-amber-300">{p.postCount}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-nw-muted">{t('topics')}</p>
|
||||
<p className="text-lg font-semibold text-amber-300">{p.topicCount}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-nw-muted">{t('memberSince')}</p>
|
||||
<p className="font-semibold text-amber-300">
|
||||
{p.joindate ? new Date(p.joindate).toLocaleDateString(locale) : '—'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{p.recentTopics.length > 0 && (
|
||||
<section className="mt-6">
|
||||
<h2 className="mb-3 text-lg font-semibold text-nw-gold-light">{t('recentTopics')}</h2>
|
||||
<ul className="divide-y divide-amber-900/40">
|
||||
{p.recentTopics.map((topic) => (
|
||||
<li key={topic.id} className="py-2">
|
||||
<Link href={`/forum/topic/${topic.id}`} className="text-amber-300 hover:text-amber-400">
|
||||
{topic.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -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}%`
|
||||
|
||||
@@ -297,7 +297,11 @@
|
||||
"moveTopic": "Move to",
|
||||
"restore": "Restore",
|
||||
"restoreTopic": "Restore topic",
|
||||
"deletedMark": "Deleted"
|
||||
"deletedMark": "Deleted",
|
||||
"memberSince": "Member since",
|
||||
"recentTopics": "Recent topics",
|
||||
"roleAdmin": "Administrator",
|
||||
"roleGm": "GM"
|
||||
},
|
||||
"Admin": {
|
||||
"title": "Admin panel",
|
||||
|
||||
@@ -297,7 +297,11 @@
|
||||
"moveTopic": "Mover a",
|
||||
"restore": "Restaurar",
|
||||
"restoreTopic": "Restaurar tema",
|
||||
"deletedMark": "Borrado"
|
||||
"deletedMark": "Borrado",
|
||||
"memberSince": "Miembro desde",
|
||||
"recentTopics": "Temas recientes",
|
||||
"roleAdmin": "Administrador",
|
||||
"roleGm": "GM"
|
||||
},
|
||||
"Admin": {
|
||||
"title": "Panel de administración",
|
||||
|
||||
Reference in New Issue
Block a user