← 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>
  );
}