← back to Stayclaim

src/app/film/[slug]/page.tsx

197 lines

import Link from 'next/link';
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';
import { getProductionBySlug, getLocationsForProduction, yearOf } from '@/lib/db';
import { BreadcrumbArchive } from '@/components/BreadcrumbArchive';
import { TierBadge } from '@/components/TierBadge';
import { JsonLd } from '@/components/JsonLd';
import { InteractiveMapMount, type MapPin } from '@/components/InteractiveMapMount';
import { PosterPlaceholder } from '@/components/PosterPlaceholder';
import { CrossPromo } from '@/components/CrossPromo';
import { FilmsGridServer } from '@/components/FilmsGridServer';
import { SITE_DOMAIN, currentSurface } from '@/lib/site';

const KIND_LABEL: Record<string, string> = {
  film: 'Feature film',
  tv: 'Television',
  commercial: 'Commercial',
  music_video: 'Music video',
  documentary: 'Documentary',
  short: 'Short',
  student: 'Student',
};

export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
  const p = await getProductionBySlug(params.slug);
  if (!p) return { robots: { index: false, follow: false } };
  return {
    title: `${p.title}${p.year ? ` (${p.year})` : ''}`,
    description: `${KIND_LABEL[p.kind] ?? p.kind} — addresses in the archive used as filming locations.`,
    alternates: { canonical: `/film/${p.slug}` },
    openGraph: { title: p.title, type: 'article' },
  };
}

