Add Prettier to the frontend and gate formatting in CI

Форматтера у фронтенда не было: стиль держался вручную и успел разъехаться в 50 файлах.
Ставим Prettier с настройками под уже сложившийся стиль (без точек с запятой, одинарные кавычки,
ширина 100 — подобрана замером: при 100 расходится меньше файлов, чем при 96 или 110) и
прогоняем его по коду. `src/routeTree.gen.ts` исключён — его переписывает плагин роутера.

Чтобы форматирование больше не расходилось незаметно, добавлены проверки в CI: `csharpier check`
для бэкенда (его отсутствие и позволило накопиться 79 неотформатированным файлам) и
`prettier --check` для фронтенда. Версии форматтеров прибиты точно, без кареток: минорка меняет
вывод и красит CI на файлах, которых никто не трогал.

`.editorconfig` задаёт редакторам те же отступы и LF ещё до форматтера; значения совпадают с
настройками csharpier и Prettier намеренно — оба его читают.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-07-27 01:38:12 +03:00
co-authored by Claude Opus 5
parent 0442056367
commit 0606ea3e6e
58 changed files with 5158 additions and 4903 deletions
+4 -1
View File
@@ -28,7 +28,10 @@ export async function refreshAccessToken(): Promise<boolean> {
if (!refreshInFlight) {
refreshInFlight = (async () => {
try {
const response = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' })
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include',
})
if (!response.ok) return false
const data = (await response.json()) as { accessToken?: unknown }
if (typeof data?.accessToken !== 'string') return false
+1 -1
View File
@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'
export type KeyedRow<T> = { key: string; value: T }
const toRows = <T,>(values: readonly T[]): KeyedRow<T>[] =>
const toRows = <T>(values: readonly T[]): KeyedRow<T>[] =>
values.map((value) => ({ key: crypto.randomUUID(), value }))
/**
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -10,8 +10,7 @@ import { toast } from '@/shared/ui/toast-store'
export function useApiError() {
const { t } = useTranslation()
return useCallback(
(error: unknown) =>
toast.error(error instanceof HttpError ? error.detail : t('common.error')),
(error: unknown) => toast.error(error instanceof HttpError ? error.detail : t('common.error')),
[t],
)
}
+1 -6
View File
@@ -1,10 +1,5 @@
import { useCallback, useMemo, useState, type ReactNode } from 'react'
import {
ToastContext,
registerToastPush,
type ToastItem,
type ToastVariant,
} from './toast-store'
import { ToastContext, registerToastPush, type ToastItem, type ToastVariant } from './toast-store'
let nextId = 1
+3 -1
View File
@@ -31,7 +31,9 @@ type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return <Comp className={cn(buttonVariants({ variant, size }), className)} ref={ref} {...props} />
return (
<Comp className={cn(buttonVariants({ variant, size }), className)} ref={ref} {...props} />
)
},
)
Button.displayName = 'Button'
+26 -13
View File
@@ -1,33 +1,46 @@
import { type HTMLAttributes, forwardRef } from 'react'
import { cn } from '@/shared/lib/cn'
export const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('crt-panel rounded-md', className)} {...props} />
))
export const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('crt-panel rounded-md', className)} {...props} />
),
)
Card.displayName = 'Card'
export const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col gap-1.5 p-6', className)} {...props} />
))
export const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col gap-1.5 p-6', className)} {...props} />
),
)
CardHeader.displayName = 'CardHeader'
export const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
// children разворачиваем явно: заголовок без видимого содержимого — это дыра для скринридера,
// и статический анализ такое ловит только тогда, когда содержимое видно в разметке.
({ className, children, ...props }, ref) => (
<h3 ref={ref} className={cn('crt-glow text-xl font-semibold tracking-tight', className)} {...props}>
<h3
ref={ref}
className={cn('crt-glow text-xl font-semibold tracking-tight', className)}
{...props}
>
{children}
</h3>
),
)
CardTitle.displayName = 'CardTitle'
export const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => <p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />,
)
export const CardDescription = forwardRef<
HTMLParagraphElement,
HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
))
CardDescription.displayName = 'CardDescription'
export const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
))
export const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
),
)
CardContent.displayName = 'CardContent'
+10 -2
View File
@@ -50,7 +50,11 @@ export const DialogTitle = forwardRef<
ElementRef<typeof DialogPrimitive.Title>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title ref={ref} className={cn('crt-glow text-lg font-semibold', className)} {...props} />
<DialogPrimitive.Title
ref={ref}
className={cn('crt-glow text-lg font-semibold', className)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
@@ -58,7 +62,11 @@ export const DialogDescription = forwardRef<
ElementRef<typeof DialogPrimitive.Description>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
+1 -4
View File
@@ -23,10 +23,7 @@ export function SortHeader({
return (
// aria-sort — атрибут заголовка столбца, а не кнопки внутри него: у роли button его нет,
// и скринридер там его просто не прочтёт.
<th
className={cn('px-4 py-2 font-medium', className)}
aria-sort={active ? direction : 'none'}
>
<th className={cn('px-4 py-2 font-medium', className)} aria-sort={active ? direction : 'none'}>
<button
type="button"
onClick={() => onToggle(sortKey)}
+7 -1
View File
@@ -10,7 +10,13 @@ export function Toaster() {
return (
<div className="pointer-events-none fixed bottom-4 right-4 z-[10000] flex flex-col gap-2">
{toasts.map((t) => (
<ToastItem key={t.id} id={t.id} message={t.message} variant={t.variant} onDismiss={dismiss} />
<ToastItem
key={t.id}
id={t.id}
message={t.message}
variant={t.variant}
onDismiss={dismiss}
/>
))}
</div>
)