chore: finish frontend ESLint migration #18

Enable every plugin recommended preset and replace Biome with ESLint formatting.

Preserve API contracts and JSX whitespace; verify source, tests, builds and tooling.
This commit is contained in:
2026-09-28 16:36:40 +03:00
committed by ksv741
parent 57630102e4
commit 72d7835227
253 changed files with 10059 additions and 8864 deletions
+175 -159
View File
@@ -12,9 +12,10 @@ import {
} from "@mantine/core";
import { AddIcon, DeleteIcon, EditIcon } from "@wealthledger/icons";
import { EntryCard, LoadingSkeleton, StateView } from "@wealthledger/ui";
import { type FC, useLayoutEffect, useRef } from "react";
import { type FC, type ReactNode, useLayoutEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import {
BankCardFace,
BankFeedback,
@@ -22,26 +23,28 @@ import {
BankPeriodPicker,
BankSummaryView,
} from "@/entities/bank-card";
import classes from "./CardPage.module.css";
import type { CardPageVm } from "./CardPage.vm";
import classes from "./CardPage.module.css";
type CardPageViewProps = { vm: CardPageVm };
const CardPageView: FC<CardPageViewProps> = (props) => {
const { vm } = props;
const { t } = useTranslation();
const card = vm.card.data;
const paginationRef = useRef<HTMLDivElement>(null);
const pendingPage = useRef<{
scope: string;
const pendingPageRef = useRef<null | {
page: number;
scope: string;
top: number;
} | null>(null);
}>(null);
const scope = `${card?.id}-${vm.period.from}-${vm.period.to}-${vm.pageSize}`;
useLayoutEffect(() => {
const pending = pendingPage.current;
const pending = pendingPageRef.current;
if (!pending) return;
if (pending.scope !== scope || vm.history.isError) {
pendingPage.current = null;
pendingPageRef.current = /* eslint-disable-line unicorn/no-null -- Reset the pending-pagination ref to its initial null sentinel. */ null;
return;
}
if (vm.history.isPlaceholderData || vm.history.data?.page !== pending.page)
@@ -49,9 +52,10 @@ const CardPageView: FC<CardPageViewProps> = (props) => {
const pagination = paginationRef.current;
if (pagination) {
const delta = pagination.getBoundingClientRect().top - pending.top;
if (delta) window.scrollBy({ top: delta, behavior: "instant" });
if (delta) window.scrollBy({ behavior: "instant", top: delta });
}
pendingPage.current = null;
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
pendingPageRef.current = null;
}, [
scope,
vm.history.data,
@@ -72,10 +76,7 @@ const CardPageView: FC<CardPageViewProps> = (props) => {
);
return (
<StateView
error
title={t(vm.notFound ? "bank.notFound" : "bank.error")}
text={t("bank.back")}
action={
action={(
<Group>
<Button component={Link} to="/cards">
{t("bank.back")}
@@ -86,65 +87,158 @@ const CardPageView: FC<CardPageViewProps> = (props) => {
</Button>
)}
</Group>
}
)}
error
text={t("bank.back")}
title={t(vm.notFound ? "bank.notFound" : "bank.error")}
/>
);
}
// eslint-disable-next-line sonarjs/function-return-type -- ReactNode deliberately includes lists, elements and empty output.
function renderHistory(): ReactNode {
if (!card) return;
if (vm.rows) {
if (vm.rows.length > 0) {
return vm.rows.map(row => (
<section
aria-label={`${row.title} ${row.item.date}`}
className={classes.operation}
key={row.item.id}
>
<EntryCard
actions={(
<Group gap={4} wrap="nowrap">
<ActionIcon
aria-label={t("bank.edit")}
disabled={
vm.history.isPlaceholderData
|| !row.editable
|| Boolean(card.archivedAt)
}
onClick={() => vm.setOperation(row.item)}
size="sm"
title={t("bank.edit")}
variant="subtle"
>
<EditIcon size={18} strokeWidth={1.5} />
</ActionIcon>
<ActionIcon
aria-label={t("bank.delete")}
color="red"
disabled={
vm.history.isPlaceholderData
|| !row.editable
|| Boolean(card.archivedAt)
}
onClick={() => vm.setDeleting(row.item)}
size="sm"
title={t("bank.delete")}
variant="subtle"
>
<DeleteIcon size={18} strokeWidth={1.5} />
</ActionIcon>
</Group>
)}
color={row.color}
details={
(row.otherCard)
? ((
<Anchor
component={Link}
to={`/cards/${row.otherCard}?from=${vm.period.from}&to=${vm.period.to}`}
>
{t("bank.card")}
</Anchor>
))
: undefined
}
icon={(
<BankOperationIcon
categoryName={row.item.categoryName}
kind={row.item.kind}
/>
)}
initial={row.title[0]}
metadata={row.metadata}
title={row.title}
tone={row.tone}
value={row.value}
valueLabel={t(`bank.kinds.${row.item.kind}`)}
/>
</section>
));
}
return <Text>{t("bank.noOperations")}</Text>;
}
if (vm.history.isLoading) {
return (
<LoadingSkeleton label={t("bank.loadingHistory")}>
<Stack>
<Skeleton h={90} />
<Skeleton h={90} />
</Stack>
</LoadingSkeleton>
);
}
}
return (
<Stack gap="lg" className={classes.page}>
<Stack className={classes.page} gap="lg">
<Stack>
<Title order={1} className={classes.wrap}>
<Title className={classes.wrap} order={1}>
{card.name}
</Title>
<div className={classes.card}>
<BankCardFace card={card} />
<Group gap={4} wrap="nowrap" className={classes.cardActions}>
<Group className={classes.cardActions} gap={4} wrap="nowrap">
<ActionIcon
variant="subtle"
size="md"
radius="md"
aria-label={t("bank.editCard")}
title={t("bank.editCard")}
onClick={() => vm.setEditingCard(true)}
radius="md"
size="md"
title={t("bank.editCard")}
variant="subtle"
>
<EditIcon size={18} strokeWidth={1.5} />
</ActionIcon>
<ActionIcon
variant="subtle"
size="md"
radius="md"
color="red"
aria-label={t(card.archivedAt ? "bank.restore" : "bank.archive")}
title={t(card.archivedAt ? "bank.restore" : "bank.archive")}
color="red"
loading={vm.archivePending}
onClick={() => void vm.toggleArchive()}
radius="md"
size="md"
title={t(card.archivedAt ? "bank.restore" : "bank.archive")}
variant="subtle"
>
{card.archivedAt ? (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M3 3h18v4H3z M5 7v14h14V7 M12 17V10m-3 3 3-3 3 3" />
</svg>
) : (
<DeleteIcon size={18} strokeWidth={1.5} />
)}
{card.archivedAt
? (
<svg
aria-hidden="true"
fill="none"
height="18"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="18"
>
<path d="M3 3h18v4H3z M5 7v14h14V7 M12 17V10m-3 3 3-3 3 3" />
</svg>
)
: (
<DeleteIcon size={18} strokeWidth={1.5} />
)}
</ActionIcon>
</Group>
</div>
{vm.archiveError ? (
{!!(vm.archiveError) && ((
<BankFeedback
error={vm.archiveError}
retry={() => void vm.card.refetch()}
/>
) : null}
))}
{card.archivedAt && <Text>{t("bank.archiveHint")}</Text>}
</Stack>
{vm.card.isError && (
@@ -158,136 +252,58 @@ const CardPageView: FC<CardPageViewProps> = (props) => {
{vm.card.isFetching && <Text role="status">{t("bank.refreshing")}</Text>}
{vm.cards.isError && (
<BankFeedback
loadingError
error={vm.cards.error}
cached={Boolean(vm.cards.data)}
error={vm.cards.error}
loadingError
retry={() => void vm.cards.refetch()}
/>
)}
<BankPeriodPicker period={vm.period} onChange={vm.setPeriod} />
<BankPeriodPicker onChange={vm.setPeriod} period={vm.period} />
<BankSummaryView
data={vm.summary.data}
loading={vm.summary.isLoading}
fetching={vm.summary.isFetching}
error={vm.summary.isError}
fetching={vm.summary.isFetching}
loading={vm.summary.isLoading}
retry={() => void vm.summary.refetch()}
/>
<Group justify="space-between" align="end">
<Group align="end" justify="space-between">
<Group gap="xs">
<Title order={2}>{t("bank.operations")}</Title>
<ActionIcon
variant="subtle"
size="lg"
aria-label={t("bank.addOperation")}
title={t("bank.addOperation")}
disabled={Boolean(card.archivedAt) || !vm.cards.data}
onClick={() => vm.setOperation("new")}
size="lg"
title={t("bank.addOperation")}
variant="subtle"
>
<AddIcon size={20} strokeWidth={1.5} />
</ActionIcon>
</Group>
<Select
label={t("bank.pageSize")}
value={vm.pageSize}
onChange={vm.setPageSize}
allowDeselect={false}
w={180}
data={[
"5",
"10",
"20",
"50",
"100",
{ value: "all", label: t("bank.allOperations") },
{ label: t("bank.allOperations"), value: "all" },
]}
label={t("bank.pageSize")}
onChange={vm.setPageSize}
value={vm.pageSize}
w={180}
/>
</Group>
<Stack
key={`${vm.period.from}-${vm.period.to}-${vm.pageSize}-${vm.history.data?.page ?? "loading"}`}
gap="lg"
className={classes.operations}
aria-busy={vm.history.isFetching}
className={classes.operations}
data-pending={vm.history.isPlaceholderData || undefined}
gap="lg"
key={`${vm.period.from}-${vm.period.to}-${vm.pageSize}-${vm.history.data?.page ?? "loading"}`}
>
{vm.rows ? (
vm.rows.length ? (
vm.rows.map((row) => (
<section
key={row.item.id}
className={classes.operation}
aria-label={`${row.title} ${row.item.date}`}
>
<EntryCard
title={row.title}
metadata={row.metadata}
color={row.color}
initial={row.title[0]}
icon={
<BankOperationIcon
kind={row.item.kind}
categoryName={row.item.categoryName}
/>
}
value={row.value}
valueLabel={t(`bank.kinds.${row.item.kind}`)}
tone={row.tone}
details={
row.otherCard ? (
<Anchor
component={Link}
to={`/cards/${row.otherCard}?from=${vm.period.from}&to=${vm.period.to}`}
>
{t("bank.card")}
</Anchor>
) : null
}
actions={
<Group gap={4} wrap="nowrap">
<ActionIcon
variant="subtle"
disabled={
vm.history.isPlaceholderData ||
!row.editable ||
Boolean(card.archivedAt)
}
size="sm"
aria-label={t("bank.edit")}
title={t("bank.edit")}
onClick={() => vm.setOperation(row.item)}
>
<EditIcon size={18} strokeWidth={1.5} />
</ActionIcon>
<ActionIcon
variant="subtle"
color="red"
disabled={
vm.history.isPlaceholderData ||
!row.editable ||
Boolean(card.archivedAt)
}
size="sm"
aria-label={t("bank.delete")}
title={t("bank.delete")}
onClick={() => vm.setDeleting(row.item)}
>
<DeleteIcon size={18} strokeWidth={1.5} />
</ActionIcon>
</Group>
}
/>
</section>
))
) : (
<Text>{t("bank.noOperations")}</Text>
)
) : vm.history.isLoading ? (
<LoadingSkeleton label={t("bank.loadingHistory")}>
<Stack>
<Skeleton h={90} />
<Skeleton h={90} />
</Stack>
</LoadingSkeleton>
) : null}
{renderHistory()}
</Stack>
{vm.history.isError && (
<Group role="alert">
@@ -299,34 +315,34 @@ const CardPageView: FC<CardPageViewProps> = (props) => {
)}
{vm.history.data && vm.pageSize !== "all" && (
<Group
component="nav"
ref={paginationRef}
aria-label={t("bank.pagination")}
component="nav"
justify="center"
py="sm"
ref={paginationRef}
>
<Pagination
total={vm.totalPages}
value={vm.page}
onChange={(page) => {
if (paginationRef.current)
pendingPage.current = {
scope,
page,
top: paginationRef.current.getBoundingClientRect().top,
};
vm.setPage(page);
}}
disabled={vm.history.isFetching}
siblings={0}
getItemProps={(page) => ({
"aria-label": t("bank.page", { page }),
})}
getControlProps={(control) => ({
getControlProps={control => ({
"aria-label": t(
control === "previous" ? "bank.previous" : "bank.next",
),
})}
getItemProps={page => ({
"aria-label": t("bank.page", { page }),
})}
onChange={(page) => {
if (paginationRef.current)
pendingPageRef.current = {
page,
scope,
top: paginationRef.current.getBoundingClientRect().top,
};
vm.setPage(page);
}}
siblings={0}
total={vm.totalPages}
value={vm.page}
/>
</Group>
)}