fix: allocation view as a real table (color · ticker, %, amount)

Replaces the flex-based list (coin logo + full name + ticker suffix,
prone to truncation and misaligned columns) with an actual <table>: a
color swatch + ticker, percent, and amount as three real columns that
align natively instead of fighting flexbox for it. Full coin name is
still available via a title tooltip.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
This commit is contained in:
ZProger
2026-08-30 04:52:33 +03:00
parent 7dd9c83457
commit 19d8aeda3a

View File

@@ -1,4 +1,3 @@
import { Bitcoin } from "lucide-react";
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
import { formatCurrency, maskAmount } from "@/lib/format";
import { useTranslation } from "@/lib/i18n";
@@ -31,7 +30,6 @@ interface Slice {
key: string;
name: string;
symbol: string | null;
thumbUrl: string | null;
amount: number;
color: string;
}
@@ -39,7 +37,7 @@ interface Slice {
function buildSlices(holdings: CryptoHolding[]): Slice[] {
const priced = holdings
.filter((h) => h.value !== null && Number(h.value) > 0)
.map((h) => ({ key: String(h.asset_id), name: h.name, symbol: h.symbol, thumbUrl: h.thumb_url, amount: Number(h.value) }))
.map((h) => ({ key: String(h.asset_id), name: h.name, symbol: h.symbol, amount: Number(h.value) }))
.sort((a, b) => b.amount - a.amount);
const top = priced.slice(0, MAX_SLICES).map((item, index) => ({ ...item, color: SERIES_COLORS[index] }));
@@ -47,7 +45,7 @@ function buildSlices(holdings: CryptoHolding[]): Slice[] {
if (rest.length === 0) return top;
const otherTotal = rest.reduce((sum, item) => sum + item.amount, 0);
return [...top, { key: "other", name: "Other", symbol: null, thumbUrl: null, amount: otherTotal, color: OTHER_COLOR }];
return [...top, { key: "other", name: "Other", symbol: null, amount: otherTotal, color: OTHER_COLOR }];
}
function DonutTooltip({
@@ -73,11 +71,13 @@ function DonutTooltip({
);
}
/** The "Allocation" tab's content inside CryptoOverviewCard — a donut (part-
* to-whole across few categories is exactly the case a donut is fine for)
* plus a list that also carries the $ amount, which the donut/legend alone
* can't — the user explicitly wants both percent and sum per coin, not just
* percent the way CoinMarketCap's own version shows it. */
/** The "Allocation" tab's content inside CryptoOverviewCard — a donut
* (part-to-whole across few categories is exactly the case a donut is fine
* for) plus a compact table: a color swatch identifying the ticker, its
* share, and the amount. A real <table>, not a flex row list — table
* columns size to their own content and align natively, so this can't
* stretch across whatever space is left next to the donut the way a
* flex-1 row did. */
export function CryptoAllocationBody({ holdings, isLoading, hidden }: CryptoAllocationBodyProps) {
const { t } = useTranslation();
@@ -93,7 +93,7 @@ export function CryptoAllocationBody({ holdings, isLoading, hidden }: CryptoAllo
const donutData = slices.map((slice) => ({ ...slice, percent: (slice.amount / total) * 100 }));
return (
<div className="flex flex-col items-center gap-4 sm:flex-row sm:items-center sm:justify-center sm:gap-8">
<div className="flex flex-col items-center justify-center gap-6 sm:flex-row">
<div className="h-48 w-48 shrink-0 sm:h-56 sm:w-56">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
@@ -117,35 +117,26 @@ export function CryptoAllocationBody({ holdings, isLoading, hidden }: CryptoAllo
</ResponsiveContainer>
</div>
{/* Capped width, not flex-1 — otherwise with few/short coin names the
row stretches across whatever's left of the card and the percent/
amount columns end up stranded far from the coin they describe. */}
<ul className="w-full min-w-0 divide-y divide-gridline sm:w-72">
{donutData.map((slice) => (
<li key={slice.key} className="flex items-center gap-3 py-2 first:pt-0">
{slice.thumbUrl ? (
<img src={slice.thumbUrl} alt="" className="h-6 w-6 shrink-0 rounded-full" />
) : (
<span
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full"
style={{ backgroundColor: `${slice.color}26` }}
>
<Bitcoin size={12} style={{ color: slice.color }} />
</span>
)}
<span className="min-w-0 flex-1 truncate text-sm text-text-primary">
{slice.key === "other" ? t("crypto.allocation.other") : slice.name}
{slice.symbol && <span className="text-text-muted"> · {slice.symbol}</span>}
</span>
<span className="shrink-0 text-right text-sm font-medium tabular-nums text-text-primary">
{slice.percent.toFixed(1)}%
</span>
<span className="w-20 shrink-0 text-right text-xs tabular-nums text-text-muted">
{maskAmount(formatCurrency(slice.amount), hidden)}
</span>
</li>
))}
</ul>
<table className="text-sm">
<tbody>
{donutData.map((slice) => (
<tr key={slice.key} title={slice.key === "other" ? undefined : slice.name}>
<td className="py-1.5 pr-3">
<span className="flex items-center gap-2">
<span className="h-2.5 w-2.5 shrink-0 rounded-full" style={{ backgroundColor: slice.color }} />
<span className="font-medium text-text-primary">
{slice.key === "other" ? t("crypto.allocation.other") : slice.symbol}
</span>
</span>
</td>
<td className="py-1.5 pr-3 text-right font-medium tabular-nums text-text-primary">
{slice.percent.toFixed(1)}%
</td>
<td className="py-1.5 text-right tabular-nums text-text-muted">{maskAmount(formatCurrency(slice.amount), hidden)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}