feat: polish stock position cards

This commit is contained in:
2026-08-26 11:40:36 +03:00
parent 1fd9a3a861
commit 273778c4cf
5 changed files with 129 additions and 10 deletions
+9 -9
View File
@@ -1,11 +1,11 @@
{
"src/app/routes/ProtectedLayout.tsx": 3077,
"src/pages/account-events/index.ts": 4441,
"src/pages/account-operations/index.ts": 13862,
"src/pages/account/index.ts": 22402,
"src/pages/accounts/index.ts": 6179,
"src/pages/login/index.ts": 667,
"src/pages/not-found/index.ts": 882,
"src/pages/register/index.ts": 673,
"src/pages/settings/index.ts": 39008
"src/app/routes/ProtectedLayout.tsx": 3057,
"src/pages/account-events/index.ts": 4364,
"src/pages/account-operations/index.ts": 13854,
"src/pages/account/index.ts": 24695,
"src/pages/accounts/index.ts": 6145,
"src/pages/login/index.ts": 662,
"src/pages/not-found/index.ts": 877,
"src/pages/register/index.ts": 668,
"src/pages/settings/index.ts": 38979
}
@@ -32,6 +32,36 @@
color: var(--mantine-color-red-6);
}
.stockCard {
padding: var(--mantine-spacing-md);
}
.stockMain {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: var(--mantine-spacing-sm);
align-items: center;
}
.stockIdentity,
.stockValue {
min-width: 0;
}
.stockIdentifiers {
overflow-wrap: anywhere;
}
.stockValue {
white-space: nowrap;
}
.stockMeta {
padding-top: var(--mantine-spacing-xs);
margin-top: var(--mantine-spacing-sm);
border-top: 1px solid var(--mantine-color-gray-2);
}
.mono {
font-family: var(--mantine-font-family-monospace);
}
+30
View File
@@ -214,6 +214,36 @@ describe("AccountPage states", () => {
expect(screen.queryByRole("img", { name: "Абра" })).not.toBeInTheDocument();
});
it("groups stock supporting metrics into quiet single lines", () => {
renderPage(
<AccountPageLoaded
{...loadedProps({
...portfolio,
positions: [
{
figi: "FIGI-QUIET",
displayName: "Тихая акция",
type: "STOCK",
quantity: "2",
currentValue: { currency: "RUB", value: "10" },
averagePurchasePrice: { currency: "RUB", value: "11" },
absoluteReturn: { currency: "RUB", value: "3" },
isin: "RU000QUIET1",
logoName: null,
},
],
})}
/>,
);
expect(screen.getByText("Количество: 2 · RU000QUIET1")).not.toHaveAttribute(
"data-line-clamp",
);
expect(
screen.getByText("Средняя цена: 11 ₽ · Доля: 100 %"),
).toBeInTheDocument();
});
it("renders bond cards with logo, fallback, and signed unlabeled return like stocks", () => {
renderPage(
<AccountPageLoaded
@@ -9,11 +9,13 @@ import { PositionLogo } from "./PositionLogo.view";
type PositionCardProps = {
position: AccountPagePosition;
wide?: boolean;
variant?: "stock";
};
const PositionCard: FC<PositionCardProps> = (props) => {
const { position, wide = false } = props;
const { position, wide = false, variant } = props;
const { t } = useTranslation();
const isStock = variant === "stock";
const returnClass = position.returnTone
? classes[position.returnTone]
: undefined;
@@ -25,6 +27,62 @@ const PositionCard: FC<PositionCardProps> = (props) => {
value: position.returnText,
})
: null;
const identifierText = [
t("portfolio.quantity", { value: position.quantity }),
position.isin,
]
.filter(Boolean)
.join(" · ");
const secondaryText = [
position.averagePurchasePrice
? t("portfolio.avgPrice", {
value: formatMoney(position.averagePurchasePrice),
})
: null,
position.shareText
? t("portfolio.share", { value: position.shareText })
: null,
]
.filter(Boolean)
.join(" · ");
if (isStock)
return (
<Card className={`${classes.tile} ${classes.stockCard}`} withBorder>
<div className={classes.stockMain}>
<PositionLogo
key={position.logoSrc ?? "no-logo"}
src={position.logoSrc}
label={position.displayName}
/>
<Stack gap={2} miw={0} className={classes.stockIdentity}>
<Text fw={600} lineClamp={1}>
{position.displayName}
</Text>
<Text c="dimmed" size="xs" className={classes.stockIdentifiers}>
{identifierText}
</Text>
</Stack>
<Stack gap={2} align="flex-end" className={classes.stockValue}>
{position.currentValue ? (
<Text fw={700} size="lg">
{formatMoney(position.currentValue)}
</Text>
) : null}
{returnText ? (
<Text size="sm" fw={600} className={returnClass}>
{returnText}
</Text>
) : null}
</Stack>
</div>
{secondaryText ? (
<Text c="dimmed" size="xs" className={classes.stockMeta}>
{secondaryText}
</Text>
) : null}
</Card>
);
return (
<Card className={classes.tile} withBorder>
<Group justify="space-between" align="flex-start" wrap="nowrap">
@@ -20,6 +20,7 @@ const PositionSection: FC<PositionSectionProps> = (props) => {
key={`${position.figi}-${position.displayName}-${position.quantity}`}
position={position}
wide={section.wide}
variant={section.kind === "stocks" ? "stock" : undefined}
/>
))}
</SimpleGrid>