106 lines
2.7 KiB
React
106 lines
2.7 KiB
React
import { useId } from "react";
|
|
|
|
import { errorMessage } from "./error-copy.js";
|
|
import { Button } from "./ui/button.jsx";
|
|
|
|
/** @param {{ label?: string }} props */
|
|
export function LoadingSurface({ label = "불러오는 중" }) {
|
|
return (
|
|
<section
|
|
className="state-surface state-surface--loading"
|
|
aria-busy="true"
|
|
aria-label={label}
|
|
aria-live="polite"
|
|
aria-atomic="true"
|
|
>
|
|
<div className="ui-skeleton" aria-hidden="true" />
|
|
<span className="visually-hidden">{label}</span>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* title?: string,
|
|
* description?: string,
|
|
* action?: React.ReactNode
|
|
* }} props
|
|
*/
|
|
export function EmptySurface({
|
|
title = "표시할 항목이 없습니다.",
|
|
description,
|
|
action,
|
|
}) {
|
|
return (
|
|
<section className="ui-empty state-surface" aria-live="polite">
|
|
<h2>{title}</h2>
|
|
{description ? <p>{description}</p> : null}
|
|
{action}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* userMessageKey: string,
|
|
* action: "retry" | "reauth" | "navigate" | "reload-once" |
|
|
* "contact-support" | "none",
|
|
* onAction?: () => void
|
|
* }} props
|
|
*/
|
|
export function TerminalErrorSurface({ userMessageKey, action, onAction }) {
|
|
const messageId = useId();
|
|
const actionLabels = Object.freeze({
|
|
retry: "다시 시도",
|
|
reauth: "로그인",
|
|
navigate: "안전한 화면으로 이동",
|
|
"reload-once": "한 번 새로고침",
|
|
"contact-support": "지원 정보 확인",
|
|
});
|
|
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)}</h2>
|
|
{action !== "none" && (
|
|
<Button onClick={onAction}>{actionLabels[action]}</Button>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* state: ReturnType<typeof import("../../application/view-models/async-state.js").deriveAsyncState>,
|
|
* children?: React.ReactNode,
|
|
* onAction?: () => void
|
|
* }} props
|
|
*/
|
|
export function AsyncSurface({ state, children, onAction }) {
|
|
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={onAction}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section aria-busy={state.overlay.refreshing || state.overlay.mutationPending}>
|
|
{state.indicator && (
|
|
<p role="status" aria-live="polite">
|
|
{state.indicator}
|
|
</p>
|
|
)}
|
|
{children}
|
|
</section>
|
|
);
|
|
}
|