TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { AssetDepth, AssetLiquidation } from '@/lib/queries/depth';2import { fmtMoney, fmtNum, fmtPct, cn } from '@/lib/format';3import { Card, CardHeader, Table, th, td, tdNum, Badge, Unavailable, VsRiv } from '@/components/ui/primitives';45const fmtDays = (d: number | null) => (d === null ? null : d < 1 ? '< 1 day' : `~${Math.round(d)} day${Math.round(d) === 1 ? '' : 's'}`);67/**8 * Market depth (§25), RareIndex spread (§26), days on market (§30), time-to-liquidate (§24) and the9 * fair buy / sell ladder (§213–§214). Server-rendered; every number comes from observed listings and10 * the published valuation band, and missing evidence is shown as such.11 */12export function DepthLiquidationCard({ depth, liquidation, riv, variantLabel }: { depth: AssetDepth | null; liquidation: AssetLiquidation | null; riv: number | null; variantLabel: string | null }) {13 const d = depth?.depth ?? null;14 const m = liquidation?.model ?? null;15 const f = liquidation?.fair ?? null;16 const bands = d17 ? [18 { label: '±5 %', n: d.within5 },19 { label: '±10 %', n: d.within10 },20 { label: '±20 %', n: d.within20 },21 ]22 : [];23 const max = d ? Math.max(1, d.asks) : 1;24 return (25 <Card>26 <CardHeader27 title="Market depth & time to sell"28 subtitle={`Current asks around the valuation${depth?.scope === 'variant' ? ` of ${variantLabel ?? 'the representative variant'}` : ''}, observed days on market and a time-to-sale model by asking band. Model analytics from observed listings and sales, not advice.`}29 />30 <div className="grid gap-4 p-4 lg:grid-cols-3">31 {/* Depth */}32 <section>33 <h3 className="text-[11px] font-semibold uppercase tracking-wider text-subtle">Market depth</h3>34 {d && riv !== null ? (35 <>36 <dl className="mt-2 grid grid-cols-3 gap-2 text-[12px]">37 <div>38 <dt className="text-[10px] uppercase tracking-wider text-subtle">Asks</dt>39 <dd className="num font-medium text-fg">{fmtNum(d.asks)}</dd>40 </div>41 <div>42 <dt className="text-[10px] uppercase tracking-wider text-subtle">Below RIV</dt>43 <dd className="num font-medium text-fg">{fmtNum(d.belowRiv)}</dd>44 </div>45 <div>46 <dt className="text-[10px] uppercase tracking-wider text-subtle">Above RIV</dt>47 <dd className="num font-medium text-fg">{fmtNum(d.aboveRiv)}</dd>48 </div>49 </dl>50 <ul className="mt-3 grid gap-1.5">51 {bands.map((b) => (52 <li key={b.label} className="grid grid-cols-[52px_1fr_36px] items-center gap-2 text-[11px]">53 <span className="num text-muted">{b.label}</span>54 <span className="h-2 overflow-hidden rounded-sm bg-inset" aria-hidden>55 <span className="block h-full bg-index" style={{ width: `${Math.round((100 * b.n) / max)}%` }} />56 </span>57 <span className="num text-right text-fg">{b.n}</span>58 </li>59 ))}60 </ul>61 <div className="mt-3 flex flex-wrap items-baseline gap-x-3 gap-y-1 text-[12px]">62 <span>63 <span className="text-[10px] uppercase tracking-wider text-subtle">Best ask </span>64 <span className="num font-medium">{d.lowestAsk === null ? <Unavailable reason="No active asks" /> : fmtMoney(d.lowestAsk)}</span>65 </span>66 <span>67 <span className="text-[10px] uppercase tracking-wider text-subtle">Median ask </span>68 <span className="num font-medium">{d.medianAsk === null ? <Unavailable reason="No active asks" /> : fmtMoney(d.medianAsk)}</span>69 </span>70 <span title="RareIndex spread: best ask versus the valuation of the same variant">71 <span className="text-[10px] uppercase tracking-wider text-subtle">Spread </span>72 <VsRiv discount={d.askRivSpread} className="text-[12px]" />73 </span>74 </div>75 </>76 ) : (77 <p className="mt-2 text-[12px] text-muted">Not enough data — depth needs a transaction-based valuation for this variant.</p>78 )}79 </section>8081 {/* Days on market + bands */}82 <section>83 <h3 className="text-[11px] font-semibold uppercase tracking-wider text-subtle">84 Days on market{' '}85 {liquidation ? (86 <Badge tone={liquidation.level === 'category' ? 'alert' : 'neutral'} className="ml-1 normal-case tracking-normal">87 Model estimate · {liquidation.level === 'category' ? 'category-level' : 'this asset'}88 </Badge>89 ) : null}90 </h3>91 {m ? (92 <>93 <dl className="mt-2 grid grid-cols-4 gap-2 text-[12px]">94 {[95 ['Sold', m.sold.n === 0 ? null : fmtNum(m.sold.n)],96 ['Median', fmtDays(m.sold.median)],97 ['P25 – P75', m.sold.p25 === null || m.sold.p75 === null ? null : `${Math.round(m.sold.p25)} – ${Math.round(m.sold.p75)} d`],98 ['Sell-through', m.sellThrough === null ? null : fmtPct(m.sellThrough, 0, false)],99 ].map(([k, v]) => (100 <div key={k as string}>101 <dt className="text-[10px] uppercase tracking-wider text-subtle">{k}</dt>102 <dd className="num font-medium text-fg">{v ?? <Unavailable reason="Not enough data" />}</dd>103 </div>104 ))}105 </dl>106 <p className="mt-1 text-[11px] text-subtle">107 {fmtNum(m.withdrawn.n)} withdrawn/expired without a sale{m.withdrawn.median !== null ? ` (median ${fmtDays(m.withdrawn.median)} listed)` : ''} · {fmtNum(liquidation!.lifecycles)} lifecycles observed108 </p>109 <Table className="mt-3">110 <thead>111 <tr>112 <th className={th}>Asking band</th>113 <th className={cn(th, 'text-right')}>Sold</th>114 <th className={cn(th, 'text-right')}>Median time to sale</th>115 </tr>116 </thead>117 <tbody>118 {m.bands.map((b) => (119 <tr key={b.band}>120 <td className={cn(td, 'text-muted')}>{b.label}</td>121 <td className={tdNum}>{b.n}</td>122 <td className={tdNum}>{b.medianDays === null ? <span className="text-subtle" title="Fewer than 5 sold lifecycles in this band">Not enough data</span> : fmtDays(b.medianDays)}</td>123 </tr>124 ))}125 </tbody>126 </Table>127 </>128 ) : (129 <p className="mt-2 text-[12px] text-muted">Not enough data — no completed listing lifecycles observed for this asset or its category yet.</p>130 )}131 </section>132133 {/* Fair buy / sell ladder */}134 <section>135 <h3 className="text-[11px] font-semibold uppercase tracking-wider text-subtle">Fair buy · fair sell</h3>136 {riv !== null && f ? (137 <Table className="mt-2">138 <tbody>139 {[140 ['Aggressive buy', f.buyAggressive, null, 'RIV low band'],141 ['Fair buy', f.buyFair, null, 'RIV'],142 ['Fast sale', f.sellFast, f.sellFastDays, 'band with the shortest observed time to sale'],143 ['Typical sale', f.sellTypical, f.sellTypicalDays, 'RIV'],144 ['Patient sale', f.sellPatient, f.sellPatientDays, 'RIV high band'],145 ].map(([k, v, days, basis]) => (146 <tr key={k as string}>147 <td className={cn(td, 'text-muted')} title={`Basis: ${basis}`}>148 {k}149 </td>150 <td className={cn(tdNum, 'font-medium')}>{v === null || v === undefined ? <Unavailable reason="Not enough data" /> : fmtMoney(v as number)}</td>151 <td className={cn(tdNum, 'text-[11px] text-subtle')}>{days === null || days === undefined ? '' : fmtDays(days as number)}</td>152 </tr>153 ))}154 </tbody>155 </Table>156 ) : (157 <p className="mt-2 text-[12px] text-muted">Not enough data — the ladder needs a published valuation.</p>158 )}159 <p className="mt-2 text-[11px] text-subtle">Prices come from the RIV low/high band; days from the observed asking bands above. Model analytics, not advice.</p>160 </section>161 </div>162 </Card>163 );164}165