refactor: establish local frontend workspace boundaries
Conventional commits / commits (pull_request) Successful in 24s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 5m18s
Frontend security / security (push) Successful in 10s
Frontend verify / verify (push) Successful in 6m45s
Conventional commits / commits (pull_request) Successful in 24s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 5m18s
Frontend security / security (push) Successful in 10s
Frontend verify / verify (push) Successful in 6m45s
OpenSpec change: evolve-frontend-stack
This commit was merged in pull request #34.
This commit is contained in:
@@ -23,8 +23,9 @@ import type { IncomeAnalytics } from "@/entities/income";
|
||||
import { configureAuthClient } from "@/shared/api";
|
||||
import { createQueryWrapper } from "@/test/query";
|
||||
import { server } from "@/test/server";
|
||||
import { isDate } from "../model/period";
|
||||
import { AccountIncomePage } from "./AccountIncomePage";
|
||||
import { isDate, toIncomeDisplayModel } from "./AccountIncomePage.vm";
|
||||
import { toIncomeDisplayModel } from "./AccountIncomePage.vm";
|
||||
import { IncomePayments } from "./components/IncomePayments.view";
|
||||
|
||||
const analytics = {
|
||||
|
||||
@@ -6,12 +6,17 @@ import { z } from "zod";
|
||||
import { type IncomeAnalytics, useIncomeAnalytics } from "@/entities/income";
|
||||
import { validationMessage } from "@/shared/i18n";
|
||||
import { formatDate } from "@/shared/lib";
|
||||
import {
|
||||
formatDraftDate,
|
||||
type IncomeForm,
|
||||
type IncomePeriod,
|
||||
isDate,
|
||||
moscowDate,
|
||||
parseDraftDate,
|
||||
} from "../model/period";
|
||||
|
||||
const ISO_MONTH_LENGTH = 7;
|
||||
|
||||
type IncomePeriod = { from: string; to: string };
|
||||
type IncomeForm = IncomePeriod;
|
||||
|
||||
type IncomeDisplayModel = {
|
||||
entries: { key: string; operation: IncomeAnalytics["entries"][number] }[];
|
||||
comparison: {
|
||||
@@ -41,7 +46,6 @@ type AccountIncomePageViewModel =
|
||||
onSubmit: (values: IncomeForm) => void;
|
||||
};
|
||||
|
||||
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const draftDate = z
|
||||
.string()
|
||||
.refine((value) => parseDraftDate(value) !== null, {
|
||||
@@ -129,18 +133,6 @@ const useAccountIncomePageViewModel = (
|
||||
};
|
||||
};
|
||||
|
||||
function formatDraftDate(value: string): string {
|
||||
const [year, month, day] = value.split("-");
|
||||
return `${day}.${month}.${year}`;
|
||||
}
|
||||
|
||||
function parseDraftDate(value: string): string | null {
|
||||
if (!/^\d{2}\.\d{2}\.\d{4}$/.test(value)) return null;
|
||||
const [day, month, year] = value.split(".");
|
||||
const date = `${year}-${month}-${day}`;
|
||||
return isDate(date) ? date : null;
|
||||
}
|
||||
|
||||
function draftPeriod(period: IncomePeriod): IncomeForm {
|
||||
return { from: formatDraftDate(period.from), to: formatDraftDate(period.to) };
|
||||
}
|
||||
@@ -163,15 +155,6 @@ function validPeriod(period: IncomePeriod): period is IncomePeriod {
|
||||
);
|
||||
}
|
||||
|
||||
function isDate(value: string): boolean {
|
||||
if (!datePattern.test(value)) return false;
|
||||
const parsed = new Date(`${value}T00:00:00Z`);
|
||||
return (
|
||||
Number.isFinite(parsed.valueOf()) &&
|
||||
parsed.toISOString().slice(0, 10) === value
|
||||
);
|
||||
}
|
||||
|
||||
function toIncomeDisplayModel(
|
||||
analytics: IncomeAnalytics,
|
||||
): IncomeDisplayModel | null {
|
||||
@@ -276,26 +259,9 @@ function formatIncomeChange(
|
||||
}).format(ratio);
|
||||
}
|
||||
|
||||
function moscowDate(value: Date): string {
|
||||
const parts = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: "Europe/Moscow",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}).formatToParts(value);
|
||||
const part = (type: string) =>
|
||||
parts.find((item) => item.type === type)?.value;
|
||||
return `${part("year")}-${part("month")}-${part("day")}`;
|
||||
}
|
||||
|
||||
export {
|
||||
type AccountIncomePageViewModel,
|
||||
formatDraftDate,
|
||||
type IncomeDisplayModel,
|
||||
type IncomeForm,
|
||||
isDate,
|
||||
moscowDate,
|
||||
parseDraftDate,
|
||||
toIncomeDisplayModel,
|
||||
useAccountIncomePageViewModel,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from "@mantine/core";
|
||||
import { StateView } from "@wealthledger/ui";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StateView } from "@/shared/ui";
|
||||
import type { AccountIncomePageViewModel } from "../AccountIncomePage.vm";
|
||||
|
||||
type ErrorProps = Extract<AccountIncomePageViewModel, { state: "error" }>;
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { StateView } from "@wealthledger/ui";
|
||||
import type { FC } from "react";
|
||||
import { FormProvider } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StateView } from "@/shared/ui";
|
||||
import classes from "../AccountIncomePage.module.css";
|
||||
import type { AccountIncomePageViewModel } from "../AccountIncomePage.vm";
|
||||
import { IncomeComparisons } from "./IncomeComparisons.view";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Paper, Skeleton, Stack } from "@mantine/core";
|
||||
import { LoadingSkeleton } from "@wealthledger/ui";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LoadingSkeleton } from "@/shared/ui";
|
||||
import classes from "../AccountIncomePage.module.css";
|
||||
|
||||
const AccountIncomePageLoading: FC = () => {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import type { FC } from "react";
|
||||
import { FormProvider, useForm, useFormState } from "react-hook-form";
|
||||
import type { IncomeForm } from "../../model/period";
|
||||
import { IncomePeriodForm } from "./IncomePeriodForm.view";
|
||||
|
||||
const period = { from: "01.08.2026", to: "27.08.2026" };
|
||||
const errors = {
|
||||
to: { type: "manual", message: "Синтетическая ошибка периода" },
|
||||
};
|
||||
|
||||
type PeriodExampleProps = { invalid?: boolean };
|
||||
|
||||
const PeriodExample: FC<PeriodExampleProps> = (props) => {
|
||||
const methods = useForm<IncomeForm>({
|
||||
defaultValues: period,
|
||||
errors: props.invalid ? errors : undefined,
|
||||
});
|
||||
const { isSubmitSuccessful } = useFormState({ control: methods.control });
|
||||
const submitted = isSubmitSuccessful ? methods.getValues() : null;
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<IncomePeriodForm
|
||||
onSubmit={methods.handleSubmit(() => undefined)}
|
||||
onReset={() => {
|
||||
methods.reset(period);
|
||||
}}
|
||||
/>
|
||||
<output aria-live="polite">
|
||||
{submitted
|
||||
? `Применено: ${submitted.from} – ${submitted.to}`
|
||||
: "Период не применён"}
|
||||
</output>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const meta = {
|
||||
title: "Product/IncomePeriodForm",
|
||||
component: PeriodExample,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Изолированная форма с синтетическим периодом. Submit показывает значения локально; reset возвращает fixture. Ошибка задана явно: query, URL и production validation остаются у page VM, API и router providers не используются.",
|
||||
},
|
||||
},
|
||||
},
|
||||
render: () => <PeriodExample />,
|
||||
} satisfies Meta<typeof PeriodExample>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
export const Default: Story = {};
|
||||
export const ValidationError: Story = {
|
||||
render: () => <PeriodExample invalid />,
|
||||
};
|
||||
@@ -3,13 +3,13 @@ import { DatePickerInput } from "@mantine/dates";
|
||||
import type { FC, FormEventHandler } from "react";
|
||||
import { useController, useFormContext, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classes from "../AccountIncomePage.module.css";
|
||||
import {
|
||||
formatDraftDate,
|
||||
type IncomeForm,
|
||||
moscowDate,
|
||||
parseDraftDate,
|
||||
} from "../AccountIncomePage.vm";
|
||||
} from "../../model/period";
|
||||
import classes from "../AccountIncomePage.module.css";
|
||||
|
||||
type IncomePeriodFormProps = {
|
||||
onSubmit: FormEventHandler<HTMLFormElement>;
|
||||
|
||||
Reference in New Issue
Block a user