Files
NightSpire/web-next/app/page.tsx
T
Inna 478a3bb203 Fase 0 SPA/SSR: scaffold Next.js 16 (App Router) + Tailwind v4 + home SSR
Inicia la reescritura del frontend a una app Next.js con SSR (Django pasará a ser
API JSON). No toca producción: Django sigue sirviendo www; Next corre aparte en :3001.

- web-next/: Next.js 16 App Router, TypeScript, Tailwind CSS v4, alias @/*.
- lib/api.ts: cliente de la API Django (SSR -> 127.0.0.1:8001; navegador -> /api
  relativo vía Caddy).
- app/page.tsx: home con SSR por petición (force-dynamic) que hace fetch de
  /api/home/news/ y /api/home/status/ y renderiza noticias + estado con Tailwind.
- app/layout.tsx: lang=es + SEO (title/description/keywords/OpenGraph) por el
  Metadata API de Next (renderizado en servidor).
- Servicio systemd novawow-next (next start -p 3001, DJANGO_API_BASE).

Nota Next 16 (breaking): params/searchParams son Promise (await); fetch no se
cachea por defecto. Verificado: SSR sirve HTML con datos reales de Django y SEO
en el <head>; producción Django intacta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:17:47 +00:00

69 lines
2.6 KiB
TypeScript

import { apiGet, type Noticia, type ServerStatus } from '@/lib/api'
// SSR por petición: los datos se obtienen en el servidor Next desde la API Django.
// (En Next 16 fetch no se cachea por defecto; force-dynamic lo deja explícito.)
export const dynamic = 'force-dynamic'
async function getData(): Promise<{ noticias: Noticia[]; status: ServerStatus | null }> {
const [news, status] = await Promise.allSettled([
apiGet<{ noticias: Noticia[] }>('/api/home/news/'),
apiGet<ServerStatus>('/api/home/status/'),
])
return {
noticias: news.status === 'fulfilled' ? news.value.noticias : [],
status: status.status === 'fulfilled' ? status.value : null,
}
}
export default async function HomePage() {
const { noticias, status } = await getData()
return (
<main className="mx-auto max-w-3xl px-4 py-8">
<h1 className="text-3xl font-bold text-amber-500">Nova WoW</h1>
<section className="my-6 rounded-lg border border-amber-900/60 bg-[#241812] p-4">
<h2 className="mb-3 text-lg font-semibold">Estado del servidor</h2>
{status ? (
<ul className="space-y-1 text-sm">
<li>
Servidor: <span className="text-amber-400">{status.server_name ?? '—'}</span> ({status.expansion})
</li>
<li>Personajes en línea: {status.online_characters}</li>
<li>
Login:{' '}
<span className={status.status === 'Online' ? 'text-green-400' : 'text-red-400'}>{status.status}</span>
</li>
<li>Dirección: {status.address ?? '—'}</li>
</ul>
) : (
<p>No disponible.</p>
)}
</section>
<section>
<h2 className="mb-3 text-lg font-semibold">Noticias</h2>
{noticias.length === 0 ? (
<p className="text-amber-200/70">No hay noticias disponibles en este momento.</p>
) : (
noticias.map((n) => (
<article key={n.id} className="mb-4 border-b border-amber-900/60 pb-4">
<h3 className="text-xl font-semibold">{n.titulo}</h3>
{n.fecha && <small className="text-amber-200/60">{new Date(n.fecha).toLocaleString('es-ES')}</small>}
<div className="prose prose-invert mt-2 max-w-none" dangerouslySetInnerHTML={{ __html: n.contenido }} />
{n.enlace && (
<p className="mt-2">
Enlace:{' '}
<a className="text-sky-400 underline" href={n.enlace} target="_blank" rel="noopener noreferrer">
aquí
</a>
</p>
)}
</article>
))
)}
</section>
</main>
)
}