TypeScript 61.9%
HTML 37.2%
SQL 0.7%
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