259 lines
5.8 KiB
TypeScript
259 lines
5.8 KiB
TypeScript
import type { FC } from "react";
|
||
|
||
import {
|
||
Alert,
|
||
Badge,
|
||
Button,
|
||
Group,
|
||
Skeleton,
|
||
Stack,
|
||
Text,
|
||
} from "@mantine/core";
|
||
import { LoadingSkeleton } from "@wealthledger/ui";
|
||
import { useTranslation } from "react-i18next";
|
||
|
||
import {
|
||
InstrumentHeader,
|
||
InstrumentChart as StockChart,
|
||
InstrumentEvents as StockEvents,
|
||
InstrumentPosition as StockPosition,
|
||
InstrumentSection as StockSection,
|
||
} from "@/features/instrument-details";
|
||
import {
|
||
formatDate,
|
||
formatDecimal,
|
||
formatMoney,
|
||
formatSnapshotTime,
|
||
} from "@/shared/lib";
|
||
|
||
import type { StockPageVm } from "./StockPage.vm";
|
||
|
||
import { StockDividends } from "./components/StockDividends.view";
|
||
import classes from "./StockPage.module.css";
|
||
|
||
type StockPageViewProps = { vm: StockPageVm };
|
||
|
||
const StockPageView: FC<StockPageViewProps> = (props) => {
|
||
const { vm } = props;
|
||
const { t } = useTranslation();
|
||
const stock = vm.stock;
|
||
if (!stock) return;
|
||
|
||
const subtitle = (
|
||
<>
|
||
{stock.ticker} ·{" "}
|
||
{t(`stock.shareTypes.${stock.shareType}`, {
|
||
defaultValue: t("stock.share"),
|
||
})}
|
||
</>
|
||
);
|
||
|
||
const appearanceClass = vm.quote?.change?.value.startsWith("-")
|
||
? classes.negative
|
||
: classes.positive;
|
||
|
||
let quoteContent;
|
||
|
||
if (vm.quoteState.loading) {
|
||
quoteContent = (
|
||
<LoadingSkeleton label={t("stock.loadingPrice")}>
|
||
<Skeleton h={55} mt="lg" w="50%" />
|
||
</LoadingSkeleton>
|
||
);
|
||
}
|
||
else {
|
||
quoteContent = (
|
||
<div aria-live="polite">
|
||
<Text className={classes.price} mt="lg">
|
||
{formatMoney(vm.quote?.price)}
|
||
</Text>
|
||
|
||
{!!vm.quote?.change && (
|
||
<Text className={appearanceClass} fw={600}>
|
||
{vm.quote?.change?.value.startsWith("-") ? "" : "+"}
|
||
|
||
{formatMoney(vm.quote.change)}
|
||
|
||
{vm.quote.changePercent === null
|
||
? ""
|
||
: ` (${formatDecimal(vm.quote.changePercent)} %)`}
|
||
</Text>
|
||
)}
|
||
|
||
{vm.quote?.previousCloseDate
|
||
? (
|
||
<Text c="dimmed" size="xs">
|
||
{t("stock.changeBase", {
|
||
date: formatDate(vm.quote.previousCloseDate),
|
||
})}
|
||
</Text>
|
||
)
|
||
: (
|
||
<Text c="dimmed" size="xs">
|
||
{t("stock.noComparison")}
|
||
</Text>
|
||
)}
|
||
|
||
<Group gap="sm" mt="sm">
|
||
<Badge color="gray" variant="light">
|
||
{t(`stock.trading.${vm.quote?.tradingStatus}`, {
|
||
defaultValue: t("stock.trading.unknown"),
|
||
})}
|
||
</Badge>
|
||
|
||
{!!vm.quote?.priceAt && (
|
||
<Text c="dimmed" size="xs">
|
||
{t("stock.priceAt", {
|
||
date: formatSnapshotTime(vm.quote.priceAt),
|
||
})}
|
||
</Text>
|
||
)}
|
||
</Group>
|
||
|
||
{!!vm.quote?.unavailable.length && (
|
||
<Text c="dimmed" mt="xs" size="xs">
|
||
{t("stock.partialQuote")}
|
||
</Text>
|
||
)}
|
||
|
||
{!!vm.quoteState.refreshing && (
|
||
<Text c="dimmed" role="status" size="xs">
|
||
{t("accounts.refreshing")}
|
||
</Text>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
let companyMetricsContent;
|
||
|
||
if (vm.company) {
|
||
if (vm.company.metrics.length > 0) {
|
||
companyMetricsContent = (
|
||
<div className={classes.companyMetrics}>
|
||
{vm.company.metrics.map(metric => (
|
||
<div key={metric.key}>
|
||
<Text c="dimmed" size="xs">
|
||
{t(`stock.metrics.${metric.key}`)}
|
||
|
||
{metric.period ? ` · ${metric.period}` : ""}
|
||
</Text>
|
||
|
||
<Text fw={600} size="lg">
|
||
{new Intl.NumberFormat("ru-RU", {
|
||
maximumFractionDigits: 2,
|
||
notation:
|
||
metric.unit === "money" ? "compact" : "standard",
|
||
}).format(metric.value)}{" "}
|
||
{metricUnit(metric, t("stock.currencyUnknown"))}
|
||
</Text>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
else {
|
||
companyMetricsContent = <Text c="dimmed">{t("stock.noFundamentals")}</Text>;
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Stack className={classes.page} gap="lg">
|
||
<InstrumentHeader
|
||
logoSrc={vm.logoSrc}
|
||
name={stock.name}
|
||
onRefresh={vm.refresh}
|
||
refreshing={vm.refreshing}
|
||
subtitle={subtitle}
|
||
>
|
||
|
||
{!!vm.company?.description && (
|
||
<Text mt="md" size="sm" style={{ whiteSpace: "pre-line" }}>
|
||
{vm.company.description}
|
||
</Text>
|
||
)}
|
||
|
||
{!!(vm.refreshError || vm.stockState.error || vm.quoteState.error) && (
|
||
<Alert aria-live="polite" color="orange" mt="md" role="status">
|
||
{t(vm.quote ? "accounts.refreshError" : "stock.sectionError")}
|
||
</Alert>
|
||
)}
|
||
|
||
{quoteContent}
|
||
|
||
<StockSection
|
||
embedded
|
||
state={vm.companyState}
|
||
title={t("stock.company")}
|
||
>
|
||
<Group gap="xl" mb="md">
|
||
{!!stock.sector && (
|
||
<Text size="sm">
|
||
{t("stock.sector")}:{" "}
|
||
{t(`stock.sectors.${stock.sector}`, {
|
||
defaultValue: stock.sector,
|
||
})}
|
||
</Text>
|
||
)}
|
||
|
||
{stock.lot !== null && <Text size="sm">{t("stock.lot", { value: stock.lot })}</Text>}
|
||
</Group>
|
||
|
||
{!!vm.company && (
|
||
<Stack gap="lg">
|
||
{vm.company.unavailable.length > 0 && (
|
||
<Alert aria-live="polite" color="orange" role="status">
|
||
<Group justify="space-between">
|
||
<Text size="sm">{t("stock.partialCompany")}</Text>
|
||
|
||
<Button
|
||
onClick={vm.companyState.retry}
|
||
size="xs"
|
||
variant="default"
|
||
>
|
||
{t("accounts.retry")}
|
||
</Button>
|
||
</Group>
|
||
</Alert>
|
||
)}
|
||
|
||
{companyMetricsContent}
|
||
|
||
<Text c="dimmed" size="xs">
|
||
{t("stock.periodNote")}
|
||
</Text>
|
||
</Stack>
|
||
)}
|
||
</StockSection>
|
||
</InstrumentHeader>
|
||
|
||
<StockChart vm={vm} />
|
||
|
||
<StockPosition vm={vm} />
|
||
|
||
<StockDividends
|
||
dividends={vm.dividends}
|
||
pastDividends={vm.pastDividends}
|
||
state={vm.dividendsState}
|
||
upcoming={vm.upcoming}
|
||
/>
|
||
|
||
<StockEvents vm={vm} />
|
||
</Stack>
|
||
);
|
||
};
|
||
|
||
function metricUnit(
|
||
metric: NonNullable<StockPageVm["company"]>["metrics"][number],
|
||
unknownCurrency: string,
|
||
) {
|
||
if (metric.unit === "money")
|
||
return metric.currency === "RUB"
|
||
? "₽"
|
||
: (metric.currency ?? unknownCurrency);
|
||
|
||
return metric.unit === "percent" ? "%" : "×";
|
||
}
|
||
|
||
export { StockPageView };
|