← back to Stayclaim

src/app/submit/SubmitFormClient.tsx

259 lines

'use client';
/**
 * Single-search submit form.
 *
 * Replaces the previous 4-field address grid (street/city/state/zip) with
 * one big search input that hits the live LA County records lookup
 * (/api/la-records/lookup). The lookup auto-fills every canonical field
 * from the assessor record AND shows a preview card so the user can see
 * the value of the product before submitting.
 *
 * If LA records doesn't find a match (out of county, unusual address, typo),
 * the user can still submit — we'll do a manual lookup on our side.
 */
import { useState, useEffect, useRef } from 'react';
import { SubmitMiniMap } from '@/components/SubmitMiniMap';

type LookupHit = {
  ok: true;
  found: true;
  apn: string;
  situs_address: string | null;
  city: string | null;
  zip: string | null;
  year_built: number | null;
  sqft_main: number | null;
  bedrooms: number | null;
  bathrooms: number | null;
  use_description: string | null;
  total_value: number | null;
  roll_year: number | null;
  lat: number | null;
  lng: number | null;
};

type LookupState =
  | { kind: 'idle' }
  | { kind: 'looking' }
  | { kind: 'hit'; data: LookupHit; query: string }
  | { kind: 'miss' }
  | { kind: 'err'; msg: string };

