Normalize interface naming — remove I prefix convention (#255)

* update pr workflow

* interface to types

* use consistent import

* import type syntax

* test plan note
This commit is contained in:
tmshv authored and GitHub committed 2026-02-08 15:57:11 +07:00
1 parent 7ab9147183
commit 9ee9bce197
20 files changed
+130 -100

No files matched your search

+48 -27
View File
@@ -4,39 +4,60 @@ on:
pull_request:
jobs:
build:
name: Build Image Without Push
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
node-version: 22
cache: npm
- name: Build image (no push)
uses: docker/build-push-action@v5
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
context: ./
push: false # Do NOT push
platforms: linux/amd64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
node-version: 22
cache: npm
- name: Move cache
if: steps.buildx.outcome == 'success'
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
- name: Install dependencies
run: npm ci
- name: Test
run: npm test
build:
name: Build
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
+10 -3
View File
@@ -20,13 +20,13 @@ Node 22 required (see `mise.toml`). Deployed on Vercel.
## Git Workflow
Feature branches are named `issue-XXX` where `XXX` is the related GitHub issue number.
Feature branches are named `issue-XXX` where `XXX` is the related GitHub issue number. PR descriptions should not include a test plan section.
## 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
- `.github/workflows/test-app.yml` — runs lint, tests, and build on PRs (build runs only after lint and tests pass)
## Code Style
@@ -75,7 +75,14 @@ CSS Modules (`.module.css`) for component-scoped styles. Global theme via CSS cu
## TypeScript
Path alias: `@/*` → `src/*`. Strict mode enabled. Target ES6. `moduleDetection: "force"` in tsconfig (required because Next.js auto-generates `.next/types/validator.ts` without exports, which breaks `isolatedModules`). Both `@/` and `src/` import styles are used in the codebase — prefer `@/` for new code.
Path alias: `@/*` → `src/*`. Strict mode enabled. Target ES6. `moduleDetection: "force"` in tsconfig (required because Next.js auto-generates `.next/types/validator.ts` without exports, which breaks `verbatimModuleSyntax`). Use `@/` path alias for all local imports, not `src/`. Legacy `src/` imports exist in the codebase but should not be used in new or modified code.
`verbatimModuleSyntax` is enabled — use `import type` for type-only imports. When importing both values and types from the same module, use separate `import` and `import type` statements:
```ts
import { createPage } from "@/remote/factory"
import type { Page } from "@/types"
```
## Accessibility
+1 -1
View File
@@ -1,4 +1,4 @@
import { NextPage } from "next"
import type { NextPage } from "next"
import { Wrapper } from "@/components/Wrapper"
import { NotFound } from "@/components/NotFound"
import Copyright from "@/components/Copyright"
+10 -10
View File
@@ -1,11 +1,11 @@
import Head from "next/head"
import { App } from "src/components/App"
import { Page } from "src/components/Page"
import { App } from "@/components/App"
import { Page } from "@/components/Page"
import { NextSeo } from "next-seo"
import { MetaBuilder } from "src/lib/meta"
import { GetStaticProps, NextPage } from "next"
import { IBreadcumbsPart, IMenu, IMeta, ITag, Sign, Token } from "@/types"
import { Html } from "src/components/Html"
import { MetaBuilder } from "@/lib/meta"
import type { GetStaticProps, NextPage } from "next"
import type { BreadcrumbPart, MenuItem, Meta, Tag, Sign, Token } from "@/types"
import { Html } from "@/components/Html"
import { Youtube } from "@/components/Youtube"
import { paramsToSlug } from "@/remote/lib"
import { PageGrid } from "@/components/PageGrid"
@@ -17,11 +17,11 @@ import { getMenu, getPageBySlug, getUrls } from "@/remote/api"
type Props = {
title: string
tags: ITag[]
tags: Tag[]
date: string
breadcrumb: IBreadcumbsPart[]
meta?: IMeta
menu: IMenu[],
breadcrumb: BreadcrumbPart[]
meta?: Meta
menu: MenuItem[],
tokens: Token[]
documentSignature: Sign
}
+8 -8
View File
@@ -1,18 +1,18 @@
import Head from "next/head"
import { App } from "src/components/App"
import { App } from "@/components/App"
import { NextSeo } from "next-seo"
import { PageGrid } from "src/components/PageGrid"
import { HudozkaTitle } from "src/components/HudozkaTitle"
import { MetaBuilder } from "src/lib/meta"
import { GetStaticProps, NextPage } from "next"
import { IMenu, IMeta, PageCardDto } from "src/types"
import { PageGrid } from "@/components/PageGrid"
import { HudozkaTitle } from "@/components/HudozkaTitle"
import { MetaBuilder } from "@/lib/meta"
import type { GetStaticProps, NextPage } from "next"
import type { MenuItem, Meta, PageCardDto } from "@/types"
import { useMobile } from "@/hooks/useMobile"
import { getHomeCards, getMenu } from "@/remote/api"
type Props = {
title: string
meta: IMeta
menu: IMenu[]
meta: Meta
menu: MenuItem[]
items: PageCardDto[]
}
+4 -4
View File
@@ -3,9 +3,9 @@ import { MenuContext } from "@/context/MenuContext"
import { Footer } from "../Footer"
import { Content } from "../Content"
import { Breadcrumbs } from "../Breadcrumbs"
import { IBreadcumbsPart, IMenu } from "src/types"
import type { BreadcrumbPart, MenuItem } from "@/types"
import { useRouter } from "next/router"
import { useMobile } from "src/hooks/useMobile"
import { useMobile } from "@/hooks/useMobile"
import { Navigation } from "@/components/Navigation"
import { Box } from "@/ui/Box"
import { AccessibilityButton } from "../AccessibilityButton"
@@ -29,8 +29,8 @@ const Gosuslugi = dynamic(import("../Gosuslugi").then(module => module.Gosuslugi
export type AppProps = {
showAuthor?: boolean
contentStyle?: React.CSSProperties
breadcrumbs?: IBreadcumbsPart[]
menu: IMenu[]
breadcrumbs?: BreadcrumbPart[]
menu: MenuItem[]
children?: React.ReactNode
}
+3 -3
View File
@@ -1,12 +1,12 @@
import s from "./styles.module.css"
import { IBreadcumbsPart } from "src/types"
import type { BreadcrumbPart } from "@/types"
import { Button } from "@/ui/Button"
import { insertBetween } from "src/lib/array"
import { insertBetween } from "@/lib/array"
export type BreadcrumbsProps = {
style?: React.CSSProperties
items: IBreadcumbsPart[]
items: BreadcrumbPart[]
path: string
}
+1 -1
View File
@@ -1,6 +1,6 @@
import s from "./styles.module.css"
import { FileDefinition } from "src/types"
import type { FileDefinition } from "@/types"
import { ext, size } from "src/lib/file"
import Link from "next/link"
import Image from "next/image"
+1 -1
View File
@@ -1,6 +1,6 @@
import s from "./signature.module.css"
import { Sign } from "@/types"
import type { Sign } from "@/types"
import { HiOutlineShieldCheck } from "react-icons/hi"
import * as Tooltip from "@radix-ui/react-tooltip"
+1 -1
View File
@@ -2,7 +2,7 @@ import s from "./filecard.module.css"
import Image from "next/image"
import dynamic from "next/dynamic"
import { FileTokenData, Sign } from "@/types"
import type { FileTokenData, Sign } from "@/types"
import { size, ext } from "@/lib/file"
import { getResizedUrl } from "@/lib/image"
import Link from "next/link"
+3 -3
View File
@@ -1,6 +1,6 @@
import s from "./styles.module.css"
import { IMenu } from "@/types"
import type { MenuItem } from "@/types"
import * as NavigationMenu from "@radix-ui/react-navigation-menu"
import classnames from "classnames/bind"
import { useRouter } from "next/router"
@@ -10,8 +10,8 @@ let cx = classnames.bind(s)
export type MenuProps = {
vertical?: boolean
items: IMenu[]
onItemClick?: (item: IMenu) => void
items: MenuItem[]
onItemClick?: (item: MenuItem) => void
}
export const Menu: React.FC<MenuProps> = ({ vertical = false, onItemClick, items }) => {
+2 -2
View File
@@ -1,13 +1,13 @@
import s from "./styles.module.css"
import { PageMeta } from "../PageMeta"
import { ITag } from "src/types"
import type { Tag } from "@/types"
export type PageProps = {
children?: React.ReactNode
style?: React.CSSProperties
date?: Date
tags: ITag[]
tags: Tag[]
}
export const Page: React.FC<PageProps> = props => (
+3 -2
View File
@@ -1,7 +1,8 @@
import Image from "next/image"
import { PageCardDto } from "@/types"
import type { PageCardDto } from "@/types"
import { CardGrid } from "../CardGrid"
import { Card, CardLayout } from "../Card"
import { Card } from "../Card"
import type { CardLayout } from "../Card"
import { Date } from "./Date"
import { Spacer } from "../Spacer"
import { Box } from "@/ui/Box"
+6 -5
View File
@@ -1,15 +1,16 @@
import s from "./styles.module.css"
import cx from "classnames"
import { dateFormat } from "src/lib/date"
import { ITag } from "src/types"
import { TagList, Direction } from "../TagList"
import { useMobile } from "src/hooks/useMobile"
import { dateFormat } from "@/lib/date"
import type { Tag } from "@/types"
import { TagList } from "../TagList"
import type { Direction } from "../TagList"
import { useMobile } from "@/hooks/useMobile"
export type PageMetaProps = {
style?: React.CSSProperties
date?: Date
tags?: ITag[]
tags?: Tag[]
}
export const PageMeta: React.FC<PageMetaProps> = props => {
+6 -6
View File
@@ -1,7 +1,7 @@
import s from "./styles.module.css"
import cx from "classnames"
import { ITag } from "src/types"
import type { Tag } from "@/types"
import { Button } from "@/ui/Button"
const directionClass = {
@@ -11,13 +11,13 @@ const directionClass = {
export type Direction = "horizontal" | "vertical"
export type TagProps = {
export type TagItemProps = {
children?: React.ReactNode
href: string
direction: Direction
}
export const Tag: React.FC<TagProps> = props => (
export const TagItem: React.FC<TagItemProps> = props => (
<li>
<Button
href={props.href}
@@ -30,20 +30,20 @@ export const Tag: React.FC<TagProps> = props => (
)
export type TagListProps = {
items: ITag[]
items: Tag[]
direction: Direction
}
export const TagList: React.FC<TagListProps> = props => (
<ul className={cx(s.tags, directionClass[props.direction])}>
{props.items.map(x => (
<Tag
<TagItem
key={x.slug}
href={x.href}
direction={props.direction}
>
{x.name}
</Tag>
</TagItem>
))}
</ul>
)
+4 -4
View File
@@ -1,6 +1,6 @@
import { IMeta, Pic } from "src/types"
import type { Meta, Pic } from "@/types"
export function meta(props: Partial<IMeta>): IMeta {
export function meta(props: Partial<Meta>): Meta {
const path = props.url || "/"
const url = `https://art.shlisselburg.org${path}`
@@ -28,9 +28,9 @@ export class MetaBuilder {
private image?: Pic
private title?: string
private description?: string
private data?: Partial<IMeta>
private data?: Partial<Meta>
setData(param: Partial<IMeta>) {
setData(param: Partial<Meta>) {
this.data = param
return this
}
+3 -3
View File
@@ -1,5 +1,5 @@
import { createHomeCards, createMenu, createPage, createPageUrls } from "@/remote/factory"
import { IMenu, IPage, PageCardDto } from "@/types"
import type { MenuItem, Page, PageCardDto } from "@/types"
const backendUrl = "https://hudozka.tmshv.com"
@@ -43,7 +43,7 @@ export async function getUrls(): Promise<string[]> {
return urls
}
export async function getPageBySlug(slug: string): Promise<IPage | null> {
export async function getPageBySlug(slug: string): Promise<Page | null> {
const url = `${backendUrl}/pages?slug=${slug}`
const page = await apiGet(createPage)(url, () => null)
if (!page) {
@@ -57,7 +57,7 @@ export async function getHomeCards(): Promise<PageCardDto[]> {
return items
}
export async function getMenu(): Promise<IMenu[]> {
export async function getMenu(): Promise<MenuItem[]> {
const menu = await apiGet(createMenu)(`${backendUrl}/menu`, () => [])
return menu
}
+7 -7
View File
@@ -1,7 +1,7 @@
import { IMenu, IPage, ITag, PageCardDto, Pic, Token } from "@/types"
import type { MenuItem, Page, Tag, PageCardDto, Pic, Token } from "@/types"
import { asItem } from "./lib"
import { StrapiComponentEmbed, StrapiComponent, StrapiHome, StrapiPage, StrapiPageCard, StrapiTag, StrapiMedia, StrapiMenu } from "./types"
import { typograf, markdownToHtml } from "src/lib/text"
import type { StrapiComponentEmbed, StrapiComponent, StrapiHome, StrapiPage, StrapiPageCard, StrapiTag, StrapiMedia, StrapiMenu } from "./types"
import { typograf, markdownToHtml } from "@/lib/text"
import { encodeImageToBlurhash } from "./image"
const md = (text: string) => typograf(markdownToHtml(text))
@@ -95,8 +95,8 @@ export async function createPageToken(component: StrapiComponent): Promise<Token
}
}
export function createMenu(res: StrapiMenu): IMenu[] {
const items: IMenu[] = res.menu.items.map(x => ({
export function createMenu(res: StrapiMenu): MenuItem[] {
const items: MenuItem[] = res.menu.items.map(x => ({
href: x.page.slug,
name: x.page.title,
}))
@@ -107,14 +107,14 @@ export function createMenu(res: StrapiMenu): IMenu[] {
}, ...items]
}
export async function createPage(res: StrapiPage | StrapiPage[]): Promise<IPage | null> {
export async function createPage(res: StrapiPage | StrapiPage[]): Promise<Page | null> {
const item = asItem(res)
if (!item) {
return null
}
const cover = await getCoverImage(item.cover)
const tags: ITag[] = item.tags.map(tag => ({
const tags: Tag[] = item.tags.map(tag => ({
id: tag.id,
name: tag.name,
slug: tag.slug,
+8 -8
View File
@@ -1,9 +1,9 @@
export interface IMenu {
export type MenuItem = {
name: string
href: string
}
export interface IBreadcumbsPart {
export type BreadcrumbPart = {
name: string
href: string
}
@@ -30,7 +30,7 @@ export type FileDefinition = {
}
}
export interface IPage {
export type Page = {
id: string
title: string
url: string
@@ -39,8 +39,8 @@ export interface IPage {
cover: Pic
description: string
tokens: Token[]
tags: ITag[]
breadcrumb?: IBreadcumbsPart[]
tags: Tag[]
breadcrumb?: BreadcrumbPart[]
featured: boolean
}
@@ -51,7 +51,7 @@ export type Sign = {
signature: string
}
export interface IDocument {
export type DocFile = {
// category: string
fileName: string
fileSize: number
@@ -61,7 +61,7 @@ export interface IDocument {
url: string
}
export interface IMeta {
export type Meta = {
url: string
siteName: string
locale: string
@@ -78,7 +78,7 @@ export interface IMeta {
}
export interface ITag {
export type Tag = {
id: number
name: string
slug: string
+1 -1
View File
@@ -21,7 +21,7 @@
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"jsx": "preserve",
"incremental": true,