feat: polish bank operation cards and dates

This commit is contained in:
2026-09-12 19:56:12 +03:00
parent dedfec10d1
commit 6b310db0b8
20 changed files with 382 additions and 26 deletions
+2
View File
@@ -11,6 +11,7 @@ export {
useBankSummary,
} from "./api/queries";
export {
bankDateInputValue,
bankDateSchema,
bankMoneyValue,
bankMonth,
@@ -20,6 +21,7 @@ export {
} from "./model/form-validation";
export { BankCardForm } from "./ui/BankCardForm";
export { BankFeedback } from "./ui/BankFeedback";
export { BankOperationIcon } from "./ui/BankOperationIcon";
export { BankSummaryView } from "./ui/BankSummary.view";
export { CategoryManager } from "./ui/CategoryManager";
export { useBankFormFocus } from "./ui/useBankFormFocus";
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
bankCardSchema,
bankDateInputValue,
bankDateSchema,
bankMoneyValue,
bankMonth,
@@ -54,3 +55,15 @@ describe("bank form input", () => {
expect(bankDateSchema.safeParse("2026-02-30").success).toBe(false);
});
});
it("converts local date input without accepting impossible or unfinished dates", () => {
expect(bankDateInputValue("07.08.2026")).toBe("2026-08-07");
expect(
bankDateSchema.safeParse(bankDateInputValue("29.02.2024")).success,
).toBe(true);
for (const value of ["31.02.2026", "07.08.", ""]) {
expect(bankDateSchema.safeParse(bankDateInputValue(value)).success).toBe(
false,
);
}
});
@@ -9,6 +9,9 @@ function bankToday() {
day: "2-digit",
}).format(new Date());
}
function bankDateInputValue(value: string) {
return value.replace(/^(\d{2})\.(\d{2})\.(\d{4})$/, "$3-$2-$1");
}
function bankMonth(value: string | null) {
return value && /^\d{4}-(0[1-9]|1[0-2])$/.test(value)
? value
@@ -76,6 +79,7 @@ type BankCardValues = z.infer<typeof bankCardSchema>;
export {
type BankCardValues,
bankCardSchema,
bankDateInputValue,
bankDateSchema,
bankMoneyValue,
bankMonth,
@@ -0,0 +1,118 @@
import type { BankOperation } from "../api/queries";
type OperationIcon = { path: string; color: string };
const categoryIcons: Record<string, OperationIcon> = {
алкоголь: {
path: "M7 3h10v5a5 5 0 0 1-10 0V3Z M7 7h10 M12 13v8 M8 21h8",
color: "grape",
},
вклады: {
path: "M4 8c0-4 16-4 16 0s-16 4-16 0Z M4 8v5c0 4 16 4 16 0V8 M4 13v5c0 4 16 4 16 0v-5",
color: "green",
},
"инвестиции и накопления": {
path: "M3 3v18h18 M6 15l5-5 4 3 6-8 M16 5h5v5",
color: "green",
},
образование: {
path: "M2 8l10-5 10 5-10 5L2 8Z M6 10v7c4 3 8 3 12 0v-7 M22 8v9",
color: "blue",
},
переводы: { path: "M3 7h18l-4-4 M21 17H3l4 4", color: "blue" },
"погашение кредита": {
path: "M3 5h18v14H3V5Z M3 9h18 M8 14l3 3 5-5",
color: "orange",
},
подписки: {
path: "M4 10a8 8 0 0 1 13-6l3 3 M20 3v4h-4 M20 14a8 8 0 0 1-13 6l-3-3 M4 21v-4h4",
color: "grape",
},
путешествия: {
path: "M10 3h4v7l7 5v3l-7-3v5l2 1v1l-4-1-4 1v-1l2-1v-5l-7 3v-3l7-5V3Z",
color: "blue",
},
"связь и интернет": {
path: "M3 7a14 14 0 0 1 18 0 M6 11a9 9 0 0 1 12 0 M9 15a4 4 0 0 1 6 0 M12 19h.01",
color: "blue",
},
"снятие наличных": {
path: "M4 10V3h16v7 M7 7h10v14H7V7Z M7 11h10 M10 16h4",
color: "green",
},
ставки: {
path: "M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z M7 7h.01 M17 7h.01 M12 12h.01 M7 17h.01 M17 17h.01",
color: "grape",
},
страхование: {
path: "M12 2l8 4v6c0 5-8 10-8 10S4 17 4 12V6l8-4Z M8 12l3 3 5-6",
color: "green",
},
табак: {
path: "M3 14h18v5H3v-5Z M16 14v5 M8 10V8c0-2 3-2 3-4V2 M15 10V8c0-2 3-2 3-4",
color: "orange",
},
"хостинг и ии-сервисы": {
path: "M3 3h18v7H3V3Z M3 14h18v7H3v-7Z M7 6.5h.01 M7 17.5h.01 M15 6.5h3 M15 17.5h3",
color: "grape",
},
"цифровые покупки": {
path: "M3 3h18v13H3V3Z M8 21h8 M12 16v5 M12 6v7 M9 10l3 3 3-3",
color: "blue",
},
продукты: {
path: "M3 9h18l-2 11H5L3 9Z M8 9l4-6 4 6 M9 13v3 M15 13v3",
color: "green",
},
кафе: {
path: "M4 8h12v7a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V8Z M16 9h2a3 3 0 0 1 0 6h-2 M8 3v2 M12 3v2 M3 22h15",
color: "orange",
},
такси: {
path: "M4 10l2-6h12l2 6 M3 10h18v8H3v-8Z M6 18v3 M18 18v3 M6 14h2 M16 14h2 M10 4V2h4v2",
color: "yellow",
},
транспорт: {
path: "M6 3h12a2 2 0 0 1 2 2v13H4V5a2 2 0 0 1 2-2Z M4 11h16 M7 15h1 M16 15h1 M7 18v3 M17 18v3 M9 6h6",
color: "blue",
},
жилье: { path: "M3 11l9-8 9 8 M5 10v11h14V10 M9 21v-8h6v8", color: "orange" },
здоровье: { path: "M9 3h6v6h6v6h-6v6H9v-6H3V9h6V3Z", color: "red" },
покупки: {
path: "M5 7h14l2 14H3L5 7Z M8 8V6a4 4 0 0 1 8 0v2",
color: "grape",
},
развлечения: {
path: "M9 5v14a3 3 0 1 1-3-3h3 M9 5l11-2v14a3 3 0 1 1-3-3h3 M9 9l11-2",
color: "grape",
},
};
const otherIcon = {
path: "M4 4h6v6H4V4Z M14 4h6v6h-6V4Z M4 14h6v6H4v-6Z M14 14h6v6h-6v-6Z",
color: "gray",
};
const operationIcons = {
topup: {
path: "M20 8V5H5a2 2 0 0 1 0-4h13v4 M3 3v16a2 2 0 0 0 2 2h15V8H5 M16 12v5 M13.5 14.5h5",
color: "green",
},
transfer: { path: "M3 7h18l-4-4 M21 17H3l4 4", color: "blue" },
refund: { path: "M8 4L3 9l5 5 M3 9h11a7 7 0 0 1 0 14", color: "teal" },
};
function bankOperationAppearance(
kind: BankOperation["kind"],
categoryName?: string | null,
) {
const name = categoryName
?.trim()
.toLocaleLowerCase("ru-RU")
.replaceAll("ё", "е");
return kind === "expense"
? name && Object.hasOwn(categoryIcons, name)
? categoryIcons[name]
: otherIcon
: operationIcons[kind];
}
export { bankOperationAppearance };
@@ -67,7 +67,7 @@ describe("BankCardForm", () => {
it("retains an invalid opening date draft and explains conflict with saved history", async () => {
const close = setup(card);
fireEvent.change(screen.getByLabelText(/Дата начала/), {
target: { value: "2026-08-01" },
target: { value: "01.08.2026" },
});
server.use(
http.patch("*/api/v1/bank-cards/a", () =>
@@ -81,7 +81,7 @@ describe("BankCardForm", () => {
await screen.findByText(
"Дата начала противоречит сохранённой истории карты.",
);
expect(screen.getByLabelText(/Дата начала/)).toHaveValue("2026-08-01");
expect(screen.getByLabelText(/Дата начала/)).toHaveValue("01.08.2026");
expect(close).not.toHaveBeenCalled();
});
it("keeps card edit version until explicit refresh adoption", async () => {
+2 -1
View File
@@ -3,6 +3,7 @@ import { Button, Group, Modal, Stack, Text } from "@mantine/core";
import { type FC, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { formatDate } from "@/shared/lib";
import { useBankMutations } from "../api/mutations";
import { type BankCard, useBankCard } from "../api/queries";
import {
@@ -110,7 +111,7 @@ const BankCardForm: FC<BankCardFormProps> = (props) => {
<>
<Text>
{t("bank.versionHint", {
details: `${latest.name} · ${latest.openingBalance.value} RUB · ${latest.openingDate}`,
details: `${latest.name} · ${latest.openingBalance.value} RUB · ${formatDate(latest.openingDate)}`,
})}
</Text>
<Button
@@ -5,6 +5,7 @@ import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
type BankCardValues,
bankDateInputValue,
bankDateSchema,
bankToday,
} from "../model/form-validation";
@@ -52,18 +53,22 @@ const BankCardFields: FC<BankCardFieldsProps> = (props) => {
{...field}
label={t("bank.openingDate")}
required
valueFormat="YYYY-MM-DD"
valueFormat="DD.MM.YYYY"
value={
bankDateSchema.safeParse(field.value).success ? field.value : null
}
dateParser={(value) =>
bankDateSchema.safeParse(value).success ? value : null
bankDateSchema.safeParse(bankDateInputValue(value)).success
? bankDateInputValue(value)
: null
}
fixOnBlur={false}
onInput={(event) => field.onChange(event.currentTarget.value)}
onInput={(event) =>
field.onChange(bankDateInputValue(event.currentTarget.value))
}
onChange={(value) => field.onChange(value ?? "")}
onBlur={(event) => {
field.onChange(event.currentTarget.value);
field.onChange(bankDateInputValue(event.currentTarget.value));
field.onBlur();
}}
maxDate={bankToday()}
@@ -0,0 +1,68 @@
import { Stack } from "@mantine/core";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { EntryCard } from "@/shared/ui";
import { BankOperationIcon } from "./BankOperationIcon";
const meta = {
title: "Product/BankCards/OperationIcons",
component: BankOperationIcon,
tags: ["autodocs"],
args: { kind: "expense", categoryName: "Продукты" },
} satisfies Meta<typeof BankOperationIcon>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Categories: Story = {
render: () => (
<Stack maw={640}>
{[
"Алкоголь",
"Вклады",
"Другое",
"Жильё",
"Здоровье",
"Инвестиции и накопления",
"Кафе",
"Образование",
"Переводы",
"Погашение кредита",
"Подписки",
"Покупки",
"Продукты",
"Путешествия",
"Развлечения",
"Связь и интернет",
"Снятие наличных",
"Ставки",
"Страхование",
"Табак",
"Такси",
"Транспорт",
"Хостинг и ИИ-сервисы",
"Цифровые покупки",
"Моя категория",
].map((name) => (
<EntryCard
key={name}
title="Расход"
metadata={`12.09.2026 · ${name}`}
initial="Р"
color="red"
icon={<BankOperationIcon kind="expense" categoryName={name} />}
value="−1 250,00 ₽"
valueLabel="Расход"
tone="negative"
/>
))}
<EntryCard
title="Пополнение"
metadata="12.09.2026"
initial="П"
color="green"
icon={<BankOperationIcon kind="topup" />}
value="+15 000,00 ₽"
valueLabel="Пополнение"
tone="positive"
/>
</Stack>
),
};
@@ -0,0 +1,66 @@
import { render } from "@testing-library/react";
import { expect, it } from "vitest";
import { BankOperationIcon } from "./BankOperationIcon";
it("matches normalized category names, falls back after renaming and distinguishes operation kinds", () => {
const { container, rerender } = render(
<BankOperationIcon kind="expense" categoryName="Жильё" />,
);
const path = () => container.querySelector("path")?.getAttribute("d");
const housing = path();
rerender(<BankOperationIcon kind="expense" categoryName=" ЖИЛЬЕ " />);
expect(path()).toBe(housing);
rerender(<BankOperationIcon kind="expense" categoryName="Другое" />);
const fallback = path();
expect(fallback).not.toBe(housing);
for (const categoryName of ["Моя категория", "constructor", null]) {
rerender(<BankOperationIcon kind="expense" categoryName={categoryName} />);
expect(path()).toBe(fallback);
}
const paths = [fallback, housing];
for (const kind of ["topup", "transfer", "refund"] as const) {
rerender(<BankOperationIcon kind={kind} categoryName="Жильё" />);
expect(path()).toBeTruthy();
paths.push(path());
}
expect(new Set(paths).size).toBe(5);
expect(container.querySelector("svg")).toHaveAttribute("aria-hidden", "true");
});
it("provides a distinct icon for every existing named category", () => {
const { container, rerender } = render(
<BankOperationIcon kind="expense" categoryName="Другое" />,
);
const paths = new Set([container.querySelector("path")?.getAttribute("d")]);
for (const categoryName of [
"Алкоголь",
"Вклады",
"Жильё",
"Здоровье",
"Инвестиции и накопления",
"Кафе",
"Образование",
"Переводы",
"Погашение кредита",
"Подписки",
"Покупки",
"Продукты",
"Путешествия",
"Развлечения",
"Связь и интернет",
"Снятие наличных",
"Ставки",
"Страхование",
"Табак",
"Такси",
"Транспорт",
"Хостинг и ИИ-сервисы",
"Цифровые покупки",
]) {
rerender(<BankOperationIcon kind="expense" categoryName={categoryName} />);
const path = container.querySelector("path")?.getAttribute("d");
expect(path).toBeTruthy();
expect(paths.has(path), categoryName).toBe(false);
paths.add(path);
}
});
@@ -0,0 +1,31 @@
import type { FC } from "react";
import type { BankOperation } from "../api/queries";
import { bankOperationAppearance } from "../model/operation-appearance";
type BankOperationIconProps = {
kind: BankOperation["kind"];
categoryName?: string | null;
};
const BankOperationIcon: FC<BankOperationIconProps> = (props) => {
const icon = bankOperationAppearance(props.kind, props.categoryName);
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
style={{ flexShrink: 0, color: `var(--mantine-color-${icon.color}-7)` }}
>
<path d={icon.path} />
</svg>
);
};
export { BankOperationIcon };
@@ -13,6 +13,7 @@ import { useTranslation } from "react-i18next";
import { formatMoney } from "@/shared/lib";
import { LoadingSkeleton } from "@/shared/ui";
import type { BankSummary } from "../api/queries";
import { BankOperationIcon } from "./BankOperationIcon";
type BankSummaryViewProps = {
data?: BankSummary;
@@ -48,7 +49,13 @@ const BankSummaryView: FC<BankSummaryViewProps> = (props) => {
{props.data.categories.length ? (
props.data.categories.map((category) => (
<Paper p="sm" key={category.categoryId} withBorder>
<Text fw={600}>{category.categoryName}</Text>
<Group gap="xs" wrap="nowrap">
<BankOperationIcon
kind="expense"
categoryName={category.categoryName}
/>
<Text fw={600}>{category.categoryName}</Text>
</Group>
<Group>
{(["expenses", "refunds", "net"] as const).map((key) => (
<Text key={key} style={{ overflowWrap: "anywhere" }}>
@@ -22,6 +22,7 @@ import { useBankMutations } from "../api/mutations";
import { type ExpenseCategory, useBankCategories } from "../api/queries";
import { BankFeedback } from "./BankFeedback";
import classes from "./BankForms.module.css";
import { BankOperationIcon } from "./BankOperationIcon";
import { useBankFormFocus } from "./useBankFormFocus";
type CategoryManagerProps = {
@@ -120,6 +121,10 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
{categories.data?.map((category) => (
<Group justify="space-between" key={category.id}>
<Group gap="xs">
<BankOperationIcon
kind="expense"
categoryName={category.name}
/>
<Text>{category.name}</Text>
{category.isDefault && (
<Badge>{t("bank.defaultCategory")}</Badge>
+1
View File
@@ -111,6 +111,7 @@ describe("CardPage", () => {
expect(
await screen.findByText("Перевод на «Запасная»"),
).toBeInTheDocument();
expect(screen.getByText("03.08.2026")).toBeInTheDocument();
expect(screen.getByText("−50,01 ₽")).toBeInTheDocument();
expect(screen.getByText("-50 ₽")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Далее" }));
+12 -1
View File
@@ -14,7 +14,11 @@ import { MonthPickerInput } from "@mantine/dates";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { BankFeedback, BankSummaryView } from "@/entities/bank-card";
import {
BankFeedback,
BankOperationIcon,
BankSummaryView,
} from "@/entities/bank-card";
import { formatMoney } from "@/shared/lib";
import { EntryCard, LoadingSkeleton, StateView } from "@/shared/ui";
import classes from "./CardPage.module.css";
@@ -148,7 +152,14 @@ const CardPageView: FC<CardPageViewProps> = (props) => {
<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}
+3 -2
View File
@@ -11,7 +11,7 @@ import {
useBankSummary,
} from "@/entities/bank-card";
import { apiErrorCode } from "@/shared/api";
import { formatMoney } from "@/shared/lib";
import { formatDate, formatMoney } from "@/shared/lib";
function useCardPageVm() {
const { cardId } = useParams();
@@ -37,13 +37,14 @@ function useCardPageVm() {
(item.kind === "transfer" && item.cardId === cardId);
return {
item,
color: outgoing ? "red" : "green",
title:
item.kind === "transfer"
? t(outgoing ? "bank.transferTo" : "bank.transferFrom", {
name: outgoing ? item.targetCardName : item.cardName,
})
: t(`bank.kinds.${item.kind}`),
metadata: [item.date, item.categoryName, item.description]
metadata: [formatDate(item.date), item.categoryName, item.description]
.filter(Boolean)
.join(" · "),
value: `${outgoing ? "−" : "+"}${formatMoney(item.amount)}`,
+2 -2
View File
@@ -8,7 +8,7 @@ import {
useBankMutations,
useBankOperation,
} from "@/entities/bank-card";
import { formatMoney } from "@/shared/lib";
import { formatDate, formatMoney } from "@/shared/lib";
type DeleteOperationProps = { operation: BankOperation; onClose: () => void };
const DeleteOperation: FC<DeleteOperationProps> = (props) => {
@@ -64,7 +64,7 @@ const DeleteOperation: FC<DeleteOperationProps> = (props) => {
<>
<Text>
{t("bank.versionHint", {
details: `${latest.date} · ${formatMoney(latest.amount)} · ${latest.description ?? ""}`,
details: `${formatDate(latest.date)} · ${formatMoney(latest.amount)} · ${latest.description ?? ""}`,
})}
</Text>
<Button
+9 -3
View File
@@ -76,6 +76,11 @@ describe("OperationForm", () => {
"Остальное",
),
);
fireEvent.change(screen.getByLabelText(/Дата операции/), {
target: { value: "07.08.2026" },
});
fireEvent.blur(screen.getByLabelText(/Дата операции/));
expect(screen.getByLabelText(/Дата операции/)).toHaveValue("07.08.2026");
let body: unknown;
server.use(
http.post("*/api/v1/bank-cards/operations", async ({ request }) => {
@@ -90,6 +95,7 @@ describe("OperationForm", () => {
await waitFor(() => expect(close).toHaveBeenCalled());
expect(body).toMatchObject({
categoryId: "default",
date: "2026-08-07",
amount: money("92233720368547758.07"),
});
});
@@ -101,7 +107,7 @@ describe("OperationForm", () => {
target: { value: "0" },
});
fireEvent.change(screen.getByLabelText(/Дата операции/), {
target: { value: "2026-07-31" },
target: { value: "31.07.2026" },
});
const bodies: unknown[] = [];
server.use(
@@ -122,7 +128,7 @@ describe("OperationForm", () => {
target: { value: "150,01" },
});
fireEvent.change(screen.getByLabelText(/Дата операции/), {
target: { value: "2026-08-02" },
target: { value: "02.08.2026" },
});
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
await screen.findByText(/Не удалось подтвердить сохранение/);
@@ -229,7 +235,7 @@ describe("OperationForm", () => {
target: { value: "500" },
});
fireEvent.change(screen.getByLabelText(/Дата операции/), {
target: { value: "2026-08-01" },
target: { value: "01.08.2026" },
});
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
await screen.findByText(/Не удалось подтвердить сохранение/);
+4 -4
View File
@@ -21,7 +21,7 @@ import {
useBankOperation,
} from "@/entities/bank-card";
import { validationMessage } from "@/shared/i18n";
import { formatMoney } from "@/shared/lib";
import { formatDate, formatMoney } from "@/shared/lib";
import classes from "./OperationForm.module.css";
import type { OperationValues } from "./OperationForm.types";
import { OperationFields, RefundFields } from "./OperationForm.view";
@@ -249,7 +249,7 @@ const OperationForm: FC<OperationFormProps> = (props) => {
expenses.data?.items.map((item) => ({
value: item.id,
label: t("bank.expenseChoice", {
date: item.date,
date: formatDate(item.date),
name: item.description || item.categoryName,
amount: formatMoney(item.amount),
available: formatMoney(item.availableRefund),
@@ -261,7 +261,7 @@ const OperationForm: FC<OperationFormProps> = (props) => {
? {
value: sourceQuery.data.id,
label: t("bank.expenseChoice", {
date: sourceQuery.data.date,
date: formatDate(sourceQuery.data.date),
name:
sourceQuery.data.description ||
sourceQuery.data.categoryName,
@@ -366,7 +366,7 @@ const OperationForm: FC<OperationFormProps> = (props) => {
<>
<Text>
{t("bank.versionHint", {
details: `${latest.date} · ${formatMoney(latest.amount)} · ${latest.description ?? ""}`,
details: `${formatDate(latest.date)} · ${formatMoney(latest.amount)} · ${latest.description ?? ""}`,
})}
</Text>
<Button
+9 -4
View File
@@ -5,6 +5,7 @@ import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
type BankOperation,
bankDateInputValue,
bankDateSchema,
bankToday,
} from "@/entities/bank-card";
@@ -97,18 +98,22 @@ const OperationFields: FC<OperationFieldsProps> = (props) => {
{...field}
label={t("bank.date")}
required
valueFormat="YYYY-MM-DD"
valueFormat="DD.MM.YYYY"
value={
bankDateSchema.safeParse(field.value).success ? field.value : null
}
dateParser={(value) =>
bankDateSchema.safeParse(value).success ? value : null
bankDateSchema.safeParse(bankDateInputValue(value)).success
? bankDateInputValue(value)
: null
}
fixOnBlur={false}
onInput={(event) => field.onChange(event.currentTarget.value)}
onInput={(event) =>
field.onChange(bankDateInputValue(event.currentTarget.value))
}
onChange={(value) => field.onChange(value ?? "")}
onBlur={(event) => {
field.onChange(event.currentTarget.value);
field.onChange(bankDateInputValue(event.currentTarget.value));
field.onBlur();
}}
maxDate={bankToday()}
+14 -2
View File
@@ -8,6 +8,8 @@ type EntryCardProps = {
details?: ReactNode;
actions?: ReactNode;
initial: string;
icon?: ReactNode;
color?: string;
imageSrc?: string | null;
value: string;
valueLabel: string;
@@ -17,7 +19,17 @@ type EntryCardProps = {
const EntryCard: FC<EntryCardProps> = (props) => {
const [imageFailed, setImageFailed] = useState(false);
return (
<Paper className={classes.row} withBorder>
<Paper
className={classes.row}
withBorder
style={
props.color
? {
background: `color-mix(in srgb, var(--mantine-color-${props.color}-0) 45%, white)`,
}
: undefined
}
>
<Avatar
alt=""
aria-hidden="true"
@@ -26,7 +38,7 @@ const EntryCard: FC<EntryCardProps> = (props) => {
src={imageFailed ? null : props.imageSrc}
onError={() => setImageFailed(true)}
>
{props.initial}
{props.icon ?? props.initial}
</Avatar>
<Stack className={classes.identity} gap={2}>
<Text fw={700} lineClamp={1}>