← back to Nineoh Guide

apps/web/app/search/page.tsx

177 lines

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

export const dynamic = "force-dynamic";
export const metadata = { title: "Search" };

async function search(q: string) {
  const like = `%${q}%`;
  try {
    const eps = await pool.query(
      `select season_number, episode_number, title, summary_short_original
         from episodes
        where title ilike $1 or summary_short_original ilike $1
        order by season_number, episode_number
        limit 50`,
      [like]
    );
    const cast = await pool.query(
      `select cp.name, ch.name as character_name, cp.biography_original
         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
        where cp.name ilike $1 or cp.biography_original ilike $1
           or ch.name ilike $1 or ch.description_original ilike $1
        order by cp.name
        limit 50`,
      [like]
    );
    const news = await pool.query(
      `select id, headline, canonical_url, publisher_name from news_items
        where headline ilike $1 order by published_at desc nulls last limit 40`,
      [like]
    );
    return { eps: eps.rows, cast: cast.rows, news: news.rows };
  } catch {
    return { eps: [], cast: [], news: [] };
  }
}

// Static pages + games are searchable too (no DB — always current).
const PAGES = [
  { label: "Series overview", href: "/show", kw: "series overview show seasons about" },
  { label: "Episode Guide", href: "/episodes", kw: "episodes guide recaps seasons" },
  { label: "Cast", href: "/cast", kw: "cast actors bios" },
  { label: "News", href: "/news", kw: "news articles press" },
  { label: "Community", href: "/community", kw: "community reddit discord podcast forum" },
  { label: "Where to Watch", href: "https://www.justwatch.com/us/tv-show/90210", kw: "where to watch stream streaming hulu justwatch" },
  { label: "🎮 90210 Trivia", href: "/games/trivia.html", kw: "games trivia quiz play" },
  { label: "🎲 90210 Backgammon", href: "/games/backgammon.html", kw: "games backgammon board play" },
  { label: "🏎️ Rodeo Drive Runner", href: "/games/rodeo-runner.html", kw: "games runner arcade play" },
  { label: "🎰 Beverly Hills Slots", href: "/games/slots.html", kw: "games slots casino play jackpot" },
  { label: "🂡 90210 Blackjack", href: "/games/blackjack.html", kw: "games blackjack casino cards play" },
  { label: "🎡 90210 Roulette", href: "/games/roulette.html", kw: "games roulette casino wheel play" },
];

export default async function Search({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>;
}) {
  const { q } = await searchParams;
  const query = (q ?? "").trim();
  const results = query ? await search(query) : { eps: [], cast: [], news: [] };
  const pageHits = query
    ? PAGES.filter((p) => `${p.label} ${p.kw}`.toLowerCase().includes(query.toLowerCase()))
    : [];
  const total =
    results.eps.length + results.cast.length + results.news.length + pageHits.length;

  return (
    <section>
      <h1>Search</h1>
      <form action="/search" method="get" style={{ margin: "12px 0" }}>
        <input
          type="search"
          name="q"
          defaultValue={query}
          placeholder="Search episodes, cast, characters…"
          aria-label="Search"
          style={{
            padding: "8px 10px",
            width: "100%",
            maxWidth: 460,
            border: "1px solid #ccc",
            borderRadius: 6,
            fontSize: 14,
          }}
        />
      </form>

      {query && (
        <p style={{ color: "var(--muted)", fontSize: 13 }}>
          {total} result{total === 1 ? "" : "s"} for “{query}”.
        </p>
      )}

      {pageHits.length > 0 && (
        <>
          <h2 style={{ fontSize: 16 }}>Pages &amp; Games</h2>
          <ul style={{ listStyle: "none", padding: 0 }}>
            {pageHits.map((p) => (
              <li key={p.href} style={{ padding: "6px 0", borderBottom: "1px solid var(--line)" }}>
                <a
                  href={p.href}
                  {...(p.href.startsWith("http") ? { target: "_blank", rel: "noopener noreferrer" } : {})}
                >
                  {p.label}
                  {p.href.startsWith("http") ? " ↗" : " →"}
                </a>
              </li>
            ))}
          </ul>
        </>
      )}

      {results.cast.length > 0 && (
        <>
          <h2 style={{ fontSize: 16 }}>Cast</h2>
          <ul style={{ listStyle: "none", padding: 0 }}>
            {results.cast.map((c) => (
              <li key={c.name} style={{ padding: "6px 0", borderBottom: "1px solid var(--line)" }}>
                <a href={castPath(c.name)}>
                  <strong>{c.name}</strong>
                </a>
                {c.character_name ? (
                  <span style={{ color: "var(--maroon)" }}> as {c.character_name}</span>
                ) : null}
              </li>
            ))}
          </ul>
        </>
      )}

      {results.eps.length > 0 && (
        <>
          <h2 style={{ fontSize: 16 }}>Episodes</h2>
          <ul style={{ listStyle: "none", padding: 0 }}>
            {results.eps.map((e) => (
              <li
                key={`${e.season_number}-${e.episode_number}`}
                style={{ padding: "6px 0", borderBottom: "1px solid var(--line)" }}
              >
                <a href={`/episodes/${e.season_number}/${e.episode_number}`}>
                  <span style={{ color: "var(--maroon)", fontWeight: 600 }}>
                    S{e.season_number}E{e.episode_number}
                  </span>{" "}
                  {e.title}
                </a>
              </li>
            ))}
          </ul>
        </>
      )}

      {results.news.length > 0 && (
        <>
          <h2 style={{ fontSize: 16 }}>News ({results.news.length})</h2>
          <ul style={{ listStyle: "none", padding: 0 }}>
            {results.news.map((n) => (
              <li key={n.id} style={{ padding: "6px 0", borderBottom: "1px solid var(--line)" }}>
                <a href={n.canonical_url} target="_blank" rel="noopener noreferrer">
                  {n.headline}
                </a>
                {n.publisher_name ? (
                  <span style={{ color: "var(--muted)", fontSize: 12 }}> · {n.publisher_name} ↗</span>
                ) : null}
              </li>
            ))}
          </ul>
        </>
      )}

      {query && total === 0 && <p>No matches. Try a character name, episode title, or game.</p>}
    </section>
  );
}