159 lines
4.4 KiB
TypeScript
159 lines
4.4 KiB
TypeScript
import {
|
|
CheckCircleOutlined,
|
|
ExclamationCircleOutlined,
|
|
InboxOutlined,
|
|
LoadingOutlined,
|
|
} from '@ant-design/icons';
|
|
import { Card, Flex, Skeleton, Typography } from 'antd';
|
|
import type { CardProps } from 'antd';
|
|
import type { ReactNode } from 'react';
|
|
import './DataStatePanel.css';
|
|
|
|
const { Paragraph, Text, Title } = Typography;
|
|
|
|
export type DataStatePanelStatus = 'loading' | 'empty' | 'error' | 'ready';
|
|
|
|
export type DataStatePanelProps = {
|
|
state: DataStatePanelStatus;
|
|
title?: ReactNode;
|
|
description?: ReactNode;
|
|
actions?: ReactNode;
|
|
children?: ReactNode;
|
|
compact?: boolean;
|
|
icon?: ReactNode;
|
|
loadingRows?: number;
|
|
className?: string;
|
|
cardProps?: Omit<CardProps, 'children' | 'className'>;
|
|
};
|
|
|
|
function getDefaultIcon(state: DataStatePanelStatus) {
|
|
switch (state) {
|
|
case 'loading':
|
|
return <LoadingOutlined />;
|
|
case 'empty':
|
|
return <InboxOutlined />;
|
|
case 'error':
|
|
return <ExclamationCircleOutlined />;
|
|
case 'ready':
|
|
return <CheckCircleOutlined />;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getDefaultTitle(state: DataStatePanelStatus) {
|
|
switch (state) {
|
|
case 'loading':
|
|
return '데이터를 불러오는 중입니다.';
|
|
case 'empty':
|
|
return '표시할 데이터가 없습니다.';
|
|
case 'error':
|
|
return '데이터를 불러오지 못했습니다.';
|
|
case 'ready':
|
|
return '데이터를 확인할 수 있습니다.';
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function getDefaultDescription(state: DataStatePanelStatus) {
|
|
switch (state) {
|
|
case 'loading':
|
|
return '응답을 정리하는 동안 패널 레이아웃을 유지합니다.';
|
|
case 'empty':
|
|
return '조건을 조정하거나 새 항목을 추가하면 이 영역이 채워집니다.';
|
|
case 'error':
|
|
return '네트워크 또는 권한 상태를 확인한 뒤 다시 시도하세요.';
|
|
case 'ready':
|
|
return undefined;
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function getSkeletonWidths(compact: boolean, rows: number) {
|
|
if (compact) {
|
|
return Array.from({ length: rows }, (_, index) => (index === rows - 1 ? '84%' : '100%'));
|
|
}
|
|
|
|
return Array.from({ length: rows }, (_, index) => {
|
|
if (index === rows - 1) {
|
|
return '74%';
|
|
}
|
|
|
|
return '100%';
|
|
});
|
|
}
|
|
|
|
function renderLoadingBody(compact: boolean, rows: number) {
|
|
return (
|
|
<div className="data-state-panel__skeleton">
|
|
<Skeleton
|
|
active
|
|
title={{ width: compact ? '52%' : '38%' }}
|
|
paragraph={{ rows, width: getSkeletonWidths(compact, rows) }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function DataStatePanel({
|
|
state,
|
|
title,
|
|
description,
|
|
actions,
|
|
children,
|
|
compact = false,
|
|
icon,
|
|
loadingRows = compact ? 2 : 3,
|
|
className,
|
|
cardProps,
|
|
}: DataStatePanelProps) {
|
|
const resolvedTitle = title ?? getDefaultTitle(state);
|
|
const resolvedDescription = description ?? getDefaultDescription(state);
|
|
const panelClassName = [
|
|
'data-state-panel',
|
|
`data-state-panel--${state}`,
|
|
compact ? 'data-state-panel--compact' : '',
|
|
className ?? '',
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
|
|
return (
|
|
<Card {...cardProps} className={panelClassName}>
|
|
<Flex vertical gap={compact ? 14 : 18} className="data-state-panel__inner">
|
|
<div className="data-state-panel__hero">
|
|
<span className="data-state-panel__icon" aria-hidden="true">
|
|
{icon ?? getDefaultIcon(state)}
|
|
</span>
|
|
<Flex vertical gap={4} className="data-state-panel__copy">
|
|
{resolvedTitle ? (
|
|
compact ? (
|
|
<Text strong className="data-state-panel__title data-state-panel__title--compact">
|
|
{resolvedTitle}
|
|
</Text>
|
|
) : (
|
|
<Title level={5} className="data-state-panel__title">
|
|
{resolvedTitle}
|
|
</Title>
|
|
)
|
|
) : null}
|
|
{resolvedDescription ? (
|
|
<Paragraph className="data-state-panel__description" type="secondary">
|
|
{resolvedDescription}
|
|
</Paragraph>
|
|
) : null}
|
|
</Flex>
|
|
</div>
|
|
|
|
{state === 'loading' ? renderLoadingBody(compact, loadingRows) : children ? (
|
|
<div className="data-state-panel__content">{children}</div>
|
|
) : null}
|
|
|
|
{actions ? <div className="data-state-panel__actions">{actions}</div> : null}
|
|
</Flex>
|
|
</Card>
|
|
);
|
|
}
|