From 86c9b01c767d60eb7a7a64d0cfbd1f057e68829b Mon Sep 17 00:00:00 2001 From: adevopg Date: Tue, 14 Jul 2026 19:14:07 +0000 Subject: [PATCH] Noticias multiidioma (ES/EN) con fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit home_noticia gana columnas titulo_en/contenido_en (sql/add_news_en.sql). getNoticias(locale) usa la versión EN si existe, si no cae al español (título y contenido independientes); la fecha también se formatea por idioma. El panel de admin permite escribir la traducción al crear y EDITAR noticias existentes (nuevo PUT /api/admin/news/[id], updateNews). Claves Admin nuevas (título/ contenido EN, editar/guardar). Probado: /en/ muestra EN, /es/ ES; PUT guardado (403 sin sesión). Co-Authored-By: Claude Opus 4.8 (1M context) --- web-next/app/[locale]/page.tsx | 12 +-- web-next/app/api/admin/news/[id]/route.ts | 18 ++++- web-next/app/api/admin/news/route.ts | 4 +- web-next/components/AdminNewsManager.tsx | 97 ++++++++++++++++++----- web-next/lib/admin-news.ts | 35 +++++++- web-next/lib/home.ts | 14 +++- web-next/messages/en.json | 8 +- web-next/messages/es.json | 8 +- web-next/sql/add_news_en.sql | 5 ++ 9 files changed, 162 insertions(+), 39 deletions(-) create mode 100644 web-next/sql/add_news_en.sql diff --git a/web-next/app/[locale]/page.tsx b/web-next/app/[locale]/page.tsx index fbada07..80212b0 100644 --- a/web-next/app/[locale]/page.tsx +++ b/web-next/app/[locale]/page.tsx @@ -7,8 +7,8 @@ import { getRealmName } from '@/lib/realm' // SSR por petición: los datos se leen DIRECTAMENTE de MySQL desde el servidor Next. export const dynamic = 'force-dynamic' -async function getData(): Promise<{ noticias: Noticia[]; status: ServerStatus | null }> { - const [news, status] = await Promise.allSettled([getNoticias(), getServerStatus()]) +async function getData(locale: string): Promise<{ noticias: Noticia[]; status: ServerStatus | null }> { + const [news, status] = await Promise.allSettled([getNoticias(locale), getServerStatus()]) return { noticias: news.status === 'fulfilled' ? news.value : [], status: status.status === 'fulfilled' ? status.value : null, @@ -24,11 +24,11 @@ function brandify(html: string, realm: string): string { return html.replace(/<[^>]*>|[^<]+/g, (seg) => (seg.startsWith('<') ? seg : seg.replace(re, repl))) } -function formatFecha(iso: string | Date | null): string { +function formatFecha(iso: string | Date | null, locale: string): string { if (!iso) return '' const d = new Date(iso) if (isNaN(d.getTime())) return String(iso) - return d.toLocaleDateString('es-ES', { + return d.toLocaleDateString(locale === 'en' ? 'en-US' : 'es-ES', { day: 'numeric', month: 'long', year: 'numeric', @@ -39,7 +39,7 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s const { locale } = await params setRequestLocale(locale) const t = await getTranslations('Misc') - const { noticias, status } = await getData() + const { noticias, status } = await getData(locale) const realmName = await getRealmName() const worldStatus = status ? status.world_status : 'Offline' @@ -125,7 +125,7 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s {noticias.map((n) => (

{brandify(n.titulo, realmName)}

