Files
frontend/src/pages/card/ui/CardPage/CardPage.view.tsx
T
ksv741 e38ecd87d6
Frontend verify / commits (pull_request) Successful in 39s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Failing after 5m50s
fix(auth,bank): guard switch-auth nav and surface banking retry
AuthForm disables the switch-auth Link while submit is pending so pointer
click and Enter can no longer leave the auth route and lose the draft/error
context. The form restores a true Link after the request settles.

CardsPage/CardPage distinguish cold vs cached banking failures:
- cold card/section errors expose accessible alert + retry
- cached failures keep the previous data and offer a section GET retry
- summary-only/history-only outages do not hide successful sections
- notFound still skips the retry CTA

Tests cover the pending link guard, cold/cached cards error with retry,
summary failure with retry, and the prerequisite card list recovery.

Closes #56, #57
2026-10-14 10:43:34 +03:00

223 lines
4.7 KiB
TypeScript

import {
ActionIcon,
Button,
Group,
Pagination,
Select,
Skeleton,
Stack,
Text,
Title,
} from "@mantine/core";
import {
AddIcon,
DeleteIcon,
EditIcon,
RestoreIcon,
} from "@wealthledger/icons";
import { LoadingSkeleton, StateView } from "@wealthledger/ui";
import { type FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import {
BankCardFace,
BankFeedback,
BankPeriodPicker,
BankSummaryView,
} from "@/entities/bank-card";
import type { CardPageVm } from "./CardPage.vm";
import { CardHistory } from "./CardHistory.view";
import classes from "./CardPage.module.css";
import { useCardPagePresentationVm } from "./CardPagePresentation.vm";
type CardPageViewProps = { vm: CardPageVm };
const CardPageView: FC<CardPageViewProps> = (props) => {
const { vm } = props;
const {
getControlProps,
getItemProps,
onPageChange,
paginationRef,
} = useCardPagePresentationVm(vm);
const { t } = useTranslation();
const card = vm.card;
const archiveActionLabel = t(card?.archivedAt ? "bank.restore" : "bank.archive");
if (!card) {
if (vm.cardLoading)
return (
<LoadingSkeleton label={t("bank.loading")}>
<Stack>
<Skeleton h={130} />
<Skeleton h={160} />
<Skeleton h={90} />
<Skeleton h={90} />
</Stack>
</LoadingSkeleton>
);
const errorTitle = t(vm.notFound ? "bank.notFound" : "bank.error");
return (
<StateView
action={(
<Group>
<Button component={Link} to="/cards">
{t("bank.back")}
</Button>
{!vm.notFound && (
<Button onClick={vm.cardRetry} variant="default">
{t("bank.retry")}
</Button>
)}
</Group>
)}
error
text={t("bank.back")}
title={errorTitle}
/>
);
}
return (
<Stack className={classes.page} gap="lg">
<Stack>
<Title className={classes.wrap} order={1}>
{card.name}
</Title>
<div className={classes.card}>
<BankCardFace card={card} />
<Group className={classes.cardActions} gap={4} wrap="nowrap">
<ActionIcon
aria-label={t("bank.editCard")}
onClick={vm.openCardEditor}
radius="md"
size="md"
title={t("bank.editCard")}
variant="subtle"
>
<EditIcon size={18} strokeWidth={1.5} />
</ActionIcon>
<ActionIcon
aria-label={archiveActionLabel}
color="red"
loading={vm.archivePending}
onClick={vm.toggleArchive}
radius="md"
size="md"
title={archiveActionLabel}
variant="subtle"
>
{card.archivedAt
? <RestoreIcon size={18} />
: <DeleteIcon size={18} strokeWidth={1.5} />}
</ActionIcon>
</Group>
</div>
{!!vm.archiveError && (
<BankFeedback error={vm.archiveError} />
)}
{card.archivedAt && <Text>{t("bank.archiveHint")}</Text>}
{vm.cardError && vm.card && (
<BankFeedback
cached
error={vm.cardError}
retry={vm.cardRetry}
/>
)}
</Stack>
{vm.cardsFeedback && (
<BankFeedback
{...vm.cardsFeedback}
loadingError
retry={vm.cardsRetry}
/>
)}
<BankPeriodPicker onChange={vm.setPeriod} period={vm.period} />
<BankSummaryView {...vm.summary} retry={vm.summary.refetch} />
<Group align="end" justify="space-between">
<Group gap="xs">
<Title order={2}>{t("bank.operations")}</Title>
<ActionIcon
aria-label={t("bank.addOperation")}
disabled={!vm.canAddOperation}
onClick={vm.openNewOperation}
size="lg"
title={t("bank.addOperation")}
variant="subtle"
>
<AddIcon size={20} strokeWidth={1.5} />
</ActionIcon>
</Group>
<Select
allowDeselect={false}
data={[
"5",
"10",
"20",
"50",
"100",
{ label: t("bank.allOperations"), value: "all" },
]}
label={t("bank.pageSize")}
onChange={vm.setPageSize}
value={vm.pageSize}
w={180}
/>
</Group>
<Stack
className={classes.operations}
data-pending={vm.history.changingPage || undefined}
gap="lg"
key={`${vm.period.from}-${vm.period.to}-${vm.pageSize}-${vm.history.page ?? "loading"}`}
>
<CardHistory history={vm.history} period={vm.period} rows={vm.rows} />
</Stack>
{vm.history.page !== undefined && vm.pageSize !== "all" && (
<Group
aria-label={t("bank.pagination")}
component="nav"
justify="center"
py="sm"
ref={paginationRef}
>
<Pagination
getControlProps={getControlProps}
getItemProps={getItemProps}
onChange={onPageChange}
siblings={0}
total={vm.totalPages}
value={vm.page}
/>
</Group>
)}
</Stack>
);
};
export { CardPageView };