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