← back to Stayclaim
src/app/films/page.tsx
219 lines
import Link from 'next/link';
import type { Metadata } from 'next';
import { listProductionsWithCounts } from '@/lib/db';
import { BreadcrumbArchive } from '@/components/BreadcrumbArchive';
import { PosterPlaceholder } from '@/components/PosterPlaceholder';
import { FilmsGridServer } from '@/components/FilmsGridServer';
export async function generateMetadata({
searchParams,
}: {
searchParams: { kind?: string; decade?: string };
}): Promise<Metadata> {
const filtered = !!(searchParams.kind || searchParams.decade);
return {
title: 'Films + TV',
description: 'Productions filmed at addresses in the archive — features, television, music videos, commercials. Sourced from FilmLA / LA City Open Data permits.',
alternates: { canonical: '/films' },
robots: filtered ? { index: false, follow: true } : { index: true, follow: true },
};
}
const KIND_LABEL: Record<string, string> = {
film: 'Feature',
tv: 'Television',
commercial: 'Commercial',
music_video: 'Music video',
documentary: 'Documentary',
short: 'Short',
student: 'Student',
};
const KINDS = ['film', 'tv', 'commercial', 'music_video', 'documentary'] as const;
const DECADES = ['2020s', '2010s', '2000s', '1990s', '1980s', '1970s', '1960s', '1950s'] as const;
function FilterChip({
active,
href,
children,
}: {
active: boolean;
href: string;
children: React.ReactNode;
}) {
return (
<Link
href={href}
className={`inline-flex items-center px-3 py-1 text-[11px] uppercase tracking-[0.15em] border transition ${
active
? 'bg-ink text-sand border-ink'
: 'border-ink/20 text-ink/65 hover:border-coral hover:text-coral'
}`}
>
{children}
</Link>
);
}
function buildHref(current: { kind?: string; decade?: string }, patch: Record<string, string | undefined>) {
const next = { ...current, ...patch };
const params = new URLSearchParams();
for (const [k, v] of Object.entries(next)) {
if (v) params.set(k, v);
}
const qs = params.toString();
return qs ? `/films?${qs}` : '/films';
}
export default async function FilmsIndex({
searchParams,
}: {
searchParams: { kind?: string; decade?: string; show?: string };
}) {
const safeKind = (KINDS as readonly string[]).includes(searchParams.kind ?? '')
? searchParams.kind
: undefined;
const safeDecade = (DECADES as readonly string[]).includes(searchParams.decade ?? '')
? searchParams.decade
: undefined;
const allProductions = await listProductionsWithCounts({
kind: safeKind,
decade: safeDecade,
});
// Page weight: 1085 productions × ~3KB = ~3MB. Cap at 60 unless explicit ?show=all.
const PAGE_LIMIT = 60;
const showAll = searchParams.show === 'all';
const productions = showAll ? allProductions : allProductions.slice(0, PAGE_LIMIT);
const truncated = !showAll && allProductions.length > PAGE_LIMIT;
const byKind: Record<string, typeof productions> = {};
for (const p of productions) (byKind[p.kind] ??= []).push(p);
const kindOrder = ['film', 'tv', 'documentary', 'commercial', 'music_video', 'short', 'student'];
return (
<>
<div className="max-w-5xl mx-auto px-6 pt-6">
<BreadcrumbArchive items={[{ label: 'Archive', href: '/' }, { label: 'Films + TV' }]} />
</div>
<header className="max-w-5xl mx-auto px-6 py-12 border-b border-ink/10">
<p className="text-xs uppercase tracking-[0.15em] text-ink/50 mb-2">Films + television</p>
<h1 className="font-display text-5xl md:text-6xl text-ink tracking-[-0.02em] leading-[1.02]">
What was filmed where.
</h1>
<p className="mt-4 font-display italic text-xl text-ink/70 max-w-prose">
Productions that pulled a public film permit at an address in the archive.
</p>
<p className="mt-3 font-mono text-[11px] text-ink/45">
{allProductions.length} {allProductions.length === 1 ? 'production' : 'productions'} on file ·
source: FilmLA / data.lacity.org
</p>
<div className="mt-8 flex flex-col gap-3">
<div className="flex flex-wrap items-center gap-2">
<span className="text-[10px] uppercase tracking-[0.15em] text-ink/45 mr-2">Kind</span>
<FilterChip active={!searchParams.kind} href={buildHref(searchParams, { kind: undefined })}>All</FilterChip>
{KINDS.map(k => (
<FilterChip key={k} active={searchParams.kind === k} href={buildHref(searchParams, { kind: k })}>
{KIND_LABEL[k]}
</FilterChip>
))}
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-[10px] uppercase tracking-[0.15em] text-ink/45 mr-2">Decade</span>
<FilterChip active={!searchParams.decade} href={buildHref(searchParams, { decade: undefined })}>All</FilterChip>
{DECADES.map(d => (
<FilterChip key={d} active={searchParams.decade === d} href={buildHref(searchParams, { decade: d })}>
{d}
</FilterChip>
))}
</div>
</div>
</header>
<FilmsGridServer
heading="From the public-domain film archive"
kicker="Wikimedia · Internet Archive · LoC · MHDL · Met · Openverse"
defaultCols={5}
limit={24}
/>
<section className="max-w-5xl mx-auto px-6 py-10">
{productions.length === 0 ? (
<p className="text-sm italic text-ink/55">
No productions match these filters.
</p>
) : (
kindOrder
.filter(k => byKind[k]?.length)
.map(kind => (
<div key={kind} className="mb-12">
<h2 className="font-display text-3xl text-ink tracking-[-0.01em] mb-4 pb-2 border-b border-ink/10">
{KIND_LABEL[kind] ?? kind}
<span className="ml-3 font-mono text-[11px] uppercase tracking-[0.15em] text-ink/45">
{byKind[kind]!.length}
</span>
</h2>
<ul className="divide-y divide-ink/10">
{byKind[kind]!.map(p => (
<li key={p.id}>
<Link
href={`/film/${p.slug}`}
className="grid grid-cols-[64px_1fr_auto] gap-4 items-center py-4 hover:text-coral transition group"
>
<div className="border border-ink/10 overflow-hidden bg-ink/[0.04]">
{p.poster_url ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={p.poster_url} alt="" loading="lazy" decoding="async" width={64} height={96} className="w-full block aspect-[2/3] object-cover" />
) : (
<PosterPlaceholder
title={p.title}
year={p.year}
kind={p.kind}
width={64}
height={96}
className="w-full block"
/>
)}
</div>
<div>
<h3 className="font-display text-2xl text-ink group-hover:text-coral tracking-[-0.005em]">
{p.title}
</h3>
{p.blurb && (
<p className="mt-1 text-sm text-ink/65 leading-relaxed max-w-prose">
{p.blurb}
</p>
)}
</div>
<div className="text-right">
<p className="font-mono text-xs text-ink/55">{p.year ?? '—'}</p>
<p className="font-mono text-xs uppercase tracking-[0.15em] text-ink/45 mt-1">
{p.location_count} {p.location_count === 1 ? 'location' : 'locations'}
</p>
</div>
</Link>
</li>
))}
</ul>
</div>
))
)}
{truncated && (
<div className="mt-12 pt-8 border-t border-ink/10 text-center">
<p className="text-sm text-ink/55 mb-4">
Showing {productions.length} of {allProductions.length} productions on file.
</p>
<Link
href={buildHref(searchParams, { show: 'all' })}
className="inline-block bg-ink text-sand px-6 py-2 text-sm hover:opacity-90 transition"
>
Show all {allProductions.length}
</Link>
</div>
)}
</section>
</>
);
}