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%
7.3 KB · 148 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { requireUser } from '@/lib/auth/session';4import { activeListingsForAssets, loadWatchlist } from '@/lib/account/queries';5import { getDisplay } from '@/lib/account/display';6import { toggleWatchAction } from '@/lib/account/actions';7import { PageHeader, btnDanger } from '@/components/account/page-header';8import { Card, CardHeader, Delta, EmptyState, Table, th, td, tdNum, Badge } from '@/components/ui/primitives';9import { fmtRelative, fmtPct, confidenceLabel } from '@/lib/format';10import { WatchAddForm, WatchNoteForm } from './forms';1112export const metadata: Metadata = { title: 'Watchlist', robots: { index: false } };1314export default async function WatchlistPage() {15  const u = await requireUser('/watchlist');16  const d = await getDisplay();17  const rows = await loadWatchlist(u.id);18  const assetRows = rows.filter((r) => r.item.targetType === 'asset');19  const otherRows = rows.filter((r) => r.item.targetType !== 'asset');20  const listingAgg = await activeListingsForAssets(assetRows.map((r) => r.item.targetId));21  const lMap = new Map(listingAgg.map((l) => [l.assetId, l]));2223  return (24    <>25      <PageHeader title="Watchlist" description="Assets, categories, sets and sources you follow, with live RareIndex Valuation, change and the cheapest active ask." />26      <Card className="mb-5">27        <CardHeader title={`Assets · ${assetRows.length}`} subtitle="Change since watching uses the RIV at the time you added the asset." />28        {assetRows.length === 0 ? (29          <EmptyState title="No assets watched yet" description="Add an asset below, or use the watch button on any asset page." />30        ) : (31          <Table>32            <thead>33              <tr>34                <th className={th}>Asset</th>35                <th className={`${th} text-right`}>RIV</th>36                <th className={`${th} text-right`}>1d</th>37                <th className={`${th} text-right`}>30d</th>38                <th className={`${th} text-right`}>Since watch</th>39                <th className={`${th} text-right`}>Listings</th>40                <th className={`${th} text-right`}>Min ask</th>41                <th className={`${th} text-right`}>Target</th>42                <th className={th}>Note</th>43                <th className={th}></th>44              </tr>45            </thead>46            <tbody>47              {assetRows.map(({ item, asset, stats }) => {48                const l = lMap.get(item.targetId);49                const since = item.baselineUsd && stats?.rivUsd ? (stats.rivUsd - item.baselineUsd) / item.baselineUsd : null;50                const targetHit = item.targetPriceUsd && stats?.rivUsd ? stats.rivUsd <= item.targetPriceUsd : false;51                return (52                  <tr key={item.id} className="hover:bg-sunken">53                    <td className={td}>54                      {asset ? (55                        <div className="max-w-[280px]">56                          <Link href={`/asset/${asset.slug}`} className="block truncate font-medium hover:underline">57                            {asset.title}58                          </Link>59                          <p className="text-[11px] text-subtle">60                            {asset.categorySlug.replace(/_/g, ' ')} · added {fmtRelative(item.createdAt)}61                          </p>62                        </div>63                      ) : (64                        <span className="text-muted">Asset removed</span>65                      )}66                    </td>67                    <td className={tdNum}>{stats?.rivUsd ? <span title={`confidence ${confidenceLabel(stats.rivConfidence)} · ${stats.rivSampleSize} sales`}>{d.money(stats.rivUsd)}</span> : <span className="text-subtle">—</span>}</td>68                    <td className={tdNum}>69                      <Delta value={stats?.change1d} />70                    </td>71                    <td className={tdNum}>72                      <Delta value={stats?.change30d} />73                    </td>74                    <td className={tdNum}>75                      <Delta value={since} />76                    </td>77                    <td className={tdNum}>{l ? Number(l.n) : 0}</td>78                    <td className={tdNum}>{l?.minAsk ? d.money(Number(l.minAsk)) : <span className="text-subtle">—</span>}</td>79                    <td className={tdNum}>80                      {item.targetPriceUsd ? (81                        <span className={targetHit ? 'text-gain' : ''}>82                          {d.money(item.targetPriceUsd)}83                          {targetHit ? <Badge tone="gain" className="ml-1">hit</Badge> : null}84                        </span>85                      ) : (86                        <span className="text-subtle">—</span>87                      )}88                    </td>89                    <td className={`${td} max-w-[220px]`}>90                      <WatchNoteForm itemId={item.id} note={item.note} targetPriceUsd={item.targetPriceUsd} />91                    </td>92                    <td className={td}>93                      <form action={toggleWatchAction}>94                        <input type="hidden" name="targetType" value="asset" />95                        <input type="hidden" name="targetId" value={item.targetId} />96                        <button className={btnDanger}>Unwatch</button>97                      </form>98                    </td>99                  </tr>100                );101              })}102            </tbody>103          </Table>104        )}105      </Card>106      <div className="grid gap-4 lg:grid-cols-2">107        <Card>108          <CardHeader title={`Markets, sets & sources · ${otherRows.length}`} subtitle="Category watches feed Deal Radar and market-move alerts." />109          {otherRows.length === 0 ? (110            <p className="px-4 py-6 text-xs text-muted">Nothing here yet. Watch a category from its market page, or add one below.</p>111          ) : (112            <ul className="divide-y divide-border">113              {otherRows.map(({ item, category }) => (114                <li key={item.id} className="flex items-center justify-between gap-3 px-4 py-2.5 text-sm">115                  <div>116                    <Badge tone="neutral" className="mr-2">117                      {item.targetType}118                    </Badge>119                    {item.targetType === 'category' ? (120                      <Link href={`/markets/${item.targetId}`} className="hover:underline">121                        {category?.name ?? item.targetId}122                      </Link>123                    ) : (124                      <span>{item.label ?? item.targetId}</span>125                    )}126                    {item.targetType === 'category' && category ? <span className="ml-2 text-xs text-subtle">{fmtPct(null)}</span> : null}127                  </div>128                  <form action={toggleWatchAction}>129                    <input type="hidden" name="targetType" value={item.targetType} />130                    <input type="hidden" name="targetId" value={item.targetId} />131                    <button className={btnDanger}>Unwatch</button>132                  </form>133                </li>134              ))}135            </ul>136          )}137        </Card>138        <Card>139          <CardHeader title="Add to watchlist" />140          <div className="p-4">141            <WatchAddForm />142          </div>143        </Card>144      </div>145    </>146  );147}148