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 { listTargets } from '@/lib/account/queries';5import { getDisplay } from '@/lib/account/display';6import { deleteTargetAction } from '@/lib/account/actions';7import { PageHeader, btnDanger } from '@/components/account/page-header';8import { Card, CardHeader, EmptyState, Table, th, td, tdNum, Badge } from '@/components/ui/primitives';9import { Progress } from '@/components/account/charts';10import { fmtRelative } from '@/lib/format';11import { TargetForm } from './target-form';1213export const metadata: Metadata = { title: 'Price targets', robots: { index: false } };1415export default async function TargetsPage() {16 const u = await requireUser('/targets');17 const d = await getDisplay();18 const rows = await listTargets(u.id);19 return (20 <>21 <PageHeader title="Price targets" description="Set a RIV level you are waiting for — to buy below or to sell above — and watch progress. You are notified once when it is reached." />22 <div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_360px]">23 <Card>24 <CardHeader title={`Targets · ${rows.length}`} />25 {rows.length === 0 ? (26 <EmptyState title="No targets yet" description="Add one on the right. Progress is measured from the RIV at the time you set the target." />27 ) : (28 <Table>29 <thead>30 <tr>31 <th className={th}>Asset</th>32 <th className={th}>Direction</th>33 <th className={`${th} text-right`}>Baseline</th>34 <th className={`${th} text-right`}>Now</th>35 <th className={`${th} text-right`}>Target</th>36 <th className={`${th} w-[160px]`}>Progress</th>37 <th className={th}>Status</th>38 <th className={th}></th>39 </tr>40 </thead>41 <tbody>42 {rows.map(({ target: t, asset, stats }) => {43 const now = stats?.rivUsd ?? null;44 const base = t.baselineUsd ?? now;45 let progress = 0;46 if (now !== null && base !== null && base !== t.targetUsd) progress = (now - base) / (t.targetUsd - base);47 const hit = t.hitAt !== null || (now !== null && (t.direction === 'above' ? now >= t.targetUsd : now <= t.targetUsd));48 return (49 <tr key={t.id}>50 <td className={td}>51 <div className="max-w-[240px]">52 <Link href={`/asset/${asset.slug}`} className="block truncate font-medium hover:underline">53 {asset.title}54 </Link>55 <p className="text-[11px] text-subtle">56 set {fmtRelative(t.createdAt)}57 {t.note ? ` · ${t.note}` : ''}58 </p>59 </div>60 </td>61 <td className={td}>62 <Badge tone={t.direction === 'above' ? 'gain' : 'index'}>{t.direction === 'above' ? 'sell above' : 'buy below'}</Badge>63 </td>64 <td className={tdNum}>{t.baselineUsd ? d.money(t.baselineUsd) : '—'}</td>65 <td className={tdNum}>{now ? d.money(now) : <span className="text-subtle">—</span>}</td>66 <td className={tdNum}>{d.money(t.targetUsd)}</td>67 <td className={td}>68 <Progress value={hit ? 1 : progress} />69 </td>70 <td className={td}>{hit ? <Badge tone="gain">reached</Badge> : now === null ? <Badge tone="neutral">no RIV yet</Badge> : <span className="text-xs text-muted">{Math.round(Math.max(0, Math.min(1, progress)) * 100)}%</span>}</td>71 <td className={td}>72 <form action={deleteTargetAction}>73 <input type="hidden" name="id" value={t.id} />74 <button className={btnDanger}>Remove</button>75 </form>76 </td>77 </tr>78 );79 })}80 </tbody>81 </Table>82 )}83 </Card>84 <Card>85 <CardHeader title="New target" />86 <div className="p-4">87 <TargetForm />88 </div>89 </Card>90 </div>91 </>92 );93}94