← back to Nineoh Guide
apps/web/app/api/cast/route.ts
53 lines
import { NextResponse } from "next/server";
import { pool } from "@/lib/db";
export const dynamic = "force-dynamic";
/**
* Cast list for the Expo app. Mirrors the web /cast page query.
* Photo attribution is carried through — CC-BY-SA requires the caption be shown.
*/
export async function GET() {
try {
// Main cast + recurring/guest stars. `kind` lets the client group them.
const { rows } = await pool.query(
`select distinct on (cp.id)
cp.id, cp.name, cp.biography_original,
ch.name as character_name, cr.credit_type as kind, cr.billing_order,
a.file_url, a.attribution_text, a.license_type, a.license_url
from cast_people cp
left join credits cr on cr.person_id = cp.id
and cr.credit_type in ('main-cast','recurring')
left join characters ch on ch.id = cr.character_id
left join assets a on a.id = cp.headshot_asset_id
order by cp.id,
(cr.credit_type = 'main-cast') desc nulls last,
cr.billing_order nulls last`
);
const cast = rows
.map((r) => ({
id: r.id,
name: r.name,
characterName: r.character_name ?? null,
bio: r.biography_original ?? null,
photoUrl: r.file_url ?? null,
attribution: r.attribution_text ?? null,
licenseType: r.license_type ?? null,
licenseUrl: r.license_url ?? null,
kind: (r.kind ?? "recurring") as string,
billingOrder: r.billing_order as number | null,
}))
// main cast first, then recurring by episode count (billing_order = -eps)
.sort((a, b) => {
const am = a.kind === "main-cast" ? 0 : 1;
const bm = b.kind === "main-cast" ? 0 : 1;
if (am !== bm) return am - bm;
return (a.billingOrder ?? 0) - (b.billingOrder ?? 0);
});
return NextResponse.json({ cast });
} catch (err) {
console.error("[api/cast] DB error:", err);
return NextResponse.json({ cast: [] }, { status: 500 });
}
}