textmachine/frontend/src/showcase/Loaded.tsx

68 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { ReactNode } from 'react';
import { ApiError } from '../api';
import { Blank } from './Blank';
export interface Query<T> {
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<T> {
query: Query<T>;
/** 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<T>({ query, waiting, isEmpty, empty, idle, children }: Props<T>) {
if (query.isPending) {
if (query.fetchStatus === 'idle') return <Blank {...(idle ?? nothingChosen)} />;
return <Blank title={waiting} description="Идёт загрузка." waiting />;
}
// 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 <Blank title="Не удалось загрузить" description={reasonOf(query.error)} />;
}
if (empty && isEmpty?.(query.data)) {
return <Blank title={empty.title} description={empty.description} />;
}
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 'Неизвестная ошибка.';
}