Initial import
This commit is contained in:
158
src/components/dataStatePanel/DataStatePanel.tsx
Executable file
158
src/components/dataStatePanel/DataStatePanel.tsx
Executable file
@@ -0,0 +1,158 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user