SPB Git

Repository Public

Activity.tsx 6.4 KB 60 lines tsx

1import { useQuery } from '@tanstack/react-query';2import { useContext, useState } from 'react';3import { get } from '../api';4import { useNetworks, useSettings } from '../hooks';5import { I18n, useT } from '../i18n';6import { BridgeFacts, BridgePill, BridgeProgress, type BridgeV } from '../bridge';7import { XswapFacts, XswapPill, XswapProgress, type XswapV } from '../xswap';8import { useQueryClient } from '@tanstack/react-query';9import { PlanList } from '../plan'; // v7 any-to-any plans10import { Addr, Card, Coin, Icon, inputCls, Money, short, Skeleton, StatePill, testNotSent, trimAmt, when } from '../ui';1112interface Item { swap?: { toSymbol: string; receivedDisplay: string | null; minOutDisplay: string } | null; bridge?: BridgeV | null; xswap?: XswapV | null; kind: 'send' | 'receive' | 'swap' | 'bridge'; at: string; id: string; network: string; networkName: string; symbol: string; amount: string; amountDisplay: string; state: string; txid: string | null; explorer: string | null;13  to?: string; from?: string; feeDisplay?: string | null; feeSymbol?: string; error?: string | null; note?: string; confirmations?: number }1415export function Activity() {16  const t = useT(); const { locale } = useContext(I18n); const { settings } = useSettings(); const nets = useNetworks();17  const q = useQuery({ queryKey: ['activity'], queryFn: () => get<{ items: Item[] }>('/activity?limit=200'), refetchInterval: 20_000 });18  const qc = useQueryClient();19  const [kind, setKind] = useState<'all' | 'send' | 'receive'>('all'), [net, setNet] = useState(''), [open, setOpen] = useState('');20  const items = (q.data?.items ?? []).filter((i) => (kind === 'all' || i.kind === kind || (kind === 'send' && (i.kind === 'swap' || i.kind === 'bridge'))) && (!net || i.network === net));21  return (22    <div className="space-y-4">23      <h1 className="pt-2 text-[30px] font-semibold tracking-[-0.02em]">{t('activity')}</h1>24      <div className="flex gap-2">25        <div className="flex flex-1 rounded-2xl bg-elev p-1" role="tablist">{(['all', 'send', 'receive'] as const).map((k) =>26          <button key={k} role="tab" aria-selected={kind === k} onClick={() => setKind(k)} className={`min-h-11 flex-1 rounded-xl text-[13.5px] ${kind === k ? 'bg-surface font-semibold shadow-sm' : 'text-mute'}`}>{t(k === 'all' ? 'all' : k === 'send' ? 'sent' : 'received')}</button>)}</div>27        <select aria-label={t('network')} className={`${inputCls} w-auto max-w-[42%] py-2`} value={net} onChange={(e) => setNet(e.target.value)}><option value="">{t('allNetworks')}</option>{(nets.data ?? []).map((n) => <option key={n.id} value={n.id}>{n.name}</option>)}</select>28      </div>29      <PlanList />30      {q.isLoading ? <Skeleton h="h-48" /> : items.length === 0 ? <Card><p className="py-6 text-center text-sm text-mute">{t('noActivity')}</p></Card> :31        <Card className="!p-0"><ul className="divide-y divide-line">{items.map((i) => { const isOpen = open === i.id; return (32          <li key={i.id}>33            <button className="flex w-full items-center gap-3 px-4 py-3.5 text-left" aria-expanded={isOpen} onClick={() => setOpen(isOpen ? '' : i.id)}>34              <Coin sym={i.symbol} net={i.network} size={42} />35              <span className="min-w-0 flex-1"><span className="block font-medium">{i.kind === 'bridge' ? `${t('brActivity')} · ${i.symbol} → ${i.bridge?.toNetworkName ?? ''}` : i.kind === 'swap' ? `${t('swap')} · ${i.symbol} → ${i.swap?.toSymbol ?? ''}` : `${i.kind === 'receive' ? t('received') : t('sent')} · ${i.symbol}`}</span>36                <span className="block truncate text-xs text-mute">{i.networkName} · {when(i.at, locale)}</span></span>37              <span className="text-right"><span className={`block font-semibold ${i.kind === 'receive' ? 'text-emerald' : ''}`}><Money hidden={settings.hideBalances}>{i.kind === 'receive' ? '+' : '−'}{trimAmt(i.amountDisplay)}</Money></span>{testNotSent(i) ? <span className="rounded-full bg-warn/12 px-2 py-0.5 text-[11.5px] font-semibold text-warn">{t('qaSignedNotSent')}</span> : i.kind === 'bridge' && i.bridge ? <BridgePill b={i.bridge} /> : i.xswap ? <XswapPill x={i.xswap} /> : <StatePill state={i.state} />}</span>38            </button>39            {isOpen && <div className="space-y-2.5 border-t border-line bg-bg/60 px-4 py-3 text-[13px]">40              <D k={t('amount')}>{i.amountDisplay} {i.symbol} <span className="text-mute">({i.amount} {t('baseUnits')})</span></D>41              {i.xswap && <dl className="space-y-1.5"><XswapFacts x={i.xswap} symbol={i.symbol} fromName={i.networkName} /></dl>}42              {i.xswap && <XswapProgress id={i.id} x={i.xswap} symbol={i.symbol} onUpdate={() => void qc.invalidateQueries({ queryKey: ['activity'] })} />}43              {i.kind === 'swap' && i.swap && !i.xswap && <D k={i.swap.receivedDisplay ? t('swReceived') : t('swMinGuaranteed')}>{i.swap.receivedDisplay ?? i.swap.minOutDisplay} {i.swap.toSymbol}</D>}44              {i.kind === 'bridge' && i.bridge && <dl className="space-y-1.5"><BridgeFacts b={i.bridge} symbol={i.symbol} /></dl>}45              {i.kind === 'bridge' && i.bridge && !testNotSent(i) && <BridgeProgress id={i.id} b={i.bridge} onUpdate={() => void qc.invalidateQueries({ queryKey: ['activity'] })} />}46              {i.to && i.kind !== 'swap' && i.kind !== 'bridge' && <D k={t('to')}><Addr value={i.to} /></D>}47              {i.from && i.kind === 'receive' && <D k={t('from')}><span className="font-mono">{short(i.from)}</span></D>}48              {i.feeDisplay && <D k={t('fee')}>≈ {trimAmt(i.feeDisplay, 9)} {i.feeSymbol}</D>}49              <D k={t('time')}>{new Date(i.at).toISOString()}</D>50              {i.note && <D k={t('note')}>{i.note}</D>}51              {testNotSent(i) ? <D k={t('status')}><span className="text-warn">{t('qaTestNotSent')}</span></D> : i.error && <D k={t('error')}><span className="text-danger">{i.error}</span></D>}52              {i.txid && <D k={t('txid')}><span className="break-all font-mono text-[11.5px]">{i.txid}</span></D>}53              {i.explorer && !testNotSent(i) && <a className="inline-flex min-h-11 items-center gap-1.5 text-blue" href={i.explorer} target="_blank" rel="noreferrer noopener"><Icon n="ext" className="h-4 w-4" />{t('viewExplorer')}</a>}54            </div>}55          </li>); })}</ul></Card>}56    </div>57  );58}59function D({ k, children }: { k: string; children: React.ReactNode }) { return <div><div className="text-[11.5px] uppercase tracking-wide text-mute">{k}</div><div>{children}</div></div>; }60
Files main
Menu

Theme