import type { ReactNode } from 'react'; import { ApiError } from '../api'; import { Blank } from './Blank'; export interface Query { isPending: boolean; isError: boolean; /** `idle` means the query is switched off — pending, but nothing is on its way. */ fetchStatus: 'fetching' | 'paused' | 'idle'; error: Error | null; data: T | undefined; } interface Props { query: Query; /** What the place is called while it waits — "loading" alone says nothing about what. */ waiting: string; /** True when the answer arrived and there is nothing in it. */ isEmpty?: (data: T) => boolean; empty?: { title: string; description: string }; /** What a switched-off query means HERE. "Pick a book" is wrong when there is none to pick. */ idle?: { title: string; description: string }; children: (data: T) => ReactNode; } // A switched-off query is PENDING forever, and drawing that as "loading" is a lie the screenshot // cycle caught: an empty library showed a bank eternally on its way. const nothingChosen = { title: 'Книга не выбрана', description: 'Выберите книгу слева — сведения о ней появятся здесь.', }; /** * The three branches every read has, in one place. They arrive together with the data layer rather * than after the first product screen — a screen written against data that is always there grows a * shape that has nowhere to put "not yet" and "did not work" (BACKLOG Ф-15). */ export function Loaded({ query, waiting, isEmpty, empty, idle, children }: Props) { if (query.isPending) { if (query.fetchStatus === 'idle') return ; return ; } // A failed refetch does NOT blank a panel that already has something in it. The query layer keeps // the previous data alongside the error, and replacing a rendered chapter with "не удалось // загрузить" because a background refresh failed loses the user's place for no reason. if (query.data === undefined) { return ; } if (empty && isEmpty?.(query.data)) { return ; } return <>{children(query.data)}; } /** * The phrase the user reads on a failure. Taken from `problem+json`, which the contract requires to * be product language on BOTH fields — the engine's own wording ("CJK leak in the ru output: 第一节") * never crosses the boundary, and this is the place that would put it on screen if it did. * `detail` first, because it is the specific sentence; `title` is the class of the failure. */ function reasonOf(error: Error | null): string { if (error instanceof ApiError) { if (error.status === 0) return 'Сервер недоступен. Попробуйте ещё раз.'; return error.problem?.detail ?? error.problem?.title ?? 'Сервер ответил отказом.'; } return 'Неизвестная ошибка.'; }