← back to Stayclaim

src/app/promote/[slug]/PromoteFormClient.tsx

187 lines

'use client';
import { useEffect, useRef, useState } from 'react';

export function PromoteFormClient({
  listingId,
  defaultHeadline,
}: {
  listingId: string;
  defaultHeadline: string;
}) {
  const [status, setStatus] = useState<'idle' | 'submitting' | 'ok' | 'error'>('idle');
  const [error, setError] = useState<string | null>(null);
  const [submissionId, setSubmissionId] = useState<string | null>(null);
  const abortRef = useRef<AbortController | null>(null);

  useEffect(() => {
    return () => {
      abortRef.current?.abort();
    };
  }, []);

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus('submitting');
    setError(null);
    const fd = new FormData(e.currentTarget);
    const s = (k: string) => String(fd.get(k) ?? '').trim();
    const body = {
      listing_id: listingId,
      source: s('source'),
      external_url: s('external_url'),
      headline: s('headline'),
      price_label: s('price_label') || undefined,
      contact_email: s('contact_email'),
      affiliation: s('affiliation'),
      permit_number: s('permit_number') || undefined,
    };
    abortRef.current?.abort();
    const ctrl = new AbortController();
    abortRef.current = ctrl;
    try {
      const res = await fetch('/api/promote', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
        signal: ctrl.signal,
      });
      if (!res.ok) {
        const j = await res.json().catch(() => ({}));
        setError(typeof j?.error === 'string' ? j.error : `HTTP ${res.status}`);
        setStatus('error');
        return;
      }
      const j = await res.json().catch(() => ({}));
      if (typeof j?.id !== 'string' || !j.id) {
        setError('Bad server response');
        setStatus('error');
        return;
      }
      setSubmissionId(j.id);
      setStatus('ok');
    } catch (err) {
      if ((err as { name?: string })?.name === 'AbortError') return;
      setError(err instanceof Error ? err.message : 'Network error');
      setStatus('error');
    }
  }

  if (status === 'ok' && submissionId) {
    return (
      <div className="mt-10 border border-moss/30 bg-moss/5 p-6">
        <p className="font-display text-2xl text-ink">Submission received.</p>
        <p className="mt-2 text-sm text-ink/70 leading-relaxed">
          Confirmation #{' '}
          <span className="font-mono text-xs">{submissionId.slice(0, 8)}</span>
          . We&rsquo;ll verify your affiliation within one business day and email you when your placement is approved.
        </p>
      </div>
    );
  }

  return (
    <form onSubmit={onSubmit} className="mt-10 grid gap-5">
      <label className="flex flex-col gap-1 text-[10px] uppercase tracking-[0.15em] text-ink/55">
        Listing source
        <select
          name="source"
          required
          className="border border-ink/20 px-3 py-2 text-sm font-sans focus:outline-none focus:border-moss bg-sand"
        >
          <option value="">Select…</option>
          <option value="zillow">Zillow</option>
          <option value="redfin">Redfin</option>
          <option value="airbnb">Airbnb</option>
          <option value="other">Other</option>
        </select>
      </label>

      <label className="flex flex-col gap-1 text-[10px] uppercase tracking-[0.15em] text-ink/55">
        Listing URL
        <input
          required
          type="url"
          name="external_url"
          placeholder="https://www.zillow.com/homedetails/…"
          className="border border-ink/20 px-3 py-2 text-sm font-sans focus:outline-none focus:border-moss bg-sand"
        />
      </label>

      <label className="flex flex-col gap-1 text-[10px] uppercase tracking-[0.15em] text-ink/55">
        Headline (max 200 chars)
        <input
          type="text"
          name="headline"
          required
          maxLength={200}
          defaultValue={defaultHeadline}
          className="border border-ink/20 px-3 py-2 text-sm font-sans focus:outline-none focus:border-moss bg-sand"
        />
      </label>

      <label className="flex flex-col gap-1 text-[10px] uppercase tracking-[0.15em] text-ink/55">
        Price label (optional)
        <input
          type="text"
          name="price_label"
          maxLength={80}
          placeholder="$4,250,000 · Open Saturday 1–4pm"
          className="border border-ink/20 px-3 py-2 text-sm font-sans focus:outline-none focus:border-moss bg-sand"
        />
      </label>

      <label className="flex flex-col gap-1 text-[10px] uppercase tracking-[0.15em] text-ink/55">
        Your email
        <input
          type="email"
          name="contact_email"
          required
          placeholder="agent@brokerage.com"
          className="border border-ink/20 px-3 py-2 text-sm font-sans focus:outline-none focus:border-moss bg-sand"
        />
      </label>

      <label className="flex flex-col gap-1 text-[10px] uppercase tracking-[0.15em] text-ink/55">
        How are you affiliated with this address? (min 10 chars)
        <textarea
          required
          name="affiliation"
          rows={3}
          minLength={10}
          placeholder="Listing agent, owner, or representative. We verify before the placement goes live."
          className="border border-ink/20 px-3 py-2 text-sm font-sans focus:outline-none focus:border-moss bg-sand"
        />
      </label>

      <label className="flex flex-col gap-1 text-[10px] uppercase tracking-[0.15em] text-ink/55">
        Permit # (Beverly Hills / Santa Monica / etc., if required by your jurisdiction)
        <input
          type="text"
          name="permit_number"
          placeholder="Optional — self-attested"
          className="border border-ink/20 px-3 py-2 text-sm font-sans focus:outline-none focus:border-moss bg-sand"
        />
      </label>

      <button
        type="submit"
        disabled={status === 'submitting'}
        className="bg-coral text-ink px-5 py-3 text-xs uppercase tracking-wider hover:bg-ink hover:text-sand transition self-start disabled:opacity-50"
      >
        {status === 'submitting' ? 'Submitting…' : 'Submit for review'}
      </button>

      {error && (
        <p className="text-xs text-coral" role="alert">
          {error}
        </p>
      )}

      <p className="text-[11px] text-ink/45 max-w-prose leading-relaxed">
        We&rsquo;ll verify the affiliation and load your placement within 1 business day. Stripe checkout sent
        after approval. We do not write your listing into the historical ledger.
      </p>
    </form>
  );
}