Frontend:
- apps/web (Vite+TS, TanStack Router/Query, shadcn/ui @cfdm/ui base-nova)
- 10 страниц в routes/_auth/, Recharts через shadcn Chart, lucide-react
- формы на RHF + Zod (FormSheet/FormField)
- удалены Tabler, Chart.js, react-router-dom
Backend (параллельный трек):
- apps/api (Fastify 5 + Drizzle + better-sqlite3)
- packages/db: Drizzle-схема и repositories по сущностям
- packages/shared: Zod-контракты
- роуты с валидацией и единым форматом ошибок { error: { code, message } }
- sync/backup — заглушки 501 (billmanager-адаптеры переносятся отдельно)
- legacy Express оставлен как runtime по умолчанию (RUNTIME=express)
Infra:
- Dockerfile multi-stage под pnpm workspaces
- .dockerignore и docker-compose обновлены под monorepo
Rules:
- удалены нерелевантные правила (rust, cloudflare, server/frontend-conventions)
- project-structure.mdc и AGENTS.md переписаны под monorepo
- frontend-shadcn.mdc, shadcn-ui-production.mdc, sqlite.mdc обновлены
Co-authored-by: Cursor <cursoragent@cursor.com>
68 lines
1.7 KiB
TypeScript
68 lines
1.7 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import { AlertCircle, RefreshCwIcon } from 'lucide-react'
|
|
import { Button } from '@cfdm/ui/components/button'
|
|
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
|
import { EmptyState } from './empty-state'
|
|
|
|
interface QueryStateProps<T> {
|
|
data: T | undefined
|
|
isLoading: boolean
|
|
isError: boolean
|
|
error?: unknown
|
|
empty?: boolean
|
|
emptyTitle?: string
|
|
emptyDescription?: string
|
|
emptyAction?: ReactNode
|
|
onRetry?: () => void
|
|
skeleton?: ReactNode
|
|
children: (data: T) => ReactNode
|
|
}
|
|
|
|
export function QueryState<T>({
|
|
data,
|
|
isLoading,
|
|
isError,
|
|
error,
|
|
empty,
|
|
emptyTitle = 'Нет данных',
|
|
emptyDescription,
|
|
emptyAction,
|
|
onRetry,
|
|
skeleton,
|
|
children,
|
|
}: QueryStateProps<T>) {
|
|
if (isLoading) {
|
|
return <>{skeleton ?? <DefaultSkeleton />}</>
|
|
}
|
|
if (isError) {
|
|
return (
|
|
<EmptyState
|
|
icon={<AlertCircle className="size-8" />}
|
|
title="Ошибка загрузки"
|
|
description={error instanceof Error ? error.message : 'Не удалось загрузить данные'}
|
|
action={
|
|
onRetry ? (
|
|
<Button variant="outline" size="sm" onClick={onRetry}>
|
|
<RefreshCwIcon data-icon="inline-start" />
|
|
Повторить
|
|
</Button>
|
|
) : null
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
if (empty || data == null) {
|
|
return <EmptyState title={emptyTitle} description={emptyDescription} action={emptyAction} />
|
|
}
|
|
return <>{children(data)}</>
|
|
}
|
|
|
|
function DefaultSkeleton() {
|
|
return (
|
|
<div className="flex flex-col gap-3">
|
|
<Skeleton className="h-8 w-48" />
|
|
<Skeleton className="h-32 w-full" />
|
|
</div>
|
|
)
|
|
}
|