solve any problem (#248)

This commit is contained in:
tmshv authored and GitHub committed 2026-02-08 12:22:52 +07:00
1 parent 6da5a7a07b
commit 562b4f6408
9 files changed
+27 -16

No files matched your search

+8 -3
View File
@@ -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`.
+1 -1
View File
@@ -17,7 +17,7 @@ export type FileCardProps = FileTokenData & {
}
export const FileCard: React.FC<FileCardProps> = props => {
const fileSize = size(props.file_size)
const fileSize = props.file_size != null ? size(props.file_size) : ""
const format = ext(props.file_format)
return (
+1 -1
View File
@@ -32,7 +32,7 @@ export const Gosuslugi: React.FC<GosuslugiProps> = () => {
}
// This call taken from getCode <script>Widget("https://pos.gosuslugi.ru/form", 312519)</script>
(window as any).Widget("https://pos.gosuslugi.ru/form", 312519)
window.Widget("https://pos.gosuslugi.ru/form", 312519)
})
return () => {
+3
View File
@@ -0,0 +1,3 @@
interface Window {
Widget(url: string, id: number): void
}
+1
View File
@@ -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", () => {
+2 -2
View File
@@ -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)
+1 -7
View File
@@ -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
+8
View File
@@ -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(<Button onClick={onClick}>Press</Button>)
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(<Button disabled>Disabled</Button>)
expect(container.firstElementChild!.className).toMatch(/disabled/)
+2 -2
View File
@@ -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<ButtonProps> = ({ size = "default", theme = "default", disabled = false, ...props }) => {