refactor: clarify frontend model ownership
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
Alert,
|
||||
Anchor,
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
@@ -21,7 +19,6 @@ import {
|
||||
InstrumentChart,
|
||||
InstrumentEvents,
|
||||
InstrumentPosition,
|
||||
InstrumentSection,
|
||||
} from "@/features/instrument-details";
|
||||
import {
|
||||
formatDate,
|
||||
@@ -33,6 +30,9 @@ import {
|
||||
import type { BondPageVm } from "./BondPage.vm";
|
||||
|
||||
import classes from "./BondPage.module.css";
|
||||
import { BondCoupons } from "./components/BondCoupons.view";
|
||||
import { BondMetrics } from "./components/BondMetrics.view";
|
||||
import { Metric } from "./components/Metric.view";
|
||||
|
||||
type BondPageViewProps = { vm: BondPageVm };
|
||||
|
||||
@@ -226,7 +226,7 @@ const BondPageView: FC<BondPageViewProps> = (props) => {
|
||||
</div>
|
||||
</Paper>
|
||||
|
||||
<BondMetrics vm={vm} />
|
||||
<BondMetrics metrics={vm.metrics} state={vm.metricsState} />
|
||||
|
||||
<InstrumentChart priceUnit="percent" vm={vm} />
|
||||
|
||||
@@ -236,7 +236,12 @@ const BondPageView: FC<BondPageViewProps> = (props) => {
|
||||
vm={vm}
|
||||
/>
|
||||
|
||||
<BondCoupons vm={vm} />
|
||||
<BondCoupons
|
||||
coupons={vm.coupons}
|
||||
pastCoupons={vm.pastCoupons}
|
||||
state={vm.couponsState}
|
||||
upcoming={vm.upcoming}
|
||||
/>
|
||||
|
||||
<InstrumentEvents
|
||||
historyNote={t("bond.historyNote")}
|
||||
@@ -248,283 +253,4 @@ const BondPageView: FC<BondPageViewProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const BondMetrics: FC<BondPageViewProps> = (props) => {
|
||||
const { vm } = props;
|
||||
const { t } = useTranslation();
|
||||
const creditRating = vm.metrics?.creditRating;
|
||||
|
||||
let rating = t(
|
||||
vm.metrics?.unavailable.includes("rating")
|
||||
? "bond.ratingUnavailable"
|
||||
: "bond.ratingMissing",
|
||||
);
|
||||
|
||||
if (creditRating) {
|
||||
rating
|
||||
= creditRating.status === "withdrawn"
|
||||
? t("bond.ratingWithdrawn")
|
||||
: (creditRating.value ?? t("bond.unknown"));
|
||||
}
|
||||
|
||||
const couponDate = vm.metrics?.couponDate
|
||||
? formatDate(vm.metrics?.couponDate)
|
||||
: t("bond.unknown");
|
||||
|
||||
const effectiveYield = vm.metrics?.effectiveYield
|
||||
? `${formatDecimal(vm.metrics?.effectiveYield.value)} %`
|
||||
: t("bond.unknown");
|
||||
|
||||
const couponYield
|
||||
= vm.metrics?.couponYieldPercent == undefined
|
||||
? t("bond.unknown")
|
||||
: `${formatDecimal(vm.metrics?.couponYieldPercent)} %`;
|
||||
|
||||
let yieldContent;
|
||||
|
||||
if (vm.metrics?.effectiveYield) {
|
||||
yieldContent = (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.yieldSource", {
|
||||
at: formatSnapshotTime(vm.metrics?.effectiveYield.tradeAt),
|
||||
date: formatDate(vm.metrics?.effectiveYield.date),
|
||||
price: formatDecimal(
|
||||
vm.metrics?.effectiveYield.pricePercent,
|
||||
),
|
||||
target: t(
|
||||
`bond.yieldTargets.${vm.metrics?.effectiveYield.dateType}`,
|
||||
{ defaultValue: t("bond.yieldTargets.unknown") },
|
||||
),
|
||||
})}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
else {
|
||||
yieldContent = (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.yieldUnavailable")}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InstrumentSection state={vm.metricsState} title={t("bond.metrics")}>
|
||||
{!!vm.metrics && (
|
||||
<Stack gap="md">
|
||||
<div className={classes.companyMetrics}>
|
||||
<Metric label={t("bond.creditRating")} value={rating} />
|
||||
|
||||
<Metric
|
||||
label={t("bond.couponAmount")}
|
||||
value={formatMoney(vm.metrics.coupon)}
|
||||
/>
|
||||
|
||||
<Metric
|
||||
label={t("bond.couponType")}
|
||||
value={t(`bond.couponTypes.${vm.metrics.couponType}`, {
|
||||
defaultValue: t("bond.unknown"),
|
||||
})}
|
||||
/>
|
||||
|
||||
<Metric label={t("bond.couponDate")} value={couponDate} />
|
||||
|
||||
<Metric label={t("bond.effectiveYield")} value={effectiveYield} />
|
||||
|
||||
<Metric label={t("bond.couponYield")} value={couponYield} />
|
||||
</div>
|
||||
|
||||
{!!vm.metrics.creditRating && (
|
||||
<Group gap="xs">
|
||||
<Anchor
|
||||
href={vm.metrics.creditRating.sourceUrl}
|
||||
rel="noopener noreferrer"
|
||||
size="sm"
|
||||
target="_blank"
|
||||
>
|
||||
{t("bond.ratingSource", {
|
||||
date: formatDate(vm.metrics.creditRating.date),
|
||||
})}
|
||||
</Anchor>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.ratingChecked", {
|
||||
date: formatSnapshotTime(vm.metrics.creditRating.observedAt),
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.ratingCoverage")}
|
||||
</Text>
|
||||
|
||||
{yieldContent}
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.couponYieldNote")}
|
||||
</Text>
|
||||
|
||||
{!!vm.metrics?.couponYieldPriceAt && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.couponYieldPriceAt", {
|
||||
date: formatSnapshotTime(vm.metrics?.couponYieldPriceAt),
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{vm.metrics.unavailable.length > 0 && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.metricsPartial")}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</InstrumentSection>
|
||||
);
|
||||
};
|
||||
|
||||
const BondCoupons: FC<BondPageViewProps> = (props) => {
|
||||
const { vm } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<InstrumentSection
|
||||
state={vm.couponsState}
|
||||
title={t("bond.upcomingCoupons")}
|
||||
>
|
||||
{!!vm.coupons && (
|
||||
<Stack gap="md">
|
||||
{!!(vm.coupons.estimateUnavailable || vm.coupons.positionIsStale) && (
|
||||
<Alert aria-live="polite" color="orange" role="status">
|
||||
{t("stock.estimateUnavailable")}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{vm.upcoming.length > 0
|
||||
? (
|
||||
vm.upcoming.map(coupon => (
|
||||
<Coupon
|
||||
coupon={coupon}
|
||||
key={`${coupon.number}:${coupon.paymentDate}`}
|
||||
/>
|
||||
))
|
||||
)
|
||||
: <Text c="dimmed">{t("bond.noCoupons")}</Text>}
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.couponWindow", {
|
||||
from: formatDate(vm.coupons.from),
|
||||
to: formatDate(vm.coupons.to),
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.couponNote")}
|
||||
</Text>
|
||||
|
||||
{!!vm.coupons.positionAt && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("stock.snapshotAt", {
|
||||
date: formatSnapshotTime(vm.coupons.positionAt),
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Accordion radius="md" variant="contained">
|
||||
<Accordion.Item value="history">
|
||||
<Accordion.Control>{t("bond.couponHistory")}</Accordion.Control>
|
||||
|
||||
<Accordion.Panel>
|
||||
<Stack gap="sm">
|
||||
{vm.pastCoupons.length > 0
|
||||
? (
|
||||
vm.pastCoupons.map(coupon => (
|
||||
<Coupon
|
||||
coupon={coupon}
|
||||
key={`${coupon.number}:${coupon.paymentDate}`}
|
||||
/>
|
||||
))
|
||||
)
|
||||
: <Text c="dimmed">{t("bond.noCoupons")}</Text>}
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
)}
|
||||
</InstrumentSection>
|
||||
);
|
||||
};
|
||||
|
||||
type MetricProps = { label: string; value: string };
|
||||
|
||||
const Metric: FC<MetricProps> = props => (
|
||||
<div>
|
||||
<Text c="dimmed" size="xs">
|
||||
{props.label}
|
||||
</Text>
|
||||
|
||||
<Text fw={600} size="lg">
|
||||
{props.value}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
|
||||
type CouponProps = {
|
||||
coupon: NonNullable<BondPageVm["coupons"]>["coupons"][number];
|
||||
};
|
||||
|
||||
const Coupon: FC<CouponProps> = (props) => {
|
||||
const { coupon } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Paper p="md" radius="md" withBorder>
|
||||
<Group align="start" justify="space-between">
|
||||
<div>
|
||||
<Text fw={600}>
|
||||
{coupon.paymentDate
|
||||
? formatDate(coupon.paymentDate)
|
||||
: t("stock.dateUnknown")}
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.couponNumber", {
|
||||
number: coupon.number ?? t("bond.unknown"),
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Metric label={t("bond.perBond")} value={formatMoney(coupon.perBond)} />
|
||||
|
||||
{!!coupon.estimatedAmount && (
|
||||
<Metric
|
||||
label={t("stock.yourEstimate")}
|
||||
value={formatMoney(coupon.estimatedAmount)}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Group gap="md" mt="sm">
|
||||
<Badge color="gray" variant="light">
|
||||
{t(`bond.couponTypes.${coupon.couponType}`, {
|
||||
defaultValue: t("bond.unknown"),
|
||||
})}
|
||||
</Badge>
|
||||
|
||||
{coupon.periodDays != undefined && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.couponDays", { count: coupon.periodDays })}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!!coupon.recordDate && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("stock.record")}: {formatDate(coupon.recordDate)}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export { BondPageView };
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useParams, useSearchParams } from "react-router";
|
||||
|
||||
import type { OperationCategory } from "@/entities/broker-operation";
|
||||
|
||||
import {
|
||||
type BondPeriod,
|
||||
useBond,
|
||||
@@ -11,10 +9,8 @@ import {
|
||||
useBondPosition,
|
||||
useBondQuote,
|
||||
} from "@/entities/bond";
|
||||
import {
|
||||
section,
|
||||
useInstrumentOperations,
|
||||
} from "@/features/instrument-details";
|
||||
import { type OperationCategory, useBrokerageOperationHistory } from "@/entities/broker-operation";
|
||||
import { section } from "@/features/instrument-details";
|
||||
import { useScreenSync } from "@/features/tbank-connection";
|
||||
import { apiErrorCode } from "@/shared/api";
|
||||
import { instrumentLogoUrl } from "@/shared/lib";
|
||||
@@ -51,12 +47,12 @@ function useBondPageVm() {
|
||||
const position = useBondPosition(uid);
|
||||
const coupons = useBondCoupons(uid);
|
||||
|
||||
const history = useInstrumentOperations(
|
||||
uid,
|
||||
filter,
|
||||
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[filter],
|
||||
);
|
||||
categories: categories[filter],
|
||||
instrumentUid: uid,
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
const { operations } = history;
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { Accordion, Alert, Badge, Group, Paper, Stack, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { BondCoupons as Coupons } from "@/entities/bond";
|
||||
|
||||
import { InstrumentSection, type SectionState } from "@/features/instrument-details";
|
||||
import { formatDate, formatMoney, formatSnapshotTime } from "@/shared/lib";
|
||||
|
||||
import { Metric } from "./Metric.view";
|
||||
|
||||
type BondCouponsProps = {
|
||||
coupons?: Coupons;
|
||||
pastCoupons: Coupons["coupons"];
|
||||
state: SectionState;
|
||||
upcoming: Coupons["coupons"];
|
||||
};
|
||||
|
||||
const BondCoupons: FC<BondCouponsProps> = (props) => {
|
||||
const { coupons, pastCoupons, state, upcoming } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<InstrumentSection
|
||||
state={state}
|
||||
title={t("bond.upcomingCoupons")}
|
||||
>
|
||||
{!!coupons && (
|
||||
<Stack gap="md">
|
||||
{!!(coupons.estimateUnavailable || coupons.positionIsStale) && (
|
||||
<Alert aria-live="polite" color="orange" role="status">
|
||||
{t("stock.estimateUnavailable")}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{upcoming.length > 0
|
||||
? (
|
||||
upcoming.map(coupon => (
|
||||
<Coupon
|
||||
coupon={coupon}
|
||||
key={`${coupon.number}:${coupon.paymentDate}`}
|
||||
/>
|
||||
))
|
||||
)
|
||||
: <Text c="dimmed">{t("bond.noCoupons")}</Text>}
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.couponWindow", {
|
||||
from: formatDate(coupons.from),
|
||||
to: formatDate(coupons.to),
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.couponNote")}
|
||||
</Text>
|
||||
|
||||
{!!coupons.positionAt && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("stock.snapshotAt", {
|
||||
date: formatSnapshotTime(coupons.positionAt),
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Accordion radius="md" variant="contained">
|
||||
<Accordion.Item value="history">
|
||||
<Accordion.Control>{t("bond.couponHistory")}</Accordion.Control>
|
||||
|
||||
<Accordion.Panel>
|
||||
<Stack gap="sm">
|
||||
{pastCoupons.length > 0
|
||||
? (
|
||||
pastCoupons.map(coupon => (
|
||||
<Coupon
|
||||
coupon={coupon}
|
||||
key={`${coupon.number}:${coupon.paymentDate}`}
|
||||
/>
|
||||
))
|
||||
)
|
||||
: <Text c="dimmed">{t("bond.noCoupons")}</Text>}
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
)}
|
||||
</InstrumentSection>
|
||||
);
|
||||
};
|
||||
|
||||
type CouponProps = {
|
||||
coupon: Coupons["coupons"][number];
|
||||
};
|
||||
|
||||
const Coupon: FC<CouponProps> = (props) => {
|
||||
const { coupon } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Paper p="md" radius="md" withBorder>
|
||||
<Group align="start" justify="space-between">
|
||||
<div>
|
||||
<Text fw={600}>
|
||||
{coupon.paymentDate
|
||||
? formatDate(coupon.paymentDate)
|
||||
: t("stock.dateUnknown")}
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.couponNumber", {
|
||||
number: coupon.number ?? t("bond.unknown"),
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Metric label={t("bond.perBond")} value={formatMoney(coupon.perBond)} />
|
||||
|
||||
{!!coupon.estimatedAmount && (
|
||||
<Metric
|
||||
label={t("stock.yourEstimate")}
|
||||
value={formatMoney(coupon.estimatedAmount)}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Group gap="md" mt="sm">
|
||||
<Badge color="gray" variant="light">
|
||||
{t(`bond.couponTypes.${coupon.couponType}`, {
|
||||
defaultValue: t("bond.unknown"),
|
||||
})}
|
||||
</Badge>
|
||||
|
||||
{coupon.periodDays != undefined && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.couponDays", { count: coupon.periodDays })}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!!coupon.recordDate && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("stock.record")}: {formatDate(coupon.recordDate)}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export { BondCoupons };
|
||||
@@ -0,0 +1,150 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { Anchor, Group, Stack, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { BondMetrics as Metrics } from "@/entities/bond";
|
||||
|
||||
import { InstrumentSection, type SectionState } from "@/features/instrument-details";
|
||||
import { formatDate, formatDecimal, formatMoney, formatSnapshotTime } from "@/shared/lib";
|
||||
|
||||
import classes from "../BondPage.module.css";
|
||||
import { Metric } from "./Metric.view";
|
||||
|
||||
type BondMetricsProps = { metrics?: Metrics; state: SectionState };
|
||||
|
||||
const BondMetrics: FC<BondMetricsProps> = (props) => {
|
||||
const { metrics, state } = props;
|
||||
const { t } = useTranslation();
|
||||
const creditRating = metrics?.creditRating;
|
||||
|
||||
let rating = t(
|
||||
metrics?.unavailable.includes("rating")
|
||||
? "bond.ratingUnavailable"
|
||||
: "bond.ratingMissing",
|
||||
);
|
||||
|
||||
if (creditRating) {
|
||||
rating
|
||||
= creditRating.status === "withdrawn"
|
||||
? t("bond.ratingWithdrawn")
|
||||
: (creditRating.value ?? t("bond.unknown"));
|
||||
}
|
||||
|
||||
const couponDate = metrics?.couponDate
|
||||
? formatDate(metrics?.couponDate)
|
||||
: t("bond.unknown");
|
||||
|
||||
const effectiveYield = metrics?.effectiveYield
|
||||
? `${formatDecimal(metrics?.effectiveYield.value)} %`
|
||||
: t("bond.unknown");
|
||||
|
||||
const couponYield
|
||||
= metrics?.couponYieldPercent == undefined
|
||||
? t("bond.unknown")
|
||||
: `${formatDecimal(metrics?.couponYieldPercent)} %`;
|
||||
|
||||
let yieldContent;
|
||||
|
||||
if (metrics?.effectiveYield) {
|
||||
yieldContent = (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.yieldSource", {
|
||||
at: formatSnapshotTime(metrics?.effectiveYield.tradeAt),
|
||||
date: formatDate(metrics?.effectiveYield.date),
|
||||
price: formatDecimal(
|
||||
metrics?.effectiveYield.pricePercent,
|
||||
),
|
||||
target: t(
|
||||
`bond.yieldTargets.${metrics?.effectiveYield.dateType}`,
|
||||
{ defaultValue: t("bond.yieldTargets.unknown") },
|
||||
),
|
||||
})}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
else {
|
||||
yieldContent = (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.yieldUnavailable")}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InstrumentSection state={state} title={t("bond.metrics")}>
|
||||
{!!metrics && (
|
||||
<Stack gap="md">
|
||||
<div className={classes.companyMetrics}>
|
||||
<Metric label={t("bond.creditRating")} value={rating} />
|
||||
|
||||
<Metric
|
||||
label={t("bond.couponAmount")}
|
||||
value={formatMoney(metrics.coupon)}
|
||||
/>
|
||||
|
||||
<Metric
|
||||
label={t("bond.couponType")}
|
||||
value={t(`bond.couponTypes.${metrics.couponType}`, {
|
||||
defaultValue: t("bond.unknown"),
|
||||
})}
|
||||
/>
|
||||
|
||||
<Metric label={t("bond.couponDate")} value={couponDate} />
|
||||
|
||||
<Metric label={t("bond.effectiveYield")} value={effectiveYield} />
|
||||
|
||||
<Metric label={t("bond.couponYield")} value={couponYield} />
|
||||
</div>
|
||||
|
||||
{!!metrics.creditRating && (
|
||||
<Group gap="xs">
|
||||
<Anchor
|
||||
href={metrics.creditRating.sourceUrl}
|
||||
rel="noopener noreferrer"
|
||||
size="sm"
|
||||
target="_blank"
|
||||
>
|
||||
{t("bond.ratingSource", {
|
||||
date: formatDate(metrics.creditRating.date),
|
||||
})}
|
||||
</Anchor>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.ratingChecked", {
|
||||
date: formatSnapshotTime(metrics.creditRating.observedAt),
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.ratingCoverage")}
|
||||
</Text>
|
||||
|
||||
{yieldContent}
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.couponYieldNote")}
|
||||
</Text>
|
||||
|
||||
{!!metrics?.couponYieldPriceAt && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.couponYieldPriceAt", {
|
||||
date: formatSnapshotTime(metrics?.couponYieldPriceAt),
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{metrics.unavailable.length > 0 && (
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.metricsPartial")}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</InstrumentSection>
|
||||
);
|
||||
};
|
||||
|
||||
export { BondMetrics };
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { Text } from "@mantine/core";
|
||||
|
||||
type MetricProps = { label: string; value: string };
|
||||
|
||||
const Metric: FC<MetricProps> = props => (
|
||||
<div>
|
||||
<Text c="dimmed" size="xs">
|
||||
{props.label}
|
||||
</Text>
|
||||
|
||||
<Text fw={600} size="lg">
|
||||
{props.value}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
|
||||
export { Metric };
|
||||
Reference in New Issue
Block a user