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