← back to Stayclaim

src/app/browse/page.tsx

130 lines

import Link from 'next/link';
import type { Metadata } from 'next';
import { searchListings } from '@/lib/db';
import { NodeCard } from '@/components/NodeCard';
import { BreadcrumbArchive } from '@/components/BreadcrumbArchive';

const CITIES = ['Beverly Hills', 'Los Angeles', 'Malibu', 'Joshua Tree', 'Big Bear Lake'];

// Index the bare /browse page; noindex any filtered/searched variant so we
// don't pollute the index with arbitrary query-string URLs.
function pickStr(v: string | string[] | undefined): string | undefined {
  if (Array.isArray(v)) return v[0];
  return v;
}

export async function generateMetadata({
  searchParams,
}: {
  searchParams: { q?: string | string[]; city?: string | string[] };
}): Promise<Metadata> {
  const filtered = !!(pickStr(searchParams.q) || pickStr(searchParams.city));
  return {
    title: 'Browse the archive',
    alternates: { canonical: '/browse' },
    robots: filtered
      ? { index: false, follow: true }
      : { index: true, follow: true },
  };
}

export default async function BrowsePage({
  searchParams,
}: {
  searchParams: { q?: string | string[]; city?: string | string[] };
}) {
  const rawCity = pickStr(searchParams.city);
  const city = rawCity && CITIES.includes(rawCity) ? rawCity : undefined;
  const q = (pickStr(searchParams.q) ?? '').trim().slice(0, 200);
  const filtered = await searchListings({ text: q || undefined, city, limit: 60 });

  return (
    <>
      <div className="max-w-6xl mx-auto px-6 pt-6">
        <BreadcrumbArchive items={[{ label: 'Archive' }, { label: 'Browse' }]} />
      </div>

      <header className="max-w-6xl mx-auto px-6 py-10 border-b border-ink/10">
        <p className="text-xs uppercase tracking-[0.15em] text-ink/50 mb-2">Browse the archive</p>
        <h1 className="font-display text-5xl md:text-6xl text-ink tracking-[-0.02em] leading-[1.02]">
          Every address, recorded.
        </h1>
        <p className="mt-3 font-display italic text-xl text-ink/70 max-w-prose">
          Beverly Hills first. The rest of Los Angeles County, then westward.
        </p>

        <form className="mt-8 flex flex-wrap items-end gap-3" action="/browse">
          <label className="flex flex-col gap-1 text-[10px] uppercase tracking-[0.15em] text-ink/50">
            Address or keyword
            <input
              type="search"
              name="q"
              defaultValue={q}
              placeholder="Roxbury · Tudor · 1928…"
              className="w-72 border border-ink/20 bg-sand px-3 py-2 text-sm font-sans focus:outline-none focus:border-moss"
            />
          </label>
          <label className="flex flex-col gap-1 text-[10px] uppercase tracking-[0.15em] text-ink/50">
            Neighborhood
            <select
              name="city"
              defaultValue={city ?? ''}
              className="border border-ink/20 bg-sand px-3 py-2 text-sm font-sans focus:outline-none focus:border-moss"
            >
              <option value="">All</option>
              {CITIES.map(c => (
                <option key={c} value={c}>
                  {c}
                </option>
              ))}
            </select>
          </label>
          <button
            type="submit"
            className="bg-ink text-sand px-5 py-2 text-xs uppercase tracking-wider hover:bg-moss transition"
          >
            Search the archive
          </button>
          {(q || city) && (
            <Link href="/browse" className="text-xs uppercase tracking-wider text-ink/50 hover:text-coral transition">
              Reset
            </Link>
          )}
        </form>
      </header>

      <section className="max-w-6xl mx-auto px-6 py-10">
        <div className="flex items-baseline justify-between mb-6">
          <h2 className="font-display text-3xl text-ink tracking-[-0.01em]">
            {filtered.length} {filtered.length === 1 ? 'address' : 'addresses'}
            {city ? ` in ${city}` : ''}
          </h2>
          <span className="text-[10px] uppercase tracking-[0.15em] text-ink/40">
            ordered by ingest date
          </span>
        </div>

        {filtered.length === 0 ? (
          <p className="text-sm text-ink/50 italic py-10">
            No addresses match. Try a different city or clear the search.
          </p>
        ) : (
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
            {filtered.map(l => (
              <NodeCard
                key={l.id}
                kind="address"
                href={`/address/${l.slug}`}
                title={l.title}
                subtitle={l.address_line1 ?? l.city ?? undefined}
                imageUrl={l.hero_image ?? undefined}
                eyebrow={l.property_type ?? 'Address'}
              />
            ))}
          </div>
        )}
      </section>
    </>
  );
}