← back to Nineoh Guide
apps/web/app/episodes/page.tsx
104 lines
import { pool } from "@/lib/db";
import { SpoilerSettings, SpoilerRecap } from "@/components/SpoilerGuard";
import { EpisodeControls } from "@/components/EpisodeControls";
export const dynamic = "force-dynamic";
export const metadata = { title: "Episode Guide" };
async function getEpisodesBySeason() {
try {
const { rows } = await pool.query(
`select season_number, episode_number, title, air_date, summary_short_original
from episodes order by season_number, episode_number`
);
const bySeason = new Map<number, typeof rows>();
for (const r of rows) {
if (!bySeason.has(r.season_number)) bySeason.set(r.season_number, []);
bySeason.get(r.season_number)!.push(r);
}
return bySeason;
} catch {
return new Map<number, any[]>();
}
}
export default async function Episodes({
searchParams,
}: {
searchParams: Promise<{ order?: string }>;
}) {
const { order: orderRaw } = await searchParams;
const order = orderRaw === "desc" ? "desc" : "asc";
const bySeason = await getEpisodesBySeason();
const seasons = [...bySeason.keys()].sort((a, b) => (order === "desc" ? b - a : a - b));
return (
<section>
<h1>Episode Guide</h1>
<p style={{ color: "var(--muted)", fontSize: 13 }}>
Factual episode index across all five seasons. Original spoiler-safe
recaps are written by our editors — use the spoiler guard to hide ones
past where you've watched.
</p>
<EpisodeControls order={order} />
<SpoilerSettings seasons={Math.max(5, ...seasons, 1)} />
{seasons.length === 0 && <p>No episodes indexed yet.</p>}
{seasons.map((s) => (
<div key={s} style={{ marginTop: 26 }}>
<h2 style={{ margin: "0 0 10px" }}>Season {s}</h2>
<div
style={{
background: "var(--paper-2)",
border: "1px solid var(--line)",
borderRadius: "var(--radius)",
boxShadow: "var(--shadow)",
overflow: "hidden",
}}
>
{bySeason.get(s)!.map((ep, i) => (
<div
key={ep.episode_number}
style={{
padding: "11px 16px",
borderTop: i === 0 ? "none" : "1px solid var(--line)",
display: "flex",
gap: 12,
alignItems: "baseline",
}}
>
<span className="se" style={{ minWidth: 52, fontSize: 13 }}>
S{s}E{ep.episode_number}
</span>
<span style={{ flex: 1 }}>
<a
href={`/episodes/${s}/${ep.episode_number}`}
style={{ fontFamily: "var(--font-display)", fontSize: 15.5 }}
>
{ep.title}
</a>
<span className="ep-recap">
<SpoilerRecap
season={s}
episode={ep.episode_number}
text={ep.summary_short_original}
/>
</span>
</span>
<span style={{ color: "var(--muted)", fontSize: 12, whiteSpace: "nowrap" }}>
{ep.air_date
? new Date(ep.air_date).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})
: ""}
</span>
</div>
))}
</div>
</div>
))}
</section>
);
}