← back to Nineoh Guide
apps/web/app/characters/[slug]/page.tsx
93 lines
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { pool } from "@/lib/db";
import { SITE_URL, slugify, castPath } from "@nineoh/core";
export const revalidate = 3600;
type Params = { slug: string };
async function getCharacter(slug: string) {
try {
const { rows } = await pool.query(
`select c.id, c.name, c.description_original, cp.name as actor
from characters c
left join credits cr on cr.character_id = c.id and cr.credit_type='main-cast'
left join cast_people cp on cp.id = cr.person_id`
);
const c = rows.find((r) => slugify(r.name) === slug);
if (!c) return null;
const eps = await pool.query(
`select e.season_number, e.episode_number, e.title
from episode_guest_cast g join episodes e on e.id = g.episode_id
where g.character_name = $1
order by e.season_number, e.episode_number`,
[c.name]
);
return { ...c, episodes: eps.rows };
} catch {
return null;
}
}
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
const { slug } = await params;
const c = await getCharacter(slug);
if (!c) return { title: "Character not found" };
return {
title: `${c.name} — 90210 Character`,
description: c.description_original ?? `${c.name} on 90210.`,
alternates: { canonical: `${SITE_URL}/characters/${slug}` },
};
}
export default async function CharacterPage({ params }: { params: Promise<Params> }) {
const { slug } = await params;
const c = await getCharacter(slug);
if (!c) notFound();
return (
<article style={{ lineHeight: 1.7 }}>
<p style={{ fontSize: 12, color: "var(--muted)" }}>
<a href="/characters">← Characters</a>
</p>
<h1 style={{ marginBottom: 2 }}>{c.name}</h1>
{c.actor ? (
<p style={{ marginTop: 0, color: "var(--muted)", fontSize: 14 }}>
Played by{" "}
<a href={castPath(c.actor)} style={{ color: "var(--maroon)", fontWeight: 600 }}>
{c.actor}
</a>
</p>
) : null}
<p style={{ fontSize: 15.5 }}>{c.description_original}</p>
{c.episodes.length === 0 && c.actor && (
<p style={{ color: "var(--muted)", fontSize: 13 }}>
A series regular — appears across the run of the show. (Per-episode
listings below cover guest & recurring appearances only.)
</p>
)}
{c.episodes.length > 0 && (
<>
<h2 style={{ fontSize: 17 }}>Appears in ({c.episodes.length})</h2>
<div
style={{
display: "grid",
gap: 6,
gridTemplateColumns: "repeat(auto-fill, minmax(210px, 1fr))",
fontSize: 13,
}}
>
{c.episodes.map((e: any) => (
<a key={`${e.season_number}-${e.episode_number}`} href={`/episodes/${e.season_number}/${e.episode_number}`}>
<span className="se">S{e.season_number}E{e.episode_number}</span> {e.title}
</a>
))}
</div>
</>
)}
</article>
);
}