← back to Nineoh Guide

apps/web/app/show/page.tsx

143 lines

import { pool } from "@/lib/db";
import { castPath, slugify } from "@nineoh/core";

export const revalidate = 3600; // ISR: cached HTML, hourly background refresh
export const metadata = {
  title: "90210 — Series Overview",
  description:
    "An unofficial overview of the 2008 CW teen drama 90210: five seasons, 114 episodes, cast, and where to watch.",
};

// Original season overviews — static editorial content (5 entries), no DB column needed.
const SEASON_SUMMARIES: Record<number, string> = {
  1: "The debut season follows the Wilson family's move from Kansas to Beverly Hills as siblings Annie and Dixon start at West Beverly Hills High — first loves, new friendships with Naomi, Silver, Ethan, Navid and Adrianna, and a family secret that reshapes the household.",
  2: "Sophomore year deepens the drama: the bond between Naomi, Silver and Adrianna is tested, Silver faces a family cancer crisis, and a predatory teacher, Mr. Cannon, arrives at West Bev — setting up the season's darkest turn.",
  3: "Senior year confronts the aftermath of the Mr. Cannon storyline head-on as Naomi seeks justice, the group chases music and studio dreams at Shirazi, and graduation looms over everyone's next chapter.",
  4: "The college years begin at CU: Naomi rushes a sorority and clashes with rival Holly, Annie hides a secret double life, Dixon pursues music, and Naomi's whirlwind path toward marriage drives the season.",
  5: "The final season pushes the group into adulthood — marriages tested, careers launched (a surf shop, a restaurant, event planning for mogul Jordan Welland), and a catastrophic finale that puts the friendships on the line one last time.",
};

async function getData() {
  try {
    const show = await pool.query(
      "select display_title, description_original from shows limit 1"
    );
    const seasons = await pool.query(
      `select season_number,
              count(*)::int as eps,
              min(air_date) as first_aired,
              max(air_date) as last_aired
         from episodes group by season_number order by season_number`
    );
    const characters = await pool.query(
      `select 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
        where c.description_original is not null
        order by c.name`
    );
    const totals = await pool.query(
      `select count(*)::int as eps,
              (select count(*)::int from cast_people) as cast,
              min(air_date) as first, max(air_date) as last
         from episodes`
    );
    const cast = await pool.query(`select name from cast_people order by name`);
    return {
      show: show.rows[0] ?? null,
      seasons: seasons.rows,
      totals: totals.rows[0],
      cast: cast.rows,
      characters: characters.rows,
    };
  } catch {
    return { show: null, seasons: [], totals: null, cast: [], characters: [] };
  }
}

const yr = (d: any) => (d ? new Date(d).getFullYear() : "");

export default async function Show() {
  const { show, seasons, totals, cast, characters } = await getData();
  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "TVSeries",
    name: "90210 (2008)",
    description: show?.description_original ?? undefined,
    numberOfSeasons: seasons.length,
    numberOfEpisodes: totals?.eps ?? 0,
    startDate: totals?.first ? new Date(totals.first).toISOString().slice(0, 10) : undefined,
    endDate: totals?.last ? new Date(totals.last).toISOString().slice(0, 10) : undefined,
  };

  return (
    <section>
      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
      <h1>{show?.display_title ?? "90210"} — Series Overview</h1>
      <p style={{ color: "var(--ink-soft)", lineHeight: 1.6 }}>{show?.description_original}</p>
      <p style={{ color: "var(--muted)", fontSize: 13 }}>
        {seasons.length} seasons · {totals?.eps ?? 0} episodes · The CW ·{" "}
        {yr(totals?.first)}–{yr(totals?.last)} ·{" "}
        <a href="https://www.justwatch.com/us/tv-show/90210" target="_blank" rel="noopener noreferrer">
          Where to watch ↗
        </a>
      </p>

      <h2>Seasons</h2>
      <div style={{ display: "grid", gap: 12 }}>
        {seasons.map((s) => (
          <div key={s.season_number} className="card">
            <div style={{ display: "flex", gap: 12, alignItems: "baseline" }}>
              <strong className="se" style={{ fontFamily: "var(--font-display)", fontSize: 16 }}>
                Season {s.season_number}
              </strong>
              <span style={{ flex: 1, color: "var(--muted)", fontSize: 13 }}>
                {s.eps} episodes · {yr(s.first_aired)}
                {yr(s.last_aired) && yr(s.last_aired) !== yr(s.first_aired) ? `–${yr(s.last_aired)}` : ""}
              </span>
            </div>
            {SEASON_SUMMARIES[s.season_number] ? (
              <p style={{ margin: "6px 0 0", color: "var(--ink-soft)", fontSize: 13.5, lineHeight: 1.55 }}>
                {SEASON_SUMMARIES[s.season_number]}
              </p>
            ) : null}
          </div>
        ))}
      </div>

      {characters.length > 0 && (
        <>
          <h2>Characters</h2>
          <div style={{ display: "grid", gap: 10, gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))" }}>
            {characters.map((c) => (
              <a key={c.name} className="card" href={`/characters/${slugify(c.name)}`} style={{ display: "block", color: "inherit" }}>
                <strong style={{ fontFamily: "var(--font-display)", fontSize: 15 }}>{c.name}</strong>
                {c.actor ? (
                  <span style={{ color: "var(--muted)", fontSize: 12 }}> · {c.actor}</span>
                ) : null}
                <p style={{ margin: "4px 0 0", color: "var(--ink-soft)", fontSize: 12.5, lineHeight: 1.5 }}>
                  {c.description_original}
                </p>
              </a>
            ))}
          </div>
        </>
      )}

      <h2>Cast at a glance</h2>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
        {cast.map((c) => (
          <a key={c.name} className="pill" href={castPath(c.name)} style={{ textDecoration: "none" }}>
            {c.name}
          </a>
        ))}
      </div>

      <p style={{ marginTop: 24 }}>
        <a href="/episodes">Full episode guide →</a>
      </p>
    </section>
  );
}