export default async function FilmPage({ params }: { params: { slug: string } }) {
  const production = await getProductionBySlug(params.slug);
  if (!production) notFound();
  const [surface, locs] = await Promise.all([
    currentSurface(),
    getLocationsForProduction(production.id),
  ]);

  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? `https://${SITE_DOMAIN}`;
  const jsonLd: Record<string, unknown> = {
    '@context': 'https://schema.org',
    '@type': production.kind === 'tv' ? 'TVSeries' : 'Movie',
    name: production.title,
    url: `${baseUrl}/film/${production.slug}`,
  };
  if (production.year) jsonLd.dateCreated = String(production.year);
  if (production.imdb_id) jsonLd.sameAs = [`https://www.imdb.com/title/${encodeURIComponent(production.imdb_id)}/`];

  const pins: MapPin[] = locs
    .map(l => {
      const lat = Number(l.listing_lat);
      const lng = Number(l.listing_lng);
      if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null;
      return {
        slug: l.listing_slug,
        title: l.listing_title,
        subtitle: l.role ?? 'Location',
        lat,
        lng,
      };
    })
    .filter((p): p is NonNullable<typeof p> => p !== null);

  return (
    <>
      <JsonLd data={jsonLd} />
      <div className="max-w-4xl mx-auto px-6 pt-6">
        <BreadcrumbArchive
          items={[
            { label: 'Archive', href: '/' },
            { label: 'Films + TV', href: '/films' },
            { label: production.title },
          ]}
        />
      </div>

      <header className="max-w-4xl mx-auto px-6 py-12 border-b border-ink/10 grid md:grid-cols-[200px_1fr] gap-8 items-start">
        <div className="border border-ink/10 overflow-hidden">
          {production.poster_url && /^https?:\/\//i.test(production.poster_url) ? (
            // eslint-disable-next-line @next/next/no-img-element
            <img src={production.poster_url} alt={production.title} className="w-full block" />
          ) : (
            <PosterPlaceholder
              title={production.title}
              year={production.year}
              kind={production.kind}
              width={200}
              height={300}
              className="w-full block"
            />
          )}
        </div>
        <div>
        <p className="text-xs uppercase tracking-[0.15em] text-ink/50 mb-2">
          {KIND_LABEL[production.kind] ?? production.kind} {production.year ? `· ${production.year}` : ''}
        </p>
        <h1 className="font-display text-5xl md:text-6xl text-ink tracking-[-0.02em] leading-[1.02]">
          {production.title}
        </h1>
        {production.blurb && (
          <p className="mt-5 font-display italic text-xl text-ink/80 max-w-prose">{production.blurb}</p>
        )}
        <div className="mt-4 flex flex-wrap items-center gap-4">
          <p className="font-mono text-[11px] text-ink/50">
            {locs.length} {locs.length === 1 ? 'location' : 'locations'} pulled from public film permits
          </p>
          {production.imdb_id && (
            <a
              href={`https://www.imdb.com/title/${encodeURIComponent(production.imdb_id)}/`}
              target="_blank"
              rel="noopener noreferrer"
              className="font-mono text-[11px] uppercase tracking-[0.15em] text-ink/60 hover:text-coral transition"
            >
              IMDb · {production.imdb_id} →
            </a>
          )}
          {production.wikidata_qid && (
            <a
              href={`https://www.wikidata.org/wiki/${encodeURIComponent(production.wikidata_qid)}`}
              target="_blank"
              rel="noopener noreferrer"
              className="font-mono text-[11px] uppercase tracking-[0.15em] text-ink/60 hover:text-coral transition"
            >
              Wikidata · {production.wikidata_qid} →
            </a>
          )}
        </div>
        </div>
      </header>

      {pins.length > 0 && (
        <div className="max-w-4xl mx-auto px-6 mt-10">
          <InteractiveMapMount pins={pins} height={320} variant="positron" />
        </div>
      )}

      <section className="max-w-4xl mx-auto px-6 py-12">
        <h2 className="font-display text-3xl text-ink tracking-[-0.01em] mb-6">Where it filmed</h2>
        {locs.length === 0 ? (
          <p className="text-sm italic text-ink/55">No public location permits on file yet.</p>
        ) : (
          <ol className="divide-y divide-ink/10">
            {locs.map(l => (
              <li key={l.id} className="py-5 grid md:grid-cols-[120px_1fr_auto] gap-3 md:gap-6 items-baseline">
                <time className="font-mono text-xs uppercase tracking-wider text-ink/60">
                  {yearOf(l.shoot_date_from)}
                </time>
                <div>
                  <Link href={`/address/${l.listing_slug}`} className="font-display text-2xl text-ink hover:text-coral transition leading-tight">
                    {l.listing_title}
                  </Link>
                  {l.scene_note && (
                    <p className="mt-1 text-sm text-ink/65 leading-relaxed">{l.scene_note}</p>
                  )}
                  <div className="mt-2 flex gap-3 items-center flex-wrap">
                    <span className="text-[10px] uppercase tracking-[0.15em] text-ink/50">{l.role ?? 'on location'}</span>
                    {(() => {
                      const tier = (['A', 'B', 'C'] as const).includes(l.source_tier as 'A' | 'B' | 'C')
                        ? (l.source_tier as 'A' | 'B' | 'C')
                        : 'C';
                      const safeUrl = l.source_url && /^https?:\/\//i.test(l.source_url) ? l.source_url : null;
                      return safeUrl ? (
                        <a href={safeUrl} target="_blank" rel="noopener noreferrer">
                          <TierBadge tier={tier} sourceLabel={l.source_label ?? undefined} />
                        </a>
                      ) : (
                        <TierBadge tier={tier} sourceLabel={l.source_label ?? undefined} />
                      );
                    })()}
                  </div>
                </div>
                <span className="font-mono text-[10px] uppercase tracking-[0.15em] text-ink/45 md:text-right">
                  {l.permit_number}
                </span>
              </li>
            ))}
          </ol>
        )}
      </section>

      <FilmsGridServer
        heading="Public-domain stills + posters"
        kicker="Provenance: Wikimedia · Internet Archive · LoC · NARA"
        defaultCols={5}
        productionId={production.id}
        limit={36}
      />

      <CrossPromo current={surface.key} />
    </>
  );
}