refactor: align frontend ownership boundaries

Close audit gaps across API drift, query policy, UI, and tests.
This commit is contained in:
2026-09-06 18:20:05 +03:00
parent 175727bc93
commit c97f62bd98
118 changed files with 2190 additions and 2254 deletions
+28
View File
@@ -0,0 +1,28 @@
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { useBrokerageAccounts } from "@/entities/brokerage-account";
import { BreadcrumbTrail } from "./BreadcrumbTrail";
type AccountBreadcrumbsProps = {
accountId: string;
section?: "events" | "income" | "operations";
};
const AccountBreadcrumbs: FC<AccountBreadcrumbsProps> = (props) => {
const { accountId, section } = props;
const { data: accounts } = useBrokerageAccounts();
const { t } = useTranslation();
const accountName =
accounts?.find((account) => account.id === accountId)?.name ??
t("breadcrumbs.accountFallback");
const accountPath = `/accounts/${accountId}`;
const items = [
{ label: t("breadcrumbs.accounts"), to: "/accounts" },
section ? { label: accountName, to: accountPath } : { label: accountName },
...(section ? [{ label: t(`breadcrumbs.${section}`) }] : []),
];
return <BreadcrumbTrail items={items} />;
};
export { AccountBreadcrumbs };
+3 -1
View File
@@ -8,7 +8,9 @@ import { theme } from "@/app/theme";
import { AuthProvider } from "@/features/auth";
const queryClient = new QueryClient({
defaultOptions: { queries: { refetchOnWindowFocus: false } },
defaultOptions: {
queries: { refetchOnWindowFocus: false, retry: false },
},
});
const App: FC = () => {
+13
View File
@@ -0,0 +1,13 @@
.trail {
margin-bottom: var(--mantine-spacing-md);
}
.link {
color: var(--mantine-primary-color-filled);
}
@media (max-width: 47.99375em) {
.trail[data-depth="3"] > :nth-child(-n + 2) {
display: none;
}
}
+1 -2
View File
@@ -1,5 +1,5 @@
import { MantineProvider } from "@mantine/core";
import { cleanup, render, screen } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router";
import { afterEach, describe, expect, it, vi } from "vitest";
import { AppBreadcrumbs } from "./AppBreadcrumbs";
@@ -26,7 +26,6 @@ function renderBreadcrumbs(
describe("AppBreadcrumbs", () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
+16 -113
View File
@@ -1,141 +1,44 @@
import { Breadcrumbs } from "@mantine/core";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link, useMatch } from "react-router";
import { useBrokerageAccounts } from "@/entities/brokerage-account";
type Crumb = { label: string; to?: string };
type BreadcrumbTrailProps = {
items: Crumb[];
};
type AccountBreadcrumbsProps = {
accountId: string;
events: boolean;
income: boolean;
operations: boolean;
};
const BreadcrumbTrail: FC<BreadcrumbTrailProps> = (props) => {
const { items } = props;
const { t } = useTranslation();
return (
<Breadcrumbs
aria-label={t("breadcrumbs.label")}
component="nav"
data-depth={items.length}
separator={<span aria-hidden="true">→</span>}
>
{items.map((item) =>
item.to ? (
<Link key={item.to} to={item.to}>
{item.label}
</Link>
) : (
<span aria-current="page" key={item.label}>
{item.label}
</span>
),
)}
</Breadcrumbs>
);
};
const AccountBreadcrumbs: FC<AccountBreadcrumbsProps> = (props) => {
const { accountId, events, income, operations } = props;
const { data: accounts } = useBrokerageAccounts();
const { t } = useTranslation();
const accountName =
accounts?.find((account) => account.id === accountId)?.name ??
t("breadcrumbs.accountFallback");
const accountPath = `/accounts/${accountId}`;
return (
<BreadcrumbTrail
items={
events
? [
{ label: t("breadcrumbs.accounts"), to: "/accounts" },
{ label: accountName, to: accountPath },
{ label: t("breadcrumbs.events") },
]
: operations
? [
{ label: t("breadcrumbs.accounts"), to: "/accounts" },
{ label: accountName, to: accountPath },
{ label: t("breadcrumbs.operations") },
]
: income
? [
{ label: t("breadcrumbs.accounts"), to: "/accounts" },
{ label: accountName, to: accountPath },
{ label: t("breadcrumbs.income") },
]
: [
{ label: t("breadcrumbs.accounts"), to: "/accounts" },
{ label: accountName },
]
}
/>
);
};
import { useMatch } from "react-router";
import { AccountBreadcrumbs } from "./AccountBreadcrumbs";
import { BreadcrumbTrail } from "./BreadcrumbTrail";
const AppBreadcrumbs: FC = () => {
const { t } = useTranslation();
const accountOperations = useMatch({
const operations = useMatch({
path: "/accounts/:accountId/operations",
end: true,
});
const accountEvents = useMatch({
path: "/accounts/:accountId/events",
end: true,
});
const accountIncome = useMatch({
path: "/accounts/:accountId/income",
end: true,
});
const events = useMatch({ path: "/accounts/:accountId/events", end: true });
const income = useMatch({ path: "/accounts/:accountId/income", end: true });
const account = useMatch({ path: "/accounts/:accountId", end: true });
const accounts = useMatch({ path: "/accounts", end: true });
const settings = useMatch({ path: "/settings", end: true });
if (accountOperations)
if (operations)
return (
<AccountBreadcrumbs
accountId={accountOperations.params.accountId ?? ""}
events={false}
income={false}
operations
accountId={operations.params.accountId ?? ""}
section="operations"
/>
);
if (accountEvents)
if (events)
return (
<AccountBreadcrumbs
accountId={accountEvents.params.accountId ?? ""}
events
income={false}
operations={false}
accountId={events.params.accountId ?? ""}
section="events"
/>
);
if (accountIncome)
if (income)
return (
<AccountBreadcrumbs
accountId={accountIncome.params.accountId ?? ""}
events={false}
income
operations={false}
accountId={income.params.accountId ?? ""}
section="income"
/>
);
if (account)
return (
<AccountBreadcrumbs
accountId={account.params.accountId ?? ""}
events={false}
income={false}
operations={false}
/>
);
return <AccountBreadcrumbs accountId={account.params.accountId ?? ""} />;
if (accounts)
return <BreadcrumbTrail items={[{ label: t("breadcrumbs.accounts") }]} />;
if (settings)
+36
View File
@@ -0,0 +1,36 @@
import { Breadcrumbs } from "@mantine/core";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import classes from "./AppBreadcrumbs.module.css";
type Crumb = { label: string; to?: string };
type BreadcrumbTrailProps = { items: Crumb[] };
const BreadcrumbTrail: FC<BreadcrumbTrailProps> = (props) => {
const { items } = props;
const { t } = useTranslation();
return (
<Breadcrumbs
aria-label={t("breadcrumbs.label")}
className={classes.trail}
component="nav"
data-depth={items.length}
separator={<span aria-hidden="true">→</span>}
>
{items.map((item) =>
item.to ? (
<Link className={classes.link} key={item.to} to={item.to}>
{item.label}
</Link>
) : (
<span aria-current="page" key={item.label}>
{item.label}
</span>
),
)}
</Breadcrumbs>
);
};
export { BreadcrumbTrail };