mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
ChartErrorState took no error, so a chart whose read was refused or whose session had ended offered a retry that could not help. It now takes the error and branches the way QueryErrorCard and QueryErrorRow do, the chunk boundary hands it the error it caught, and the error-prop guard covers it. QueryErrorRow gains an inset form for a failure inside a card body, and its frame takes the md:p-6 step. QueryErrorCard drops its fixed px-6 for the card's own inset and both icons move onto the size scale.
94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
"use client";
|
|
|
|
import type { ReactNode } from "react";
|
|
import { AlertTriangle, RotateCw } from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { useReadFailure } from "@/hooks/use-read-failure";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
/**
|
|
* Read-failure card for list / data surfaces.
|
|
*
|
|
* Mirrors `<ChartErrorState>`'s shape + tone (AlertTriangle, a title line, a
|
|
* muted description, an optional Retry button) but renders inside a `Card` so
|
|
* a list page can tell "load failed" apart from an honest empty state — a
|
|
* failed query must never fall through to the "no data yet" copy. Defaults are
|
|
* the generic `common.*` strings so a caller can drop it in with just
|
|
* `onRetry`; a surface with its own "couldn't load X" string passes it through
|
|
* `title` / `description`.
|
|
*
|
|
* The card reads the failure itself (`error`), exactly as `QueryErrorRow`
|
|
* does: a refusal draws `refused` (nothing by default), a spent budget says
|
|
* so without a retry, an ended session offers sign-in, and only a real
|
|
* failure shows `title` / `description` with `onRetry`.
|
|
*/
|
|
export function QueryErrorCard({
|
|
error,
|
|
refused = null,
|
|
title,
|
|
description,
|
|
onRetry,
|
|
retryLabel,
|
|
className,
|
|
}: {
|
|
/** The query's error. It decides what the card offers. */
|
|
error: unknown;
|
|
/** What a refused (403) read draws instead of the card. */
|
|
refused?: ReactNode;
|
|
title?: ReactNode;
|
|
description?: ReactNode;
|
|
onRetry?: () => void;
|
|
retryLabel?: string;
|
|
className?: string;
|
|
}) {
|
|
const failure = useReadFailure(error, { message: title, onRetry });
|
|
if (!failure) return <>{refused}</>;
|
|
const { action } = failure;
|
|
// The description belongs to a real failure; a spent budget or an ended
|
|
// session replaces the title, and the surface's own detail would not fit.
|
|
const detail = failure.kind === "failed" ? description : null;
|
|
return (
|
|
<Card data-slot="query-error-card" className={cn(className)}>
|
|
<CardContent
|
|
role="alert"
|
|
className="flex flex-col items-center justify-center gap-3 py-10 text-center"
|
|
>
|
|
<AlertTriangle
|
|
className="text-muted-foreground size-6"
|
|
aria-hidden="true"
|
|
/>
|
|
<p className="text-sm font-medium">{failure.message}</p>
|
|
{detail ? (
|
|
<p className="text-muted-foreground text-sm">{detail}</p>
|
|
) : null}
|
|
{action?.kind === "retry" ? (
|
|
// The slot is the accessibility suite's handle on the one control
|
|
// this card offers: the error state has to be recoverable by
|
|
// keyboard, and asserting that against viewport text would pin the
|
|
// copy instead of the affordance.
|
|
<Button
|
|
data-slot="query-error-retry"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={action.onClick}
|
|
>
|
|
<RotateCw className="size-3.5" aria-hidden="true" />
|
|
{retryLabel ?? action.label}
|
|
</Button>
|
|
) : action ? (
|
|
<Button
|
|
data-slot="query-error-sign-in"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={action.onClick}
|
|
>
|
|
{action.label}
|
|
</Button>
|
|
) : null}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|