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%
2.9 KB · 65 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { requireUser } from '@/lib/auth/session';4import { listNotifications } from '@/lib/account/queries';5import { clearNotificationsAction, markReadAction } from '@/lib/account/actions';6import { PageHeader, btnSecondary, btnGhost } from '@/components/account/page-header';7import { Card, EmptyState, Badge } from '@/components/ui/primitives';8import { fmtRelative } from '@/lib/format';910export const metadata: Metadata = { title: 'Inbox', robots: { index: false } };1112const TONE: Record<string, 'index' | 'alert' | 'gain' | 'neutral' | 'rarity'> = { alert: 'alert', security: 'loss' as never, system: 'neutral', digest: 'index', target_hit: 'gain', radar: 'rarity' };1314export default async function InboxPage() {15  const u = await requireUser('/notifications');16  const rows = await listNotifications(u.id, 100);17  const unread = rows.filter((r) => !r.readAt).length;18  return (19    <>20      <PageHeader21        title="Inbox"22        description={`${unread} unread · alerts, price targets, security events and digests.`}23        actions={24          <>25            <form action={markReadAction}>26              <input type="hidden" name="id" value="all" />27              <button className={btnSecondary}>Mark all read</button>28            </form>29            <form action={clearNotificationsAction}>30              <button className={btnGhost}>Clear read</button>31            </form>32          </>33        }34      />35      <Card>36        {rows.length === 0 ? (37          <EmptyState title="Nothing here yet" description="When an alert triggers or a price target is hit, it lands here first." action={<Link href="/alerts" className={btnSecondary}>Create an alert</Link>} />38        ) : (39          <ul className="divide-y divide-border">40            {rows.map((n) => (41              <li key={n.id} className={`flex items-start gap-3 px-4 py-3 ${n.readAt ? '' : 'bg-sunken'}`}>42                <span className={`mt-1.5 h-2 w-2 shrink-0 rounded-full ${n.readAt ? 'bg-transparent' : 'bg-index'}`} />43                <div className="min-w-0 flex-1">44                  <div className="flex flex-wrap items-center gap-2">45                    <Badge tone={TONE[n.kind] ?? 'neutral'}>{n.kind.replace(/_/g, ' ')}</Badge>46                    <p className="text-sm font-medium">{n.href ? <Link href={n.href} className="hover:underline">{n.title}</Link> : n.title}</p>47                    <span className="text-[11px] text-subtle">{fmtRelative(n.createdAt)}</span>48                  </div>49                  {n.body ? <p className="mt-0.5 text-xs text-muted">{n.body}</p> : null}50                </div>51                {!n.readAt ? (52                  <form action={markReadAction}>53                    <input type="hidden" name="id" value={n.id} />54                    <button className={btnGhost}>Read</button>55                  </form>56                ) : null}57              </li>58            ))}59          </ul>60        )}61      </Card>62    </>63  );64}65