- {formatFecha(n.fecha)} + {formatFecha(n.fecha, locale)}

}) { const session = await getSession() @@ -9,3 +9,19 @@ export async function DELETE(_request: Request, { params }: { params: Promise<{ await deleteNews(Number(id)) return Response.json({ success: true }) } + +export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession() + if (!(await isAdmin(session))) return Response.json({ success: false, error: 'forbidden' }, { status: 403 }) + const { id } = await params + let b: Record = {} + try { b = await request.json() } catch { return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 }) } + const titulo = String(b.titulo ?? '').trim() + const contenido = String(b.contenido ?? '').trim() + const enlace = String(b.enlace ?? '').trim() || null + const tituloEn = String(b.tituloEn ?? '').trim() || null + const contenidoEn = String(b.contenidoEn ?? '').trim() || null + if (!titulo || !contenido) return Response.json({ success: false, error: 'emptyError' }) + await updateNews(Number(id), titulo, contenido, enlace, tituloEn, contenidoEn) + return Response.json({ success: true }) +} diff --git a/web-next/app/api/admin/news/route.ts b/web-next/app/api/admin/news/route.ts index 03540bf..2f2775a 100644 --- a/web-next/app/api/admin/news/route.ts +++ b/web-next/app/api/admin/news/route.ts @@ -10,7 +10,9 @@ export async function POST(request: Request) { const titulo = String(b.titulo ?? '').trim() const contenido = String(b.contenido ?? '').trim() const enlace = String(b.enlace ?? '').trim() || null + const tituloEn = String(b.tituloEn ?? '').trim() || null + const contenidoEn = String(b.contenidoEn ?? '').trim() || null if (!titulo || !contenido) return Response.json({ success: false, error: 'emptyError' }) - const id = await createNews(titulo, contenido, enlace) + const id = await createNews(titulo, contenido, enlace, tituloEn, contenidoEn) return Response.json({ success: true, id }) } diff --git a/web-next/components/AdminNewsManager.tsx b/web-next/components/AdminNewsManager.tsx index 1060ae6..b0d8c1c 100644 --- a/web-next/components/AdminNewsManager.tsx +++ b/web-next/components/AdminNewsManager.tsx @@ -4,19 +4,22 @@ import { useState } from 'react' import { useTranslations, useLocale } from 'next-intl' import type { NewsItem } from '@/lib/admin-news' +const EMPTY = { titulo: '', tituloEn: '', contenido: '', contenidoEn: '', enlace: '' } +type Draft = typeof EMPTY + export function AdminNewsManager({ initialNews }: { initialNews: NewsItem[] }) { const t = useTranslations('Admin') const locale = useLocale() const [news, setNews] = useState(initialNews) - const [titulo, setTitulo] = useState('') - const [contenido, setContenido] = useState('') - const [enlace, setEnlace] = useState('') + const [draft, setDraft] = useState(EMPTY) const [busy, setBusy] = useState(false) const [error, setError] = useState(null) + const [editId, setEditId] = useState(null) + const [edit, setEdit] = useState(EMPTY) async function create(e: React.FormEvent) { e.preventDefault() - if (busy || !titulo.trim() || !contenido.trim()) return + if (busy || !draft.titulo.trim() || !draft.contenido.trim()) return setBusy(true) setError(null) try { @@ -24,14 +27,41 @@ export function AdminNewsManager({ initialNews }: { initialNews: NewsItem[] }) { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', - body: JSON.stringify({ titulo, contenido, enlace }), + body: JSON.stringify(draft), }) const data: { success?: boolean; id?: number } = await res.json() if (data.success && data.id) { - setNews([{ id: data.id, titulo, fecha: new Date().toISOString(), enlace: enlace || null }, ...news]) - setTitulo('') - setContenido('') - setEnlace('') + setNews([{ id: data.id, ...draft, fecha: new Date().toISOString(), enlace: draft.enlace || null }, ...news]) + setDraft(EMPTY) + } else setError(t('genericError')) + } catch { + setError(t('genericError')) + } finally { + setBusy(false) + } + } + + function startEdit(n: NewsItem) { + setEditId(n.id) + setEdit({ titulo: n.titulo, tituloEn: n.tituloEn, contenido: n.contenido, contenidoEn: n.contenidoEn, enlace: n.enlace || '' }) + setError(null) + } + + async function save(id: number) { + if (busy || !edit.titulo.trim() || !edit.contenido.trim()) return + setBusy(true) + setError(null) + try { + const res = await fetch(`/api/admin/news/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify(edit), + }) + const data: { success?: boolean } = await res.json() + if (data.success) { + setNews(news.map((n) => (n.id === id ? { ...n, ...edit, enlace: edit.enlace || null } : n))) + setEditId(null) } else setError(t('genericError')) } catch { setError(t('genericError')) @@ -50,9 +80,12 @@ export function AdminNewsManager({ initialNews }: { initialNews: NewsItem[] }) { return (

- setTitulo(e.target.value)} placeholder={t('newsTitle')} maxLength={200} /> -