feat: make brokerage account list compact (#111)
This commit is contained in:
+59
-110
@@ -1,61 +1,68 @@
|
|||||||
.list {
|
.list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.row {
|
||||||
min-width: 0;
|
display: grid;
|
||||||
gap: var(--mantine-spacing-xl);
|
grid-template-areas:
|
||||||
|
"identity balance"
|
||||||
|
"details balance";
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, auto);
|
||||||
|
gap: var(--mantine-spacing-xs) var(--mantine-spacing-xl);
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--mantine-spacing-md);
|
||||||
|
border: 1px solid var(--wl-color-border);
|
||||||
|
border-left: 3px solid var(--mantine-color-blue-5);
|
||||||
|
border-radius: var(--mantine-radius-md);
|
||||||
background: var(--mantine-color-white);
|
background: var(--mantine-color-white);
|
||||||
border-color: var(--mantine-color-gray-3);
|
|
||||||
box-shadow: 0 4px 16px rgb(15 23 42 / 3%);
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition:
|
transition: background-color 160ms ease, border-color 160ms ease;
|
||||||
transform 160ms ease,
|
|
||||||
box-shadow 160ms ease,
|
|
||||||
border-color 160ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover,
|
.row:hover,
|
||||||
.card:focus-visible {
|
.row:focus-visible {
|
||||||
border-color: var(--mantine-color-blue-3);
|
border-color: var(--mantine-color-blue-3);
|
||||||
box-shadow: 0 12px 28px rgb(15 23 42 / 8%);
|
border-left-color: var(--mantine-color-blue-6);
|
||||||
transform: translateY(-3px);
|
background: var(--mantine-color-blue-0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:focus-visible {
|
.row:focus-visible {
|
||||||
outline: 2px solid var(--mantine-color-blue-6);
|
outline: 2px solid var(--mantine-color-blue-6);
|
||||||
outline-offset: 3px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.identity {
|
.identity {
|
||||||
|
grid-area: identity;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kicker {
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: var(--mantine-radius-sm);
|
|
||||||
background: var(--mantine-color-blue-0);
|
|
||||||
color: var(--mantine-color-blue-8);
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accountIcon {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: var(--mantine-color-blue-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: var(--mantine-h3-font-size);
|
color: var(--mantine-color-blue-9);
|
||||||
line-height: 1.3;
|
font-size: var(--mantine-font-size-md);
|
||||||
|
line-height: var(--mantine-line-height-md);
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accountType {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 2px var(--mantine-spacing-xs);
|
||||||
|
border-radius: var(--mantine-radius-sm);
|
||||||
|
background: var(--mantine-color-blue-0);
|
||||||
|
color: var(--mantine-color-blue-8);
|
||||||
|
}
|
||||||
|
|
||||||
.balance {
|
.balance {
|
||||||
flex: 1;
|
grid-area: balance;
|
||||||
|
min-width: 0;
|
||||||
|
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
||||||
|
border-radius: var(--mantine-radius-sm);
|
||||||
|
background: var(--wl-color-subtle);
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
@@ -63,104 +70,46 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.total {
|
.total {
|
||||||
font-size: clamp(1.5rem, 2.8vw, 2.25rem);
|
color: var(--mantine-color-dark-7);
|
||||||
letter-spacing: -0.035em;
|
font-size: var(--mantine-font-size-lg);
|
||||||
line-height: 1.2;
|
line-height: var(--mantine-line-height-md);
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total + .total {
|
.total + .total {
|
||||||
font-size: var(--mantine-font-size-lg);
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0;
|
}
|
||||||
color: var(--mantine-color-gray-7);
|
|
||||||
|
.details {
|
||||||
|
grid-area: details;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.positions {
|
.positions {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: var(--mantine-spacing-sm);
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.positions > div {
|
.positions > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
gap: var(--mantine-spacing-xs);
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 4px var(--mantine-spacing-sm);
|
|
||||||
padding: var(--mantine-spacing-sm) var(--mantine-spacing-md);
|
|
||||||
border-radius: var(--mantine-radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stocks {
|
|
||||||
background: var(--mantine-color-blue-0);
|
|
||||||
color: var(--mantine-color-blue-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bonds {
|
|
||||||
background: var(--mantine-color-grape-0);
|
|
||||||
color: var(--mantine-color-grape-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.positions dt {
|
|
||||||
font-size: var(--mantine-font-size-sm);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.positions dd {
|
.positions dd {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.75rem;
|
}
|
||||||
font-weight: 700;
|
|
||||||
font-variant-numeric: tabular-nums;
|
.status {
|
||||||
line-height: 1.25;
|
min-width: 0;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
@media (max-width: 48em) {
|
||||||
border-top: 1px solid var(--mantine-color-gray-2);
|
.row {
|
||||||
padding-top: var(--mantine-spacing-md);
|
column-gap: var(--mantine-spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.open {
|
.balance {
|
||||||
display: inline-flex;
|
max-width: 50vw;
|
||||||
align-items: center;
|
|
||||||
gap: var(--mantine-spacing-xs);
|
|
||||||
margin-left: auto;
|
|
||||||
color: var(--mantine-color-blue-7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
font-size: var(--mantine-font-size-lg);
|
|
||||||
transition: transform 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover .arrow {
|
|
||||||
transform: translateX(3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 36em) {
|
|
||||||
.card {
|
|
||||||
padding: var(--mantine-spacing-md);
|
|
||||||
gap: var(--mantine-spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.positions > div {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: var(--mantine-spacing-sm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.card,
|
|
||||||
.arrow {
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover,
|
|
||||||
.card:focus-visible,
|
|
||||||
.card:hover .arrow {
|
|
||||||
transform: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ const meta = {
|
|||||||
docs: {
|
docs: {
|
||||||
description: {
|
description: {
|
||||||
component:
|
component:
|
||||||
"Карточки счетов WealthLedger со сводкой по валютам и числом наименований акций и облигаций. Используйте на экране выбора счёта; отсутствие сводки и отсутствие сумм имеют разные сообщения. Примеры содержат вымышленные данные. Ссылки меняют только локальный маршрут MemoryRouter; страница назначения в этой истории не отображается.",
|
"Компактные строки счетов WealthLedger: название и отличающийся тип слева, текущая стоимость с двумя знаками справа на desktop и под названием на mobile. Видимы не более двух валют и счётчик остальных, позиции и локальный статус источника. Отсутствие сводки и отсутствие сумм имеют разные сообщения. Примеры содержат вымышленные данные. Ссылки меняют только локальный маршрут MemoryRouter; страница назначения в этой истории не отображается.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+17
-11
@@ -23,7 +23,10 @@ describe("BrokerageAccountList", () => {
|
|||||||
bondPositionCount: 12,
|
bondPositionCount: 12,
|
||||||
stockPositionCount: 5,
|
stockPositionCount: 5,
|
||||||
totalsByCurrency: [
|
totalsByCurrency: [
|
||||||
{ currency: "RUB", value: "1500" },
|
{
|
||||||
|
currency: "RUB",
|
||||||
|
value: "123456789012345678901.995",
|
||||||
|
},
|
||||||
{ currency: "USD", value: "20" },
|
{ currency: "USD", value: "20" },
|
||||||
{ currency: "EUR", value: "5" },
|
{ currency: "EUR", value: "5" },
|
||||||
],
|
],
|
||||||
@@ -33,7 +36,7 @@ describe("BrokerageAccountList", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "account-2",
|
id: "account-2",
|
||||||
name: "Недоступный",
|
name: "Брокерский счет",
|
||||||
openedOn: null,
|
openedOn: null,
|
||||||
portfolioSummary: null,
|
portfolioSummary: null,
|
||||||
type: "BROKERAGE",
|
type: "BROKERAGE",
|
||||||
@@ -60,24 +63,27 @@ describe("BrokerageAccountList", () => {
|
|||||||
"/accounts/account-1",
|
"/accounts/account-1",
|
||||||
);
|
);
|
||||||
expect(screen.getAllByText("Брокерский счет")).toHaveLength(3);
|
expect(screen.getAllByText("Брокерский счет")).toHaveLength(3);
|
||||||
expect(screen.getByText(/^Открыт /)).toBeInTheDocument();
|
expect(screen.getByText("123 456 789 012 345 678 902,00 ₽"))
|
||||||
expect(screen.getByText("1 500 ₽")).toBeInTheDocument();
|
.toBeInTheDocument();
|
||||||
expect(screen.getByText("20 USD")).toBeInTheDocument();
|
expect(screen.getByText("20,00 USD")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Еще 1 валюта")).toBeInTheDocument();
|
expect(screen.getByText("Еще 1 валюта")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Сводка временно недоступна")).toBeInTheDocument();
|
expect(screen.getByText("Сводка временно недоступна")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Нет текущих сумм")).toBeInTheDocument();
|
expect(screen.getByText("Нет текущих сумм")).toBeInTheDocument();
|
||||||
expect(screen.getAllByText("Открыть счет")).toHaveLength(3);
|
|
||||||
const available = within(screen.getByRole("link", { name: /Основной/ }));
|
const available = within(screen.getByRole("link", { name: /Основной/ }));
|
||||||
expect(available.getAllByRole("definition")[0]).toHaveTextContent("5");
|
expect(available.getAllByRole("definition")[0]).toHaveTextContent("5");
|
||||||
expect(available.getAllByRole("definition")[1]).toHaveTextContent("12");
|
expect(available.getAllByRole("definition")[1]).toHaveTextContent("12");
|
||||||
|
|
||||||
const unvalued = within(screen.getByRole("link", { name: /Пустой/ }));
|
const unvalued = within(screen.getByRole("link", { name: /Пустой/ }));
|
||||||
expect(unvalued.getAllByRole("definition")[0]).toHaveTextContent("2");
|
expect(unvalued.getAllByRole("definition")[0]).toHaveTextContent("2");
|
||||||
expect(unvalued.getAllByRole("definition")[1]).toHaveTextContent("0");
|
expect(unvalued.getAllByRole("definition")[1]).toHaveTextContent("0");
|
||||||
expect(
|
|
||||||
within(screen.getByRole("link", { name: /Недоступный/ })).queryByText(
|
const duplicatedName = within(screen.getByRole("link", {
|
||||||
"Акции",
|
name: /Сводка временно недоступна/,
|
||||||
),
|
}));
|
||||||
).not.toBeInTheDocument();
|
|
||||||
|
expect(duplicatedName.getAllByText("Брокерский счет")).toHaveLength(1);
|
||||||
|
expect(duplicatedName.queryByText("Акции")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
|
|
||||||
import { Card, Group, SimpleGrid, Stack, Text } from "@mantine/core";
|
import { Group, Stack, Text } from "@mantine/core";
|
||||||
import { WalletIcon } from "@wealthledger/icons";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
import { formatDate, formatMoney } from "@/shared/lib";
|
import { formatMoney } from "@/shared/lib";
|
||||||
import { SourceSyncStatus } from "@/shared/ui";
|
import { SourceSyncStatus } from "@/shared/ui";
|
||||||
|
|
||||||
import type { BrokerageAccountCard } from "../../model/display";
|
import type { BrokerageAccountCard } from "../../model/display";
|
||||||
@@ -39,7 +38,7 @@ const BrokerageAccountSummary: FC<BrokerageAccountSummaryProps> = (props) => {
|
|||||||
<Stack className={classes.summary} gap={4}>
|
<Stack className={classes.summary} gap={4}>
|
||||||
{summary.totals.map(total => (
|
{summary.totals.map(total => (
|
||||||
<Text className={classes.total} fw={700} key={total.currency}>
|
<Text className={classes.total} fw={700} key={total.currency}>
|
||||||
{formatMoney(total)}
|
{formatMoney(total, { withRoundedCents: true })}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -92,81 +91,75 @@ const BrokerageAccountList: FC<BrokerageAccountListProps> = (props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleGrid className={classes.list} cols={1} spacing="lg">
|
<Stack className={classes.list} component="ul" gap="sm">
|
||||||
{accounts.map(({ account, positionCounts, source, summary }) => (
|
{accounts.map(({ account, positionCounts, source, summary }) => {
|
||||||
<Card
|
const accountType = t(accountTypeKey(account.type));
|
||||||
className={classes.card}
|
|
||||||
component={Link}
|
|
||||||
key={account.id}
|
|
||||||
padding="xl"
|
|
||||||
radius="lg"
|
|
||||||
to={`/accounts/${account.id}`}
|
|
||||||
withBorder
|
|
||||||
>
|
|
||||||
<Stack className={classes.identity} gap="sm">
|
|
||||||
<Group gap="sm" justify="space-between" wrap="nowrap">
|
|
||||||
<Text className={classes.kicker} fw={700} size="xs">
|
|
||||||
{t(accountTypeKey(account.type))}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<WalletIcon className={classes.accountIcon} />
|
return (
|
||||||
</Group>
|
<li key={account.id}>
|
||||||
|
<Link className={classes.row} to={`/accounts/${account.id}`}>
|
||||||
<Text className={classes.name} component="h2" fw={700}>
|
<Stack className={classes.identity} gap={2}>
|
||||||
|
<Text className={classes.name} component="h2" fw={600}>
|
||||||
{account.name}
|
{account.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{account.name !== accountType && (
|
||||||
|
<Text className={classes.accountType} size="xs">
|
||||||
|
{accountType}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack className={classes.balance} gap="xs">
|
<Stack className={classes.balance} gap={2}>
|
||||||
<Text c="dimmed" size="sm">
|
<Text c="dimmed" size="xs">
|
||||||
{t("accounts.currentValue")}
|
{t("accounts.currentValue")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<BrokerageAccountSummary summary={summary} />
|
<BrokerageAccountSummary summary={summary} />
|
||||||
|
|
||||||
<BrokerageAccountSourceStatus source={source} />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
<Group className={classes.details} gap="md">
|
||||||
{!!positionCounts && (
|
{!!positionCounts && (
|
||||||
<dl className={classes.positions}>
|
<Group
|
||||||
<div className={classes.stocks}>
|
className={classes.positions}
|
||||||
<dt>{t("accounts.stocks")}</dt>
|
component="dl"
|
||||||
|
gap="md"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Text c="dimmed" component="dt" size="sm">
|
||||||
|
{t("accounts.stocks")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
<dd>
|
<Text component="dd" fw={600} size="sm">
|
||||||
{new Intl.NumberFormat("ru-RU").format(positionCounts.stocks)}
|
{new Intl.NumberFormat("ru-RU").format(
|
||||||
</dd>
|
positionCounts.stocks,
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={classes.bonds}>
|
<div>
|
||||||
<dt>{t("accounts.bonds")}</dt>
|
<Text c="dimmed" component="dt" size="sm">
|
||||||
|
{t("accounts.bonds")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
<dd>
|
<Text component="dd" fw={600} size="sm">
|
||||||
{new Intl.NumberFormat("ru-RU").format(positionCounts.bonds)}
|
{new Intl.NumberFormat("ru-RU").format(
|
||||||
</dd>
|
positionCounts.bonds,
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Group className={classes.footer} gap="sm" justify="space-between">
|
|
||||||
{!!account.openedOn && (
|
|
||||||
<Text c="dimmed" size="xs">
|
|
||||||
{t("accounts.openedOn", {
|
|
||||||
date: formatDate(account.openedOn),
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Text className={classes.open} component="span" fw={600} size="sm">
|
|
||||||
{t("accounts.openAccount")}
|
|
||||||
|
|
||||||
<span aria-hidden="true" className={classes.arrow}>
|
|
||||||
→
|
|
||||||
</span>
|
|
||||||
</Text>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
)}
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
<Stack className={classes.status} gap={2}>
|
||||||
|
<BrokerageAccountSourceStatus source={source} />
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const DECIMAL_PART_COUNT = 2;
|
|||||||
const CENTS_PRECISION = 2;
|
const CENTS_PRECISION = 2;
|
||||||
|
|
||||||
type FormatMoneyOptions = {
|
type FormatMoneyOptions = {
|
||||||
|
withRoundedCents?: boolean;
|
||||||
withZeroCents?: boolean;
|
withZeroCents?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,16 +40,20 @@ function formatMoney(
|
|||||||
money: Money
|
money: Money
|
||||||
| null
|
| null
|
||||||
| undefined,
|
| undefined,
|
||||||
{ withZeroCents = false }: FormatMoneyOptions = {},
|
{ withRoundedCents = false, withZeroCents = false }: FormatMoneyOptions = {},
|
||||||
): string {
|
): string {
|
||||||
if (!money) return "—";
|
if (!money) return "—";
|
||||||
|
|
||||||
const [integerPart = "", fractionPart = ""] = money.value.split(
|
const value = withRoundedCents
|
||||||
|
? roundDecimalToCents(money.value)
|
||||||
|
: money.value;
|
||||||
|
|
||||||
|
const [integerPart = "", fractionPart = ""] = value.split(
|
||||||
".",
|
".",
|
||||||
DECIMAL_PART_COUNT,
|
DECIMAL_PART_COUNT,
|
||||||
);
|
);
|
||||||
|
|
||||||
const normalizedFraction = withZeroCents
|
const normalizedFraction = withRoundedCents || withZeroCents
|
||||||
? fractionPart.padEnd(CENTS_PRECISION, "0")
|
? fractionPart.padEnd(CENTS_PRECISION, "0")
|
||||||
: fractionPart;
|
: fractionPart;
|
||||||
|
|
||||||
@@ -61,6 +66,23 @@ function formatMoney(
|
|||||||
return `${formatDecimal(decimal)} ${currency}`;
|
return `${formatDecimal(decimal)} ${currency}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function roundDecimalToCents(value: string): string {
|
||||||
|
const [integer = "", fraction = ""] = value.split(".", DECIMAL_PART_COUNT);
|
||||||
|
const sign = integer.startsWith("-") ? "-" : "";
|
||||||
|
const digits = sign ? integer.slice(1) : integer;
|
||||||
|
const roundedFraction = fraction.padEnd(CENTS_PRECISION + 1, "0");
|
||||||
|
const cents = `${digits}${roundedFraction.slice(0, CENTS_PRECISION)}`;
|
||||||
|
const roundingDigit = roundedFraction.at(CENTS_PRECISION) ?? "0";
|
||||||
|
|
||||||
|
const rounded = roundingDigit >= "5"
|
||||||
|
? (BigInt(cents) + 1n).toString().padStart(cents.length, "0")
|
||||||
|
: cents;
|
||||||
|
|
||||||
|
const roundedInteger = rounded.slice(0, -CENTS_PRECISION) || "0";
|
||||||
|
|
||||||
|
return `${sign}${roundedInteger}.${rounded.slice(-CENTS_PRECISION)}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Без timeZone Intl берёт часовой пояс браузера пользователя.
|
// Без timeZone Intl берёт часовой пояс браузера пользователя.
|
||||||
const snapshotFormatter = new Intl.DateTimeFormat("ru-RU", {
|
const snapshotFormatter = new Intl.DateTimeFormat("ru-RU", {
|
||||||
dateStyle: "short",
|
dateStyle: "short",
|
||||||
|
|||||||
Reference in New Issue
Block a user