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