feat: compact portfolio currency totals
This commit is contained in:
@@ -1,14 +1,31 @@
|
||||
import { Skeleton } from "@mantine/core";
|
||||
import { Paper, SimpleGrid, Skeleton } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LoadingSkeleton } from "@/shared/ui";
|
||||
import classes from "../AccountPage.module.css";
|
||||
|
||||
const AccountPageLoading: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<LoadingSkeleton label={t("states.loadingTitle")}>
|
||||
<Skeleton height={32} width="35%" />
|
||||
<Skeleton height={120} radius="md" />
|
||||
<SimpleGrid
|
||||
cols={{ base: 1, sm: 2, md: 3 }}
|
||||
spacing={10}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{[0, 1, 2].map((index) => (
|
||||
<Paper key={index} className={classes.total} withBorder>
|
||||
<Skeleton height={24} width={40} radius={4} />
|
||||
<Skeleton
|
||||
height={21}
|
||||
width={180}
|
||||
radius={4}
|
||||
className={classes.totalAmountSkeleton}
|
||||
/>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
{[0, 1, 2].map((index) => (
|
||||
<Skeleton key={index} height={52} radius="sm" />
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PortfolioTotals } from "./PortfolioTotals.view";
|
||||
|
||||
describe("PortfolioTotals currency presentation", () => {
|
||||
it.each([0, 1, 3, 5])(
|
||||
"renders only the %i supplied totals in upstream order",
|
||||
(count) => {
|
||||
const totals = [
|
||||
{ currency: "EUR", value: "3.00" },
|
||||
{ currency: "USD", value: "2.00" },
|
||||
{ currency: "RUB", value: "1.00" },
|
||||
{ currency: "CAD", value: "4.00" },
|
||||
{ currency: "XYZ", value: "5.00" },
|
||||
].slice(0, count);
|
||||
const { container } = render(
|
||||
<MantineProvider>
|
||||
<PortfolioTotals totals={totals} />
|
||||
</MantineProvider>,
|
||||
);
|
||||
expect(
|
||||
screen.queryAllByRole("definition").map((node) => node.textContent),
|
||||
).toEqual(
|
||||
["3,00 €", "2,00 $", "1,00 ₽", "4,00 CAD", "5,00 XYZ"].slice(0, count),
|
||||
);
|
||||
expect(container.querySelectorAll("dl")).toHaveLength(count);
|
||||
expect(
|
||||
container.querySelector("[tabindex], button, a, input"),
|
||||
).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["RUB", "125000.50", "125 000,50 ₽", "Российский рубль", "₽"],
|
||||
["USD", "320.00", "320,00 $", "Доллар США", "$"],
|
||||
["EUR", "9876.54321", "9 876,54321 €", "Евро", "€"],
|
||||
[
|
||||
"RUB",
|
||||
"123456789012.345678901",
|
||||
"123 456 789 012,345678901 ₽",
|
||||
"Российский рубль",
|
||||
"₽",
|
||||
],
|
||||
["USD", "-1234567.890123", "-1 234 567,890123 $", "Доллар США", "$"],
|
||||
["EUR", "987654321.000001", "987 654 321,000001 €", "Евро", "€"],
|
||||
["USD", "-0.5", "-0,5 $", "Доллар США", "$"],
|
||||
["CAD", "20.00", "20,00 CAD", "CAD", "CAD"],
|
||||
["XYZ", "1.000", "1,000 XYZ", "XYZ", "XYZ"],
|
||||
])(
|
||||
"preserves %s %s and its currency identity",
|
||||
(currency, value, expected, name, symbol) => {
|
||||
render(
|
||||
<MantineProvider>
|
||||
<PortfolioTotals totals={[{ currency, value }]} />
|
||||
</MantineProvider>,
|
||||
);
|
||||
const term = screen.getByRole("term");
|
||||
expect(term).toHaveTextContent(name);
|
||||
expect(term.querySelector('[aria-hidden="true"]')).toHaveTextContent(
|
||||
symbol,
|
||||
);
|
||||
expect(screen.getByRole("definition")).toHaveTextContent(expected);
|
||||
expect(term.parentElement).toBe(
|
||||
screen.getByRole("definition").parentElement,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps USD and CAD as distinct supplied values", () => {
|
||||
render(
|
||||
<MantineProvider>
|
||||
<PortfolioTotals
|
||||
totals={[
|
||||
{ currency: "USD", value: "1.00" },
|
||||
{ currency: "CAD", value: "2.00" },
|
||||
]}
|
||||
/>
|
||||
</MantineProvider>,
|
||||
);
|
||||
expect(
|
||||
screen.getAllByRole("definition").map((node) => node.textContent),
|
||||
).toEqual(["1,00 $", "2,00 CAD"]);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Paper, SimpleGrid, Text } from "@mantine/core";
|
||||
import { Paper, SimpleGrid, Text, VisuallyHidden } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Portfolio } from "@/entities/portfolio";
|
||||
import { formatMoney } from "@/shared/lib";
|
||||
import { formatDecimal } from "@/shared/lib";
|
||||
import classes from "../AccountPage.module.css";
|
||||
|
||||
type PortfolioTotalsProps = {
|
||||
@@ -10,19 +11,49 @@ type PortfolioTotalsProps = {
|
||||
|
||||
const PortfolioTotals: FC<PortfolioTotalsProps> = (props) => {
|
||||
const { totals } = props;
|
||||
const { t } = useTranslation();
|
||||
const currencies: Record<string, { symbol: string; name: string }> = {
|
||||
RUB: { symbol: "₽", name: t("portfolio.currencies.RUB") },
|
||||
USD: { symbol: "$", name: t("portfolio.currencies.USD") },
|
||||
EUR: { symbol: "€", name: t("portfolio.currencies.EUR") },
|
||||
};
|
||||
|
||||
return (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }}>
|
||||
{totals.map((total) => (
|
||||
<Paper className={classes.total} key={total.currency} withBorder>
|
||||
<Text c="dimmed" size="xs">
|
||||
{total.currency}
|
||||
</Text>
|
||||
<Text fw={800} size="xl">
|
||||
{formatMoney(total)}
|
||||
</Text>
|
||||
</Paper>
|
||||
))}
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing={10}>
|
||||
{totals.map((total) => {
|
||||
const amount = formatDecimal(total.value);
|
||||
return (
|
||||
<Paper
|
||||
component="dl"
|
||||
m={0}
|
||||
className={classes.total}
|
||||
key={total.currency}
|
||||
withBorder
|
||||
>
|
||||
<Text component="dt" className={classes.totalBadge}>
|
||||
<span aria-hidden="true">
|
||||
{currencies[total.currency]?.symbol ?? total.currency}
|
||||
</span>
|
||||
<VisuallyHidden>
|
||||
{currencies[total.currency]?.name ?? total.currency}
|
||||
</VisuallyHidden>
|
||||
</Text>
|
||||
<Text component="dd" m={0} className={classes.totalAmount}>
|
||||
{amount.startsWith("-") ? (
|
||||
<>
|
||||
<span className={classes.totalLeadingDigits}>
|
||||
{amount.slice(0, 2)}
|
||||
</span>
|
||||
{amount.slice(2)}
|
||||
</>
|
||||
) : (
|
||||
amount
|
||||
)}{" "}
|
||||
{currencies[total.currency]?.symbol ?? total.currency}
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user