solve any problem (#248)
This commit is contained in:
1 parent
6da5a7a07b
commit
562b4f6408
9 files changed
+27
-16
No files matched your search
@@ -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`.
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
interface Window {
|
||||
Widget(url: string, id: number): void
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
Reference in new issue
Block a user