mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-12 18:46:40 +07:00
fix: "hide balance" on Crypto tab only masked the chart's header number
The eye toggle lived as local state inside CryptoHistoryChart, so it only masked the big total figure — the holdings table, allocation card, and stats tiles below stayed fully visible. Lifted `hidden` to CryptoPage and threaded it through every money-displaying section (chart, stats row, allocation list, holdings table, trade history modal) via a shared maskAmount() helper. Percentages (1h/24h/7d%, profit/loss %) stay visible on purpose — a relative change doesn't reveal the actual amount of money involved, only absolute currency figures and coin quantities (which combined with a visible price also reveal an amount) are masked. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
import { Bitcoin } from "lucide-react";
|
import { Bitcoin } from "lucide-react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||||
import { formatCurrency } from "@/lib/format";
|
import { formatCurrency, maskAmount } from "@/lib/format";
|
||||||
import { useTranslation } from "@/lib/i18n";
|
import { useTranslation } from "@/lib/i18n";
|
||||||
import type { CryptoHolding } from "@/types";
|
import type { CryptoHolding } from "@/types";
|
||||||
|
|
||||||
interface CryptoAllocationCardProps {
|
interface CryptoAllocationCardProps {
|
||||||
holdings: CryptoHolding[];
|
holdings: CryptoHolding[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
hidden: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixed categorical order, same 8-slot ramp the rest of the app already
|
// Fixed categorical order, same 8-slot ramp the rest of the app already
|
||||||
@@ -49,7 +50,7 @@ function buildSlices(holdings: CryptoHolding[]): Slice[] {
|
|||||||
return [...top, { key: "other", name: "Other", symbol: null, thumbUrl: null, amount: otherTotal, color: OTHER_COLOR }];
|
return [...top, { key: "other", name: "Other", symbol: null, thumbUrl: null, amount: otherTotal, color: OTHER_COLOR }];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CryptoAllocationCard({ holdings, isLoading }: CryptoAllocationCardProps) {
|
export function CryptoAllocationCard({ holdings, isLoading, hidden }: CryptoAllocationCardProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const slices = buildSlices(holdings);
|
const slices = buildSlices(holdings);
|
||||||
const total = slices.reduce((sum, slice) => sum + slice.amount, 0);
|
const total = slices.reduce((sum, slice) => sum + slice.amount, 0);
|
||||||
@@ -105,7 +106,7 @@ export function CryptoAllocationCard({ holdings, isLoading }: CryptoAllocationCa
|
|||||||
{percent.toFixed(0)}%
|
{percent.toFixed(0)}%
|
||||||
</span>
|
</span>
|
||||||
<span className="shrink-0 text-sm font-medium tabular-nums text-text-primary">
|
<span className="shrink-0 text-sm font-medium tabular-nums text-text-primary">
|
||||||
{formatCurrency(slice.amount)}
|
{maskAmount(formatCurrency(slice.amount), hidden)}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Area, AreaChart, ResponsiveContainer, Tooltip, YAxis } from "recharts";
|
import { Area, AreaChart, ResponsiveContainer, Tooltip, YAxis } from "recharts";
|
||||||
import { ArrowDown, ArrowUp, Eye, EyeOff } from "lucide-react";
|
import { ArrowDown, ArrowUp, Eye, EyeOff } from "lucide-react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||||
import { CryptoRangeSelector } from "@/components/crypto/CryptoRangeSelector";
|
import { CryptoRangeSelector } from "@/components/crypto/CryptoRangeSelector";
|
||||||
import { formatCurrency, formatSignedCurrency, getIntlLocale } from "@/lib/format";
|
import { formatCurrency, formatSignedCurrency, getIntlLocale, maskAmount } from "@/lib/format";
|
||||||
import { useTranslation } from "@/lib/i18n";
|
import { useTranslation } from "@/lib/i18n";
|
||||||
import type { CryptoHistoryResponse, CryptoRange } from "@/types";
|
import type { CryptoHistoryResponse, CryptoRange } from "@/types";
|
||||||
|
|
||||||
@@ -12,29 +11,46 @@ interface CryptoHistoryChartProps {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
range: CryptoRange;
|
range: CryptoRange;
|
||||||
onRangeChange: (range: CryptoRange) => void;
|
onRangeChange: (range: CryptoRange) => void;
|
||||||
|
hidden: boolean;
|
||||||
|
onToggleHidden: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAxisDate(iso: string): string {
|
function formatAxisDate(iso: string): string {
|
||||||
return new Intl.DateTimeFormat(getIntlLocale(), { month: "short", day: "numeric" }).format(new Date(`${iso}T00:00:00`));
|
return new Intl.DateTimeFormat(getIntlLocale(), { month: "short", day: "numeric" }).format(new Date(`${iso}T00:00:00`));
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChartTooltip({ active, payload }: { active?: boolean; payload?: Array<{ payload: { date: string; value: number } }> }) {
|
function ChartTooltip({
|
||||||
|
active,
|
||||||
|
payload,
|
||||||
|
hidden,
|
||||||
|
}: {
|
||||||
|
active?: boolean;
|
||||||
|
payload?: Array<{ payload: { date: string; value: number } }>;
|
||||||
|
hidden: boolean;
|
||||||
|
}) {
|
||||||
if (!active || !payload?.length) return null;
|
if (!active || !payload?.length) return null;
|
||||||
const point = payload[0].payload;
|
const point = payload[0].payload;
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-border bg-surface-1 px-3 py-2 text-sm shadow-md">
|
<div className="rounded-lg border border-border bg-surface-1 px-3 py-2 text-sm shadow-md">
|
||||||
<p className="text-text-muted">{formatAxisDate(point.date)}</p>
|
<p className="text-text-muted">{formatAxisDate(point.date)}</p>
|
||||||
<p className="font-medium text-text-primary">{formatCurrency(point.value)}</p>
|
<p className="font-medium text-text-primary">{maskAmount(formatCurrency(point.value), hidden)}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The Crypto tab's headline chart — same recipe as NetWorthChart, plus an
|
/** The Crypto tab's headline chart — same recipe as NetWorthChart, plus an
|
||||||
* eye toggle to mask the big number (screen-over-someone's-shoulder
|
* eye toggle. `hidden` is owned by CryptoPage and passed to every other
|
||||||
* privacy, doesn't hide anything else on the page). */
|
* money-displaying section of the tab too — this is just where the toggle
|
||||||
export function CryptoHistoryChart({ history, isLoading, range, onRangeChange }: CryptoHistoryChartProps) {
|
* button itself lives. */
|
||||||
|
export function CryptoHistoryChart({
|
||||||
|
history,
|
||||||
|
isLoading,
|
||||||
|
range,
|
||||||
|
onRangeChange,
|
||||||
|
hidden,
|
||||||
|
onToggleHidden,
|
||||||
|
}: CryptoHistoryChartProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [hidden, setHidden] = useState(false);
|
|
||||||
const isPositive = history ? Number(history.change_amount) >= 0 : true;
|
const isPositive = history ? Number(history.change_amount) >= 0 : true;
|
||||||
const trendColor = isPositive ? "var(--success)" : "var(--danger)";
|
const trendColor = isPositive ? "var(--success)" : "var(--danger)";
|
||||||
const chartData = history?.series.map((point) => ({ date: point.date, value: Number(point.value) })) ?? [];
|
const chartData = history?.series.map((point) => ({ date: point.date, value: Number(point.value) })) ?? [];
|
||||||
@@ -45,20 +61,20 @@ export function CryptoHistoryChart({ history, isLoading, range, onRangeChange }:
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle>{t("nav.crypto")}</CardTitle>
|
<CardTitle>{t("nav.crypto")}</CardTitle>
|
||||||
<p className="mt-1.5 flex items-center gap-2 text-2xl font-semibold tabular-nums text-text-primary sm:text-[28px]">
|
<p className="mt-1.5 flex items-center gap-2 text-2xl font-semibold tabular-nums text-text-primary sm:text-[28px]">
|
||||||
{isLoading ? "…" : hidden ? "••••••" : formatCurrency(history?.current ?? 0)}
|
{isLoading ? "…" : maskAmount(formatCurrency(history?.current ?? 0), hidden)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={hidden ? t("crypto.chart.show") : t("crypto.chart.hide")}
|
aria-label={hidden ? t("crypto.chart.show") : t("crypto.chart.hide")}
|
||||||
onClick={() => setHidden((prev) => !prev)}
|
onClick={onToggleHidden}
|
||||||
className="text-text-muted hover:text-text-primary"
|
className="text-text-muted hover:text-text-primary"
|
||||||
>
|
>
|
||||||
{hidden ? <EyeOff size={18} /> : <Eye size={18} />}
|
{hidden ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
{history && !hidden && (
|
{history && (
|
||||||
<p className="mt-1 flex items-center gap-1 text-sm font-medium" style={{ color: trendColor }}>
|
<p className="mt-1 flex items-center gap-1 text-sm font-medium" style={{ color: trendColor }}>
|
||||||
{isPositive ? <ArrowUp size={14} /> : <ArrowDown size={14} />}
|
{isPositive ? <ArrowUp size={14} /> : <ArrowDown size={14} />}
|
||||||
{formatSignedCurrency(history.change_amount)}
|
{maskAmount(formatSignedCurrency(history.change_amount), hidden)}
|
||||||
{history.change_percent !== null && ` (${isPositive ? "+" : ""}${history.change_percent.toFixed(1)}%)`}
|
{history.change_percent !== null && ` (${isPositive ? "+" : ""}${history.change_percent.toFixed(1)}%)`}
|
||||||
<span className="font-normal text-text-muted">{t("netWorth.periodSuffix")}</span>
|
<span className="font-normal text-text-muted">{t("netWorth.periodSuffix")}</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -82,7 +98,7 @@ export function CryptoHistoryChart({ history, isLoading, range, onRangeChange }:
|
|||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<YAxis hide domain={["auto", "auto"]} />
|
<YAxis hide domain={["auto", "auto"]} />
|
||||||
<Tooltip content={<ChartTooltip />} cursor={{ stroke: "var(--gridline)", strokeWidth: 1 }} />
|
<Tooltip content={<ChartTooltip hidden={hidden} />} cursor={{ stroke: "var(--gridline)", strokeWidth: 1 }} />
|
||||||
<Area
|
<Area
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="value"
|
dataKey="value"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Bitcoin, History, Plus, Trash2 } from "lucide-react";
|
import { Bitcoin, History, Plus, Trash2 } from "lucide-react";
|
||||||
import { formatCurrency } from "@/lib/format";
|
import { formatCurrency, maskAmount } from "@/lib/format";
|
||||||
import { useTranslation } from "@/lib/i18n";
|
import { useTranslation } from "@/lib/i18n";
|
||||||
import type { CryptoHolding } from "@/types";
|
import type { CryptoHolding } from "@/types";
|
||||||
|
|
||||||
interface CryptoHoldingsTableProps {
|
interface CryptoHoldingsTableProps {
|
||||||
items: CryptoHolding[];
|
items: CryptoHolding[];
|
||||||
|
hidden: boolean;
|
||||||
onTrade: (holding: CryptoHolding) => void;
|
onTrade: (holding: CryptoHolding) => void;
|
||||||
onViewHistory: (holding: CryptoHolding) => void;
|
onViewHistory: (holding: CryptoHolding) => void;
|
||||||
onDelete: (holding: CryptoHolding) => void;
|
onDelete: (holding: CryptoHolding) => void;
|
||||||
@@ -22,7 +23,7 @@ function PercentCell({ value }: { value: string | null }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CryptoHoldingsTable({ items, onTrade, onViewHistory, onDelete }: CryptoHoldingsTableProps) {
|
export function CryptoHoldingsTable({ items, hidden, onTrade, onViewHistory, onDelete }: CryptoHoldingsTableProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
@@ -75,7 +76,9 @@ export function CryptoHoldingsTable({ items, onTrade, onViewHistory, onDelete }:
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-3 text-right tabular-nums text-text-primary">
|
<td className="py-3 pr-3 text-right tabular-nums text-text-primary">
|
||||||
{holding.current_price !== null ? formatCurrency(holding.current_price) : t("crypto.pendingPrice")}
|
{holding.current_price !== null
|
||||||
|
? maskAmount(formatCurrency(holding.current_price), hidden)
|
||||||
|
: t("crypto.pendingPrice")}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-3 text-right">
|
<td className="py-3 pr-3 text-right">
|
||||||
<PercentCell value={holding.price_change_1h} />
|
<PercentCell value={holding.price_change_1h} />
|
||||||
@@ -88,21 +91,20 @@ export function CryptoHoldingsTable({ items, onTrade, onViewHistory, onDelete }:
|
|||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-3 text-right">
|
<td className="py-3 pr-3 text-right">
|
||||||
<span className="block tabular-nums text-text-primary">
|
<span className="block tabular-nums text-text-primary">
|
||||||
{holding.value !== null ? formatCurrency(holding.value) : t("crypto.pendingPrice")}
|
{holding.value !== null ? maskAmount(formatCurrency(holding.value), hidden) : t("crypto.pendingPrice")}
|
||||||
</span>
|
</span>
|
||||||
<span className="block text-xs tabular-nums text-text-muted">
|
<span className="block text-xs tabular-nums text-text-muted">
|
||||||
{Number(holding.quantity)} {holding.symbol}
|
{maskAmount(`${Number(holding.quantity)} ${holding.symbol}`, hidden)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-3 text-right tabular-nums text-text-primary">
|
<td className="py-3 pr-3 text-right tabular-nums text-text-primary">
|
||||||
{holding.avg_buy_price !== null ? formatCurrency(holding.avg_buy_price) : "—"}
|
{holding.avg_buy_price !== null ? maskAmount(formatCurrency(holding.avg_buy_price), hidden) : "—"}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-3 text-right">
|
<td className="py-3 pr-3 text-right">
|
||||||
{holding.profit_loss !== null && holding.profit_loss_percent !== null ? (
|
{holding.profit_loss !== null && holding.profit_loss_percent !== null ? (
|
||||||
<>
|
<>
|
||||||
<span className="block tabular-nums" style={{ color: profitColor }}>
|
<span className="block tabular-nums" style={{ color: profitColor }}>
|
||||||
{sign}
|
{maskAmount(`${sign}${formatCurrency(holding.profit_loss)}`, hidden)}
|
||||||
{formatCurrency(holding.profit_loss)}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="block text-xs tabular-nums" style={{ color: profitColor }}>
|
<span className="block text-xs tabular-nums" style={{ color: profitColor }}>
|
||||||
{sign}
|
{sign}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Card, CardContent } from "@/components/ui/Card";
|
import { Card, CardContent } from "@/components/ui/Card";
|
||||||
import { formatCurrency } from "@/lib/format";
|
import { formatCurrency, maskAmount } from "@/lib/format";
|
||||||
import { useTranslation } from "@/lib/i18n";
|
import { useTranslation } from "@/lib/i18n";
|
||||||
import type { CryptoHolding } from "@/types";
|
import type { CryptoHolding } from "@/types";
|
||||||
|
|
||||||
interface CryptoStatsRowProps {
|
interface CryptoStatsRowProps {
|
||||||
holdings: CryptoHolding[];
|
holdings: CryptoHolding[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
hidden: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Totals {
|
interface Totals {
|
||||||
@@ -79,7 +80,7 @@ function PerformerTile({ label, holding }: { label: string; holding: CryptoHoldi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CryptoStatsRow({ holdings, isLoading }: CryptoStatsRowProps) {
|
export function CryptoStatsRow({ holdings, isLoading, hidden }: CryptoStatsRowProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -94,8 +95,7 @@ export function CryptoStatsRow({ holdings, isLoading }: CryptoStatsRowProps) {
|
|||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||||
<StatTile label={t("crypto.stats.allTimeProfit")}>
|
<StatTile label={t("crypto.stats.allTimeProfit")}>
|
||||||
<p className="text-lg font-semibold tabular-nums" style={{ color: profitColor }}>
|
<p className="text-lg font-semibold tabular-nums" style={{ color: profitColor }}>
|
||||||
{totals.totalProfitLoss >= 0 ? "+" : ""}
|
{maskAmount(`${totals.totalProfitLoss >= 0 ? "+" : ""}${formatCurrency(totals.totalProfitLoss)}`, hidden)}
|
||||||
{formatCurrency(totals.totalProfitLoss)}
|
|
||||||
</p>
|
</p>
|
||||||
{totals.totalProfitLossPercent !== null && (
|
{totals.totalProfitLossPercent !== null && (
|
||||||
<p className="mt-0.5 text-sm font-medium tabular-nums" style={{ color: profitColor }}>
|
<p className="mt-0.5 text-sm font-medium tabular-nums" style={{ color: profitColor }}>
|
||||||
@@ -106,7 +106,9 @@ export function CryptoStatsRow({ holdings, isLoading }: CryptoStatsRowProps) {
|
|||||||
</StatTile>
|
</StatTile>
|
||||||
|
|
||||||
<StatTile label={t("crypto.stats.costBasis")}>
|
<StatTile label={t("crypto.stats.costBasis")}>
|
||||||
<p className="text-lg font-semibold tabular-nums text-text-primary">{formatCurrency(totals.totalCostBasis)}</p>
|
<p className="text-lg font-semibold tabular-nums text-text-primary">
|
||||||
|
{maskAmount(formatCurrency(totals.totalCostBasis), hidden)}
|
||||||
|
</p>
|
||||||
<p className="mt-0.5 text-xs text-text-muted">{t("crypto.stats.costBasisHint")}</p>
|
<p className="mt-0.5 text-xs text-text-muted">{t("crypto.stats.costBasisHint")}</p>
|
||||||
</StatTile>
|
</StatTile>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Trash2 } from "lucide-react";
|
import { Trash2 } from "lucide-react";
|
||||||
import { Dialog } from "@/components/ui/Dialog";
|
import { Dialog } from "@/components/ui/Dialog";
|
||||||
import { useCryptoTransactions, useDeleteCryptoTransaction } from "@/hooks/useCrypto";
|
import { useCryptoTransactions, useDeleteCryptoTransaction } from "@/hooks/useCrypto";
|
||||||
import { formatCurrency, formatTransactionDate } from "@/lib/format";
|
import { formatCurrency, formatTransactionDate, maskAmount } from "@/lib/format";
|
||||||
import { useTranslation } from "@/lib/i18n";
|
import { useTranslation } from "@/lib/i18n";
|
||||||
import type { CryptoHolding } from "@/types";
|
import type { CryptoHolding } from "@/types";
|
||||||
|
|
||||||
@@ -9,9 +9,10 @@ interface CryptoTransactionHistoryModalProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
holding: CryptoHolding | null;
|
holding: CryptoHolding | null;
|
||||||
|
hidden: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CryptoTransactionHistoryModal({ open, onClose, holding }: CryptoTransactionHistoryModalProps) {
|
export function CryptoTransactionHistoryModal({ open, onClose, holding, hidden }: CryptoTransactionHistoryModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data: transactions, isLoading } = useCryptoTransactions(holding ? holding.asset_id : null);
|
const { data: transactions, isLoading } = useCryptoTransactions(holding ? holding.asset_id : null);
|
||||||
const deleteTransaction = useDeleteCryptoTransaction();
|
const deleteTransaction = useDeleteCryptoTransaction();
|
||||||
@@ -45,7 +46,7 @@ export function CryptoTransactionHistoryModal({ open, onClose, holding }: Crypto
|
|||||||
</span>
|
</span>
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block text-sm text-text-primary">
|
<span className="block text-sm text-text-primary">
|
||||||
{Number(tx.quantity)} {holding.symbol} · {formatCurrency(tx.price_per_unit)}
|
{maskAmount(`${Number(tx.quantity)} ${holding.symbol} · ${formatCurrency(tx.price_per_unit)}`, hidden)}
|
||||||
</span>
|
</span>
|
||||||
<span className="block text-xs text-text-muted">
|
<span className="block text-xs text-text-muted">
|
||||||
{formatTransactionDate(tx.date, true)}
|
{formatTransactionDate(tx.date, true)}
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ export function formatSignedCurrency(amount: number | string, currency: string =
|
|||||||
return `${sign}${formatCurrency(value, currency)}`;
|
return `${sign}${formatCurrency(value, currency)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** "Hide balance" masking — wraps an already-formatted amount (currency,
|
||||||
|
* quantity, whatever reveals a dollar figure) rather than reformatting it,
|
||||||
|
* so call sites don't need a separate hidden-vs-shown branch for every
|
||||||
|
* number. Percentages are deliberately never masked by callers — a % move
|
||||||
|
* doesn't reveal how much money is actually involved. */
|
||||||
|
export function maskAmount(formatted: string, hidden: boolean): string {
|
||||||
|
return hidden ? "••••" : formatted;
|
||||||
|
}
|
||||||
|
|
||||||
const MONTH_LABELS_EN = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
const MONTH_LABELS_EN = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
||||||
const MONTH_LABELS_RU = ["Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"] as const;
|
const MONTH_LABELS_RU = ["Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ function formatSyncedAt(iso: string): string {
|
|||||||
export function CryptoPage() {
|
export function CryptoPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [range, setRange] = useState<CryptoRange>("30d");
|
const [range, setRange] = useState<CryptoRange>("30d");
|
||||||
|
// Single "hide balance" switch for the whole tab — owned here so every
|
||||||
|
// money-displaying section (chart, stats, allocation, table, trade
|
||||||
|
// history) masks together instead of the toggle only affecting whichever
|
||||||
|
// component happened to own it.
|
||||||
|
const [hidden, setHidden] = useState(false);
|
||||||
const { data, isLoading } = useCryptoHoldings();
|
const { data, isLoading } = useCryptoHoldings();
|
||||||
const { data: history, isLoading: isHistoryLoading } = useCryptoHistory(range);
|
const { data: history, isLoading: isHistoryLoading } = useCryptoHistory(range);
|
||||||
const refresh = useRefreshCryptoPrices();
|
const refresh = useRefreshCryptoPrices();
|
||||||
@@ -40,11 +45,18 @@ export function CryptoPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<CryptoHistoryChart history={history} isLoading={isHistoryLoading} range={range} onRangeChange={setRange} />
|
<CryptoHistoryChart
|
||||||
|
history={history}
|
||||||
|
isLoading={isHistoryLoading}
|
||||||
|
range={range}
|
||||||
|
onRangeChange={setRange}
|
||||||
|
hidden={hidden}
|
||||||
|
onToggleHidden={() => setHidden((prev) => !prev)}
|
||||||
|
/>
|
||||||
|
|
||||||
<CryptoStatsRow holdings={holdings} isLoading={isLoading} />
|
<CryptoStatsRow holdings={holdings} isLoading={isLoading} hidden={hidden} />
|
||||||
|
|
||||||
<CryptoAllocationCard holdings={holdings} isLoading={isLoading} />
|
<CryptoAllocationCard holdings={holdings} isLoading={isLoading} hidden={hidden} />
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex-col items-stretch gap-3 sm:flex-row sm:items-center">
|
<CardHeader className="flex-col items-stretch gap-3 sm:flex-row sm:items-center">
|
||||||
@@ -75,6 +87,7 @@ export function CryptoPage() {
|
|||||||
{data?.error_key && <p className="mb-3 text-sm text-danger">{t(`crypto.syncError.${data.error_key}`)}</p>}
|
{data?.error_key && <p className="mb-3 text-sm text-danger">{t(`crypto.syncError.${data.error_key}`)}</p>}
|
||||||
<CryptoHoldingsTable
|
<CryptoHoldingsTable
|
||||||
items={holdings}
|
items={holdings}
|
||||||
|
hidden={hidden}
|
||||||
onTrade={setTradingHolding}
|
onTrade={setTradingHolding}
|
||||||
onViewHistory={setHistoryHolding}
|
onViewHistory={setHistoryHolding}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
@@ -90,6 +103,7 @@ export function CryptoPage() {
|
|||||||
open={historyHolding !== null}
|
open={historyHolding !== null}
|
||||||
onClose={() => setHistoryHolding(null)}
|
onClose={() => setHistoryHolding(null)}
|
||||||
holding={historyHolding}
|
holding={historyHolding}
|
||||||
|
hidden={hidden}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user