refactor: simplify auth and instrument pages
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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} />
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user