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