SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
6.0 KB · 157 lines tsx
Raw Blame History
1'use client';23import { useRouter, useSearchParams } from 'next/navigation';4import { useEffect, useState } from 'react';5import { Check, Link2, Search, X } from 'lucide-react';67interface Suggestion {8  type: string;9  label: string;10  sublabel: string | null;11  href: string;12}1314const SLOTS = ['a', 'b', 'c', 'd'] as const;1516/** Four slot pickers with suggestions; values are asset slugs or index tickers, stored in the URL. */17export function CompareForm({ initial }: { initial: string[] }) {18  const router = useRouter();19  const params = useSearchParams();20  const [values, setValues] = useState<string[]>([...initial, '', '', '', ''].slice(0, 4));21  const [typing, setTyping] = useState<{ i: number; q: string } | null>(null);22  const [items, setItems] = useState<Suggestion[]>([]);2324  useEffect(() => {25    if (!typing || typing.q.trim().length < 2) return;26    const ctrl = new AbortController();27    const t = setTimeout(() => {28      fetch(`/api/search/suggest?q=${encodeURIComponent(typing.q)}`, { signal: ctrl.signal })29        .then((r) => r.json())30        .then((d: { items: Suggestion[] }) => setItems((d.items ?? []).filter((x) => x.type === 'asset' || x.type === 'index').slice(0, 8)))31        .catch(() => {});32    }, 150);33    return () => {34      clearTimeout(t);35      ctrl.abort();36    };37  }, [typing]);3839  const apply = (vals: string[]) => {40    const sp = new URLSearchParams();41    vals.forEach((v, i) => {42      if (v.trim()) sp.set(SLOTS[i]!, v.trim());43    });44    const w = params?.get('w');45    if (w) sp.set('w', w);46    router.push(`/compare?${sp.toString()}`);47  };4849  return (50    <form51      className="card grid gap-2 p-3 sm:grid-cols-2 lg:grid-cols-4"52      onSubmit={(e) => {53        e.preventDefault();54        apply(values);55      }}56    >57      {SLOTS.map((s, i) => (58        <label key={s} className="relative flex flex-col gap-1 text-[10px] font-medium uppercase tracking-wider text-subtle">59          Slot {s.toUpperCase()}60          <span className="relative">61            <Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-subtle" />62            <input63              value={values[i] ?? ''}64              onChange={(e) => {65                const v = [...values];66                v[i] = e.target.value;67                setValues(v);68                setTyping({ i, q: e.target.value });69                if (e.target.value.trim().length < 2) setItems([]);70              }}71              onBlur={() =>72                setTimeout(() => {73                  setTyping(null);74                  setItems([]);75                }, 150)76              }77              placeholder="Search an asset or RARE-TCG"78              autoComplete="off"79              className="h-10 w-full rounded-md border border-border bg-elevated pl-8 pr-8 text-base normal-case tracking-normal text-fg placeholder:text-subtle focus:border-border-strong focus:outline-none md:h-9 md:text-[13px]"80            />81            {values[i] ? (82              <button83                type="button"84                aria-label={`Clear slot ${s.toUpperCase()}`}85                onClick={() => {86                  const v = [...values];87                  v[i] = '';88                  setValues(v);89                  apply(v);90                }}91                className="absolute right-1.5 top-1/2 inline-flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-md text-subtle hover:bg-inset hover:text-fg"92              >93                <X className="h-3.5 w-3.5" />94              </button>95            ) : null}96          </span>97          {typing?.i === i && items.length ? (98            <ul className="absolute left-0 top-full z-20 mt-1 w-full overflow-hidden rounded-md border border-border bg-elevated text-[13px] normal-case tracking-normal shadow-pop">99              {items.map((it) => {100                const val = it.href.startsWith('/asset/') ? it.href.slice(7) : it.href.startsWith('/rareindex/') ? it.href.slice(11) : it.label;101                return (102                  <li key={it.href}>103                    <button104                      type="button"105                      onMouseDown={(e) => e.preventDefault()}106                      onClick={() => {107                        const v = [...values];108                        v[i] = val;109                        setValues(v);110                        setTyping(null);111                        apply(v);112                      }}113                      className="block w-full truncate px-2.5 py-2 text-left text-fg hover:bg-sunken"114                    >115                      {it.label} <span className="text-subtle">· {it.sublabel}</span>116                    </button>117                  </li>118                );119              })}120            </ul>121          ) : null}122        </label>123      ))}124      <div className="flex items-center justify-between gap-3 text-[11px] text-subtle sm:col-span-2 lg:col-span-4">125        <span className="hidden sm:inline">Tickers: RARE, RARE-TCG, RARE-SPORT, RARE-WATCH, RARE-SNEAKER, RARE-LEGO, RARE-COMIC, RARE-GAME…</span>126        <button type="submit" className="inline-flex h-10 items-center rounded-md bg-accent px-4 text-xs font-medium text-accent-fg md:h-9">127          Compare128        </button>129      </div>130    </form>131  );132}133134export function ShareButton() {135  const [done, setDone] = useState(false);136  return (137    <button138      type="button"139      onClick={async () => {140        const url = window.location.href;141        try {142          if (navigator.share) await navigator.share({ title: 'RareIndex — Compare', url });143          else await navigator.clipboard.writeText(url);144          setDone(true);145          setTimeout(() => setDone(false), 1800);146        } catch {147          /* cancelled */148        }149      }}150      className="inline-flex h-9 items-center gap-1.5 rounded-md border border-border bg-elevated px-3 text-[12px] font-medium text-fg hover:bg-inset"151    >152      {done ? <Check className="h-3.5 w-3.5 text-gain" /> : <Link2 className="h-3.5 w-3.5" />}153      {done ? 'Link copied' : 'Share'}154    </button>155  );156}157