← back to Nineoh Guide
apps/web/app/page.tsx
172 lines
import { pool } from "@/lib/db";
import { HeroArt } from "@/components/HeroArt";
export const revalidate = 3600; // ISR: cached HTML, hourly background refresh
async function getStats() {
try {
const show = await pool.query(
"select display_title, description_original from shows limit 1"
);
const eps = await pool.query("select count(*)::int as n from episodes");
const cast = await pool.query("select count(*)::int as n from cast_people");
const recapped = await pool.query(
"select count(*)::int as n from episodes where summary_short_original is not null"
);
return {
show: show.rows[0] ?? null,
episodes: eps.rows[0]?.n ?? 0,
cast: cast.rows[0]?.n ?? 0,
recapped: recapped.rows[0]?.n ?? 0,
};
} catch {
return { show: null, episodes: 0, cast: 0, recapped: 0 };
}
}
async function getScenery() {
try {
const { rows } = await pool.query(
`select file_url, caption, attribution_text
from assets where rights_notes = 'beverly-hills-location'
order by created_at limit 6`
);
return rows;
} catch {
return [];
}
}
export default async function Home() {
const { episodes, cast, recapped } = await getStats();
const scenery = await getScenery();
const jsonLd = {
"@context": "https://schema.org",
"@type": "TVSeries",
name: "90210 (2008)",
description:
"An unofficial fan guide to the 2008 teen drama 90210 — original episode recaps and cast profiles.",
numberOfSeasons: 5,
numberOfEpisodes: episodes,
};
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<section className="hero">
<div className="sky" />
<HeroArt />
<p className="eyebrow rise">Unofficial Fan Guide</p>
<h1 className="rise-2">Every episode of 90210, recapped.</h1>
<p className="lede rise-3">
An independent companion to the 2008 Beverly Hills teen drama —
original recaps, cast profiles, and a spoiler guard that keeps you one
step ahead. Written by fans, for fans.
</p>
<a className="cta rise-3" href="/episodes">
Browse the episode guide →
</a>
<div className="hero-stats rise-3">
<GlassStat num={episodes} lab="Episodes" />
<GlassStat num={recapped} lab="Recaps" />
<GlassStat num={cast} lab="Cast" />
</div>
</section>
<h2>Start exploring</h2>
<div style={{ display: "grid", gap: 12, gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))" }}>
<NavCard href="/episodes" title="Episode Guide" sub="All 5 seasons · spoiler-safe recaps" />
<NavCard href="/cast" title="Cast & Characters" sub="Who played who, with original bios" />
<NavCard href="/games" title="Games" sub="Trivia, slots, backgammon & more — free to play" />
<NavCard href="/search" title="Search" sub="Find any episode, character, or moment" />
<a
className="card"
href="https://www.justwatch.com/us/tv-show/90210"
target="_blank"
rel="noopener noreferrer"
style={{ display: "block", color: "inherit" }}
>
<strong style={{ fontFamily: "var(--font-display)", fontSize: 17 }}>
Where to Watch ↗
</strong>
<div style={{ color: "var(--muted)", fontSize: 13, marginTop: 4 }}>
Current streaming, rent & buy options (live via JustWatch)
</div>
</a>
</div>
{scenery.length > 0 && (
<>
<h2>The Beverly Hills backdrop</h2>
<p style={{ color: "var(--muted)", fontSize: 13, marginTop: -4 }}>
The real 90210 — photographed by the Wikimedia Commons community, used
under free licenses.
</p>
<div
style={{
display: "grid",
gap: 10,
gridTemplateColumns: "repeat(auto-fill, minmax(190px, 1fr))",
marginTop: 12,
}}
>
{scenery.map((s) => (
<figure
key={s.file_url}
style={{
margin: 0,
borderRadius: "var(--radius)",
overflow: "hidden",
border: "1px solid var(--line)",
boxShadow: "var(--shadow)",
background: "var(--paper-2)",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={s.file_url}
alt={s.caption ?? "Beverly Hills"}
loading="lazy"
style={{ width: "100%", height: 130, objectFit: "cover", display: "block" }}
/>
<figcaption style={{ padding: "6px 8px" }}>
<div style={{ fontSize: 12, fontWeight: 600 }}>{s.caption}</div>
<div style={{ fontSize: 10, color: "var(--muted)" }}>
{s.attribution_text}
</div>
</figcaption>
</figure>
))}
</div>
</>
)}
{episodes === 0 && (
<p style={{ marginTop: 20, color: "var(--maroon)", fontSize: 13 }}>
Content is being curated — recaps and profiles will populate shortly.
</p>
)}
</>
);
}
function GlassStat({ num, lab }: { num: number; lab: string }) {
return (
<div className="glass-dark hero-stat">
<div className="hero-stat-num">{num}</div>
<div className="hero-stat-lab">{lab}</div>
</div>
);
}
function NavCard({ href, title, sub }: { href: string; title: string; sub: string }) {
return (
<a className="card" href={href} style={{ display: "block", color: "inherit" }}>
<strong style={{ fontFamily: "var(--font-display)", fontSize: 17 }}>{title}</strong>
<div style={{ color: "var(--muted)", fontSize: 13, marginTop: 4 }}>{sub}</div>
</a>
);
}