refactor: simplify auth and instrument pages
Frontend verify / commits (pull_request) Successful in 24s
Frontend verify / security (pull_request) Failing after 9s
Frontend verify / verify (pull_request) Skipped

This commit is contained in:
2026-09-29 05:45:08 +03:00
parent 32a77860c8
commit b404c1802a
38 changed files with 644 additions and 790 deletions
@@ -3,17 +3,6 @@
margin: 0 auto;
min-width: 0;
}
.section {
padding: var(--mantine-spacing-lg);
border-radius: var(--mantine-radius-lg);
min-width: 0;
}
.identity {
display: flex;
align-items: center;
gap: var(--mantine-spacing-md);
min-width: 0;
}
.price {
font-size: clamp(2rem, 4vw, 3.2rem);
font-weight: 700;
@@ -38,9 +27,6 @@
border-radius: var(--mantine-radius-md);
}
@media (max-width: 48em) {
.section {
padding: var(--mantine-spacing-md);
}
.companyMetrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+10 -53
View File
@@ -1,66 +1,23 @@
import type { FC } from "react";
import { Anchor, Button, Skeleton, Stack, Text, Title } from "@mantine/core";
import { LoadingSkeleton } from "@wealthledger/ui";
import { useTranslation } from "react-i18next";
import { Link, Navigate } from "react-router";
import { Navigate } from "react-router";
import { ApiErrorCode, isApiErrorCode } from "@/shared/api";
import { InstrumentPageState } from "@/features/instrument-details";
import { BondPageView } from "./BondPage.view";
import { useBondPageVm } from "./BondPage.vm";
const BondPage: FC = () => {
const vm = useBondPageVm();
const { t } = useTranslation();
if (vm.canonicalTo) return <Navigate replace to={vm.canonicalTo} />;
if (vm.bondState.loading)
return (
<LoadingSkeleton label={t("bond.loading")}>
<Stack>
<Skeleton h={110} />
<Skeleton h={350} />
<Skeleton h={170} />
</Stack>
</LoadingSkeleton>
);
const unavailableMessage = isApiErrorCode(
vm.code,
ApiErrorCode.BondTickerAmbiguous,
)
? "bond.ambiguousTicker"
: "bond.unavailableText";
if (!vm.bond)
return (
<Stack role="alert">
<Title order={1}>{t("bond.unavailable")}</Title>
<Text>
{t(
isApiErrorCode(vm.code, ApiErrorCode.TbankNotConnected)
|| isApiErrorCode(vm.code, ApiErrorCode.InvalidTbankToken)
? "bond.connect"
: unavailableMessage,
)}
</Text>
<Anchor component={Link} to="/bonds">
{t("bond.backToSearch")}
</Anchor>
<Anchor component={Link} to="/settings">
{t("accounts.openSettings")}
</Anchor>
<Button onClick={vm.bondState.retry} variant="default">
{t("accounts.retry")}
</Button>
</Stack>
);
if (vm.bondState.loading || !vm.bond) return (
<InstrumentPageState
code={vm.code}
kind="bond"
loading={vm.bondState.loading}
retry={vm.bondState.retry}
/>
);
return <BondPageView vm={vm} />;
};
+11 -30
View File
@@ -2,11 +2,8 @@ import type { FC } from "react";
import {
Alert,
Avatar,
Badge,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
@@ -18,6 +15,7 @@ import { useTranslation } from "react-i18next";
import {
InstrumentChart,
InstrumentEvents,
InstrumentHeader,
InstrumentPosition,
} from "@/features/instrument-details";
import {
@@ -42,6 +40,8 @@ const BondPageView: FC<BondPageViewProps> = (props) => {
const bond = vm.bond;
if (!bond) return;
const subtitle = `${bond.ticker ?? ""} · ${bond.isin ?? ""}`;
const maturity = bond.maturityDate
? formatDate(bond.maturityDate)
: t("bond.unknown");
@@ -144,32 +144,13 @@ const BondPageView: FC<BondPageViewProps> = (props) => {
return (
<Stack className={classes.page} gap="lg">
<Paper className={classes.section} component="header" withBorder>
<Group align="start" justify="space-between">
<div className={classes.identity}>
<Avatar alt="" radius="md" size={56} src={vm.logoSrc}>
{bond.name[0]}
</Avatar>
<div>
<Title order={1} size="h2">
{bond.name}
</Title>
<Text c="dimmed" size="sm">
{bond.ticker} · {bond.isin}
</Text>
</div>
</div>
<Button
disabled={vm.refreshing}
onClick={vm.refresh}
variant="subtle"
>
{t("investment.refresh")}
</Button>
</Group>
<InstrumentHeader
logoSrc={vm.logoSrc}
name={bond.name}
onRefresh={vm.refresh}
refreshing={vm.refreshing}
subtitle={subtitle}
>
<Group gap="xs" mt="md">
{!!bond.floatingCoupon && <Badge variant="light">{t("bond.floatingCoupon")}</Badge>}
@@ -224,7 +205,7 @@ const BondPageView: FC<BondPageViewProps> = (props) => {
<Metric label={t("bond.lot")} value={lotSize} />
</div>
</div>
</Paper>
</InstrumentHeader>
<BondMetrics metrics={vm.metrics} state={vm.metricsState} />
+9 -37
View File
@@ -1,7 +1,6 @@
import { useParams, useSearchParams } from "react-router";
import { useParams } from "react-router";
import {
type BondPeriod,
useBond,
useBondCandles,
useBondCoupons,
@@ -9,37 +8,21 @@ import {
useBondPosition,
useBondQuote,
} from "@/entities/bond";
import { type OperationCategory, useBrokerageOperationHistory } from "@/entities/broker-operation";
import { section } from "@/features/instrument-details";
import { useBrokerageOperationHistory } from "@/entities/broker-operation";
import { section, useInstrumentParameters } from "@/features/instrument-details";
import { useScreenSync } from "@/features/tbank-connection";
import { apiErrorCode } from "@/shared/api";
import { instrumentLogoUrl } from "@/shared/lib";
const periods: BondPeriod[] = ["month", "halfYear", "year", "all"];
const filters = ["all", "trades", "coupons", "other"] as const;
type BondEventFilter = (typeof filters)[number];
const categories: Record<BondEventFilter, OperationCategory[]> = {
all: [],
coupons: ["income"],
other: ["tax", "fee", "transfer", "other"],
trades: ["trade"],
};
type BondPageVm = ReturnType<typeof useBondPageVm>;
function useBondPageVm() {
const { bondId = "" } = useParams();
const bond = useBond(bondId);
const uid = bond.data?.instrumentUid ?? "";
const [parameters, setParameters] = useSearchParams();
const period
= periods.find(value => value === parameters.get("period")) ?? "month";
const filter
= filters.find(value => value === parameters.get("events")) ?? "all";
const { categories, filter, period, search, setFilter, setPeriod }
= useInstrumentParameters("coupons");
const quote = useBondQuote(uid);
const metrics = useBondMetrics(uid);
@@ -48,8 +31,7 @@ function useBondPageVm() {
const coupons = useBondCoupons(uid);
const history = useBrokerageOperationHistory(uid ? "all" : undefined, {
// eslint-disable-next-line security/detect-object-injection -- This lookup uses a fixed typed key set, not an arbitrary external property name.
categories: categories[filter],
categories,
instrumentUid: uid,
limit: 20,
});
@@ -60,9 +42,7 @@ function useBondPageVm() {
{
accountId: "all",
candlePeriod: period,
categories:
// eslint-disable-next-line security/detect-object-injection -- This lookup uses a fixed typed key set, not an arbitrary external property name.
categories[filter],
categories,
instrumentUid: uid,
limit: 20,
screen: "bond",
@@ -70,14 +50,6 @@ function useBondPageVm() {
[bond, quote, candles, position, metrics, coupons, operations],
);
const setParameter = (key: string, value: string) => {
const next = new URLSearchParams(parameters);
next.set(key, value);
setParameters(next, { replace: true });
};
const search = parameters.size > 0 ? `?${parameters.toString()}` : "";
return {
bond: bond.data,
bondState: section(bond, uid ? sync.retry : () => bond.refetch()),
@@ -132,8 +104,8 @@ function useBondPageVm() {
refresh: sync.refresh,
refreshError: sync.transportError || sync.sourceError,
refreshing: sync.refreshing,
setFilter: (value: string) => setParameter("events", value),
setPeriod: (value: string) => setParameter("period", value),
setFilter,
setPeriod,
upcoming:
coupons.data?.coupons
.filter(event => event.timing === "upcoming")