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%
5.0 KB · 113 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { requireUser } from '@/lib/auth/session';4import { listAlerts } from '@/lib/account/queries';5import { getDisplay } from '@/lib/account/display';6import { deleteAlertAction, toggleAlertAction } from '@/lib/account/actions';7import { PageHeader, btnDanger, btnGhost } from '@/components/account/page-header';8import { Card, CardHeader, EmptyState, Table, th, td, tdNum, Badge } from '@/components/ui/primitives';9import { fmtRelative } from '@/lib/format';10import { AlertForm } from './alert-form';1112export const metadata: Metadata = { title: 'Alerts', robots: { index: false } };1314const ALERT_LABELS: Record<string, string> = {15  price_below: 'RIV falls below',16  price_above: 'RIV rises above',17  new_listing: 'New listing',18  new_auction: 'New auction lot',19  auction_ending: 'Auction ending soon',20  auction_below_riv: 'Auction bid below RIV (all-in)',21  record_sale: 'New record sale',22  unusual_volume: 'Unusual volume',23  market_move: 'Market move',24  rare_item: 'Rare item appears',25  population_update: 'Population update',26};2728export default async function AlertsPage() {29  const u = await requireUser('/alerts');30  const d = await getDisplay();31  const rows = await listAlerts(u.id);32  const prefs = (u.preferences ?? {}) as { emailAlerts?: boolean };33  return (34    <>35      <PageHeader title="Alerts" description="Evaluated continuously by the RareIndex worker against valuations, listings, sales, auctions, radar findings and population reports. Delivered to your inbox and, if enabled, by e-mail." />36      {prefs.emailAlerts === false ? (37        <p className="mb-4 text-xs text-alert">38          E-mail delivery is off in{' '}39          <Link href="/account/notifications" className="underline">40            notification settings41          </Link>42          ; alerts will only appear in your inbox.43        </p>44      ) : null}45      <div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_360px]">46        <Card>47          <CardHeader title={`Your alerts · ${rows.length}`} />48          {rows.length === 0 ? (49            <EmptyState title="No alerts yet" description="Create one on the right — for example, be told when the RIV of a card you want drops below your budget, or when a new listing appears." />50          ) : (51            <Table>52              <thead>53                <tr>54                  <th className={th}>Target</th>55                  <th className={th}>Condition</th>56                  <th className={`${th} text-right`}>Threshold</th>57                  <th className={th}>Channel</th>58                  <th className={th}>Last triggered</th>59                  <th className={`${th} text-right`}>Count</th>60                  <th className={th}></th>61                </tr>62              </thead>63              <tbody>64                {rows.map(({ alert: a, asset }) => (65                  <tr key={a.id} className={a.active ? '' : 'opacity-50'}>66                    <td className={td}>67                      <div className="max-w-[240px]">68                        {asset ? (69                          <Link href={`/asset/${asset.slug}`} className="block truncate font-medium hover:underline">70                            {asset.title}71                          </Link>72                        ) : (73                          <span className="font-medium">{a.name ?? a.targetId}</span>74                        )}75                        <p className="text-[11px] text-subtle">{a.targetType}</p>76                      </div>77                    </td>78                    <td className={td}>{ALERT_LABELS[a.alertType] ?? a.alertType}</td>79                    <td className={tdNum}>{a.threshold !== null ? (a.alertType === 'market_move' || a.alertType === 'unusual_volume' ? `${a.threshold}%` : d.money(a.threshold)) : '—'}</td>80                    <td className={td}>81                      <Badge tone="neutral">{a.channel}</Badge>82                    </td>83                    <td className={`${td} text-xs text-muted`}>{a.lastTriggeredAt ? fmtRelative(a.lastTriggeredAt) : 'never'}</td>84                    <td className={tdNum}>{a.triggerCount}</td>85                    <td className={td}>86                      <div className="flex gap-1">87                        <form action={toggleAlertAction}>88                          <input type="hidden" name="alertId" value={a.id} />89                          <button className={btnGhost}>{a.active ? 'Pause' : 'Resume'}</button>90                        </form>91                        <form action={deleteAlertAction}>92                          <input type="hidden" name="alertId" value={a.id} />93                          <button className={btnDanger}>Delete</button>94                        </form>95                      </div>96                    </td>97                  </tr>98                ))}99              </tbody>100            </Table>101          )}102        </Card>103        <Card>104          <CardHeader title="New alert" />105          <div className="p-4">106            <AlertForm />107          </div>108        </Card>109      </div>110    </>111  );112}113