refactor: enforce frontend view model boundaries
Frontend verify / commits (pull_request) Successful in 28s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 8m19s

This commit is contained in:
2026-09-28 18:48:23 +03:00
parent 5b69003516
commit ca0ad4f824
287 changed files with 8558 additions and 4538 deletions
@@ -4,53 +4,66 @@ import { SimpleGrid } from "@mantine/core";
import { BankCardFace } from "./BankCardFace";
const meta = (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
args: {
card: {
archivedAt: null,
balance: { currency: "RUB", value: "128450.75" },
bankName: "Сбербанк",
id: "fictional",
lastFour: "4829",
name: "Для повседневных покупок",
openingBalance: { currency: "RUB", value: "0" },
openingDate: "2026-01-01",
paymentSystem: "mir",
version: 1,
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const meta
= {
args: {
card: {
archivedAt: null,
balance: { currency: "RUB", value: "128450.75" },
bankName: "Сбербанк",
id: "fictional",
lastFour: "4829",
name: "Для повседневных покупок",
openingBalance: { currency: "RUB", value: "0" },
openingDate: "2026-01-01",
paymentSystem: "mir",
version: 1,
},
},
},
component: BankCardFace,
tags: ["autodocs"],
title: "Product/BankCards/CardAppearance",
}
/* eslint-enable unicorn/no-null */) satisfies Meta<typeof BankCardFace>;
component: BankCardFace,
tags: ["autodocs"],
title: "Product/BankCards/CardAppearance",
} satisfies /* eslint-enable unicorn/no-null */ Meta<typeof BankCardFace>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Banks: Story = {
render: arguments_ => (
<SimpleGrid cols={{ base: 1, sm: 2 }} maw={920}>
{["Сбербанк", "Т-Банк", "Альфа-Банк", "ВТБ", /* eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract. */ null].map(
(bankName, index) => (
<BankCardFace
card={(/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
...arguments_.card,
bankName,
paymentSystem:
/* eslint-disable-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key. */ (["mir", "mastercard", "visa", "mir", null] as const)[index]
{[
"Сбербанк",
"Т-Банк",
"Альфа-Банк",
"ВТБ",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null,
].map((bankName, index) => (
<BankCardFace
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
card={
{
...arguments_.card,
bankName,
paymentSystem:
// eslint-disable-next-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key.
(["mir", "mastercard", "visa", "mir", null] as const)[index]
?? null,
}
/* eslint-enable unicorn/no-null */)}
key={bankName ?? "unknown"}
/>
),
)}
}
/* eslint-enable unicorn/no-null */
}
key={bankName ?? "unknown"}
/>
))}
</SimpleGrid>
),
};
export const MissingDetails: Story = (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const MissingDetails: Story
= {
args: {
card: {
...meta.args.card,
@@ -59,8 +72,8 @@ export const MissingDetails: Story = (/* eslint-disable unicorn/no-null -- Fixtu
paymentSystem: null,
},
},
}
/* eslint-enable unicorn/no-null */);
};
/* eslint-enable unicorn/no-null */
export const LongArchived: Story = {
args: {
card: {
@@ -1,5 +1,6 @@
import type { FC } from "react";
import { ContactlessIcon } from "@wealthledger/icons";
import { useTranslation } from "react-i18next";
import { formatMoney } from "@/shared/lib";
@@ -9,51 +10,46 @@ import type { BankCard } from "../../api/queries";
import classes from "./BankCardFace.module.css";
type BankCardFaceProps = { card: BankCard };
const BankCardFace: FC<BankCardFaceProps> = (props) => {
const { card } = props;
const { t } = useTranslation();
const bank = card.bankName?.toLocaleLowerCase("ru") ?? "";
const tone = bankTone(bank);
return (
<div className={classes.face} data-tone={tone}>
<div className={classes.top}>
<span className={classes.bank}>{card.bankName || t("bank.card")}</span>
{card.archivedAt && (
<span className={classes.archive}>{t("bank.archived")}</span>
)}
</div>
<div aria-hidden="true" className={classes.hardware}>
<span className={classes.chip} />
<svg
aria-hidden="true"
fill="none"
height="30"
viewBox="0 0 24 30"
width="24"
>
<path
d="M7 11a8 8 0 0 1 0 8m5-12a15 15 0 0 1 0 16m5-20a23 23 0 0 1 0 24"
stroke="currentColor"
strokeLinecap="round"
strokeWidth="2"
/>
</svg>
<ContactlessIcon height="30" viewBox="0 0 24 30" />
</div>
<div
className={classes.number}
>
<div className={classes.number}>
{`•••• •••• •••• ${card.lastFour || "••••"}`}
</div>
<div className={classes.bottom}>
<div className={classes.details}>
<div className={classes.name}>{card.name}</div>
<div className={classes.balance}>{formatMoney(card.balance)}</div>
</div>
{card.paymentSystem && (
<span className={classes.network} data-network={card.paymentSystem}>
{card.paymentSystem === "mastercard" && (
<span aria-hidden="true" className={classes.circles} />
)}
{t(`bank.paymentSystems.${card.paymentSystem}`)}
</span>
)}
@@ -66,6 +62,7 @@ function bankTone(bank: string) {
if (/сбер|sber/.test(bank)) return "green";
if (/тинькофф|т[ -]?банк|t[ -]?bank|tinkoff/.test(bank)) return "yellow";
if (/альфа|alfa/.test(bank)) return "red";
return /втб|vtb|газпром|gazprom/.test(bank) ? "blue" : "graphite";
}