From 562b4f64081b9705e1e7b637da9e5c63f6523bf3 Mon Sep 17 00:00:00 2001 From: Roman Timashev Date: Sun, 8 Feb 2026 12:22:52 +0700 Subject: [PATCH] solve any problem (#248) --- CLAUDE.md | 11 ++++++++--- src/components/FileCard/index.tsx | 2 +- src/components/Gosuslugi/index.tsx | 2 +- src/global.d.ts | 3 +++ src/lib/file.test.ts | 1 + src/lib/file.ts | 4 ++-- src/types.ts | 8 +------- src/ui/Button/Button.test.tsx | 8 ++++++++ src/ui/Button/index.tsx | 4 ++-- 9 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 src/global.d.ts diff --git a/CLAUDE.md b/CLAUDE.md index ae0ed338..00e42077 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,6 +22,12 @@ Node 22 required (see `mise.toml`). Deployed on Vercel. Feature branches are named `issue-XXX` where `XXX` is the related GitHub issue number. +## CI/CD + +- `Dockerfile` — multi-stage Alpine build with standalone Next.js output, pushed to GHCR +- `.github/workflows/build-app.yaml` — builds and pushes Docker image on push to master +- `.github/workflows/test-app.yml` — builds Docker image (no push) on PRs + ## Code Style ESLint 9 flat config (`eslint.config.js`) using `FlatCompat` from `@eslint/eslintrc` to bridge `eslint-config-next` (which doesn't support native flat config in Next.js 15.x). Extends `next/core-web-vitals`. Enforces: no semicolons, double quotes, 4-space indentation, always-multiline trailing commas, `eol-last`. @@ -50,7 +56,7 @@ When adding or updating packages in `package.json`, do not specify the PATCH ver **Content tokens:** Pages are composed of flexible content blocks (tokens): `text`, `image`, `file`, `html`, `youtube`, `instagram`, `grid`. The `Token` discriminated union is defined in `src/types.ts`. Each token type maps to a renderer component. -**State management:** Valtio for reactive state (`src/store/`). Theme/accessibility preferences in `theme` store, playback state in `play` store. Navigation data passed via React Context (`src/context/MenuContext`). +**State management:** Valtio for reactive state (`src/store/`). Theme/accessibility preferences in `theme` store, playback state in `play` store, school contact info and year range in `config` store. Navigation data passed via React Context (`src/context/MenuContext`). ## Key Directories @@ -81,7 +87,7 @@ Remote images from `hudozkacdn.tmshv.com` with `images.weserv.nl` as proxy. Blur ## Testing -Vitest for unit tests. Config in `vitest.config.ts` with `@/` and `src/` path aliases. Test files are co-located with source (`src/lib/*.test.ts`). Currently covers `src/lib/` utility functions: array, string, url, file, image, date, text, meta. +Vitest for unit tests. Config in `vitest.config.ts` with `@/` and `src/` path aliases. Test files are co-located with source across `src/lib/`, `src/ui/`, and `src/components/`. Covers utility functions (array, string, url, file, image, date, text, meta), UI primitives (Box, Button, Panel), and components (Breadcrumbs, YMetrika). ## Known Legacy / Technical Debt @@ -90,7 +96,6 @@ Active modernization is tracked in GitHub issues #222–#238. Key items: - `pages/_app.tsx` is the only class component (extends `App`). Should be a function component. - `src/types.ts` has `I`-prefix interfaces (`IPage`, `IMenu`, etc.) from the Angular era alongside modern `type` aliases. The `tokens` field on `IPage` is `any[]` despite a proper `Token` union type existing. - `src/store/theme.ts` manipulates DOM directly outside React lifecycle (sets CSS vars on `document.documentElement`, toggles `document.body` classes). Has a TODO to fix after App Router migration. -- `src/components/YMetrika.jsx` is the only `.jsx` file — currently unused (commented out in `_app.tsx`). - Two orphaned `.scss` files in `src/style/` (`tape-viewer.scss`, `vertical-image.scss`) — not imported anywhere, no SCSS preprocessor configured. - Some inline styles in `src/components/App/index.tsx` should be CSS modules. - `react-use` and `use-media` dependencies are unmaintained; only `useToggle` and `useLockBodyScroll` are used from `react-use`. diff --git a/src/components/FileCard/index.tsx b/src/components/FileCard/index.tsx index 0c736988..b8d8ce16 100644 --- a/src/components/FileCard/index.tsx +++ b/src/components/FileCard/index.tsx @@ -17,7 +17,7 @@ export type FileCardProps = FileTokenData & { } export const FileCard: React.FC = props => { - const fileSize = size(props.file_size) + const fileSize = props.file_size != null ? size(props.file_size) : "" const format = ext(props.file_format) return ( diff --git a/src/components/Gosuslugi/index.tsx b/src/components/Gosuslugi/index.tsx index 30d89733..d021f944 100644 --- a/src/components/Gosuslugi/index.tsx +++ b/src/components/Gosuslugi/index.tsx @@ -32,7 +32,7 @@ export const Gosuslugi: React.FC = () => { } // This call taken from getCode - (window as any).Widget("https://pos.gosuslugi.ru/form", 312519) + window.Widget("https://pos.gosuslugi.ru/form", 312519) }) return () => { diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..24a510f4 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,3 @@ +interface Window { + Widget(url: string, id: number): void +} diff --git a/src/lib/file.test.ts b/src/lib/file.test.ts index e85d4a7e..5b40d38f 100644 --- a/src/lib/file.test.ts +++ b/src/lib/file.test.ts @@ -26,6 +26,7 @@ describe("size", () => { it("should return empty string for non-finite values", () => { expect(size(Infinity)).toBe("") + expect(size(-Infinity)).toBe("") }) it("should format bytes", () => { diff --git a/src/lib/file.ts b/src/lib/file.ts index 443838a8..d4182a4a 100644 --- a/src/lib/file.ts +++ b/src/lib/file.ts @@ -14,8 +14,8 @@ export function ext(mimeType: string): string { return mimeType } -export function size(bytes: any, precision = 1): string { - if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return "" +export function size(bytes: number, precision = 1): string { + if (Number.isNaN(bytes) || !Number.isFinite(bytes)) return "" const number = Math.floor(Math.log(bytes) / Math.log(1024)) const size = (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) diff --git a/src/types.ts b/src/types.ts index 4d8dd5af..1de31a63 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,7 +24,7 @@ export type FileDefinition = { cover?: Pic file: { name: string - size: string + size: number type: string src: string } @@ -77,12 +77,6 @@ export interface IMeta { twitterCreator: string } -export interface IImage { - alt: string - src: string - srcSet: Array<{ url: string, density: number }> - set: any // TODO -} export interface ITag { id: number diff --git a/src/ui/Button/Button.test.tsx b/src/ui/Button/Button.test.tsx index 85a1e33e..95aa7e8e 100644 --- a/src/ui/Button/Button.test.tsx +++ b/src/ui/Button/Button.test.tsx @@ -25,6 +25,14 @@ describe("Button", () => { expect(onClick).toHaveBeenCalledWith("test-value", expect.anything()) }) + it("calls onClick with undefined when no value is provided", () => { + const onClick = vi.fn() + render() + fireEvent.click(screen.getByRole("button", { name: "Press" })) + expect(onClick).toHaveBeenCalledOnce() + expect(onClick).toHaveBeenCalledWith(undefined, expect.anything()) + }) + it("applies disabled class when disabled is true", () => { const { container } = render() expect(container.firstElementChild!.className).toMatch(/disabled/) diff --git a/src/ui/Button/index.tsx b/src/ui/Button/index.tsx index 7cf7a51c..85c86b97 100644 --- a/src/ui/Button/index.tsx +++ b/src/ui/Button/index.tsx @@ -18,13 +18,13 @@ const themeClass = { export type ButtonProps = { children?: React.ReactNode - value?: any + value?: string style?: React.CSSProperties href?: string theme?: "default" | "ghost" | "icon" size?: "default" | "small" disabled?: boolean - onClick?: (value: any, event: MouseEvent) => void + onClick?: (value: string | undefined, event: MouseEvent) => void } export const Button: React.FC = ({ size = "default", theme = "default", disabled = false, ...props }) => {