feat: 기능 추가 과정중
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import { useId, type ReactNode } from "react";
|
||||
|
||||
import type { AsyncState } from "../../application/view-models/async-state.ts";
|
||||
import type { AppFailure } from "../../contracts/errors.ts";
|
||||
import { useLocale } from "../i18n/index.ts";
|
||||
import { errorMessage } from "./error-copy.ts";
|
||||
import { Button } from "./ui/button.ts";
|
||||
|
||||
export function LoadingSurface({ label }: Readonly<{ label?: string }>) {
|
||||
const { message } = useLocale();
|
||||
const accessibleLabel = label ?? message("async.loading");
|
||||
return (
|
||||
<section
|
||||
className="state-surface state-surface--loading"
|
||||
aria-busy="true"
|
||||
aria-label={accessibleLabel}
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
<div className="ui-skeleton" aria-hidden="true" />
|
||||
<span className="visually-hidden">{accessibleLabel}</span>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export type EmptySurfaceProps = Readonly<{
|
||||
title?: string;
|
||||
description?: string;
|
||||
action?: ReactNode;
|
||||
}>;
|
||||
|
||||
export function EmptySurface({
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
}: EmptySurfaceProps) {
|
||||
const { message } = useLocale();
|
||||
return (
|
||||
<section className="ui-empty state-surface" aria-live="polite">
|
||||
<h2>{title ?? message("async.empty")}</h2>
|
||||
{description ? <p>{description}</p> : null}
|
||||
{action}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export type TerminalErrorSurfaceProps = Readonly<{
|
||||
userMessageKey: string;
|
||||
action: AppFailure["action"];
|
||||
onAction?: () => void;
|
||||
}>;
|
||||
|
||||
export function TerminalErrorSurface({
|
||||
userMessageKey,
|
||||
action,
|
||||
onAction,
|
||||
}: TerminalErrorSurfaceProps) {
|
||||
const { locale, message } = useLocale();
|
||||
const messageId = useId();
|
||||
const actionLabels: Readonly<
|
||||
Record<Exclude<AppFailure["action"], "none">, string>
|
||||
> = Object.freeze({
|
||||
retry: message("action.retry"),
|
||||
reauth: message("action.reauth"),
|
||||
navigate: message("action.navigateSafe"),
|
||||
"reload-once": message("action.reloadOnce"),
|
||||
"contact-support": message("action.contactSupport"),
|
||||
});
|
||||
return (
|
||||
<section
|
||||
className="ui-terminal-error state-surface state-surface--danger"
|
||||
role="alert"
|
||||
aria-labelledby={messageId}
|
||||
data-message-key={userMessageKey}
|
||||
>
|
||||
<h2 id={messageId}>{errorMessage(userMessageKey, locale)}</h2>
|
||||
{action !== "none" && onAction && (
|
||||
<Button onClick={onAction}>{actionLabels[action]}</Button>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export type AsyncSurfaceProps = Readonly<{
|
||||
state: AsyncState;
|
||||
children?: ReactNode;
|
||||
onAction?: () => void;
|
||||
onRetry?: () => void;
|
||||
onResolveConflict?: () => void;
|
||||
}>;
|
||||
|
||||
export function AsyncSurface({
|
||||
state,
|
||||
children,
|
||||
onAction,
|
||||
onRetry,
|
||||
onResolveConflict,
|
||||
}: AsyncSurfaceProps) {
|
||||
const { message } = useLocale();
|
||||
if (state.base === "initial-loading") return <LoadingSurface />;
|
||||
if (state.base === "empty") return <EmptySurface />;
|
||||
if (state.base === "terminal-error" && state.failure) {
|
||||
return (
|
||||
<TerminalErrorSurface
|
||||
userMessageKey={state.failure.userMessageKey}
|
||||
action={state.failure.action}
|
||||
onAction={
|
||||
state.failure.action === "retry"
|
||||
? onRetry ?? onAction
|
||||
: onAction
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section aria-busy={state.overlay.refreshing || state.overlay.mutationPending}>
|
||||
{state.indicator ? (
|
||||
<div role="status" aria-live="polite">
|
||||
<span>
|
||||
{message(
|
||||
state.indicator === "stale-degraded"
|
||||
? "async.staleDegraded"
|
||||
: state.indicator === "mutation-conflict"
|
||||
? "async.mutationConflict"
|
||||
: state.indicator === "mutation-pending"
|
||||
? "async.mutationPending"
|
||||
: "async.refreshing",
|
||||
)}
|
||||
</span>
|
||||
{state.indicator === "stale-degraded" && onRetry ? (
|
||||
<Button onClick={onRetry}>{message("action.retry")}</Button>
|
||||
) : null}
|
||||
{state.indicator === "mutation-conflict" && onResolveConflict ? (
|
||||
<Button onClick={onResolveConflict}>
|
||||
{message("action.resolveConflict")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user