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%
4.9 KB · 105 lines tsx
Raw Blame History
1'use client';23import { useActionState, useState } from 'react';4import { createAlertAction } from '@/lib/account/actions';5import { idle } from '@/lib/auth/state';6import { Field, FormMessage, Select, SubmitButton, TextInput } from '@/components/account/form';7import { AssetPicker } from '@/components/account/asset-picker';8import { CATEGORIES, INDICES } from '@rareindex/taxonomy';910const TYPES: Array<{ v: string; l: string; targets: Array<'asset' | 'category' | 'index'>; threshold?: 'usd' | 'pct' }> = [11  { v: 'price_below', l: 'RIV falls below a value', targets: ['asset'], threshold: 'usd' },12  { v: 'price_above', l: 'RIV rises above a value', targets: ['asset'], threshold: 'usd' },13  { v: 'new_listing', l: 'New listing appears', targets: ['asset'] },14  { v: 'new_auction', l: 'New auction lot', targets: ['asset', 'category'] },15  { v: 'auction_ending', l: 'Auction ending within 24h', targets: ['asset', 'category'] },16  { v: 'auction_below_riv', l: 'Auction bid (with buyer premium) ≥ 10 % below RIV', targets: ['asset'] },17  { v: 'record_sale', l: 'New record (all-time high) sale', targets: ['asset', 'category'] },18  { v: 'unusual_volume', l: 'Unusual sales volume (% above 90-day average)', targets: ['asset', 'category'], threshold: 'pct' },19  { v: 'market_move', l: 'Index / category moves more than %', targets: ['category', 'index'], threshold: 'pct' },20  { v: 'rare_item', l: 'Rare item appears (Rare Radar)', targets: ['category'] },21  { v: 'population_update', l: 'Grading population changes', targets: ['asset'] },22];2324export function AlertForm() {25  const [state, action] = useActionState(createAlertAction, idle);26  const [type, setType] = useState('price_below');27  const def = TYPES.find((t) => t.v === type)!;28  const [target, setTarget] = useState<'asset' | 'category' | 'index'>(def.targets[0]!);29  const effTarget = def.targets.includes(target) ? target : def.targets[0]!;30  return (31    <form action={action} className="space-y-3" noValidate key={state.ok ? 'reset' : 'form'}>32      <FormMessage state={state} />33      <Field label="When" name="alertType">34        <Select name="alertType" value={type} onChange={(e) => setType(e.target.value)}>35          {TYPES.map((t) => (36            <option key={t.v} value={t.v}>37              {t.l}38            </option>39          ))}40        </Select>41      </Field>42      {def.targets.length > 1 ? (43        <div className="flex gap-1 text-xs">44          {def.targets.map((t) => (45            <button key={t} type="button" onClick={() => setTarget(t)} className={`rounded-full border px-3 py-1 ${effTarget === t ? 'border-fg bg-accent text-accent-fg' : 'border-border text-muted'}`}>46              {t}47            </button>48          ))}49        </div>50      ) : null}51      <input type="hidden" name="targetType" value={effTarget} />52      {effTarget === 'asset' ? (53        <AssetPicker name="targetId" withVariant={false} error={state.fieldErrors?.targetId} />54      ) : effTarget === 'category' ? (55        <Field label="Category" name="targetId">56          <Select name="targetId" defaultValue="pokemon">57            {CATEGORIES.map((c) => (58              <option key={c.slug} value={c.slug}>59                {'— '.repeat(c.level)}60                {c.name}61              </option>62            ))}63          </Select>64        </Field>65      ) : (66        <Field label="Index" name="targetId">67          <Select name="targetId" defaultValue="RARE">68            {INDICES.map((i) => (69              <option key={i.ticker} value={i.ticker}>70                {i.ticker} — {i.name}71              </option>72            ))}73          </Select>74        </Field>75      )}76      {def.threshold ? (77        <Field label={def.threshold === 'usd' ? 'Threshold (USD)' : 'Threshold (%)'} name="threshold" error={state.fieldErrors?.threshold}>78          <TextInput name="threshold" type="number" min={0} step={def.threshold === 'usd' ? '0.01' : '0.5'} required placeholder={def.threshold === 'usd' ? '2500' : '5'} />79        </Field>80      ) : null}81      <div className="grid grid-cols-2 gap-3">82        <Field label="Deliver via" name="channel">83          <Select name="channel" defaultValue="both">84            <option value="both">Inbox + e-mail</option>85            <option value="inapp">Inbox only</option>86            <option value="email">E-mail only</option>87          </Select>88        </Field>89        <Field label="Cooldown" name="cooldownMinutes" hint="Minimum time between triggers.">90          <Select name="cooldownMinutes" defaultValue="1440">91            <option value="60">1 hour</option>92            <option value="360">6 hours</option>93            <option value="1440">1 day</option>94            <option value="10080">1 week</option>95          </Select>96        </Field>97      </div>98      <Field label="Name (optional)" name="name">99        <TextInput name="name" maxLength={80} placeholder="Defaults to the asset title" />100      </Field>101      <SubmitButton pendingText="Creating…">Create alert</SubmitButton>102    </form>103  );104}105