From e743d9877798fe6bb9f671ab67ea437ee701366f Mon Sep 17 00:00:00 2001 From: adevopg Date: Sun, 12 Jul 2026 23:45:39 +0000 Subject: [PATCH] =?UTF-8?q?Foro=20(lectura):=20=C3=ADndice,=20temas=20y=20?= =?UTF-8?q?mensajes=20en=20Next.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../app/[locale]/forum/[forumId]/page.tsx | 57 ++++++++ web-next/app/[locale]/forum/page.tsx | 52 +++++++ .../[locale]/forum/topic/[topicId]/page.tsx | 52 +++++++ web-next/components/Header.tsx | 3 + web-next/lib/forum.ts | 134 ++++++++++++++++++ web-next/messages/en.json | 20 ++- web-next/messages/es.json | 20 ++- 7 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 web-next/app/[locale]/forum/[forumId]/page.tsx create mode 100644 web-next/app/[locale]/forum/page.tsx create mode 100644 web-next/app/[locale]/forum/topic/[topicId]/page.tsx create mode 100644 web-next/lib/forum.ts diff --git a/web-next/app/[locale]/forum/[forumId]/page.tsx b/web-next/app/[locale]/forum/[forumId]/page.tsx new file mode 100644 index 0000000..5fded4f --- /dev/null +++ b/web-next/app/[locale]/forum/[forumId]/page.tsx @@ -0,0 +1,57 @@ +import { notFound } from 'next/navigation' +import { getTranslations, setRequestLocale } from 'next-intl/server' +import { Link } from '@/i18n/navigation' +import { getForum, getTopics } from '@/lib/forum' + +export const dynamic = 'force-dynamic' + +export default async function ForumPage({ + params, +}: { + params: Promise<{ locale: string; forumId: string }> +}) { + const { locale, forumId } = await params + setRequestLocale(locale) + const t = await getTranslations('Forum') + + const id = Number(forumId) + const forum = await getForum(id) + if (!forum) notFound() + const topics = await getTopics(id) + + return ( +
+

+ + ← {t('backToForum')} + +

+

{forum!.name}

+ {forum!.description &&

{forum!.description}

} + + {topics.length === 0 ? ( +

{t('noTopics')}

+ ) : ( +
    + {topics.map((topic) => ( +
  • +
    + + {topic.sticky && [{t('sticky')}]} + {topic.locked && [{t('locked')}]} + {topic.name} + +

    + {t('by')} {topic.poster} +

    +
    +
    + {topic.views} {t('views')} +
    +
  • + ))} +
+ )} +
+ ) +} diff --git a/web-next/app/[locale]/forum/page.tsx b/web-next/app/[locale]/forum/page.tsx new file mode 100644 index 0000000..5ec7bc5 --- /dev/null +++ b/web-next/app/[locale]/forum/page.tsx @@ -0,0 +1,52 @@ +import { getTranslations, setRequestLocale } from 'next-intl/server' +import { Link } from '@/i18n/navigation' +import { getForumIndex } from '@/lib/forum' + +export const dynamic = 'force-dynamic' + +export default async function ForumIndexPage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params + setRequestLocale(locale) + const t = await getTranslations('Forum') + const categories = await getForumIndex() + + return ( +
+

{t('title')}

+ {categories.length === 0 ? ( +

{t('noForums')}

+ ) : ( + categories.map((cat) => ( +
+

{cat.name}

+
    + {cat.forums.map((f) => ( +
  • +
    + + {f.name} + + {f.description &&

    {f.description}

    } +
    +
    +
    + {f.topic_count} {t('topics')} · {f.post_count} {t('posts')} +
    + {f.last_topic_id && ( +
    + + {f.last_topic_name} + {' '} + {t('by')} {f.last_topic_poster} +
    + )} +
    +
  • + ))} +
+
+ )) + )} +
+ ) +} diff --git a/web-next/app/[locale]/forum/topic/[topicId]/page.tsx b/web-next/app/[locale]/forum/topic/[topicId]/page.tsx new file mode 100644 index 0000000..e24fe31 --- /dev/null +++ b/web-next/app/[locale]/forum/topic/[topicId]/page.tsx @@ -0,0 +1,52 @@ +import { notFound } from 'next/navigation' +import { getTranslations, setRequestLocale } from 'next-intl/server' +import { Link } from '@/i18n/navigation' +import { getTopic, getPosts } from '@/lib/forum' + +export const dynamic = 'force-dynamic' + +export default async function TopicPage({ + params, +}: { + params: Promise<{ locale: string; topicId: string }> +}) { + const { locale, topicId } = await params + setRequestLocale(locale) + const t = await getTranslations('Forum') + + const id = Number(topicId) + const topic = await getTopic(id) + if (!topic) notFound() + const posts = await getPosts(id) + + return ( +
+

+ + ← {t('backToForum')} + +

+

+ {topic!.locked && [{t('locked')}]} + {topic!.name} +

+ +
+ {posts.map((post) => ( +
+
+ {post.poster} + {post.time && ( + {new Date(post.time).toLocaleString(locale)} + )} +
+
+
+ ))} +
+
+ ) +} diff --git a/web-next/components/Header.tsx b/web-next/components/Header.tsx index 6d8c264..4b82ec2 100644 --- a/web-next/components/Header.tsx +++ b/web-next/components/Header.tsx @@ -19,6 +19,9 @@ export async function Header() { {t('home')} + + {t('forum')} + {loggedIn ? ( <> diff --git a/web-next/lib/forum.ts b/web-next/lib/forum.ts new file mode 100644 index 0000000..7c30135 --- /dev/null +++ b/web-next/lib/forum.ts @@ -0,0 +1,134 @@ +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 { + try { + const [cats] = await db(DB.web).query( + 'SELECT id, name FROM forum_categories ORDER BY `order`, id', + ) + const [forums] = await db(DB.web).query( + `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() + 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( + '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 { + try { + const [rows] = await db(DB.web).query( + '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( + '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 { + try { + const [rows] = await db(DB.web).query( + '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 [] + } +} diff --git a/web-next/messages/en.json b/web-next/messages/en.json index f6f0090..6088f23 100644 --- a/web-next/messages/en.json +++ b/web-next/messages/en.json @@ -9,7 +9,8 @@ "register": "Create account", "account": "My account", "logout": "Log out", - "language": "Language" + "language": "Language", + "forum": "Forums" }, "Home": { "brand": "Nova WoW", @@ -237,5 +238,22 @@ "success": "Email changed successfully! You can now sign in with the new email.", "error": "The link is invalid, expired or the account was not found.", "goLogin": "Go to sign in" + }, + "Forum": { + "title": "Forums", + "noForums": "No forums available yet.", + "topics": "Topics", + "posts": "Posts", + "lastTopic": "Latest topic", + "by": "by", + "views": "Views", + "noTopics": "No topics in this forum yet.", + "locked": "Locked", + "sticky": "Pinned", + "backToForum": "Back to forums", + "backToTopics": "Back to forum", + "notFound": "Not found", + "newTopic": "New topic", + "reply": "Reply" } } diff --git a/web-next/messages/es.json b/web-next/messages/es.json index 8a0ce81..17a89df 100644 --- a/web-next/messages/es.json +++ b/web-next/messages/es.json @@ -9,7 +9,8 @@ "register": "Crear cuenta", "account": "Mi cuenta", "logout": "Cerrar sesión", - "language": "Idioma" + "language": "Idioma", + "forum": "Foros" }, "Home": { "brand": "Nova WoW", @@ -237,5 +238,22 @@ "success": "¡Correo cambiado con éxito! Ya puedes iniciar sesión con el nuevo correo.", "error": "El enlace no es válido, ha caducado o no se ha encontrado la cuenta.", "goLogin": "Ir a iniciar sesión" + }, + "Forum": { + "title": "Foros", + "noForums": "No hay foros disponibles todavía.", + "topics": "Temas", + "posts": "Mensajes", + "lastTopic": "Último tema", + "by": "por", + "views": "Visitas", + "noTopics": "No hay temas en este foro todavía.", + "locked": "Cerrado", + "sticky": "Fijado", + "backToForum": "Volver a los foros", + "backToTopics": "Volver al foro", + "notFound": "No encontrado", + "newTopic": "Nuevo tema", + "reply": "Responder" } }