← back to Nineoh Guide
apps/web/app/cast/page.tsx
83 lines
import { pool } from "@/lib/db";
import { RightsImage } from "@/components/RightsImage";
import { castPath, type Asset } from "@nineoh/core";
function toAsset(r: any): Asset | null {
if (!r.file_url) return null;
return {
id: r.id,
type: "image",
fileUrl: r.file_url,
thumbnailUrl: r.file_url,
licenseType: r.license_type,
licenseUrl: r.license_url ?? null,
attributionText: r.attribution_text ?? null,
usageScope: "editorial",
approvedForMarketing: false,
licensePurchaseStatus: "none",
};
}
export const revalidate = 3600; // ISR: cached HTML, hourly background refresh
export const metadata = { title: "Cast" };
async function getCast() {
try {
const { rows } = await pool.query(
`select cp.id, cp.name, cp.biography_original,
ch.name as character_name,
a.file_url, a.license_type, a.license_url, a.attribution_text
from cast_people cp
left join credits cr on cr.person_id = cp.id and cr.credit_type = 'main-cast'
left join characters ch on ch.id = cr.character_id
left join assets a on a.id = cp.headshot_asset_id
order by cp.name`
);
return rows;
} catch {
return [];
}
}
export default async function Cast() {
const cast = await getCast();
return (
<section>
<h1>Cast</h1>
{cast.length === 0 ? (
<p style={{ color: "#8a1c1c" }}>
Cast profiles are being written. Bios will be original; headshots will
appear only once their image licenses are cleared.
</p>
) : (
<div
style={{
display: "grid",
gap: 14,
gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))",
}}
>
{cast.map((p) => (
<a
key={p.id}
className="card"
href={castPath(p.name)}
style={{ display: "block", color: "inherit" }}
>
<strong style={{ fontFamily: "var(--font-display)", fontSize: 16 }}>
{p.name}
</strong>
{p.character_name ? (
<div className="se" style={{ fontSize: 13 }}>as {p.character_name}</div>
) : null}
<p style={{ margin: "5px 0 0", color: "var(--ink-soft)", fontSize: 12.5, lineHeight: 1.5 }}>
{p.biography_original ?? "Bio coming soon."}
</p>
</a>
))}
</div>
)}
</section>
);
}