chore: codify React component style
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Button, Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router";
|
||||
import {
|
||||
@@ -8,7 +9,19 @@ import {
|
||||
import { formatMoney } from "@/entities/portfolio";
|
||||
import { StateView } from "@/shared/ui";
|
||||
|
||||
export function AccountEventsPage() {
|
||||
type AccountEventsPageErrorProps = {
|
||||
retry: () => void;
|
||||
};
|
||||
|
||||
type AccountEventsPageLoadedProps = {
|
||||
events: BrokerageEvent[];
|
||||
};
|
||||
|
||||
type AccountEventRowProps = {
|
||||
event: BrokerageEvent;
|
||||
};
|
||||
|
||||
const AccountEventsPage: FC = () => {
|
||||
const { accountId } = useParams();
|
||||
const events = useBrokerageEvents(accountId);
|
||||
if (events.isLoading) return <AccountEventsPageLoading />;
|
||||
@@ -17,10 +30,11 @@ export function AccountEventsPage() {
|
||||
if (!events.data) return <AccountEventsPageLoading />;
|
||||
if (!events.data.events.length) return <AccountEventsPageEmpty />;
|
||||
return <AccountEventsPageLoaded events={events.data.events} />;
|
||||
}
|
||||
};
|
||||
|
||||
function AccountEventsPageLoading() {
|
||||
const AccountEventsPageLoading: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<StateView
|
||||
loading
|
||||
@@ -28,10 +42,12 @@ function AccountEventsPageLoading() {
|
||||
text={t("states.loadingText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function AccountEventsPageError({ retry }: { retry: () => void }) {
|
||||
const AccountEventsPageError: FC<AccountEventsPageErrorProps> = (props) => {
|
||||
const { retry } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<StateView
|
||||
error
|
||||
@@ -40,10 +56,11 @@ function AccountEventsPageError({ retry }: { retry: () => void }) {
|
||||
action={<Button onClick={retry}>{t("events.retry")}</Button>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function AccountEventsPageEmpty() {
|
||||
const AccountEventsPageEmpty: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack maw={960} gap="lg">
|
||||
<Title order={1}>{t("events.title")}</Title>
|
||||
@@ -51,10 +68,12 @@ function AccountEventsPageEmpty() {
|
||||
<StateView title={t("events.emptyTitle")} text={t("events.emptyText")} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function AccountEventsPageLoaded({ events }: { events: BrokerageEvent[] }) {
|
||||
const AccountEventsPageLoaded: FC<AccountEventsPageLoadedProps> = (props) => {
|
||||
const { events } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack maw={960} gap="lg">
|
||||
<Title order={1}>{t("events.title")}</Title>
|
||||
@@ -67,10 +86,12 @@ function AccountEventsPageLoaded({ events }: { events: BrokerageEvent[] }) {
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function AccountEventRow({ event }: { event: BrokerageEvent }) {
|
||||
const AccountEventRow: FC<AccountEventRowProps> = (props) => {
|
||||
const { event } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap">
|
||||
@@ -94,7 +115,7 @@ function AccountEventRow({ event }: { event: BrokerageEvent }) {
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function formatCalendarDate(date: string) {
|
||||
return new Intl.DateTimeFormat("ru-RU", {
|
||||
@@ -102,3 +123,5 @@ function formatCalendarDate(date: string) {
|
||||
timeZone: "UTC",
|
||||
}).format(new Date(`${date}T00:00:00Z`));
|
||||
}
|
||||
|
||||
export { AccountEventsPage };
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router";
|
||||
@@ -28,7 +29,11 @@ function dateEnd(value: string) {
|
||||
return value ? `${value}T23:59:59.999Z` : undefined;
|
||||
}
|
||||
|
||||
export function AccountOperationsPage() {
|
||||
type OperationRowProps = {
|
||||
operation: BrokerageOperation;
|
||||
};
|
||||
|
||||
const AccountOperationsPage: FC = () => {
|
||||
const { accountId } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const [from, setFrom] = useState("");
|
||||
@@ -147,10 +152,12 @@ export function AccountOperationsPage() {
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function OperationRow({ operation }: { operation: BrokerageOperation }) {
|
||||
const OperationRow: FC<OperationRowProps> = (props) => {
|
||||
const { operation } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Group justify="space-between" align="flex-start">
|
||||
@@ -176,4 +183,6 @@ export function OperationRow({ operation }: { operation: BrokerageOperation }) {
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { AccountOperationsPage, OperationRow };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FC } from "react";
|
||||
import {
|
||||
AccountPageEmpty,
|
||||
AccountPageError,
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
} from "./AccountPage.view";
|
||||
import { useAccountPageViewModel } from "./AccountPage.vm";
|
||||
|
||||
export function AccountPage() {
|
||||
const AccountPage: FC = () => {
|
||||
const viewModel = useAccountPageViewModel();
|
||||
if (viewModel.state === "loading") return <AccountPageLoading />;
|
||||
if (viewModel.state === "error") return <AccountPageError {...viewModel} />;
|
||||
@@ -33,11 +34,12 @@ export function AccountPage() {
|
||||
recentEvents={viewModel.recentEvents}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
AccountPage,
|
||||
AccountPageEmpty,
|
||||
AccountPageError,
|
||||
AccountPageLoaded,
|
||||
AccountPageLoading,
|
||||
} from "./AccountPage.view";
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Alert, Button, Stack, Text } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import type { BrokerageAccount } from "@/entities/brokerage-account";
|
||||
@@ -22,8 +23,34 @@ import {
|
||||
Section,
|
||||
} from "./components";
|
||||
|
||||
export function AccountPageLoading() {
|
||||
type AccountPageErrorProps = {
|
||||
code?: string;
|
||||
retry: () => void;
|
||||
};
|
||||
|
||||
type AccountPageEmptyProps = {
|
||||
account?: BrokerageAccount;
|
||||
refreshing?: boolean;
|
||||
refreshError?: boolean;
|
||||
retry?: () => void;
|
||||
recentOperations: AccountPageRecentOperations;
|
||||
recentEvents: AccountPageRecentEvents;
|
||||
};
|
||||
|
||||
type AccountPageLoadedProps = {
|
||||
account?: BrokerageAccount;
|
||||
portfolio: Portfolio;
|
||||
positionSections: AccountPagePositionSection[];
|
||||
refreshing?: boolean;
|
||||
refreshError?: boolean;
|
||||
retry?: () => void;
|
||||
recentOperations: AccountPageRecentOperations;
|
||||
recentEvents: AccountPageRecentEvents;
|
||||
};
|
||||
|
||||
const AccountPageLoading: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<StateView
|
||||
loading
|
||||
@@ -31,15 +58,10 @@ export function AccountPageLoading() {
|
||||
text={t("states.loadingText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function AccountPageError({
|
||||
code,
|
||||
retry,
|
||||
}: {
|
||||
code?: string;
|
||||
retry: () => void;
|
||||
}) {
|
||||
const AccountPageError: FC<AccountPageErrorProps> = (props) => {
|
||||
const { code, retry } = props;
|
||||
const { t } = useTranslation();
|
||||
if (code === "NOT_FOUND")
|
||||
return (
|
||||
@@ -84,24 +106,19 @@ export function AccountPageError({
|
||||
action={<Button onClick={retry}>{t("accounts.retry")}</Button>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function AccountPageEmpty({
|
||||
account,
|
||||
refreshing = false,
|
||||
refreshError = false,
|
||||
retry,
|
||||
recentOperations,
|
||||
recentEvents,
|
||||
}: {
|
||||
account?: BrokerageAccount;
|
||||
refreshing?: boolean;
|
||||
refreshError?: boolean;
|
||||
retry?: () => void;
|
||||
recentOperations: AccountPageRecentOperations;
|
||||
recentEvents: AccountPageRecentEvents;
|
||||
}) {
|
||||
const AccountPageEmpty: FC<AccountPageEmptyProps> = (props) => {
|
||||
const {
|
||||
account,
|
||||
refreshing = false,
|
||||
refreshError = false,
|
||||
retry,
|
||||
recentOperations,
|
||||
recentEvents,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<AccountHeading account={account} />
|
||||
@@ -118,28 +135,21 @@ export function AccountPageEmpty({
|
||||
<RecentEvents events={recentEvents} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function AccountPageLoaded({
|
||||
account,
|
||||
portfolio,
|
||||
positionSections,
|
||||
refreshing = false,
|
||||
refreshError = false,
|
||||
retry,
|
||||
recentOperations,
|
||||
recentEvents,
|
||||
}: {
|
||||
account?: BrokerageAccount;
|
||||
portfolio: Portfolio;
|
||||
positionSections: AccountPagePositionSection[];
|
||||
refreshing?: boolean;
|
||||
refreshError?: boolean;
|
||||
retry?: () => void;
|
||||
recentOperations: AccountPageRecentOperations;
|
||||
recentEvents: AccountPageRecentEvents;
|
||||
}) {
|
||||
const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
|
||||
const {
|
||||
account,
|
||||
portfolio,
|
||||
positionSections,
|
||||
refreshing = false,
|
||||
refreshError = false,
|
||||
retry,
|
||||
recentOperations,
|
||||
recentEvents,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack className={classes.page} gap="lg">
|
||||
<AccountHeading account={account} />
|
||||
@@ -171,10 +181,17 @@ export function AccountPageLoaded({
|
||||
<RecentEvents events={recentEvents} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function positionSectionTitleKey(kind: AccountPagePositionSection["kind"]) {
|
||||
if (kind === "stocks") return "portfolio.stocks";
|
||||
if (kind === "bonds") return "portfolio.bonds";
|
||||
return "portfolio.otherPositions";
|
||||
}
|
||||
|
||||
export {
|
||||
AccountPageEmpty,
|
||||
AccountPageError,
|
||||
AccountPageLoaded,
|
||||
AccountPageLoading,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MantineProvider } from "@mantine/core";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { HttpResponse, http } from "msw";
|
||||
import type { ReactNode } from "react";
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
import { MemoryRouter, Route, Routes } from "react-router";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Portfolio } from "@/entities/portfolio";
|
||||
@@ -60,7 +60,9 @@ function configureTestAuth() {
|
||||
});
|
||||
}
|
||||
|
||||
function wrapper({ children }: { children: ReactNode }) {
|
||||
const wrapper: FC<PropsWithChildren> = (props) => {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<MantineProvider>
|
||||
@@ -72,7 +74,7 @@ function wrapper({ children }: { children: ReactNode }) {
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function mockLoadedAccountPage() {
|
||||
server.use(
|
||||
|
||||
@@ -22,20 +22,20 @@ import {
|
||||
type PositionSectionKind = "stocks" | "bonds" | "other";
|
||||
type AccountPageReturnTone = "positive" | "negative";
|
||||
|
||||
export type AccountPagePosition = Portfolio["positions"][number] & {
|
||||
type AccountPagePosition = Portfolio["positions"][number] & {
|
||||
shareText: string | null;
|
||||
returnText: string | null;
|
||||
returnTone?: AccountPageReturnTone;
|
||||
logoSrc: string | null;
|
||||
};
|
||||
|
||||
export type AccountPagePositionSection = {
|
||||
type AccountPagePositionSection = {
|
||||
kind: PositionSectionKind;
|
||||
wide: boolean;
|
||||
positions: AccountPagePosition[];
|
||||
};
|
||||
|
||||
export type AccountPageRecentOperations =
|
||||
type AccountPageRecentOperations =
|
||||
| {
|
||||
state: "loading" | "error" | "empty";
|
||||
to: string;
|
||||
@@ -46,7 +46,7 @@ export type AccountPageRecentOperations =
|
||||
items: BrokerageOperation[];
|
||||
};
|
||||
|
||||
export type AccountPageRecentEvents =
|
||||
type AccountPageRecentEvents =
|
||||
| {
|
||||
state: "loading" | "error" | "empty";
|
||||
to: string;
|
||||
@@ -77,13 +77,13 @@ type AccountPageEmptyViewModel = AccountPageContent & {
|
||||
state: "empty";
|
||||
};
|
||||
|
||||
export type AccountPageViewModel =
|
||||
type AccountPageViewModel =
|
||||
| { state: "loading" }
|
||||
| { state: "error"; code?: string; retry: () => void }
|
||||
| AccountPageEmptyViewModel
|
||||
| AccountPageLoadedViewModel;
|
||||
|
||||
export function useAccountPageViewModel(): AccountPageViewModel {
|
||||
function useAccountPageViewModel(): AccountPageViewModel {
|
||||
const { accountId } = useParams();
|
||||
const portfolio = usePortfolio(accountId);
|
||||
const operations = useBrokerageOperations(accountId, {
|
||||
@@ -153,7 +153,7 @@ function toRecentEvents(
|
||||
return { state: "loaded", to, items };
|
||||
}
|
||||
|
||||
export function toAccountPagePositionSections(
|
||||
function toAccountPagePositionSections(
|
||||
portfolio: Portfolio,
|
||||
): AccountPagePositionSection[] {
|
||||
const denominators = new Map<string, number>();
|
||||
@@ -221,3 +221,12 @@ function toPosition(
|
||||
logoSrc: wide ? instrumentLogoUrl(position.logoName) : null,
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
type AccountPagePosition,
|
||||
type AccountPagePositionSection,
|
||||
type AccountPageRecentEvents,
|
||||
type AccountPageRecentOperations,
|
||||
toAccountPagePositionSections,
|
||||
useAccountPageViewModel,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { Stack, Text, Title } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { BrokerageAccount } from "@/entities/brokerage-account";
|
||||
import { accountTypeKey } from "@/entities/brokerage-account";
|
||||
|
||||
export function AccountHeading({ account }: { account?: BrokerageAccount }) {
|
||||
type AccountHeadingProps = {
|
||||
account?: BrokerageAccount;
|
||||
};
|
||||
|
||||
const AccountHeading: FC<AccountHeadingProps> = (props) => {
|
||||
const { account } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap={2}>
|
||||
<Title order={1}>{account?.name ?? t("portfolio.title")}</Title>
|
||||
@@ -15,4 +22,6 @@ export function AccountHeading({ account }: { account?: BrokerageAccount }) {
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { AccountHeading };
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Paper, Text } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import type { Portfolio } from "@/entities/portfolio";
|
||||
import { formatMoney } from "@/entities/portfolio";
|
||||
import classes from "../AccountPage.module.css";
|
||||
|
||||
export function CashBalances({
|
||||
balances,
|
||||
}: {
|
||||
type CashBalancesProps = {
|
||||
balances: Portfolio["cashBalances"];
|
||||
}) {
|
||||
};
|
||||
|
||||
const CashBalances: FC<CashBalancesProps> = (props) => {
|
||||
const { balances } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{balances.map((balance) => (
|
||||
@@ -18,4 +21,6 @@ export function CashBalances({
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { CashBalances };
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { Alert, Button, Group, Loader, Stack, Text } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function PortfolioRefreshState({
|
||||
refreshing,
|
||||
refreshError,
|
||||
retry,
|
||||
}: {
|
||||
type PortfolioRefreshStateProps = {
|
||||
refreshing: boolean;
|
||||
refreshError: boolean;
|
||||
retry?: () => void;
|
||||
}) {
|
||||
};
|
||||
|
||||
const PortfolioRefreshState: FC<PortfolioRefreshStateProps> = (props) => {
|
||||
const { refreshing, refreshError, retry } = props;
|
||||
const { t } = useTranslation();
|
||||
if (!refreshing && !refreshError) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{refreshing ? (
|
||||
@@ -34,4 +35,6 @@ export function PortfolioRefreshState({
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { PortfolioRefreshState };
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Paper, SimpleGrid, Text } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import type { Portfolio } from "@/entities/portfolio";
|
||||
import { formatMoney } from "@/entities/portfolio";
|
||||
import classes from "../AccountPage.module.css";
|
||||
|
||||
export function PortfolioTotals({
|
||||
totals,
|
||||
}: {
|
||||
type PortfolioTotalsProps = {
|
||||
totals: Portfolio["totalsByCurrency"];
|
||||
}) {
|
||||
};
|
||||
|
||||
const PortfolioTotals: FC<PortfolioTotalsProps> = (props) => {
|
||||
const { totals } = props;
|
||||
|
||||
return (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }}>
|
||||
{totals.map((total) => (
|
||||
@@ -22,4 +25,6 @@ export function PortfolioTotals({
|
||||
))}
|
||||
</SimpleGrid>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { PortfolioTotals };
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { Card, Group, Stack, Text } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatMoney } from "@/entities/portfolio";
|
||||
import classes from "../AccountPage.module.css";
|
||||
import type { AccountPagePosition } from "../AccountPage.vm";
|
||||
import { PositionLogo } from "./PositionLogo.view";
|
||||
|
||||
export function PositionCard({
|
||||
position,
|
||||
wide = false,
|
||||
}: {
|
||||
type PositionCardProps = {
|
||||
position: AccountPagePosition;
|
||||
wide?: boolean;
|
||||
}) {
|
||||
};
|
||||
|
||||
const PositionCard: FC<PositionCardProps> = (props) => {
|
||||
const { position, wide = false } = props;
|
||||
const { t } = useTranslation();
|
||||
const returnClass = position.returnTone
|
||||
? classes[position.returnTone]
|
||||
@@ -78,4 +79,6 @@ export function PositionCard({
|
||||
) : null}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { PositionCard };
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { Avatar } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
export function PositionLogo({
|
||||
src,
|
||||
label,
|
||||
}: {
|
||||
type PositionLogoProps = {
|
||||
src: string | null;
|
||||
label: string;
|
||||
}) {
|
||||
};
|
||||
|
||||
const PositionLogo: FC<PositionLogoProps> = (props) => {
|
||||
const { src, label } = props;
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
if (src && !failed)
|
||||
return (
|
||||
<Avatar
|
||||
@@ -24,4 +26,6 @@ export function PositionLogo({
|
||||
{(label.trim()[0] ?? "?").toUpperCase()}
|
||||
</Avatar>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { PositionLogo };
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { SimpleGrid } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import type { AccountPagePositionSection } from "../AccountPage.vm";
|
||||
import { PositionCard } from "./PositionCard.view";
|
||||
import { Section } from "./Section.view";
|
||||
|
||||
export function PositionSection({
|
||||
title,
|
||||
section,
|
||||
}: {
|
||||
type PositionSectionProps = {
|
||||
title: string;
|
||||
section: AccountPagePositionSection;
|
||||
}) {
|
||||
};
|
||||
|
||||
const PositionSection: FC<PositionSectionProps> = (props) => {
|
||||
const { title, section } = props;
|
||||
|
||||
return (
|
||||
<Section title={title}>
|
||||
<SimpleGrid cols={section.wide ? 1 : { base: 1, sm: 2, md: 3 }}>
|
||||
@@ -23,4 +25,6 @@ export function PositionSection({
|
||||
</SimpleGrid>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { PositionSection };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Alert, Button, Paper, Text } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import type { BrokerageEvent } from "@/entities/brokerage-event";
|
||||
@@ -7,8 +8,18 @@ import classes from "../AccountPage.module.css";
|
||||
import type { AccountPageRecentEvents } from "../AccountPage.vm";
|
||||
import { Section } from "./Section.view";
|
||||
|
||||
export function RecentEvents({ events }: { events: AccountPageRecentEvents }) {
|
||||
type RecentEventsProps = {
|
||||
events: AccountPageRecentEvents;
|
||||
};
|
||||
|
||||
type RecentEventProps = {
|
||||
event: BrokerageEvent;
|
||||
};
|
||||
|
||||
const RecentEvents: FC<RecentEventsProps> = (props) => {
|
||||
const { events } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Section title={t("events.recentTitle")}>
|
||||
{events.state === "loading" ? (
|
||||
@@ -33,10 +44,12 @@ export function RecentEvents({ events }: { events: AccountPageRecentEvents }) {
|
||||
</Button>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function RecentEvent({ event }: { event: BrokerageEvent }) {
|
||||
const RecentEvent: FC<RecentEventProps> = (props) => {
|
||||
const { event } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Paper className={classes.cash} withBorder>
|
||||
<Text>{`${t(`events.type.${event.type}`)} · ${event.displayName}`}</Text>
|
||||
@@ -47,4 +60,6 @@ function RecentEvent({ event }: { event: BrokerageEvent }) {
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { RecentEvents };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Alert, Button, Paper, Text } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import { formatMoney } from "@/entities/portfolio";
|
||||
@@ -6,12 +7,14 @@ import classes from "../AccountPage.module.css";
|
||||
import type { AccountPageRecentOperations } from "../AccountPage.vm";
|
||||
import { Section } from "./Section.view";
|
||||
|
||||
export function RecentOperations({
|
||||
operations,
|
||||
}: {
|
||||
type RecentOperationsProps = {
|
||||
operations: AccountPageRecentOperations;
|
||||
}) {
|
||||
};
|
||||
|
||||
const RecentOperations: FC<RecentOperationsProps> = (props) => {
|
||||
const { operations } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Section title={t("ledger.recentTitle")}>
|
||||
{operations.state === "loading" ? (
|
||||
@@ -40,4 +43,6 @@ export function RecentOperations({
|
||||
</Button>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { RecentOperations };
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
import type { FC, PropsWithChildren } from "react";
|
||||
|
||||
export function Section({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
type SectionProps = PropsWithChildren<{
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
}>;
|
||||
|
||||
const Section: FC<SectionProps> = (props) => {
|
||||
const { title, children } = props;
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Title order={2}>{title}</Title>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { Section };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FC } from "react";
|
||||
import {
|
||||
AccountsPageEmpty,
|
||||
AccountsPageError,
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
} from "./AccountsPage.view";
|
||||
import { useAccountsPageViewModel } from "./AccountsPage.vm";
|
||||
|
||||
export function AccountsPage() {
|
||||
const AccountsPage: FC = () => {
|
||||
const viewModel = useAccountsPageViewModel();
|
||||
if (viewModel.state === "loading") return <AccountsPageLoading />;
|
||||
if (viewModel.state === "error") return <AccountsPageError {...viewModel} />;
|
||||
@@ -26,10 +27,11 @@ export function AccountsPage() {
|
||||
retry={viewModel.retry}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
AccountsPage,
|
||||
AccountsPageEmpty,
|
||||
AccountsPageError,
|
||||
AccountsPageLoading,
|
||||
} from "./AccountsPage.view";
|
||||
};
|
||||
|
||||
@@ -7,14 +7,40 @@ import {
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import type { BrokerageAccount } from "@/entities/brokerage-account";
|
||||
import { BrokerageAccountList } from "@/entities/brokerage-account";
|
||||
import { StateView } from "@/shared/ui";
|
||||
|
||||
export function AccountsPageLoading() {
|
||||
type AccountsPageErrorProps = {
|
||||
code?: string;
|
||||
retry?: () => void;
|
||||
};
|
||||
|
||||
type AccountsPageEmptyProps = {
|
||||
refreshing?: boolean;
|
||||
refreshError?: boolean;
|
||||
retry?: () => void;
|
||||
};
|
||||
|
||||
type AccountsPageLoadedProps = {
|
||||
accounts: BrokerageAccount[];
|
||||
refreshing?: boolean;
|
||||
refreshError?: boolean;
|
||||
retry?: () => void;
|
||||
};
|
||||
|
||||
type AccountsRefreshStateProps = {
|
||||
refreshing: boolean;
|
||||
refreshError: boolean;
|
||||
retry?: () => void;
|
||||
};
|
||||
|
||||
const AccountsPageLoading: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<StateView
|
||||
loading
|
||||
@@ -22,15 +48,10 @@ export function AccountsPageLoading() {
|
||||
text={t("states.loadingText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function AccountsPageError({
|
||||
code,
|
||||
retry,
|
||||
}: {
|
||||
code?: string;
|
||||
retry?: () => void;
|
||||
}) {
|
||||
const AccountsPageError: FC<AccountsPageErrorProps> = (props) => {
|
||||
const { code, retry } = props;
|
||||
const { t } = useTranslation();
|
||||
const settingsAction = (
|
||||
<Button component={Link} to="/settings" variant="light">
|
||||
@@ -77,18 +98,12 @@ export function AccountsPageError({
|
||||
action={<Button onClick={retry}>{t("accounts.retry")}</Button>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function AccountsPageEmpty({
|
||||
refreshing = false,
|
||||
refreshError = false,
|
||||
retry,
|
||||
}: {
|
||||
refreshing?: boolean;
|
||||
refreshError?: boolean;
|
||||
retry?: () => void;
|
||||
}) {
|
||||
const AccountsPageEmpty: FC<AccountsPageEmptyProps> = (props) => {
|
||||
const { refreshing = false, refreshError = false, retry } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<AccountsRefreshState
|
||||
@@ -102,20 +117,12 @@ export function AccountsPageEmpty({
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function AccountsPageLoaded({
|
||||
accounts,
|
||||
refreshing = false,
|
||||
refreshError = false,
|
||||
retry,
|
||||
}: {
|
||||
accounts: BrokerageAccount[];
|
||||
refreshing?: boolean;
|
||||
refreshError?: boolean;
|
||||
retry?: () => void;
|
||||
}) {
|
||||
const AccountsPageLoaded: FC<AccountsPageLoadedProps> = (props) => {
|
||||
const { accounts, refreshing = false, refreshError = false, retry } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={1}>{t("accounts.title")}</Title>
|
||||
@@ -127,19 +134,13 @@ export function AccountsPageLoaded({
|
||||
<BrokerageAccountList accounts={accounts} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function AccountsRefreshState({
|
||||
refreshing,
|
||||
refreshError,
|
||||
retry,
|
||||
}: {
|
||||
refreshing: boolean;
|
||||
refreshError: boolean;
|
||||
retry?: () => void;
|
||||
}) {
|
||||
const AccountsRefreshState: FC<AccountsRefreshStateProps> = (props) => {
|
||||
const { refreshing, refreshError, retry } = props;
|
||||
const { t } = useTranslation();
|
||||
if (!refreshing && !refreshError) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{refreshing ? (
|
||||
@@ -162,4 +163,11 @@ function AccountsRefreshState({
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
AccountsPageEmpty,
|
||||
AccountsPageError,
|
||||
AccountsPageLoaded,
|
||||
AccountsPageLoading,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { errorCode, useBrokerageAccounts } from "@/entities/brokerage-account";
|
||||
|
||||
export function useAccountsPageViewModel() {
|
||||
function useAccountsPageViewModel() {
|
||||
const accounts = useBrokerageAccounts();
|
||||
if (!accounts.data) {
|
||||
if (accounts.isFetching) return { state: "loading" as const };
|
||||
@@ -26,3 +26,5 @@ export function useAccountsPageViewModel() {
|
||||
retry: () => accounts.refetch(),
|
||||
};
|
||||
}
|
||||
|
||||
export { useAccountsPageViewModel };
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Paper, Stack, Title } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LoginForm } from "@/features/auth";
|
||||
|
||||
export function LoginPage() {
|
||||
const LoginPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<main className="auth-page">
|
||||
<Paper className="auth-card" p="xl" radius="lg" withBorder>
|
||||
@@ -14,4 +16,6 @@ export function LoginPage() {
|
||||
</Paper>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { LoginPage };
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import { Button, StateView } from "@/shared/ui";
|
||||
|
||||
export function NotFoundPage() {
|
||||
const NotFoundPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={1}>{t("notFound.title")}</Title>
|
||||
@@ -19,4 +21,6 @@ export function NotFoundPage() {
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { NotFoundPage };
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Paper, Stack, Title } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RegisterForm } from "@/features/auth";
|
||||
|
||||
export function RegisterPage() {
|
||||
const RegisterPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<main className="auth-page">
|
||||
<Paper className="auth-card" p="xl" radius="lg" withBorder>
|
||||
@@ -14,4 +16,6 @@ export function RegisterPage() {
|
||||
</Paper>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { RegisterPage };
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Button, Stack, Title } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useAuth } from "@/features/auth";
|
||||
import { TbankConnectionSection } from "@/features/tbank-connection";
|
||||
|
||||
export function SettingsPage() {
|
||||
const SettingsPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={1}>{t("settings.title")}</Title>
|
||||
@@ -22,4 +24,6 @@ export function SettingsPage() {
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { SettingsPage };
|
||||
|
||||
Reference in New Issue
Block a user