Noticias multiidioma (ES/EN) con fallback
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<Draft>(EMPTY)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [editId, setEditId] = useState<number | null>(null)
|
||||
const [edit, setEdit] = useState<Draft>(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 (
|
||||
<div>
|
||||
<form onSubmit={create} className="admin-form info-box-light separate2">
|
||||
<input value={titulo} onChange={(e) => setTitulo(e.target.value)} placeholder={t('newsTitle')} maxLength={200} />
|
||||
<textarea value={contenido} onChange={(e) => setContenido(e.target.value)} placeholder={t('newsContent')} rows={5} />
|
||||
<input value={enlace} onChange={(e) => setEnlace(e.target.value)} placeholder={t('newsLink')} />
|
||||
<input value={draft.titulo} onChange={(e) => setDraft({ ...draft, titulo: e.target.value })} placeholder={t('newsTitle')} maxLength={200} />
|
||||
<textarea value={draft.contenido} onChange={(e) => setDraft({ ...draft, contenido: e.target.value })} placeholder={t('newsContent')} rows={5} />
|
||||
<input value={draft.tituloEn} onChange={(e) => setDraft({ ...draft, tituloEn: e.target.value })} placeholder={t('newsTitleEn')} maxLength={200} />
|
||||
<textarea value={draft.contenidoEn} onChange={(e) => setDraft({ ...draft, contenidoEn: e.target.value })} placeholder={t('newsContentEn')} rows={5} />
|
||||
<input value={draft.enlace} onChange={(e) => setDraft({ ...draft, enlace: e.target.value })} placeholder={t('newsLink')} />
|
||||
<p className="third-brown small-font">{t('newsEnHint')}</p>
|
||||
<button type="submit" disabled={busy}>
|
||||
{busy ? t('creating') : t('create')}
|
||||
</button>
|
||||
@@ -69,15 +102,37 @@ export function AdminNewsManager({ initialNews }: { initialNews: NewsItem[] }) {
|
||||
<tbody>
|
||||
{news.map((n) => (
|
||||
<tr key={n.id} className="team-center-table-tr">
|
||||
<td className="lefted separate">
|
||||
<span className="yellow-info">{n.titulo}</span>
|
||||
{n.fecha && <p className="third-brown small-font">{new Date(n.fecha).toLocaleString(locale)}</p>}
|
||||
</td>
|
||||
<td className="real-info-box">
|
||||
<button onClick={() => remove(n.id)} className="nw-tool-btn red-info2">
|
||||
{t('delete')}
|
||||
</button>
|
||||
</td>
|
||||
{editId === n.id ? (
|
||||
<td className="lefted separate" colSpan={2}>
|
||||
<input value={edit.titulo} onChange={(e) => setEdit({ ...edit, titulo: e.target.value })} placeholder={t('newsTitle')} maxLength={200} />
|
||||
<textarea value={edit.contenido} onChange={(e) => setEdit({ ...edit, contenido: e.target.value })} placeholder={t('newsContent')} rows={4} />
|
||||
<input value={edit.tituloEn} onChange={(e) => setEdit({ ...edit, tituloEn: e.target.value })} placeholder={t('newsTitleEn')} maxLength={200} />
|
||||
<textarea value={edit.contenidoEn} onChange={(e) => setEdit({ ...edit, contenidoEn: e.target.value })} placeholder={t('newsContentEn')} rows={4} />
|
||||
<input value={edit.enlace} onChange={(e) => setEdit({ ...edit, enlace: e.target.value })} placeholder={t('newsLink')} />
|
||||
<button onClick={() => save(n.id)} disabled={busy} className="nw-tool-btn">
|
||||
{busy ? t('saving') : t('save')}
|
||||
</button>{' '}
|
||||
<button onClick={() => setEditId(null)} className="nw-tool-btn">
|
||||
{t('cancel')}
|
||||
</button>
|
||||
</td>
|
||||
) : (
|
||||
<>
|
||||
<td className="lefted separate">
|
||||
<span className="yellow-info">{n.titulo}</span>
|
||||
{n.tituloEn && <span className="third-brown small-font"> · EN: {n.tituloEn}</span>}
|
||||
{n.fecha && <p className="third-brown small-font">{new Date(n.fecha).toLocaleString(locale)}</p>}
|
||||
</td>
|
||||
<td className="real-info-box">
|
||||
<button onClick={() => startEdit(n)} className="nw-tool-btn">
|
||||
{t('edit')}
|
||||
</button>{' '}
|
||||
<button onClick={() => remove(n.id)} className="nw-tool-btn red-info2">
|
||||
{t('delete')}
|
||||
</button>
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user