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