111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
'use client';
|
|
|
|
import type { ReactElement } from 'react';
|
|
import { formatAge, type FreshnessLabel } from '@/lib/freshness/model';
|
|
|
|
/**
|
|
* Rendering rules for non-current freshness states (RI-5-001).
|
|
*
|
|
* - `unavailable` renders an explicit failure panel — never an empty
|
|
* healthy collection.
|
|
* - `stale` may render last-known data, but only under a visible label
|
|
* carrying source identity, snapshot version, and age.
|
|
* - `partial` renders the verified parts plus an explicit list of what is
|
|
* missing.
|
|
*/
|
|
|
|
interface RetryableNoticeProps {
|
|
readonly onRetry?: () => void;
|
|
readonly retryLabel?: string;
|
|
}
|
|
|
|
function RetryButton({ onRetry, retryLabel }: RetryableNoticeProps): ReactElement | null {
|
|
if (!onRetry) return null;
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onRetry}
|
|
className="mt-2 rounded-lg border border-surface-border px-3 py-1.5 text-xs transition-colors hover:border-gray-500"
|
|
>
|
|
{retryLabel ?? 'Retry'}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export interface UnavailableDataNoticeProps extends RetryableNoticeProps {
|
|
/** What is unavailable, e.g. "Tasks". */
|
|
readonly title: string;
|
|
/** Optional underlying failure detail (network message, invalidation reason). */
|
|
readonly detail?: string | null;
|
|
}
|
|
|
|
/** Explicit `unavailable` state. Never renders as an empty healthy collection. */
|
|
export function UnavailableDataNotice({
|
|
title,
|
|
detail,
|
|
onRetry,
|
|
retryLabel,
|
|
}: UnavailableDataNoticeProps): ReactElement {
|
|
return (
|
|
<div role="alert" className="rounded-lg border border-error/40 px-4 py-3 text-sm">
|
|
<p className="font-medium text-text-primary">{title} are unavailable</p>
|
|
<p className="mt-1 text-text-muted">
|
|
This is not an empty result — the data could not be verified from the gateway.
|
|
{detail ? ` ${detail}` : ''}
|
|
</p>
|
|
<RetryButton onRetry={onRetry} retryLabel={retryLabel} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export interface StaleDataNoticeProps extends RetryableNoticeProps {
|
|
/** Provenance of the last-known snapshot being displayed. */
|
|
readonly label: FreshnessLabel;
|
|
}
|
|
|
|
/**
|
|
* Situational-awareness banner for `stale` data: last-known data may render,
|
|
* but visibly labeled with source identity, snapshot version, and age.
|
|
*/
|
|
export function StaleDataNotice({
|
|
label,
|
|
onRetry,
|
|
retryLabel,
|
|
}: StaleDataNoticeProps): ReactElement {
|
|
return (
|
|
<div role="status" className="rounded-lg border border-warning/40 px-4 py-3 text-sm">
|
|
<p className="font-medium text-warning">Showing last-known data — it may be out of date</p>
|
|
<p className="mt-1 text-xs text-text-muted">
|
|
Source {label.source} · snapshot v{label.version} · fetched{' '}
|
|
{formatAge(label.fetchedAt, Date.now())}. Verdicts derived from this data are unknown and
|
|
changes are disabled until it is revalidated.
|
|
</p>
|
|
<RetryButton onRetry={onRetry} retryLabel={retryLabel ?? 'Revalidate'} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export interface PartialDataNoticeProps extends RetryableNoticeProps {
|
|
/** Display names of the sections whose collections are unavailable. */
|
|
readonly missing: readonly string[];
|
|
}
|
|
|
|
/** `partial` surface banner: verified parts render, missing parts are explicit. */
|
|
export function PartialDataNotice({
|
|
missing,
|
|
onRetry,
|
|
retryLabel,
|
|
}: PartialDataNoticeProps): ReactElement {
|
|
return (
|
|
<div role="status" className="rounded-lg border border-warning/40 px-4 py-3 text-sm">
|
|
<p className="font-medium text-warning">Some data could not be loaded</p>
|
|
<p className="mt-1 text-xs text-text-muted">
|
|
{missing.join(', ')} {missing.length === 1 ? 'is' : 'are'} unavailable — sections below show
|
|
an explicit unavailable state instead of an empty list. Derived verdicts remain unknown
|
|
until every collection is revalidated.
|
|
</p>
|
|
<RetryButton onRetry={onRetry} retryLabel={retryLabel ?? 'Revalidate'} />
|
|
</div>
|
|
);
|
|
}
|