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
@@ -0,0 +1,195 @@
import type { FC } from "react";
import { AreaChart } from "@mantine/charts";
import {
Accordion,
Badge,
ScrollArea,
SegmentedControl,
Table,
Text,
} from "@mantine/core";
import { formatMoney } from "@/shared/lib";
import type { InstrumentChartVm } from "../../model/types";
import classes from "../InstrumentDetails.module.css";
import { InstrumentSection } from "../InstrumentSection.view";
import { useInstrumentChartVm } from "./InstrumentChart.vm";
type InstrumentChartProps = { priceUnit?: "percent"; vm: InstrumentChartVm };
const InstrumentChart: FC<InstrumentChartProps> = (props) => {
const {
chartLabel,
formatChartValue,
periodOptions,
priceRows,
referenceLines,
t,
vm,
} = useInstrumentChartVm(props);
let chartContent;
if (vm.candles) {
if (vm.chartData.length > 0) {
chartContent = (
<>
<AreaChart
areaProps={{ isAnimationActive: false }}
aria-label={chartLabel}
curveType="linear"
data={vm.chartData}
dataKey="date"
gridAxis="y"
h={310}
referenceLines={referenceLines}
series={[
{ color: "blue.6", label: t("stock.price"), name: "price" },
]}
tickLine="none"
tooltipAnimationDuration={0}
valueFormatter={formatChartValue}
withDots={vm.chartData.length === 1}
withGradient
yAxisProps={{ domain: ["auto", "auto"], width: 64 }}
/>
<Text c="dimmed" mt="sm" size="xs">
{t(
props.priceUnit === "percent"
? "bond.chartNote"
: "stock.chartNote",
)}
</Text>
{!!vm.hasIncompleteCandle && (
<Text c="dimmed" size="xs">
{t("stock.incomplete")}
</Text>
)}
{vm.averageLine !== null && (
<Text mt="xs" size="sm">
{t("stock.averageLine")}:{" "}
{formatMoney(vm.position?.averagePurchasePrice)}
</Text>
)}
<Accordion
className={classes.priceTable}
radius="md"
variant="contained"
>
<Accordion.Item value="prices">
<Accordion.Control>
<Text fw={600}>{t("stock.priceTable")}</Text>
<Text c="dimmed" size="xs">
{t("stock.priceTableHint", { count: vm.chartData.length })}
</Text>
</Accordion.Control>
<Accordion.Panel>
<ScrollArea.Autosize
mah={360}
type="auto"
viewportProps={{
"aria-label": t("stock.priceTable"),
"role": "region",
"tabIndex": 0,
}}
>
<Table
highlightOnHover
horizontalSpacing="md"
stickyHeader
striped
verticalSpacing="sm"
>
<Table.Thead>
<Table.Tr>
<Table.Th>{t("stock.date")}</Table.Th>
<Table.Th>{t("stock.price")}</Table.Th>
<Table.Th>{t("stock.candle")}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{priceRows.map((candle) => {
const displayColor = candle.isComplete
? "gray"
: "blue";
return (
<Table.Tr key={candle.time}>
<Table.Td>{candle.date}</Table.Td>
<Table.Td>
{candle.price}
</Table.Td>
<Table.Td>
<Badge
color={displayColor}
size="sm"
variant="light"
>
{t(
candle.isComplete
? "stock.complete"
: "stock.forming",
)}
</Badge>
</Table.Td>
</Table.Tr>
);
})}
</Table.Tbody>
</Table>
</ScrollArea.Autosize>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</>
);
}
else {
chartContent = (
<Text c="dimmed">
{t(
vm.candlesState.partial
? "investment.partial"
: "stock.noCandles",
)}
</Text>
);
}
}
return (
<InstrumentSection
controls={(
<div className={classes.controls}>
<SegmentedControl
aria-label={t("stock.period")}
data={periodOptions}
onChange={vm.setPeriod}
value={vm.period}
/>
</div>
)}
large
state={vm.candlesState}
title={t("stock.chart")}
>
{chartContent}
</InstrumentSection>
);
};
export { InstrumentChart };