refactor(account-operations): split page state views into components

Extract loading/error/loaded state views from the single
AccountOperationsPage.view.tsx into dedicated files under components/.
Document the "one state per view file" rule as a mandatory review
convention in CONVENTIONS.md and UI-PATTERN.md.

Co-Authored-By: opencode <opencode@anthropic.com>
This commit is contained in:
2026-08-27 10:53:23 +03:00
co-authored by opencode
parent 8f9caa946e
commit 695cda0456
6 changed files with 73 additions and 53 deletions
@@ -0,0 +1,28 @@
import { Button } from "@mantine/core";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { StateView } from "@/shared/ui";
import type { AccountOperationsPageViewModel } from "../AccountOperationsPage.vm";
type AccountOperationsPageErrorProps = Extract<
AccountOperationsPageViewModel,
{ state: "error" }
>;
const AccountOperationsPageError: FC<AccountOperationsPageErrorProps> = (
props,
) => {
const { retry } = props;
const { t } = useTranslation();
return (
<StateView
error
title={t("ledger.errorTitle")}
text={t("ledger.errorText")}
action={<Button onClick={retry}>{t("ledger.retry")}</Button>}
/>
);
};
export { AccountOperationsPageError };
@@ -0,0 +1,98 @@
import {
Button,
Checkbox,
Group,
Stack,
TextInput,
Title,
} from "@mantine/core";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import {
LedgerEntryRow,
type OperationCategory,
operationCategories,
} from "@/entities/broker-operation";
import { StateView } from "@/shared/ui";
import type { AccountOperationsPageViewModel } from "../AccountOperationsPage.vm";
type AccountOperationsPageLoadedProps = Extract<
AccountOperationsPageViewModel,
{ state: "loaded" }
>;
const AccountOperationsPageLoaded: FC<AccountOperationsPageLoadedProps> = (
props,
) => {
const {
categories,
from,
hasNext,
hasPrevious,
items,
onCategoriesChange,
onFromChange,
onNext,
onPrevious,
onToChange,
to,
} = props;
const { t } = useTranslation();
return (
<Stack maw={960} gap="lg">
<Title order={1}>{t("ledger.title")}</Title>
<Group align="end">
<TextInput
label={t("ledger.from")}
type="date"
value={from}
onChange={(event) => onFromChange(event.currentTarget.value)}
/>
<TextInput
label={t("ledger.to")}
type="date"
value={to}
onChange={(event) => onToChange(event.currentTarget.value)}
/>
</Group>
<Checkbox.Group
label={t("ledger.categories")}
value={categories}
onChange={(value) => onCategoriesChange(value as OperationCategory[])}
>
<Group mt="xs">
{operationCategories.map((category) => (
<Checkbox
key={category}
value={category}
label={t(`ledger.category.${category}`)}
/>
))}
</Group>
</Checkbox.Group>
{items.length ? (
items.map((operation) => (
<LedgerEntryRow key={operation.id} operation={operation} />
))
) : (
<StateView
title={t("ledger.emptyTitle")}
text={t("ledger.emptyText")}
/>
)}
{hasNext || hasPrevious ? (
<Group justify="space-between">
<Button disabled={!hasPrevious} onClick={onPrevious}>
{t("ledger.previous")}
</Button>
<Button disabled={!hasNext} onClick={onNext}>
{t("ledger.next")}
</Button>
</Group>
) : null}
</Stack>
);
};
export { AccountOperationsPageLoaded };
@@ -0,0 +1,17 @@
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { StateView } from "@/shared/ui";
const AccountOperationsPageLoading: FC = () => {
const { t } = useTranslation();
return (
<StateView
loading
title={t("states.loadingTitle")}
text={t("states.loadingText")}
/>
);
};
export { AccountOperationsPageLoading };