export function SubmitFormClient() {
  const [query, setQuery] = useState('');
  const [lookup, setLookup] = useState<LookupState>({ kind: 'idle' });
  const [status, setStatus] = useState<'idle' | 'submitting' | 'ok' | 'error'>('idle');
  const [error, setError] = useState<string | null>(null);
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const reqIdRef = useRef(0);

  // Debounced lookup as the user types
  useEffect(() => {
    if (debounceRef.current) clearTimeout(debounceRef.current);
    if (query.trim().length < 8) {
      setLookup({ kind: 'idle' });
      return;
    }
    const submittedQuery = query;
    debounceRef.current = setTimeout(async () => {
      const myReqId = ++reqIdRef.current;
      setLookup({ kind: 'looking' });
      try {
        const r = await fetch(`/api/la-records/lookup?q=${encodeURIComponent(submittedQuery)}`);
        if (myReqId !== reqIdRef.current) return; // stale response
        if (!r.ok) { setLookup({ kind: 'err', msg: `HTTP ${r.status}` }); return; }
        const j = await r.json();
        if (j.found) setLookup({ kind: 'hit', data: j, query: submittedQuery });
        else setLookup({ kind: 'miss' });
      } catch (e) {
        if (myReqId !== reqIdRef.current) return;
        setLookup({ kind: 'err', msg: e instanceof Error ? e.message : String(e) });
      }
    }, 450);
    return () => { if (debounceRef.current) clearTimeout(debounceRef.current); };
  }, [query]);

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus('submitting');
    setError(null);
    const fd = new FormData(e.currentTarget);
    const body: Record<string, unknown> = Object.fromEntries(fd.entries());
    // If the lookup hit, attach the canonical assessor data so the server
    // doesn't have to re-resolve. If miss, body just carries the free-form query.
    // /api/submit requires city + state. Pull them from the LA County hit
    // (situs_address format: "<street> <CITY> CA <ZIP>") or default to LA/CA.
    // Use parsed parcel fields directly (city/zip are returned by /api/la-records/lookup
    // from the SitusCity / SitusZIP attributes) — far more reliable than regexing
    // the joined situs_address string.
    // Only attach assessor data when the hit is for the CURRENT query — guards
    // against stale-lookup race when user edits address within the debounce window.
    if (lookup.kind === 'hit' && lookup.query === query) {
      body.address_line1 = lookup.data.situs_address ?? query;
      body.apn           = lookup.data.apn;
      body.year_built    = lookup.data.year_built;
      body.sqft_main     = lookup.data.sqft_main;
      body.lat           = lookup.data.lat;
      body.lng           = lookup.data.lng;
      body.city          = lookup.data.city ?? 'Los Angeles';
      body.state         = 'CA';
      body.postal_code   = lookup.data.zip ?? null;
    } else {
      body.address_line1 = query;
      body.city          = 'Los Angeles';
      body.state         = 'CA';
    }
    try {
      const res = await fetch('/api/submit', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      if (!res.ok) {
        const j = await res.json().catch(() => ({}));
        setError(j.error ?? `HTTP ${res.status}`);
        setStatus('error');
        return;
      }
      setStatus('ok');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Network error — please try again.');
      setStatus('error');
    }
  }

  if (status === 'ok') {
    return (
      <div className="border-2 border-[#c9292e] bg-[#fdf9ee] p-6">
        <p className="font-mono uppercase text-[10px] tracking-[0.2em] text-[#c9292e] mb-2">Filed</p>
        <p className="font-display text-2xl text-ink">Submission queued.</p>
        <p className="mt-2 text-sm text-ink/70 leading-relaxed">
          We&rsquo;ll cross-check against assessor data, fetch any available archival material, and notify
          you when the page is published. Typically 1–3 business days for Beverly Hills addresses; longer
          for outside the pilot area.
        </p>
      </div>
    );
  }

  return (
    <form onSubmit={onSubmit} className="grid gap-6">
      {/* Single search bar — auto-resolves to LA County parcel record */}
      <label className="flex flex-col gap-2">
        <span className="text-[10px] uppercase tracking-[0.18em] text-ink/55 font-mono">
          Address
        </span>
        <input
          required
          type="text"
          value={query}
          onChange={e => setQuery(e.target.value)}
          placeholder="Type your address — e.g. 612 N Bedford Drive Beverly Hills"
          autoComplete="street-address"
          className="border-2 border-ink/30 px-4 py-3 text-base font-sans focus:outline-none focus:border-[#c9292e] bg-white"
        />
        <span className="text-[11px] text-ink/50">
          One field. We&rsquo;ll resolve the city, ZIP, and assessor record from LA County in real time.
        </span>
      </label>

      {/* Live preview card — only renders when lookup has something to say */}
      {lookup.kind === 'looking' && (
        <div className="border border-ink/15 bg-[#fdf9ee] px-4 py-3 text-xs font-mono uppercase tracking-[0.15em] text-ink/55">
          Checking LA County records…
        </div>
      )}
      {lookup.kind === 'hit' && (
        <div className="border-l-[4px] border-[#c9292e] bg-white px-5 py-4 relative grid grid-cols-[1fr_auto] gap-5">
          <div>
            <span className="inline-block bg-[#c9292e] text-white px-2 py-0.5 text-[9px] tracking-[0.2em] font-mono uppercase font-bold mb-2">
              Found · APN {lookup.data.apn}
            </span>
            <p className="font-sans font-bold text-base text-ink">
              {lookup.data.situs_address}
            </p>
            <ul className="mt-2 space-y-0.5 text-[12px] font-mono text-ink/75">
              {lookup.data.year_built && <li>· built {lookup.data.year_built}</li>}
              {lookup.data.sqft_main && <li>· {lookup.data.sqft_main.toLocaleString()} sqft</li>}
              {lookup.data.bedrooms != null && lookup.data.bedrooms > 0 && (
                <li>· {lookup.data.bedrooms} bed{lookup.data.bathrooms != null ? ` / ${lookup.data.bathrooms} bath` : ''}</li>
              )}
              {lookup.data.use_description && <li>· {lookup.data.use_description.toLowerCase()}</li>}
              {lookup.data.total_value && (
                <li>· assessed at ${(lookup.data.total_value / 1_000_000).toFixed(2)}M ({lookup.data.roll_year} roll)</li>
              )}
            </ul>
            <p className="mt-3 text-[11px] text-ink/55 italic">
              Source: LA County Assessor (public record).
            </p>
          </div>
          {lookup.data.lat != null && lookup.data.lng != null && (
            <SubmitMiniMap
              lat={lookup.data.lat}
              lng={lookup.data.lng}
              label={lookup.data.situs_address ?? ''}
            />
          )}
        </div>
      )}
      {lookup.kind === 'miss' && (
        <div className="border border-dashed border-ink/30 bg-[#fdf9ee] px-4 py-3 text-[12px] text-ink/70">
          We couldn&rsquo;t auto-resolve that one against LA County. Submit anyway —
          we&rsquo;ll look it up by hand. Outside-LA addresses always go through the
          manual queue.
        </div>
      )}
      {lookup.kind === 'err' && (
        <div className="border border-[#c9292e]/40 bg-[#c9292e]/5 px-4 py-3 text-[12px] text-[#c9292e]">
          Lookup error: {lookup.msg}. You can still submit.
        </div>
      )}

      <label className="flex flex-col gap-2">
        <span className="text-[10px] uppercase tracking-[0.18em] text-ink/55 font-mono">
          Your email
        </span>
        <input
          required
          type="email"
          name="contact_email"
          placeholder="you@example.com"
          autoComplete="email"
          className="border border-ink/20 px-4 py-3 text-sm font-sans focus:outline-none focus:border-[#c9292e] bg-white"
        />
      </label>

      <label className="flex flex-col gap-2">
        <span className="text-[10px] uppercase tracking-[0.18em] text-ink/55 font-mono">
          Anything else? (optional)
        </span>
        <textarea
          name="notes"
          rows={3}
          placeholder="History you know, architect, prior occupants — helps us prioritize and find the right archive sources."
          className="border border-ink/20 px-4 py-3 text-sm font-sans focus:outline-none focus:border-[#c9292e] bg-white resize-none"
        />
      </label>

      <div className="flex items-center gap-4 flex-wrap">
        <button
          type="submit"
          disabled={status === 'submitting' || query.trim().length < 8}
          className="bg-[#c9292e] text-white px-6 py-3 text-xs uppercase tracking-[0.2em] font-mono font-bold hover:opacity-90 transition disabled:opacity-40"
        >
          {status === 'submitting' ? 'Submitting…' : 'Submit address'}
        </button>
        <span className="text-[11px] font-mono uppercase tracking-[0.15em] text-ink/45">
          Free forever · no card · no signup
        </span>
      </div>

      {error && (
        <p className="text-xs text-[#c9292e]" role="alert">
          {error}
        </p>
      )}
    </form>
  );
}